step.waitForEvent()
Pause until an external event arrives
Overview
step.waitForEvent() pauses a workflow until an external event is delivered. This is perfect for human-in-the-loop workflows, webhooks, or any scenario where your workflow needs to wait for something to happen in the outside world.
The event name must match a key from the workflow's events schema, and the returned payload is fully typed.
For how to define event schemas and send events from outside a workflow, see Typed Events.
Basic Usage
const workflow = defineWorkflow((t) => ({
type: 'approval-flow',
input: t.object({ requestId: t.string() }),
events: {
approval: t.object({
approved: t.boolean(),
reviewer: t.string(),
}),
},
run: async (step, payload) => {
await step.do('submit-request', async () => {
await submitForReview(payload.requestId);
});
// Wait up to 48 hours for approval
const decision = await step.waitForEvent('approval', {
timeout: '48h',
});
if (decision.approved) {
await step.do('process-approval', async () => {
await processApproval(payload.requestId, decision.reviewer);
});
}
return { approved: decision.approved, reviewer: decision.reviewer };
},
}));Timeouts
Without a timeout, workflows wait indefinitely for events. This is often exactly what you want for human-in-the-loop workflows.
// Wait forever for user approval
const approval = await step.waitForEvent('user-approval');With a timeout, the workflow is marked as errored with EventTimeoutError if the event doesn't arrive in time:
await step.waitForEvent('webhook', { timeout: '5m' }); // 5 minutes
await step.waitForEvent('approval', { timeout: '48h' }); // 2 days
await step.waitForEvent('payment', { timeout: '30s' }); // 30 secondsTimeouts use duration strings like "30s", "5m", "1h", or "7d".
If a timeout occurs, the workflow will be marked as errored with the EventTimeoutError. Design your workflows to either use generous
timeouts or handle the error externally by checking workflow status.
How It Works
Here's what happens when you call step.waitForEvent():
First Execution
- Creates a "waiting" step in SQLite with the event name and optional
timeoutAttimestamp - Throws a
WaitInterrupt(not an Error — this is normal flow control) - The workflow runner catches the interrupt
- If there's a timeout, the Durable Object sets an alarm for the timeout time
- The Durable Object hibernates
When a Buffered Event Exists
If the event was already sent before the workflow reached this step:
step.waitForEvent()checks the event buffer- If a matching buffered event is found, it is consumed immediately
- The step is persisted as "completed" with the buffered payload
- Execution continues without suspending — no
WaitInterrupt, no alarm - On subsequent replays, the step returns the cached result as usual
When an Event Arrives
- External code calls
ablauf.sendEvent() - The event payload is validated against the schema
- The Durable Object wakes up
- The waiting step is marked as "completed" with the event payload stored in SQLite
- The workflow replays from the beginning
- All previous steps return cached results instantly
step.waitForEvent()sees the step is completed and returns the event payload- Execution continues
If the Timeout Fires
- The Durable Object wakes up from the alarm
- The step is marked as "failed" with
EventTimeoutError - The workflow replays
step.waitForEvent()sees the step failed and re-throws the error- The workflow is marked as errored
Subsequent Replays
On any future replay (even if the workflow crashes and restarts), the completed event step returns the cached event payload instantly. The workflow doesn't wait again.
Type Safety
The return type of step.waitForEvent() is automatically inferred from your event schemas:
events: {
payment: t.object({
amount: t.number(),
currency: t.string(),
}),
}
// Inside run():
const payment = await step.waitForEvent("payment");
// TypeScript knows: payment is { amount: number, currency: string }
payment.amount; // ✅ number
payment.currency; // ✅ string
payment.unknownField; // ❌ TypeScript errorThe event name is also type-checked:
await step.waitForEvent('payment'); // ✅ "payment" is in the events schema
await step.waitForEvent('unknown'); // ❌ TypeScript errorThis makes it impossible to wait for an event that doesn't exist or to access fields that aren't in the schema.
Step Name Uniqueness
Like all step primitives, event wait step names must be unique:
// ❌ This will throw DuplicateStepError
await step.waitForEvent('approval');
await step.waitForEvent('approval');
// ✅ This is fine if you need to wait for the same event type multiple times
await step.waitForEvent('first-approval');
await step.waitForEvent('second-approval');