argoproj/argo-workflows · error
400
400
Error message
%s.%s %s
What it means
validateHooks requires every lifecycle hook except the 'exit' hook to define an expression; a hook without one fails with this 400 error formatted as '<hookBaseName>.<hookName> Expression required'. Lifecycle hooks conditionally run templates based on expression results, so an empty expression is meaningless.
Source
Thrown at workflow/validate/validate.go:146
substitutions[match[1]] = placeholderGenerator.NextPlaceholder()
}
// since we don't need to resolve/evaluate here we can do just a simple replacement
for old, new := range substitutions {
rmatch, _ := regexp.Compile(`{{\s*=\s*` + regexp.QuoteMeta(old) + `\s*}}`)
manifest = rmatch.ReplaceAllString(manifest, new)
}
return manifest
}
// validateHooks takes an array of hooks to validate and the name of the
// container they are in and generates an error for the first invalid hook
// or nil if they are all valid
func validateHooks(hooks wfv1.LifecycleHooks, hookBaseName string) error {
for hookName, hook := range hooks {
if hookName != wfv1.ExitLifecycleEvent && hook.Expression == "" {
return errors.Errorf(errors.CodeBadRequest, "%s.%s %s", hookBaseName, hookName, "Expression required")
}
}
return nil
}
// Workflow accepts a workflow and performs validation against it.
func Workflow(ctx context.Context, wftmplGetter templateresolution.WorkflowTemplateNamespacedGetter, cwftmplGetter templateresolution.ClusterWorkflowTemplateGetter, wf *wfv1.Workflow, wfDefaults *wfv1.Workflow, opts Opts) error {
tctx := newTemplateValidationCtx(wf, opts)
tmplCtx := templateresolution.NewContext(wftmplGetter, cwftmplGetter, wf, wf, logging.RequireLoggerFromContext(ctx))
var wfSpecHolder wfv1.WorkflowSpecHolder
var wfTmplRef *wfv1.TemplateRef
var err error
if len(wf.Name) > maxCharsInObjectName {
return fmt.Errorf("workflow name %q must not be more than 63 characters long (currently %d)", wf.Name, len(wf.Name))
}
View on GitHub (pinned to 35bff19146)
Solutions
- Add an `expression` to every hook, e.g. `expression: '{{workflow.status}} == Failed'`
- Check for YAML key typos (`expression` vs `expressions`) that silently leave the field empty
- If the hook should always run, use an expression that always evaluates true (e.g. `true`)
- The special `exit` hook is exempt and may omit the expression
Example fix
// before
hooks:
failed:
template: notify
// after
hooks:
failed:
expression: '{{workflow.status}} == Failed'
template: notify Defensive patterns
Strategy: validation
Validate before calling
func hookHasExpression(name string, h wfv1.LifecycleHook) error {
if name != wfv1.ExitLifecycleEvent && h.Expression == "" {
return fmt.Errorf("hook %q needs an expression", name)
}
return nil
} Type guard
func isExitHook(name string) bool { return name == wfv1.ExitLifecycleEvent } Try / catch
err := validate.Watchface... // on submit
if err := argoClient.Create(ctx, wf); err != nil {
if strings.Contains(err.Error(), "Expression required") {
// fix hook definition and resubmit
}
return err
} Prevention
- Always set expression on every hook except exit
- Double-check YAML key spelling: expression not expressions
- Use CI linting (`argo lint`) before submit
- Use schema validation in editors (Argo JSON schema) to catch missing fields
When it happens
Trigger: Submitting or linting a Workflow whose spec.hooks (or template-level hooks) contains an entry like `hooks.foo: {template: myTmpl}` with no `expression` field, other than the special `exit` hook.
Common situations: Copy-pasting hook examples and deleting the expression; assuming hooks run unconditionally like steps; YAML typo such as `expressions:` instead of `expression:`; building hooks programmatically and leaving Expression unset.
Related errors
- cron schedules must consist of 5 values only
- ${e.Usage()}
- --completed and --running cannot be used together
- invalid cache key: %s
- CodeBadRequest
AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03).
Data as JSON: /api/errors/6e8a7a8febd47c6a.
Report an issue: GitHub.