Defining steps
| Helper | Use it for |
|---|---|
defineStep({ type, workflowInputSchema, outputSchema, dependencies?, handler, retry?, isRetryable?, timeoutMs?, delayMs?, waitForEvent?, compensate? }) |
a normal step |
defineSleepStep({ type, sleepMs, dependencies? }) |
a durable no-op delay |
defineWaitStep({ type, outputSchema, dependencies? }) |
suspend until engine.resumeStep |
defineMapStep({ type, workflowInputSchema, itemOutputSchema, items, each, dependencies?, itemRetry?, itemIsRetryable?, itemTimeoutMs? }) |
runtime-sized fan-out |
defineSubWorkflowStep({ type, workflowInputSchema, childWorkflow, input, outputSchema?, dependencies? }) |
start + await a child workflow |
defineAiStep({ ... }) |
a step whose ctx.context is an instrumented AiContext (AI add-on) |
Keys and types are different things
Section titled “Keys and types are different things”const wf = buildWorkflow({ type: 'publish', inputSchema, steps: { draft, approval } });// ^^^^^ ^^^^^^^^// these are the step KEYStype(on the step) names the handler. It is what the registry looks up, what gate rules and metrics are keyed on, and it may repeat across workflows.- The key is the property name under
steps:inbuildWorkflow. It identifies the step within this workflow: it’s whatctx.depsis keyed by, whatengine.resumeSteptakes, and what the workflow’s output object is keyed by.
The same rule applies to dependencies. The property name — not the variable — is the
dependency key, and it must match the step’s key in buildWorkflow:
const shout = defineStep({ …, dependencies: { greet } }); // key 'greet'buildWorkflow({ …, steps: { greet, shout } }); // ✅ matches
buildWorkflow({ …, steps: { greeting: greet, shout } }); // ❌ throws:// [buildWorkflow] Step 'shout' depends on 'greet', which is not a valid step key.Note that buildWorkflow throws here rather than returning a Result — a malformed DAG is
a programming error, caught at module load. The deeper checks (cycles, duplicate keys, missing
handlers) run at start and are returned as a Result; see
validateDefinition.
The handler context
Section titled “The handler context”A handler receives a typed context:
handler: async (ctx) => { ctx.workflowInput; // validated against workflowInputSchema ctx.deps.greet; // each dependency's output, validated against ITS outputSchema ctx.stepInput; // static per-step input from the definition ctx.context; // host value from the `buildStepContext` hook ctx.signal; // AbortSignal — fires on timeout ctx.workflowId; ctx.stepId; ctx.stepKey; ctx.partitionKey; return { … }; // validated against outputSchema}To fail a step, throw. The typed handler returns the step’s output; there is no
error-return form (the Result-returning StepHandler is the untyped internal shape that
defineStep wraps for you). Whether a throw is retried is decided by
retryability — throw retryableError(…) or
nonRetryableError(…) to say so outright.
Validation is a permanent failure
Section titled “Validation is a permanent failure”defineStep runs a five-phase pipeline: parse workflowInput → parse each dependency output →
build the typed context → call the handler → parse the output. A mismatch at any of those
phases fails the step with retryable: false — it is never retried, whatever the step’s
retry policy says, because a schema mismatch is a bug rather than a blip.
The error message names the phase, which is usually enough to locate it:
[shout] Invalid dependency output 'greet': …[shout] Invalid step output: …ctx.signal is worth wiring through to anything that accepts one (fetch, database drivers) —
timeoutMs aborts it on expiry, but only a handler that observes it actually stops early.