Step<I, O>
The atomic unit of a workflow.updateContext() β override to publish side-channel metadata alongside the primary output. Called by the executor after execute(). The output parameter is the value returned by execute() β derive metadata from it directly rather than capturing state in fields:
ClassificationResult) flows forward as the next stepβs input; metadata (confidence, label) flows via context keys. Default implementation returns ctx unchanged.
AgentStep marker β implement on steps that make LLM calls for correct NodeType.AGENT cost tracking:
Built-in Step Types
ClaudeStep options
Workflow Builder
Gate
GateDecision: PASS, FAIL, ESCALATE, TIMEOUT
Implementations
Gate builder
Verdict written to AgentContext.JUDGE_VERDICT. Reflector transforms verdict into constructive feedback for the retry step.
Gate.updateContext()
Gates implementupdateContext() just like steps do. Override it to write the gateβs assessment to AgentContext β both the onPass and onFail branches can then read it:
AgentContext
Immutable, threaded through every step. Copy-on-write viamutate().
Well-known Keys
ContextKey<T> implements Blochβs Typesafe Heterogeneous Container (Effective Java Item 33).
mergeFrom
donor onto base. Donor wins on key conflict. Returns a new immutable instance. Used internally by WorkflowExecutor to propagate sub-workflow context writes back to the parent β you rarely call this directly, but it is part of the public API if you need manual context composition.
Sub-workflow Composition
AWorkflow implements Step, so it can be used anywhere a step is expected β as a sequential step, inside a branch, inside a gate path, or inside a parallel branch. The executor handles the nesting transparently:
step instanceof Workflow and runs it inline rather than dispatching through the StepRunner. The sub-workflow receives the parentβs current context, executes normally, and the final context (including all updateContext() writes from every nested step) is merged back into the parent via mergeFrom(). Leaf steps still go through the StepRunner (so TemporalStepRunner dispatch works correctly for individual steps).
Nesting is unlimited: sub-workflows can contain sub-workflows. Each level merges its context writes back up the chain.
Parallel branches: context writes from parallel sub-workflow branches are merged at the join node. If two branches write to the same key, the last branch to complete wins.
RunOptions
WorkflowGraph (IR)
Pure data structure β no execution logic, no Spring AI imports.Node Types (sealed)
Edge Conditions (sealed)
Type Checking
WorkflowGraphAssert.assertTypeCompatible(graph) walks a compiled graph and checks that each stepβs declared output type is assignable to the next stepβs declared input type. Catches ClassCastException-style bugs at test time.
Opt-in: Steps that override inputType() / outputType() participate. Lambda steps and Step.named() return Object.class by default and are silently skipped β no false positives.
WorkflowAbortException
Thrown by a step to abort the workflow and return a typed result. Unlike an unhandled exception (which propagates as an error),WorkflowAbortException is caught by the executor and its carried result becomes the workflowβs output:
WorkflowAbortException when a step detects an unrecoverable condition but can still produce a meaningful output β for example, a typed error response, a failure report, or a structured rejection. Prefer it over returning null or throwing a raw exception when the caller needs to distinguish βcompleted with degraded outputβ from βcrashed.β
StepRunner
The substrate swap seam. Same workflow code, different durability:
Same workflow code β swap the
@Bean, not the workflow. See Durability for setup and crash-recovery examples.
Operator retry (CheckpointManager): FAILED steps are not retried automatically β the system holds them until an operator decides the failure was transient. CheckpointManager.resetFailedSteps(runId) deletes FAILED records; re-running with the same runId then skips COMPLETED steps and retries the reset ones.
Workflow used as a step bypasses the StepRunner entirely and runs in-process. Only leaf steps go through the runner. This is intentional β TemporalStepRunner dispatches to a separate activity worker thread and cannot carry full parent context; sub-workflows must stay in-process to propagate context correctly.
TraceRecorder
Records every step transition:TraceRecorder.noop() is the default β zero overhead unless opted in. Trace data feeds Markov analysis, run diagnosis, and replay. When a step produces a trace file (via AgentClientStep backed by a trace-aware client), the tracePath is included in the transition. See Trace Capture for the full guide.