workflow 5.0.0-beta.1 → 5.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.d.ts +1 -1
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +2 -2
  5. package/dist/api.js +1 -1
  6. package/dist/astro.js +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/internal/builtins.js +1 -1
  9. package/dist/internal/class-serialization.js +1 -1
  10. package/dist/internal/errors.js +1 -1
  11. package/dist/nest.js +1 -1
  12. package/dist/next.cjs +1 -1
  13. package/dist/nitro.js +1 -1
  14. package/dist/nuxt.js +1 -1
  15. package/dist/observability.js +1 -1
  16. package/dist/runtime.js +1 -1
  17. package/dist/stdlib.js +1 -1
  18. package/dist/sveltekit.js +1 -1
  19. package/dist/typescript-plugin.cjs +1 -1
  20. package/dist/vite.js +1 -1
  21. package/dist/workflow.js +1 -1
  22. package/docs/ai/resumable-streams.mdx +1 -1
  23. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  24. package/docs/api-reference/workflow/get-workflow-metadata.mdx +34 -0
  25. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  26. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  27. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  28. package/docs/cookbook/advanced/child-workflows.mdx +372 -0
  29. package/docs/cookbook/advanced/distributed-abort-controller.mdx +318 -0
  30. package/docs/cookbook/advanced/meta.json +9 -0
  31. package/docs/cookbook/advanced/publishing-libraries.mdx +336 -0
  32. package/docs/cookbook/advanced/serializable-steps.mdx +147 -0
  33. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +205 -0
  34. package/docs/cookbook/agent-patterns/durable-agent.mdx +150 -0
  35. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +255 -0
  36. package/docs/cookbook/agent-patterns/meta.json +4 -0
  37. package/docs/cookbook/common-patterns/batching.mdx +105 -0
  38. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  39. package/docs/cookbook/common-patterns/meta.json +15 -0
  40. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  41. package/docs/cookbook/common-patterns/saga.mdx +247 -0
  42. package/docs/cookbook/common-patterns/scheduling.mdx +125 -0
  43. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
  44. package/docs/cookbook/common-patterns/timeouts.mdx +99 -0
  45. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  46. package/docs/cookbook/common-patterns/workflow-composition.mdx +118 -0
  47. package/docs/cookbook/index.mdx +38 -0
  48. package/docs/cookbook/integrations/ai-sdk.mdx +360 -0
  49. package/docs/cookbook/integrations/chat-sdk.mdx +303 -0
  50. package/docs/cookbook/integrations/meta.json +4 -0
  51. package/docs/cookbook/integrations/sandbox.mdx +516 -0
  52. package/docs/cookbook/meta.json +5 -0
  53. package/docs/deploying/world/local-world.mdx +1 -1
  54. package/docs/deploying/world/postgres-world.mdx +1 -1
  55. package/docs/deploying/world/vercel-world.mdx +1 -1
  56. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  57. package/docs/foundations/index.mdx +0 -3
  58. package/docs/foundations/meta.json +0 -1
  59. package/docs/foundations/serialization.mdx +1 -1
  60. package/docs/foundations/starting-workflows.mdx +1 -1
  61. package/docs/getting-started/index.mdx +8 -1
  62. package/docs/getting-started/meta.json +2 -1
  63. package/docs/getting-started/python.mdx +165 -0
  64. package/docs/meta.json +1 -0
  65. package/docs/migration-guides/index.mdx +34 -0
  66. package/docs/migration-guides/meta.json +9 -0
  67. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +363 -0
  68. package/docs/migration-guides/migrating-from-inngest.mdx +314 -0
  69. package/docs/migration-guides/migrating-from-temporal.mdx +318 -0
  70. package/docs/migration-guides/migrating-from-trigger-dev.mdx +337 -0
  71. package/package.json +13 -13
  72. package/docs/foundations/common-patterns.mdx +0 -265
