Steps
A step is a visible executable unit with stable identity, input and output contracts, and an observable attempt lifecycle. Use one when progress, retry, failure, human judgment, model work, or a side effect should be visible.
Define a contracted step
import { step } from "@dromio/workflow";
import { z } from "zod";
export const saveSummary = step({
id: "notes.save-summary",
input: {summary: z.string().min(1)},
output: {path: z.string().min(1)},
sideEffects: ["filesystem.write"],
maxRetries: 2,
async run({artifacts, input, step: attempt}) {
const path = `summaries/${attempt.idempotencyKey}.txt`;
await Bun.write(path, input.summary);
artifacts.file({mediaType: "text/plain", path});
return {path};
},
});
Returning the declared output completes the step. The SDK validates inputs and
outputs, emits attempt events, and registers artifacts. Unexpected exceptions
reject execution. Advanced steps may return done(), ask(), wait(),
retry(), goto(), or fail() from @dromio/workflow/core.
Every step.* primitive
| API | Use it for | Suspends |
|---|---|---|
step() | App-owned contracted behavior or side effects | Only if its advanced result does |
step.model() | One visible structured model call | No |
step.gate() | Deterministic decision from evaluation and policy | No |
step.delegate() | External-harness inference, tools, browser use, or subagents | Yes |
step.ask() | Human question with validated answer | Yes |
step.approval() | Typed approval request and decision | Yes |
step.waitFor() | Typed external signal | Yes |
step.sleep() | Duration or absolute-time timer | Yes |
step.workflow() | One independently meaningful child workflow | If the child waits |
step.router() | Exactly one compatible child selected at runtime | If the child waits |
step.fork() | Two or more independent child workflows | If a branch waits |
step.forEach() | One child workflow per item | If an iteration waits |
step.evaluate() and step.promptedContract() remain supported compatibility
wrappers, but they are bundled workflows rather than primitives. New code uses
workflow.judge() and workflow.clarifyUntil(). There is no generic
step.writeJson() root API.
Delegate work to a harness
step.delegate() is a primitive with the same visible step identity and
contracts as any other definition, but it suspends rather than performing the
work in process. Choose it only when an external harness must leave the
explicit input to gather context, use tools, or manage an agentic task. The
Authoring decision rule
explains when to use it; External harnesses
owns MCP connection and resumption; the API reference
owns its exact input fields.
Continue to Models and evaluation for model, gate, and prompted-contract examples; Human input and waits for ask, approval, signal, and timer steps; Composition for child primitives; and the Authoring API reference for exact option summaries. Runtime result helpers and session control are covered in Execution and durability.
Contracts and data flow
input and output accept operation contracts or compatible sources such as
Zod schemas. Keys are the default data-binding names used as state moves
through document edges:
input: {
generatedResponse: generatedResponseSchema,
prompt: promptSchema,
},
output: {
assessment: assessmentSchema,
},
A step receives values for its declared input keys from accumulated workflow
state. A completed output is merged into state. Use document node bindings
when a placement intentionally renames an input or output key; do not wrap
everything in an untyped payload to avoid explicit mappings.
Configuration and placement
Reusable definition identity is separate from placement identity. A document
node references catalogItemId and supplies its own id and optional config.
The runtime calls config.resolve(defaults, placement) once when creating the
placement:
const format = step({
id: "notes.format",
config: {
defaults: {prefix: "Note"},
resolve: (defaults, placement) => ({
prefix: typeof placement.prefix === "string"
? placement.prefix
: defaults.prefix,
}),
},
input: {summary: z.string()},
output: {formatted: z.string()},
run: ({config, input}) => ({formatted: `${config.prefix}: ${input.summary}`}),
});
Workflow configuration is merged first, then node configuration overrides it.
Catalog metadata
The executable definition is also its catalog item. Optional metadata includes
label, description, capabilities, intents, sideEffects, tags,
verbs, runtime dependencies, configuration requirements, prompts, and
examples. Do not maintain a second manifest that can drift from the code.
Retry and failure behavior
maxRetries limits explicit retry(reason) results; the default is one retry.
Each attempt has its own trace span and idempotency key. fail(message) returns
a failed session and emits failure events. A thrown exception rejects the run
with the step ID and attempt attached. The SDK does not automatically retry
arbitrary exceptions or make external side effects idempotent.
Choose a step boundary
Create a step when a user or operator would name it, its progress or failure matters, it invokes a model or human, it decides control flow, it has a reusable contract, or it owns an important side effect. Keep local parsing and formatting inside the owning step when they have no independent lifecycle.
A graph of model calls alone is usually a prompt chain. Step boundaries earn their keep when inference is punctuated by durable waits, human decisions, deterministic gates, parallel work, idempotent side effects, or other observable control boundaries.