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

  1. Add an `expression` to every hook, e.g. `expression: '{{workflow.status}} == Failed'`
  2. Check for YAML key typos (`expression` vs `expressions`) that silently leave the field empty
  3. If the hook should always run, use an expression that always evaluates true (e.g. `true`)
  4. 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

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


AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03). Data as JSON: /api/errors/6e8a7a8febd47c6a. Report an issue: GitHub.