Composition
An authored workflow can run independently and remain visible when composed inside a parent. Child contracts, waits, events, trace ancestry, failures, and artifacts are preserved rather than flattened into helper calls.
Place one child workflow
export const clarifyRequest = step.workflow({
id: "requests.clarify",
workflow: clarifyRequestWorkflow,
});
The step inherits the child's input and output contracts. If the child waits, the parent waits and resumes the same child session.
Route to exactly one child
export const processCapture = step.router({
id: "captures.process",
input: {capture: captureSchema},
routes: {
image: processImageWorkflow,
note: processNoteWorkflow,
},
select: ({input}) => input.capture.type,
mapInput: ({input}, routeId) => ({
source: input.capture.value,
kind: routeId,
}),
});
At least two routes are required. Every route must have compatible input and
identical output contracts. Without explicit input and mapInput, the router
passes matching keys. The selector must return a declared key; changing the
selected route while hydrating a waiting run is rejected.
Fork independent children
export const reviewResponse = step.fork({
id: "responses.review",
branches: {
assessment: assessResponseWorkflow,
noteAnalysis: analyzeNoteWorkflow,
},
});
At least two branches are required. Each branch receives only its declared
input keys. Shared keys must have compatible contracts, branch output keys may
not collide, and the current join policy waits for all branches. A branch
failure fails the fork.
Iterate a child workflow
The simple form derives arrays from a child with one input and one output:
export const analyzeAllNotes = step.forEach({
id: "notes.analyze-all",
items: "notes",
collect: "analyses",
workflow: analyzeNoteWorkflow,
});
Use the advanced form for mappings, child configuration, callbacks, and controlled error continuation:
const analyzeAll = step.forEach({
id: "notes.analyze-advanced",
input: {notes: z.array(noteSchema)},
output: {summary: batchSummarySchema},
config: {defaults: {language: "en"}},
prepare: ({config}) => config,
items: ({input}) => input.notes,
childInput: (note) => ({note}),
itemId: (note) => note.id,
itemLabel: (note) => note.title,
workflow: analyzeNoteWorkflow,
workflowConfig: ({prepared}) => prepared,
continueOnError: true,
onItemFailed: ({itemId, error}) => reportFailure(itemId, error),
collect: (results) => ({
summary: {
completed: results.filter((result) => result.status === "completed").length,
failed: results.filter((result) => result.status === "failed").length,
},
}),
});
With continueOnError: false or omitted, an iteration failure fails the step.
When true, failed results reach collect() alongside completed outputs.
Use edges for sequence
Ordinary document edges express ordered dependencies. The target reads values from accumulated state through matching contract keys. Do not use a signal, timer, or nested workflow merely to mean “run this next.”
Choose the shape
| Need | Shape |
|---|---|
| Ordered dependency | Document edge |
| Reusable multi-step behavior | step.workflow() |
| Assess once | workflow.judge() |
| Revise to a quality bar | workflow.judgeUntil() |
| Resolve requirements with a human | workflow.clarifyUntil() |
| Exactly one runtime choice | step.router() |
| Independent concurrent work | step.fork() |
| Same workflow over a collection | step.forEach() |
| Local transformation without its own lifecycle | A function inside the owning step |
Composition primitives define orchestration semantics; child workflows own their internal behavior. Retain stable child IDs so traces and hydrated runs remain attributable across releases.
Execution kind and semantic role
Bundle nodes expose both concepts for inspection. Kind controls execution and
rendering: model, delegate, gate, question, workflow, or ordinary step. Role
explains why a node exists inside a bundle: resolve, judge, gate, ask, merge, or
revise. Runtime behavior must never branch on role; changing or removing it
does not change validation, persistence, routing, retries, or output.
| Bundle node | Execution kind | Semantic role |
|---|---|---|
| Contract resolver | model, delegate, workflow, or step | resolve |
| Semantic assessor | model, delegate, workflow, or step | judge |
| Score policy | gate | gate |
| Human question | question | ask |
| Accepted-answer transform | step | merge |
| Candidate or contract revision | Any authored executable kind | revise |
Inference ownership changes the assessor, not the workflow semantics: