argoproj/argo-workflows · error

400

400

Error message

failed to evaluate expression %q

What it means

resolveParameter evaluates a parameter expression against the workflow scope. When the expression evaluates to nil — an unhandled absent optional, such as a skipped node's output parameter that has no default and was referenced without the `??` operator — the controller treats it as a resolution failure with code 400, mirroring the inline {{= ...}} semantics. If the caller declared valueFrom.default, that default is applied on this error path instead of failing the step.

Source

Thrown at workflow/controller/scope.go:185

func (s *wfScope) resolveParameter(p *wfv1.ValueFrom) (any, bool, error) {
	if p == nil || (p.Parameter == "" && p.Expression == "") {
		return "", false, nil
	}
	if p.Expression != "" {
		env := env.GetFuncMap(s.scope.AsAnyMap())
		program, err := expr.Compile(p.Expression, expr.Env(env))
		if err != nil {
			return nil, false, err
		}
		val, err := expr.Run(program, env)
		if err != nil {
			return nil, false, err
		}
		if val == nil {
			// A nil result is an unhandled absent optional (e.g. a skipped node's defaultless output
			// referenced without `??`). Mirror the inline {{= ...}} semantics and treat it as a
			// resolution failure; the caller's error path applies valueFrom.default when declared.
			return nil, false, errors.Errorf(errors.CodeBadRequest, "failed to evaluate expression %q", p.Expression)
		}
		return val, false, nil
	}
	tag, val, err := s.resolveVar(p.Parameter)
	// IsSkipped is true only for a placeholder written via Key.SetSkipped (a skipped/omitted node
	// output with no producer default), i.e. an absent optional.
	return val, s.scope.IsSkipped(tag), err
}

func (s *wfScope) resolveArtifact(ctx context.Context, art *wfv1.Artifact) (*wfv1.Artifact, error) {
	if art == nil || (art.From == "" && art.FromExpression == "") {
		return nil, nil
	}

	var err error
	var val any

	if art.FromExpression != "" {

View on GitHub (pinned to 35bff19146)

Solutions

  1. Add a default to the output parameter in the producing template (valueFrom.default)
  2. Use the null-coalescing operator in the expression, e.g. {{tasks.x.outputs.parameters.y ?? 'fallback'}}
  3. Guard the consuming step with `when` so it only runs when the producer executed

Example fix

// before
value: '{{tasks.build.outputs.parameters.version}}'
// after
value: '{{tasks.build.outputs.parameters.version ?? "unknown"}}'
Defensive patterns

Strategy: validation

Validate before calling

// before submitting, ensure producers have defaults or consumers have fallbacks
for _, t := range wf.Spec.Templates {
  for _, op := range t.Outputs.Parameters {
    if op.ValueFrom != nil && op.ValueFrom.Default == nil {
      // downstream refs must use `??`
    }
  }
}

Type guard

func hasFallback(expr string) bool { return strings.Contains(expr, "??") }

Try / catch

if resolved, ok, err := resolveParameter(p); err != nil || !ok {
  if p.Value != nil { useDefault(p) } else { return fmt.Errorf("expression %q unresolved: %w", p.Expression, err) }
}

Prevention

When it happens

Trigger: Referencing {{tasks.x.outputs.parameters.y}} (or similar) in an expression when task x was skipped and its output parameter has no default, without using `??` to supply a fallback.

Common situations: Conditional steps (when:), failed/skipped DAG branches whose downstream steps still reference their outputs; workflows refactored to add `when` guards without updating downstream parameter references.

Related errors


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