Waiting for Events
.waitForEvent(name, options?) durably pauses a workflow until an external signal arrives — a webhook callback, a human approval, a third-party job finishing, a payment confirmation. The run is persisted as waiting and its lease released, so the process can shut down entirely during the wait. When the event is delivered with engine.sendEvent(runId, name, payload), any engine instance reclaims the run and continues; the payload becomes the next step's prev.
const checkout = createWorkflow({ name: 'checkout', input: z.object({ orderId: z.string() }) })
.step('create-payment-intent', async ({ input }) => ({ intentId: await stripe.createIntent(input.orderId) }))
.waitForEvent('payment.succeeded', { schema: z.object({ chargeId: z.string() }), timeoutMs: 30 * 60 * 1000 })
.step('fulfil', async ({ prev, input }) => ship(input.orderId, prev.chargeId))
// ...later, in your Stripe webhook handler:
await engine.sendEvent(runId, 'payment.succeeded', { chargeId: event.data.object.id })Delivering events
engine.sendEvent(runId, eventName, payload) records the event:
- Order-independent. An event delivered before the run reaches the wait is buffered and consumed when it gets there — you don't have to race the workflow.
- Validated. If the wait declares a
schema, the payload is validated on delivery; an invalid payload throwsValidationErrorand the run keeps waiting. - Typed. The
schemainfers the type ofprev(andsteps[name]) for the steps that follow. - Returns
falseif the run does not exist; throwsConfigErrorif the workflow has nowaitForEventstep with that name.
Timeouts
Pass timeoutMs to bound the wait. If no event arrives in time, the run fails at the wait step with WaitTimeoutError, reaching onFailure / onRunFailed like any other failure:
.waitForEvent('approved', { timeoutMs: 24 * 60 * 60 * 1000 }) // give up after a dayWithout timeoutMs, the run waits indefinitely (until the event arrives or the run is cancelled).
Behaviour
- Crash-safe. The wait lives in storage, not process memory, so a restart (or a different machine) resumes it — and an event delivered while no engine is running is still there when one starts.
previs the payload. The validated event payload becomes the next step'sprevand is available assteps[name].- Observable as a step. The wait appears in
getRunStatus()as a step —waitinguntil the event (or timeout), thencompleted(orfailed). - Wake granularity for timeouts is the engine poll interval.
- One event at a time. A sequential workflow waits for one named event at a time; an event sent for a name the run will only reach later is simply buffered until then.
Sleep vs. wait
Use .sleep() when you know how long to pause; use .waitForEvent() when you're waiting for something to happen and don't know when. Both are durable and release the lease.