@@ -0,0 +1,247 @@
1
+ ---
2
+ title: Transactions & Rollbacks (Saga)
3
+ description: Coordinate multi-step transactions with automatic rollback when a step fails.
4
+ type: guide
5
+ summary: Run a sequence of steps where each registers a compensation. If any step throws a FatalError, compensations execute in reverse order to restore consistency.
6
+ ---
7
+
8
+ Use the saga pattern when a business transaction spans multiple services and you need automatic rollback if any step fails. Each forward step registers a compensation, and on failure the workflow unwinds them in reverse order.
9
+
10
+ ## When to use this
11
+
12
+ - Multi-service transactions (reserve inventory, charge payment, provision access)
13
+ - Any sequence where partial completion leaves the system in an inconsistent state
14
+ - Operations that need "all or nothing" semantics across external APIs
15
+
16
+ ## How it works
17
+
18
+ 1. Each forward step does work and registers a compensation function.
19
+ 2. If any step throws `FatalError`, the catch block runs compensations in reverse (LIFO) order to restore consistency.
20
+ 3. Regular errors are retried automatically (up to 3x by default). Use `FatalError` only for permanent failures where retrying won't help.
21
+
22
+ ## Pattern
23
+
24
+ Each step returns a result and pushes a compensation handler onto a stack. If a later step throws a `FatalError`, the workflow catches it and executes compensations in LIFO order.
25
+
26
+ ```typescript
27
+ import { FatalError } from "workflow";
28
+
29
+ declare function reserveSeats(accountId: string, seats: number): Promise<string>; // @setup
30
+ declare function releaseSeats(accountId: string, reservationId: string): Promise<void>; // @setup
31
+ declare function captureInvoice(accountId: string, seats: number): Promise<string>; // @setup
32
+ declare function refundInvoice(accountId: string, invoiceId: string): Promise<void>; // @setup
33
+ declare function provisionSeats(accountId: string, seats: number): Promise<string>; // @setup
34
+ declare function deprovisionSeats(accountId: string, entitlementId: string): Promise<void>; // @setup
35
+ declare function sendConfirmation(accountId: string, invoiceId: string, entitlementId: string): Promise<void>; // @setup
36
+
37
+ export async function subscriptionUpgradeSaga(accountId: string, seats: number) {
38
+ "use workflow";
39
+
40
+ const compensations: Array<() => Promise<void>> = [];
41
+
42
+ try {
43
+ const reservationId = await reserveSeats(accountId, seats);
44
+ compensations.push(() => releaseSeats(accountId, reservationId)); // [!code highlight]
45
+
46
+ const invoiceId = await captureInvoice(accountId, seats);
47
+ compensations.push(() => refundInvoice(accountId, invoiceId)); // [!code highlight]
48
+
49
+ const entitlementId = await provisionSeats(accountId, seats);
50
+ compensations.push(() => deprovisionSeats(accountId, entitlementId)); // [!code highlight]
51
+
52
+ // No compensation — notifications are fire-and-forget
53
+ await sendConfirmation(accountId, invoiceId, entitlementId);
54
+
55
+ return { status: "completed" };
56
+ } catch (error) {
57
+ // Unwind compensations in reverse (LIFO) order
58
+ for (const compensate of compensations.reverse()) { // [!code highlight]
59
+ await compensate(); // [!code highlight]
60
+ }
61
+
62
+ return { status: "rolled_back" };
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### Step functions
68
+
69
+ Each step is a `"use step"` function with full Node.js access (fetch, fs, npm packages). Forward steps do the work and throw `FatalError` on permanent failure; compensation steps undo it and must be idempotent — safe to call multiple times if the workflow restarts mid-rollback.
70
+
71
+ ```typescript
72
+ import { FatalError } from "workflow";
73
+
74
+ // Forward steps
75
+
76
+ async function reserveSeats(accountId: string, seats: number): Promise<string> {
77
+ "use step";
78
+ const res = await fetch(`https://api.example.com/seats/reserve`, {
79
+ method: "POST",
80
+ body: JSON.stringify({ accountId, seats }),
81
+ });
82
+ if (!res.ok) throw new FatalError("Seat reservation failed"); // [!code highlight]
83
+ const { reservationId } = await res.json();
84
+ return reservationId;
85
+ }
86
+
87
+ async function captureInvoice(accountId: string, seats: number): Promise<string> {
88
+ "use step";
89
+ const res = await fetch(`https://api.example.com/invoices`, {
90
+ method: "POST",
91
+ body: JSON.stringify({ accountId, seats }),
92
+ });
93
+ if (!res.ok) throw new FatalError("Invoice capture failed"); // [!code highlight]
94
+ const { invoiceId } = await res.json();
95
+ return invoiceId;
96
+ }
97
+
98
+ async function provisionSeats(accountId: string, seats: number): Promise<string> {
99
+ "use step";
100
+ const res = await fetch(`https://api.example.com/entitlements`, {
101
+ method: "POST",
102
+ body: JSON.stringify({ accountId, seats }),
103
+ });
104
+ if (!res.ok) throw new FatalError("Provisioning failed"); // [!code highlight]
105
+ const { entitlementId } = await res.json();
106
+ return entitlementId;
107
+ }
108
+
109
+ async function sendConfirmation(
110
+ accountId: string,
111
+ invoiceId: string,
112
+ entitlementId: string
113
+ ): Promise<void> {
114
+ "use step";
115
+ await fetch(`https://api.example.com/notifications`, {
116
+ method: "POST",
117
+ body: JSON.stringify({ accountId, invoiceId, entitlementId, template: "upgrade-complete" }),
118
+ });
119
+ }
120
+
121
+ // Compensation steps — must be idempotent
122
+
123
+ async function releaseSeats(accountId: string, reservationId: string): Promise<void> {
124
+ "use step";
125
+ await fetch(`https://api.example.com/seats/release`, {
126
+ method: "POST",
127
+ body: JSON.stringify({ accountId, reservationId }),
128
+ });
129
+ }
130
+
131
+ async function refundInvoice(accountId: string, invoiceId: string): Promise<void> {
132
+ "use step";
133
+ await fetch(`https://api.example.com/invoices/${invoiceId}/refund`, {
134
+ method: "POST",
135
+ body: JSON.stringify({ accountId }),
136
+ });
137
+ }
138
+
139
+ async function deprovisionSeats(accountId: string, entitlementId: string): Promise<void> {
140
+ "use step";
141
+ await fetch(`https://api.example.com/entitlements/${entitlementId}`, {
142
+ method: "DELETE",
143
+ body: JSON.stringify({ accountId }),
144
+ });
145
+ }
146
+ ```
147
+
148
+ ### Streaming step progress (optional)
149
+
150
+ Use `getWritable()` to stream progress events to a UI so users can see each step execute in real time.
151
+
152
+ ```typescript
153
+ import { FatalError } from "workflow";
154
+ import { getWritable } from "workflow";
155
+
156
+ type SagaEvent =
157
+ | { type: "step_start"; step: string }
158
+ | { type: "step_done"; step: string; detail: string }
159
+ | { type: "step_failed"; step: string; error: string }
160
+ | { type: "compensating"; step: string }
161
+ | { type: "compensated"; step: string }
162
+ | { type: "result"; status: "completed" | "rolled_back" };
163
+
164
+ async function emit(event: SagaEvent) {
165
+ "use step";
166
+ const writer = getWritable<SagaEvent>().getWriter();
167
+ try {
168
+ await writer.write(event);
169
+ } finally {
170
+ writer.releaseLock();
171
+ }
172
+ }
173
+
174
+ declare function reserveSeats(accountId: string, seats: number): Promise<string>; // @setup
175
+ declare function releaseSeats(accountId: string, reservationId: string): Promise<void>; // @setup
176
+ declare function captureInvoice(accountId: string, seats: number): Promise<string>; // @setup
177
+ declare function refundInvoice(accountId: string, invoiceId: string): Promise<void>; // @setup
178
+ declare function provisionSeats(accountId: string, seats: number): Promise<string>; // @setup
179
+ declare function deprovisionSeats(accountId: string, entitlementId: string): Promise<void>; // @setup
180
+ declare function sendConfirmation(accountId: string, invoiceId: string, entitlementId: string): Promise<void>; // @setup
181
+
182
+ export async function subscriptionUpgradeSaga(accountId: string, seats: number) {
183
+ "use workflow";
184
+
185
+ const compensations: Array<{ name: string; execute: () => Promise<void> }> = [];
186
+
187
+ try {
188
+ await emit({ type: "step_start", step: "Reserve Seats" });
189
+ const reservationId = await reserveSeats(accountId, seats);
190
+ compensations.push({ name: "Release Seats", execute: () => releaseSeats(accountId, reservationId) });
191
+ await emit({ type: "step_done", step: "Reserve Seats", detail: reservationId });
192
+
193
+ await emit({ type: "step_start", step: "Capture Invoice" });
194
+ const invoiceId = await captureInvoice(accountId, seats);
195
+ compensations.push({ name: "Refund Invoice", execute: () => refundInvoice(accountId, invoiceId) });
196
+ await emit({ type: "step_done", step: "Capture Invoice", detail: invoiceId });
197
+
198
+ await emit({ type: "step_start", step: "Provision Seats" });
199
+ const entitlementId = await provisionSeats(accountId, seats);
200
+ compensations.push({ name: "Deprovision Seats", execute: () => deprovisionSeats(accountId, entitlementId) });
201
+ await emit({ type: "step_done", step: "Provision Seats", detail: entitlementId });
202
+
203
+ // No compensation — notifications are fire-and-forget
204
+ await emit({ type: "step_start", step: "Send Confirmation" });
205
+ await sendConfirmation(accountId, invoiceId, entitlementId);
206
+ await emit({ type: "step_done", step: "Send Confirmation", detail: "sent" });
207
+
208
+ await emit({ type: "result", status: "completed" });
209
+ return { status: "completed" };
210
+ } catch (error) {
211
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
212
+ await emit({ type: "step_failed", step: "failed", error: errorMessage });
213
+
214
+ // Unwind compensations in reverse (LIFO) order
215
+ for (const comp of compensations.reverse()) {
216
+ await emit({ type: "compensating", step: comp.name });
217
+ await comp.execute();
218
+ await emit({ type: "compensated", step: comp.name });
219
+ }
220
+
221
+ await emit({ type: "result", status: "rolled_back" });
222
+ return { status: "rolled_back" };
223
+ }
224
+ }
225
+ ```
226
+
227
+ ## Adapting to your use case
228
+
229
+ - Replace the step functions with real API calls. Each `"use step"` function has full Node.js access.
230
+ - Add or remove steps as needed — the pattern scales to any number of steps.
231
+ - Make compensations idempotent — they may be retried if the workflow restarts mid-rollback.
232
+ - The `emit()` calls and `SagaEvent` type are optional — remove them if you don't need real-time UI progress.
233
+
234
+ ## Tips
235
+
236
+ - **Use `FatalError` for permanent failures.** Regular errors trigger automatic retries (up to 3 by default). Throw `FatalError` when retrying won't help (e.g., insufficient funds, invalid input).
237
+ - **Make compensations idempotent.** If a compensation step is retried, it should produce the same result. Check whether the resource was already released before releasing it again.
238
+ - **Compensation steps are also `"use step"` functions.** This makes them durable — if the workflow restarts mid-rollback, it resumes where it left off.
239
+ - **Capture values in closures carefully.** Use block-scoped variables or copy values before pushing compensations to avoid referencing stale state.
240
+ - **Notifications don't need compensations.** Fire-and-forget steps like sending emails or Slack messages typically don't register a compensation.
241
+
242
+ ## Key APIs
243
+
244
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) -- declares the orchestrator function
245
+ - [`"use step"`](/docs/api-reference/workflow/use-step) -- declares step functions with full Node.js access
246
+ - [`FatalError`](/docs/api-reference/workflow/fatal-error) -- non-retryable error that triggers compensation
247
+ - [`getWritable()`](/docs/api-reference/workflow/get-writable) -- streams data from workflows for real-time UI updates
@@ -0,0 +1,125 @@
1
+ ---
2
+ title: Sleep, Scheduling & Timed Workflows
3
+ description: Use durable sleep to schedule actions minutes, hours, days, or weeks into the future.
4
+ type: guide
5
+ summary: Schedule future actions with durable sleep that survives cold starts, and race sleeps against hooks to let external events cancel the workflow early.
6
+ ---
7
+
8
+ Workflow's `sleep()` is durable — it survives cold starts, restarts, and deployments. Combined with `defineHook()` and `Promise.race()`, it becomes the foundation for interruptible scheduled workflows like drip campaigns, reminders, and timed sequences.
9
+
10
+ ## When to use this
11
+
12
+ - Sending emails on a schedule (drip campaigns, onboarding sequences, reminders)
13
+ - Waiting for a deadline but allowing early cancellation
14
+ - Any pattern where "do X, wait N hours, then do Y" needs to be both reliable and interruptible
15
+
16
+ ## Drip campaign with cancellation
17
+
18
+ A drip campaign sends emails at intervals, sleeping between each. Each sleep races against a cancellation hook — if an external event fires the hook (e.g. user converts, unsubscribes), the campaign stops immediately.
19
+
20
+ ```typescript
21
+ import { defineHook, sleep } from "workflow";
22
+
23
+ // Hook that any API route can fire to cancel the drip
24
+ export const cancelDrip = defineHook<{ reason?: string }>(); // [!code highlight]
25
+
26
+ async function sendEmail(email: string, template: string): Promise<void> {
27
+ "use step";
28
+ await fetch("https://api.sendgrid.com/v3/mail/send", {
29
+ method: "POST",
30
+ headers: { Authorization: `Bearer ${process.env.SENDGRID_KEY}` },
31
+ body: JSON.stringify({ to: [{ email }], template_id: template }),
32
+ });
33
+ }
34
+
35
+ export async function emailSequence(email: string) {
36
+ "use workflow";
37
+
38
+ await sendEmail(email, "welcome");
39
+
40
+ // Race durable sleep against the cancellation hook
41
+ const hook = cancelDrip.create({ token: `cancel-drip:${email}` }); // [!code highlight]
42
+ const cancelled = await Promise.race([ // [!code highlight]
43
+ sleep("2d").then(() => false), // [!code highlight]
44
+ hook.then(() => true), // [!code highlight]
45
+ ]); // [!code highlight]
46
+ if (cancelled) return { status: "cancelled", email };
47
+
48
+ await sendEmail(email, "getting-started-tips");
49
+
50
+ // Create a fresh hook for the next sleep window
51
+ const hook2 = cancelDrip.create({ token: `cancel-drip:${email}` }); // [!code highlight]
52
+ const cancelled2 = await Promise.race([ // [!code highlight]
53
+ sleep("2d").then(() => false), // [!code highlight]
54
+ hook2.then(() => true), // [!code highlight]
55
+ ]); // [!code highlight]
56
+ if (cancelled2) return { status: "cancelled", email };
57
+
58
+ await sendEmail(email, "feature-highlights");
59
+
60
+ return { status: "drip-complete", email };
61
+ }
62
+ ```
63
+
64
+ ### Cancelling from an API route
65
+
66
+ Any server-side code can fire the hook by calling `.resume()` with the same token:
67
+
68
+ ```typescript
69
+ import { cancelDrip } from "@/workflows/email-sequence";
70
+
71
+ export async function POST(req: Request) {
72
+ const { email, reason } = await req.json();
73
+
74
+ if (!email) {
75
+ return Response.json({ error: "email is required" }, { status: 400 });
76
+ }
77
+
78
+ try {
79
+ await cancelDrip.resume(`cancel-drip:${email}`, { // [!code highlight]
80
+ reason: reason ?? "User completed action", // [!code highlight]
81
+ }); // [!code highlight]
82
+ } catch (error) {
83
+ const msg = error instanceof Error ? error.message.toLowerCase() : "";
84
+ if (msg.includes("not found") || msg.includes("expired")) {
85
+ return Response.json({
86
+ success: true,
87
+ email,
88
+ note: "No active drip found (already completed or cancelled)",
89
+ });
90
+ }
91
+ throw error;
92
+ }
93
+
94
+ return Response.json({ success: true, email });
95
+ }
96
+ ```
97
+
98
+ ## How it works
99
+
100
+ 1. **Durable sleep** — `sleep("2d")` persists through restarts at zero compute cost. The workflow resumes precisely when the timer fires.
101
+ 2. **Hook creation** — `cancelDrip.create({ token })` registers a hook that resolves when any external system calls `.resume()` with the same token.
102
+ 3. **Race** — `Promise.race([sleep(...), hook])` blocks until either the timer fires or the hook is resumed, whichever comes first.
103
+ 4. **Fresh hooks per window** — after a sleep completes normally, the previous hook instance is consumed. A new `.create()` call registers a fresh hook for the next sleep window, reusing the same token.
104
+
105
+ ## Adapting to your use case
106
+
107
+ - **Change durations** — replace `"2d"` with any duration string (`"1h"`, `"7d"`, `"30m"`) or a `Date` object for absolute times.
108
+ - **Add more steps** — the pattern scales to any number of email-then-sleep pairs.
109
+ - **Snooze instead of cancel** — resolve the hook with a `snooze` payload and sleep again: `sleep(new Date(Date.now() + payload.snoozeMs))`.
110
+ - **Timeout any operation** — the same `Promise.race(sleep, work)` pattern works for adding deadlines to slow steps.
111
+ - **Real providers** — swap the `sendEmail` step body for Resend, Postmark, or any HTTP API. The `"use step"` function has full Node.js access.
112
+
113
+ ## Tips
114
+
115
+ - **`sleep()` accepts** duration strings (`"1d"`, `"2h"`, `"30s"`), milliseconds, or `Date` objects for sleeping until a specific time.
116
+ - **Durable means durable.** A `sleep("7d")` workflow costs nothing while sleeping — no compute, no memory.
117
+ - **Use `sleep()` in workflow context only.** Step functions cannot call `sleep()` directly. If a step needs a delay, use `setTimeout` inside the step.
118
+
119
+ ## Key APIs
120
+
121
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps) — marks the orchestrator function
122
+ - [`"use step"`](/docs/foundations/workflows-and-steps) — marks functions that run with full Node.js access
123
+ - [`sleep()`](/docs/api-reference/workflow/sleep) — durable wait (survives restarts, zero compute cost)
124
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook) — creates a typed hook that external systems can fire
125
+ - [`Promise.race()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) — races sleep against hooks for interruptible waits
@@ -0,0 +1,155 @@
1
+ ---
2
+ title: Sequential & Parallel Execution
3
+ description: Compose steps with familiar async/await patterns — sequential await, Promise.all, and Promise.race.
4
+ type: guide
5
+ summary: Workflows are just async functions, so all the standard composition primitives (await, Promise.all, Promise.race) apply unchanged — including racing webhooks against durable sleeps.
6
+ related:
7
+ - /docs/foundations/workflows-and-steps
8
+ - /cookbook/common-patterns/timeouts
9
+ - /cookbook/common-patterns/scheduling
10
+ ---
11
+
12
+ Workflows are written in plain async/await — there's no new control-flow API to learn. Sequential awaits chain steps that depend on each other, `Promise.all` runs independent steps in parallel, and `Promise.race` returns whichever finishes first. These compose with workflow primitives like [`sleep()`](/docs/api-reference/workflow/sleep) and [`createWebhook()`](/docs/api-reference/workflow/create-webhook) since those are also just promises.
13
+
14
+ ## When to use this
15
+
16
+ - **Pipelines** — each step depends on the previous step's output (validate → process → store)
17
+ - **Independent fan-out** — fetch multiple resources or perform multiple actions that don't depend on each other
18
+ - **Race conditions** — return as soon as one of N operations completes (timeout, first-responder, deadline)
19
+ - **Mixing primitives** — running steps, sleeps, and webhooks side-by-side in the same control-flow expression
20
+
21
+ ## Pattern
22
+
23
+ ### Sequential
24
+
25
+ The simplest way to orchestrate steps is to execute them one after another, where each step depends on the previous step's output.
26
+
27
+ ```typescript lineNumbers
28
+ declare function validateData(data: unknown): Promise<string>; // @setup
29
+ declare function processData(data: string): Promise<string>; // @setup
30
+ declare function storeData(data: string): Promise<string>; // @setup
31
+
32
+ export async function dataPipelineWorkflow(data: unknown) {
33
+ "use workflow";
34
+
35
+ const validated = await validateData(data);
36
+ const processed = await processData(validated);
37
+ const stored = await storeData(processed);
38
+
39
+ return stored;
40
+ }
41
+ ```
42
+
43
+ ### Parallel with `Promise.all`
44
+
45
+ When steps don't depend on each other, run them concurrently with `Promise.all`. The workflow waits until all of them resolve.
46
+
47
+ ```typescript lineNumbers
48
+ declare function fetchUser(userId: string): Promise<{ name: string }>; // @setup
49
+ declare function fetchOrders(userId: string): Promise<{ items: string[] }>; // @setup
50
+ declare function fetchPreferences(userId: string): Promise<{ theme: string }>; // @setup
51
+
52
+ export async function fetchUserData(userId: string) {
53
+ "use workflow";
54
+
55
+ const [user, orders, preferences] = await Promise.all([ // [!code highlight]
56
+ fetchUser(userId), // [!code highlight]
57
+ fetchOrders(userId), // [!code highlight]
58
+ fetchPreferences(userId), // [!code highlight]
59
+ ]); // [!code highlight]
60
+
61
+ return { user, orders, preferences };
62
+ }
63
+ ```
64
+
65
+ ### Race with `Promise.race`
66
+
67
+ `Promise.race` resolves as soon as the first promise settles. Since [`sleep()`](/docs/api-reference/workflow/sleep) and [`createWebhook()`](/docs/api-reference/workflow/create-webhook) return promises, they compose naturally — for example, waiting for a webhook callback with a deadline:
68
+
69
+ ```typescript lineNumbers
70
+ import { sleep, createWebhook } from "workflow";
71
+
72
+ declare function executeExternalTask(webhookUrl: string): Promise<void>; // @setup
73
+
74
+ export async function runExternalTask(userId: string) {
75
+ "use workflow";
76
+
77
+ const webhook = createWebhook();
78
+ await executeExternalTask(webhook.url);
79
+
80
+ await Promise.race([ // [!code highlight]
81
+ webhook, // [!code highlight]
82
+ sleep("1 day"), // [!code highlight]
83
+ ]); // [!code highlight]
84
+
85
+ console.log("Done");
86
+ }
87
+ ```
88
+
89
+ For racing operations against deadlines specifically (timeouts), see the dedicated [Timeouts](/cookbook/common-patterns/timeouts) recipe — it covers result discrimination, `FatalError` semantics, and the "loser keeps running" caveat.
90
+
91
+ ### Combining sequential, parallel, and durable primitives
92
+
93
+ Most real workflows combine all three. Here's a simplified version of the [birthday card generator demo](https://github.com/vercel/workflow-examples/tree/main/birthday-card-generator) — sequential card generation, parallel RSVP fan-out, non-blocking webhook collection, and a durable sleep until the birthday:
94
+
95
+ ```typescript lineNumbers
96
+ import { createWebhook, sleep, type Webhook } from "workflow";
97
+
98
+ declare function makeCardText(prompt: string): Promise<string>; // @setup
99
+ declare function makeCardImage(text: string): Promise<string>; // @setup
100
+ declare function sendRSVPEmail(friend: string, webhook: Webhook): Promise<void>; // @setup
101
+ declare function sendBirthdayCard(text: string, image: string, rsvps: unknown[], email: string): Promise<void>; // @setup
102
+
103
+ export async function birthdayWorkflow(
104
+ prompt: string,
105
+ email: string,
106
+ friends: string[],
107
+ birthday: Date
108
+ ) {
109
+ "use workflow";
110
+
111
+ const text = await makeCardText(prompt); // [!code highlight]
112
+ const image = await makeCardImage(text); // [!code highlight]
113
+
114
+ const webhooks = friends.map(() => createWebhook());
115
+
116
+ await Promise.all( // [!code highlight]
117
+ friends.map((friend, i) => sendRSVPEmail(friend, webhooks[i])) // [!code highlight]
118
+ ); // [!code highlight]
119
+
120
+ const rsvps: unknown[] = [];
121
+ webhooks.map((webhook) =>
122
+ webhook.then((req) => req.json()).then(({ rsvp }) => rsvps.push(rsvp))
123
+ );
124
+
125
+ await sleep(birthday); // [!code highlight]
126
+
127
+ await sendBirthdayCard(text, image, rsvps, email);
128
+
129
+ return { text, image, status: "Sent" };
130
+ }
131
+ ```
132
+
133
+ ## How it works
134
+
135
+ 1. **`await` is durable.** When the workflow awaits a step, the runtime persists the step's input, suspends the workflow, runs the step, and replays the workflow with the step's result on resume. The same applies to `sleep()` and `createWebhook()`.
136
+ 2. **`Promise.all` runs steps concurrently.** Each promise in the array is suspended on its own and the workflow resumes only when all have settled. Failures propagate — if any promise rejects, the whole `Promise.all` rejects.
137
+ 3. **`Promise.race` resolves on the first settle.** The losing promises keep running in the background but their results are discarded by the workflow.
138
+ 4. **All primitives are promises.** `sleep("1 day")` and `createWebhook()` return promises, so they compose with `Promise.all` / `Promise.race` exactly like steps do — this is what makes patterns like "race a webhook against a 24-hour deadline" a one-liner.
139
+
140
+ ## Adapting to your use case
141
+
142
+ - **Replace `Promise.all` with `Promise.allSettled`** when partial failures should not abort the rest. You'll get an array of `{ status, value | reason }` instead of throwing on the first rejection.
143
+ - **Bound the parallelism** — `Promise.all` over 1000 items will fan out 1000 concurrent steps. If your downstream APIs can't handle that, batch the array into chunks (see [Batching](/cookbook/common-patterns/batching)).
144
+ - **Add a deadline to any race** — pair the operation with `sleep("30s").then(() => "timeout" as const)` and check the discriminated result. See [Timeouts](/cookbook/common-patterns/timeouts).
145
+ - **Mix steps and hooks in a race** — wait for an external signal *or* a deadline *or* a step result, all in the same `Promise.race`. The first one to resolve wins.
146
+
147
+ ## Key APIs
148
+
149
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps) — marks the orchestrator function
150
+ - [`"use step"`](/docs/foundations/workflows-and-steps) — marks functions with full Node.js access
151
+ - [`sleep()`](/docs/api-reference/workflow/sleep) — durable sleep that survives restarts
152
+ - [`createWebhook()`](/docs/api-reference/workflow/create-webhook) — webhook URL the workflow can race against
153
+ - [`Promise.all()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) — wait for all promises
154
+ - [`Promise.race()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) — wait for the first to settle
155
+ - [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) — wait for all, including failures
@@ -0,0 +1,99 @@
1
+ ---
2
+ title: Timeouts
3
+ description: Add deadlines to slow operations by racing them against a durable sleep.
4
+ type: guide
5
+ summary: Use `Promise.race` with `sleep()` to bound the time any step, hook, or webhook is allowed to take — and recover gracefully when the deadline fires first.
6
+ related:
7
+ - /docs/api-reference/workflow/sleep
8
+ - /docs/foundations/hooks
9
+ - /cookbook/common-patterns/scheduling
10
+ - /cookbook/common-patterns/webhooks
11
+ ---
12
+
13
+ A common requirement is bounding how long a workflow waits for something to finish — a slow step, an external webhook, a human approval. Race the operation against a durable `sleep()` with `Promise.race()` — whichever finishes first wins, and the loser keeps running but its result is ignored.
14
+
15
+ ## When to use this
16
+
17
+ - **Slow steps** — bound the time spent waiting on third-party APIs, model calls, or expensive computation
18
+ - **External callbacks** — give webhooks a deadline so the workflow doesn't hang forever waiting for an event that may never arrive
19
+ - **Human approvals** — auto-decline or escalate when a hook isn't resumed within a window
20
+ - **Polling loops** — give an outer poll-until-ready loop an overall budget
21
+
22
+ ## Pattern
23
+
24
+ ### Timeout on a slow step
25
+
26
+ ```typescript lineNumbers
27
+ import { sleep } from "workflow";
28
+
29
+ declare function processData(data: string): Promise<string>; // @setup
30
+
31
+ export async function processWithTimeout(data: string) {
32
+ "use workflow";
33
+
34
+ const result = await Promise.race([ // [!code highlight]
35
+ processData(data), // [!code highlight]
36
+ sleep("30s").then(() => "timeout" as const), // [!code highlight]
37
+ ]); // [!code highlight]
38
+
39
+ if (result === "timeout") {
40
+ throw new Error("Processing timed out after 30 seconds");
41
+ }
42
+
43
+ return result;
44
+ }
45
+ ```
46
+
47
+ ### Timeout on a webhook
48
+
49
+ The same pattern works for any promise — including hooks and webhooks. Here a webhook waits for an external service to call back, with a hard deadline of 7 days:
50
+
51
+ ```typescript lineNumbers
52
+ import { sleep, createWebhook } from "workflow";
53
+
54
+ declare function sendApprovalRequest(requestId: string, webhookUrl: string): Promise<void>; // @setup
55
+
56
+ export async function waitForApproval(requestId: string) {
57
+ "use workflow";
58
+
59
+ const webhook = createWebhook<{ approved: boolean }>();
60
+ await sendApprovalRequest(requestId, webhook.url);
61
+
62
+ const result = await Promise.race([ // [!code highlight]
63
+ webhook.then((req) => req.json()), // [!code highlight]
64
+ sleep("7 days").then(() => ({ timedOut: true }) as const), // [!code highlight]
65
+ ]); // [!code highlight]
66
+
67
+ if ("timedOut" in result) {
68
+ throw new Error("Approval request expired after 7 days");
69
+ }
70
+
71
+ return result.approved;
72
+ }
73
+ ```
74
+
75
+ ## How it works
76
+
77
+ 1. **Durable sleep** — `sleep("30s")` persists through restarts at zero compute cost. The workflow resumes precisely when the timer fires.
78
+ 2. **Race** — `Promise.race([work, sleep(...)])` returns the value of whichever promise resolves first. The loser keeps running in the background but its result is ignored by the workflow.
79
+ 3. **Discriminated result** — tagging the sleep branch with a sentinel value (`"timeout" as const`, `{ timedOut: true }`) lets TypeScript narrow the result and pick the right branch.
80
+ 4. **Throw to fail the workflow** — inside a workflow function, throwing an `Error` exits the run with that error. Use `FatalError` inside steps; throw plain errors inside workflows.
81
+
82
+ <Callout type="warn">
83
+ **The losing operation keeps running.** `Promise.race` doesn't cancel — when the sleep wins, the underlying step (or model call, or HTTP request) continues to completion in the background. This is fine for idempotent reads but matters when the operation has side effects or costs money. For hard cancellation across processes, see [Distributed Abort Controller](/cookbook/advanced/distributed-abort-controller).
84
+ </Callout>
85
+
86
+ ## Adapting to your use case
87
+
88
+ - **Different durations** — `sleep()` accepts duration strings (`"30s"`, `"5m"`, `"7 days"`), milliseconds, or `Date` objects for absolute deadlines.
89
+ - **Soft timeout (retry)** — instead of throwing, loop and retry with a fresh `Promise.race` and a backoff.
90
+ - **Soft timeout (fallback)** — return a default value when the timer wins instead of throwing: `if (result === "timeout") return cachedFallback`.
91
+ - **Combine with cancellation** — race three promises: the operation, a deadline `sleep()`, and a cancellation hook. See the [Scheduling cookbook](/cookbook/common-patterns/scheduling) for the cancellation half of this pattern.
92
+ - **Per-step deadlines** — wrap each step in its own `Promise.race` for independent budgets, or use a single outer race for an overall workflow deadline.
93
+
94
+ ## Key APIs
95
+
96
+ - [`sleep()`](/docs/api-reference/workflow/sleep) — durable wait (survives restarts, zero compute cost)
97
+ - [`createWebhook()`](/docs/api-reference/workflow/create-webhook) — create a webhook URL the workflow can race against
98
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook) — typed hook for in-process cancellation
99
+ - [`Promise.race()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) — race operations against deadlines