Signals & waitForEvent
const approval = defineWaitStep({ type: 'await-approval', outputSchema: z.object({ approved: z.boolean() }), dependencies: { draft }, timeoutMs: 48 * 60 * 60 * 1000, // optional; see Deadlines});
const wf = buildWorkflow({ type: 'publish', inputSchema, steps: { draft, approval, publish } });
// …elsewhere, when the webhook/human responds:await engine.resumeStep(workflowId, 'approval', { approved: true });The step suspends (waiting) once its dependencies complete — its handler never runs — until
resumeStep delivers the event payload, which becomes its output.
→ examples/08-wait-for-event.ts
The resume payload is the output
Section titled “The resume payload is the output”Whatever you pass becomes the step’s output verbatim — the engine does not validate it against
outputSchema on the way in. Validation happens on the way out, when a dependent step reads
it through ctx.deps:
const publish = defineStep({ dependencies: { approval }, handler: async (ctx) => { ctx.deps.approval.approved; // typed, and parsed against approval's outputSchema here },});So a malformed webhook body doesn’t fail at resumeStep — it fails the dependent step, as a
non-retryable validation error. Validate at your HTTP boundary if you want a 400 instead.
Idempotency and the states that ignore a resume
Section titled “Idempotency and the states that ignore a resume”resumeStep is safe to call more than once. It returns { ok: true } and does nothing when:
- the step is not
waiting(already resumed, already failed, or stillpendingbecause its dependencies haven’t completed — a resume that arrives early is dropped, not queued) - the workflow is not
pendingorrunning(already completed, failed, or cancelled)
It returns an error only when the workflow or the step key doesn’t exist. That means a
re-delivered webhook is a no-op, but so is a resume that arrives before the step is ready — if
your event can beat the DAG, persist it and replay after the step.waiting event.
Bounding the wait
Section titled “Bounding the wait”By default a waiting step waits indefinitely — it is not swept by
recoverStuckWorkflows, which only looks at
steps stuck in running. Three ways to bound it:
-
Give it a deadline.
timeoutMsplusonTimeoutis the built-in answer, and the one you usually want:const approval = defineWaitStep({type: 'await-approval',outputSchema: z.object({ approved: z.boolean() }),timeoutMs: 48 * 60 * 60 * 1000,onTimeout: { output: { approved: false } }, // or 'fail' (the default)});See Deadlines — including the worker change it requires (
engine.handleStepJob, notexecuteStep). -
Bound the whole run.
StartOptions.timeoutMsfails the workflow wherever it is, suspended included. -
Cancel it.
engine.cancelWorkflow(id)markswaitingstepsskippedand finishes the workflow ascancelled.
waiting folds to the display state running in the
public view, so a UI shows it as in-flight rather than as its own
state. Watch the step.waiting and step.resumed
events if you need to show “awaiting approval” specifically.