micro/go-micro · error

flow: step %q has no Run function

Error message

flow: step %q has no Run function

What it means

Every Step in a flow must provide a Run StepFunc; a nil Run is treated as a configuration error rather than a no-op. runStep checks for this before executing and fails the step immediately with the step's name in the message. This catches flows built by leaving the Run field unset, since the library cannot infer the step's action.

Source

Thrown at flow/steps.go:646

		return run, err
	}
	if f.opts.DeleteOnSuccess && f.checkpoint != nil {
		if err := f.checkpoint.Delete(ctx, run.ID); err != nil {
			spanErr = err
			return run, err
		}
	}
	f.record(resultFromRun(f.opts.TriggerTopic, run))
	f.log.Logf(logger.InfoLevel, "Flow %s run %s completed (%d steps)", f.name, run.ID, len(steps))
	return run, nil
}

// runStep runs one step, retrying on error up to the resolved retry count.
// A step with no Run function is a configuration error, and a canceled run
// stops retrying immediately rather than burning the rest of its budget.
func (f *Flow) runStep(ctx context.Context, step Step, in State) (State, int, Verification, error) {
	if step.Run == nil {
		return in, 0, Verification{}, fmt.Errorf("flow: step %q has no Run function", step.Name)
	}
	retries := f.opts.Retry
	if step.Retry > 0 {
		retries = step.Retry
	}
	var lastErr error
	var lastVerification Verification
	var feedback string
	for attempt := 1; attempt <= retries+1; attempt++ {
		// Stop the moment the run's context is canceled or its deadline
		// passes — a canceled run shouldn't keep retrying, and the context
		// error is surfaced so callers can detect cancellation upstream.
		if err := ctx.Err(); err != nil {
			return in, attempt - 1, lastVerification, err
		}
		attemptCtx := ctx
		if info, ok := ai.RunInfoFrom(ctx); ok {
			info.Step = step.Name

View on GitHub (pinned to 24529f1404)

Solutions

  1. Set a Run function on every Step: Run: func(ctx context.Context, in flow.State) (flow.State, error) {...} or use a helper like flow.Call/flow.LLM.
  2. Validate steps at startup: iterate your steps and fail fast if any has a nil Run before creating the flow.
  3. Check helper functions for paths that return a nil StepFunc (e.g. when config is missing) and return an error or a default step instead.
  4. If the step was a placeholder, remove it from the Steps slice.

Example fix

// before
steps := []flow.Step{{Name: "enrich"}} // Run missing

// after
steps := []flow.Step{{Name: "enrich", Run: func(ctx context.Context, in flow.State) (flow.State, error) {
  return in, nil // real work here
}}}
Defensive patterns

Strategy: validation

Validate before calling

for i, s := range steps {
    if s.Run == nil {
        return fmt.Errorf("step %d (%q) has no Run function", i, s.Name)
    }
}

Type guard

func stepRunnable(s flow.Step) bool { return s.Run != nil }

Try / catch

if _, _, err := f.runStep(ctx, step, in); err != nil {
    if strings.Contains(err.Error(), "has no Run function") {
        // configuration bug: fix the step definition, do not retry
        return fmt.Errorf("misconfigured flow: %w", err)
    }
    // genuine step failure: retry path applies
}

Prevention

When it happens

Trigger: Constructing flow.Step{Name: "x"} (or a step struct literal) without setting Run, then starting or resuming the flow; or a helper returning a nil StepFunc assigned into a Step.

Common situations: Copy-pasted step definitions where the Run field was accidentally deleted; refactoring steps into a shared slice where some placeholders have nil Run; conditional step construction where a helper returned nil on some code path; JSON/YAML-driven step config that can't express function values.

Related errors


AI-assisted analysis of micro/go-micro@24529f1404 (2026-09-01). Data as JSON: /api/errors/1d0f5233d9a5aa84. Report an issue: GitHub.