workflow 5.0.0-beta.0 → 5.0.0-beta.2

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 (86) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.d.ts +1 -3
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +2 -6
  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 +4 -2
  13. package/dist/next.d.cts +1 -1
  14. package/dist/next.d.cts.map +1 -1
  15. package/dist/nitro.js +1 -1
  16. package/dist/nuxt.js +1 -1
  17. package/dist/observability.d.ts +1 -1
  18. package/dist/observability.js +2 -2
  19. package/dist/runtime.js +1 -1
  20. package/dist/stdlib.js +1 -1
  21. package/dist/sveltekit.js +1 -1
  22. package/dist/typescript-plugin.cjs +1 -1
  23. package/dist/vite.js +1 -1
  24. package/dist/workflow.js +1 -1
  25. package/docs/ai/resumable-streams.mdx +1 -1
  26. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  27. package/docs/api-reference/workflow/get-workflow-metadata.mdx +61 -0
  28. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  29. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  30. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  31. package/docs/api-reference/workflow-api/get-world.mdx +6 -6
  32. package/docs/api-reference/workflow-api/index.mdx +1 -1
  33. package/docs/api-reference/workflow-api/world/index.mdx +2 -2
  34. package/docs/api-reference/workflow-api/world/observability.mdx +1 -1
  35. package/docs/api-reference/workflow-api/world/queue.mdx +1 -1
  36. package/docs/api-reference/workflow-api/world/storage.mdx +8 -8
  37. package/docs/api-reference/workflow-api/world/streams.mdx +38 -36
  38. package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
  39. package/docs/cookbook/advanced/durable-objects.mdx +148 -0
  40. package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
  41. package/docs/cookbook/advanced/meta.json +10 -0
  42. package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
  43. package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
  44. package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
  45. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
  46. package/docs/cookbook/agent-patterns/meta.json +10 -0
  47. package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
  48. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
  49. package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
  50. package/docs/cookbook/common-patterns/batching.mdx +179 -0
  51. package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
  52. package/docs/cookbook/common-patterns/content-router.mdx +207 -0
  53. package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
  54. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  55. package/docs/cookbook/common-patterns/meta.json +15 -0
  56. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  57. package/docs/cookbook/common-patterns/saga.mdx +152 -0
  58. package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
  59. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  60. package/docs/cookbook/index.mdx +41 -0
  61. package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
  62. package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
  63. package/docs/cookbook/integrations/meta.json +4 -0
  64. package/docs/cookbook/integrations/sandbox.mdx +128 -0
  65. package/docs/cookbook/meta.json +5 -0
  66. package/docs/deploying/building-a-world.mdx +45 -43
  67. package/docs/deploying/world/local-world.mdx +1 -1
  68. package/docs/deploying/world/postgres-world.mdx +10 -5
  69. package/docs/deploying/world/vercel-world.mdx +1 -1
  70. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  71. package/docs/getting-started/index.mdx +8 -1
  72. package/docs/getting-started/meta.json +2 -1
  73. package/docs/getting-started/next.mdx +24 -0
  74. package/docs/getting-started/python.mdx +165 -0
  75. package/docs/how-it-works/code-transform.mdx +6 -5
  76. package/docs/meta.json +1 -0
  77. package/docs/migration-guides/index.mdx +34 -0
  78. package/docs/migration-guides/meta.json +9 -0
  79. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
  80. package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
  81. package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
  82. package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
  83. package/package.json +13 -14
  84. package/dist/internal/private.d.ts +0 -6
  85. package/dist/internal/private.d.ts.map +0 -1
  86. package/dist/internal/private.js +0 -6
@@ -0,0 +1,152 @@
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
+ ## Pattern
17
+
18
+ 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.
19
+
20
+ ```typescript
21
+ import { FatalError } from "workflow";
22
+
23
+ declare function reserveSeats(accountId: string, seats: number): Promise<string>; // @setup
24
+ declare function releaseSeats(accountId: string, reservationId: string): Promise<void>; // @setup
25
+ declare function captureInvoice(accountId: string, seats: number): Promise<string>; // @setup
26
+ declare function refundInvoice(accountId: string, invoiceId: string): Promise<void>; // @setup
27
+ declare function provisionSeats(accountId: string, seats: number): Promise<string>; // @setup
28
+ declare function deprovisionSeats(accountId: string, entitlementId: string): Promise<void>; // @setup
29
+ declare function sendConfirmation(accountId: string, invoiceId: string, entitlementId: string): Promise<void>; // @setup
30
+
31
+ export async function subscriptionUpgradeSaga(accountId: string, seats: number) {
32
+ "use workflow";
33
+
34
+ const compensations: Array<() => Promise<void>> = [];
35
+
36
+ try {
37
+ // Step 1: Reserve seats
38
+ const reservationId = await reserveSeats(accountId, seats);
39
+ compensations.push(() => releaseSeats(accountId, reservationId)); // [!code highlight]
40
+
41
+ // Step 2: Capture payment
42
+ const invoiceId = await captureInvoice(accountId, seats);
43
+ compensations.push(() => refundInvoice(accountId, invoiceId)); // [!code highlight]
44
+
45
+ // Step 3: Provision access
46
+ const entitlementId = await provisionSeats(accountId, seats);
47
+ compensations.push(() => deprovisionSeats(accountId, entitlementId)); // [!code highlight]
48
+
49
+ // Step 4: Notify
50
+ await sendConfirmation(accountId, invoiceId, entitlementId);
51
+ return { status: "completed" };
52
+ } catch (error) {
53
+ // Unwind compensations in reverse order
54
+ for (const compensate of compensations.reverse()) { // [!code highlight]
55
+ await compensate(); // [!code highlight]
56
+ }
57
+
58
+ return { status: "rolled_back" };
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### Step functions
64
+
65
+ Each step is a `"use step"` function with full Node.js access. Forward steps do the work; compensation steps undo it.
66
+
67
+ ```typescript
68
+ import { FatalError } from "workflow";
69
+
70
+ async function reserveSeats(accountId: string, seats: number): Promise<string> {
71
+ "use step";
72
+ const res = await fetch(`https://api.example.com/seats/reserve`, {
73
+ method: "POST",
74
+ body: JSON.stringify({ accountId, seats }),
75
+ });
76
+ if (!res.ok) throw new FatalError("Seat reservation failed"); // [!code highlight]
77
+ const { reservationId } = await res.json();
78
+ return reservationId;
79
+ }
80
+
81
+ async function releaseSeats(accountId: string, reservationId: string): Promise<void> {
82
+ "use step";
83
+ // Compensations should be idempotent — safe to call twice
84
+ await fetch(`https://api.example.com/seats/release`, {
85
+ method: "POST",
86
+ body: JSON.stringify({ accountId, reservationId }),
87
+ });
88
+ }
89
+
90
+ async function captureInvoice(accountId: string, seats: number): Promise<string> {
91
+ "use step";
92
+ const res = await fetch(`https://api.example.com/invoices`, {
93
+ method: "POST",
94
+ body: JSON.stringify({ accountId, seats }),
95
+ });
96
+ if (!res.ok) throw new FatalError("Invoice capture failed"); // [!code highlight]
97
+ const { invoiceId } = await res.json();
98
+ return invoiceId;
99
+ }
100
+
101
+ async function refundInvoice(accountId: string, invoiceId: string): Promise<void> {
102
+ "use step";
103
+ await fetch(`https://api.example.com/invoices/${invoiceId}/refund`, {
104
+ method: "POST",
105
+ body: JSON.stringify({ accountId }),
106
+ });
107
+ }
108
+
109
+ async function provisionSeats(accountId: string, seats: number): Promise<string> {
110
+ "use step";
111
+ const res = await fetch(`https://api.example.com/entitlements`, {
112
+ method: "POST",
113
+ body: JSON.stringify({ accountId, seats }),
114
+ });
115
+ if (!res.ok) throw new FatalError("Provisioning failed"); // [!code highlight]
116
+ const { entitlementId } = await res.json();
117
+ return entitlementId;
118
+ }
119
+
120
+ async function deprovisionSeats(accountId: string, entitlementId: string): Promise<void> {
121
+ "use step";
122
+ await fetch(`https://api.example.com/entitlements/${entitlementId}`, {
123
+ method: "DELETE",
124
+ body: JSON.stringify({ accountId }),
125
+ });
126
+ }
127
+
128
+ async function sendConfirmation(
129
+ accountId: string,
130
+ invoiceId: string,
131
+ entitlementId: string
132
+ ): Promise<void> {
133
+ "use step";
134
+ await fetch(`https://api.example.com/notifications`, {
135
+ method: "POST",
136
+ body: JSON.stringify({ accountId, invoiceId, entitlementId, template: "upgrade-complete" }),
137
+ });
138
+ }
139
+ ```
140
+
141
+ ## Tips
142
+
143
+ - **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).
144
+ - **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.
145
+ - **Compensation steps are also `"use step"` functions.** This makes them durable — if the workflow restarts mid-rollback, it resumes where it left off.
146
+ - **Capture values in closures carefully.** Use block-scoped variables or copy values before pushing compensations to avoid referencing stale state.
147
+
148
+ ## Key APIs
149
+
150
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) -- declares the orchestrator function
151
+ - [`"use step"`](/docs/api-reference/workflow/use-step) -- declares step functions with full Node.js access
152
+ - [`FatalError`](/docs/api-reference/workflow/fatal-error) -- non-retryable error that triggers compensation
@@ -0,0 +1,249 @@
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 wake the workflow early.
6
+ ---
7
+
8
+ Workflow's `sleep()` is durable -- it survives cold starts, restarts, and deployments. This makes it the foundation for scheduled actions, drip campaigns, reminders, and any pattern that needs to wait for real-world time to pass.
9
+
10
+ ## When to use this
11
+
12
+ - Sending emails on a schedule (drip campaigns, reminders, digests)
13
+ - Waiting for a deadline before taking action
14
+ - Any pattern where "do X, wait N hours, then do Y" needs to be reliable
15
+
16
+ ## Pattern: Drip campaign
17
+
18
+ Send emails at scheduled intervals using `sleep()` between steps. The workflow runs for days or weeks, sleeping between each email.
19
+
20
+ ```typescript
21
+ import { sleep } from "workflow";
22
+
23
+ export async function onboardingDrip(email: string) {
24
+ "use workflow";
25
+
26
+ await sendEmail(email, "welcome");
27
+
28
+ await sleep("1d"); // [!code highlight]
29
+ await sendEmail(email, "getting-started-tips");
30
+
31
+ await sleep("2d"); // [!code highlight]
32
+ await sendEmail(email, "feature-highlights");
33
+
34
+ await sleep("4d"); // [!code highlight]
35
+ await sendEmail(email, "follow-up");
36
+
37
+ return { email, status: "completed", totalDays: 7 };
38
+ }
39
+
40
+ async function sendEmail(email: string, template: string): Promise<void> {
41
+ "use step";
42
+ await fetch("https://api.sendgrid.com/v3/mail/send", {
43
+ method: "POST",
44
+ headers: { Authorization: `Bearer ${process.env.SENDGRID_KEY}` },
45
+ body: JSON.stringify({
46
+ to: [{ email }],
47
+ template_id: template,
48
+ }),
49
+ });
50
+ }
51
+ ```
52
+
53
+ ## Pattern: Interruptible reminder (sleep vs hook)
54
+
55
+ Race a `sleep()` against a `defineHook` so external events can cancel, snooze, or send early:
56
+
57
+ ```typescript
58
+ import { defineHook, sleep } from "workflow";
59
+
60
+ type ReminderAction =
61
+ | { type: "cancel" }
62
+ | { type: "send_now" }
63
+ | { type: "snooze"; seconds: number };
64
+
65
+ export const reminderActionHook = defineHook<ReminderAction>();
66
+
67
+ export async function scheduleReminder(userId: string, delayMs: number) {
68
+ "use workflow";
69
+
70
+ let sendAt = new Date(Date.now() + delayMs);
71
+ const action = reminderActionHook.create({ token: `reminder:${userId}` });
72
+
73
+ const outcome = await Promise.race([ // [!code highlight]
74
+ sleep(sendAt).then(() => ({ kind: "time" as const })), // [!code highlight]
75
+ action.then((payload) => ({ kind: "action" as const, payload })), // [!code highlight]
76
+ ]);
77
+
78
+ if (outcome.kind === "action") {
79
+ if (outcome.payload.type === "cancel") {
80
+ return { userId, status: "cancelled" };
81
+ }
82
+ if (outcome.payload.type === "snooze") {
83
+ sendAt = new Date(Date.now() + outcome.payload.seconds * 1000);
84
+ await sleep(sendAt);
85
+ }
86
+ // "send_now" falls through to send immediately
87
+ }
88
+
89
+ await sendReminderEmail(userId);
90
+ return { userId, status: "sent" };
91
+ }
92
+
93
+ async function sendReminderEmail(userId: string): Promise<void> {
94
+ "use step";
95
+ await fetch("https://api.example.com/reminders/send", {
96
+ method: "POST",
97
+ body: JSON.stringify({ userId }),
98
+ });
99
+ }
100
+ ```
101
+
102
+ To wake the reminder early from an API route:
103
+
104
+ ```typescript
105
+ import { resumeHook } from "workflow/api";
106
+
107
+ // POST /api/reminder/cancel
108
+ export async function POST(request: Request) {
109
+ const { userId } = await request.json();
110
+ await resumeHook(`reminder:${userId}`, { type: "cancel" }); // [!code highlight]
111
+ return Response.json({ ok: true });
112
+ }
113
+ ```
114
+
115
+ ## Pattern: Timed collection window (digest)
116
+
117
+ Open a collection window using `sleep()` and accumulate events from a hook until the window closes:
118
+
119
+ ```typescript
120
+ import { sleep, defineHook } from "workflow";
121
+
122
+ type EventPayload = { type: string; message: string };
123
+
124
+ export const digestEvent = defineHook<EventPayload>();
125
+
126
+ export async function collectAndSendDigest(
127
+ digestId: string,
128
+ userId: string,
129
+ windowMs: number = 3_600_000
130
+ ) {
131
+ "use workflow";
132
+
133
+ const hook = digestEvent.create({ token: `digest:${digestId}` });
134
+ const windowClosed = sleep(`${windowMs}ms`).then(() => ({
135
+ kind: "window_closed" as const,
136
+ }));
137
+ const events: EventPayload[] = [];
138
+
139
+ while (true) {
140
+ const outcome = await Promise.race([ // [!code highlight]
141
+ hook.then((payload) => ({ kind: "event" as const, payload })),
142
+ windowClosed,
143
+ ]);
144
+
145
+ if (outcome.kind === "window_closed") break; // [!code highlight]
146
+ events.push(outcome.payload);
147
+ }
148
+
149
+ if (events.length > 0) {
150
+ await sendDigestEmail(userId, events);
151
+ }
152
+
153
+ return { digestId, status: events.length > 0 ? "sent" : "empty", eventCount: events.length };
154
+ }
155
+
156
+ async function sendDigestEmail(userId: string, events: EventPayload[]): Promise<void> {
157
+ "use step";
158
+ await fetch("https://api.example.com/digest/send", {
159
+ method: "POST",
160
+ body: JSON.stringify({ userId, events }),
161
+ });
162
+ }
163
+ ```
164
+
165
+ ## Pattern: Timeout
166
+
167
+ Add a timeout to any operation by racing it against `sleep()`:
168
+
169
+ ```typescript
170
+ import { sleep, FatalError } from "workflow";
171
+
172
+ export async function processWithTimeout(jobId: string) {
173
+ "use workflow";
174
+
175
+ const result = await Promise.race([ // [!code highlight]
176
+ processData(jobId),
177
+ sleep("30s").then(() => "timeout" as const), // [!code highlight]
178
+ ]);
179
+
180
+ if (result === "timeout") {
181
+ return { jobId, status: "timed_out" };
182
+ }
183
+
184
+ return { jobId, status: "completed", result };
185
+ }
186
+
187
+ async function processData(jobId: string): Promise<string> {
188
+ "use step";
189
+ // Long-running computation
190
+ return `result-for-${jobId}`;
191
+ }
192
+ ```
193
+
194
+ ## Polling external services
195
+
196
+ When you need to poll an external service until a job completes, define your own `sleep` as a step function and use it in a polling loop. Each iteration becomes a separate step in the event log, making the entire loop durable.
197
+
198
+ ```typescript
199
+ async function sleep(ms: number): Promise<void> {
200
+ "use step";
201
+ await new Promise(resolve => setTimeout(resolve, ms));
202
+ }
203
+
204
+ export async function waitForTranscription(jobId: string) {
205
+ "use workflow";
206
+
207
+ let status = "processing";
208
+ let attempts = 0;
209
+ const maxAttempts = 36; // ~3 minutes at 5s intervals
210
+
211
+ while (status === "processing" && attempts < maxAttempts) {
212
+ await sleep(5000); // [!code highlight]
213
+ attempts++;
214
+ const result = await checkJobStatus(jobId); // [!code highlight]
215
+ status = result.status;
216
+ }
217
+
218
+ if (status !== "completed") {
219
+ return { jobId, status: "timed_out", attempts };
220
+ }
221
+
222
+ return { jobId, status: "completed", attempts };
223
+ }
224
+
225
+ async function checkJobStatus(jobId: string): Promise<{ status: string }> {
226
+ "use step";
227
+ const res = await fetch(`https://api.example.com/jobs/${jobId}`);
228
+ return res.json();
229
+ }
230
+ ```
231
+
232
+ **When to use this vs `sleep()` from `workflow`:**
233
+ - Use `sleep()` from `workflow` for fixed, known delays (drip campaigns, reminders, cooldowns).
234
+ - Use a custom sleep-as-step for polling loops where you need to check a condition between sleeps. The custom step version also works in libraries that don't want to import from the `workflow` module directly.
235
+
236
+ ## Tips
237
+
238
+ - **`sleep()` accepts** duration strings (`"1d"`, `"2h"`, `"30s"`), milliseconds, or `Date` objects for sleeping until a specific time.
239
+ - **Durable means durable.** A `sleep("7d")` workflow costs nothing while sleeping -- no compute, no memory. It resumes precisely when the timer fires.
240
+ - **Race `sleep` against `defineHook`** for interruptible waits. This is the standard pattern for reminders, approvals with deadlines, and timed collection windows.
241
+ - **Use `sleep()` in workflow context only.** Step functions cannot call `sleep()` directly. If a step needs a delay, use a standard `setTimeout` or return control to the workflow.
242
+
243
+ ## Key APIs
244
+
245
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
246
+ - [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions that run with full Node.js access
247
+ - [`sleep()`](/docs/api-reference/workflow/sleep) -- durable wait (survives restarts, zero compute cost while sleeping)
248
+ - [`defineHook`](/docs/api-reference/workflow/define-hook) -- creates a hook that external systems can trigger
249
+ - [`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,185 @@
1
+ ---
2
+ title: Webhooks & External Callbacks
3
+ description: Receive HTTP callbacks from external services, process them durably, and respond inline.
4
+ type: guide
5
+ summary: Create webhook endpoints that your workflow can await, process incoming requests in steps, and respond to the caller — all within durable workflow context.
6
+ ---
7
+
8
+ Use webhooks when external services push events to your application via HTTP callbacks. The workflow creates a webhook URL, suspends with zero compute cost, and resumes when a request arrives.
9
+
10
+ ## When to use this
11
+
12
+ - Accepting callbacks from payment processors (Stripe, PayPal)
13
+ - Waiting for third-party verification or processing results
14
+ - Any integration where an external system calls you back asynchronously
15
+
16
+ ## Pattern: Processing webhook events
17
+
18
+ Create a webhook with manual response control, then iterate over incoming requests:
19
+
20
+ ```typescript
21
+ import { createWebhook, type RequestWithResponse } from "workflow";
22
+
23
+ declare function processEvent(request: RequestWithResponse): Promise<{ type: string }>; // @setup
24
+
25
+ export async function paymentWebhook(orderId: string) {
26
+ "use workflow";
27
+
28
+ const webhook = createWebhook({ respondWith: "manual" }); // [!code highlight]
29
+ // webhook.url is the URL to give to the external service
30
+
31
+ const ledger: { type: string }[] = [];
32
+
33
+ for await (const request of webhook) { // [!code highlight]
34
+ const entry = await processEvent(request);
35
+ ledger.push(entry);
36
+
37
+ // Break when we've received a terminal event
38
+ if (entry.type === "payment.succeeded" || entry.type === "refund.created") {
39
+ break;
40
+ }
41
+ }
42
+
43
+ return { orderId, webhookUrl: webhook.url, ledger, status: "settled" };
44
+ }
45
+ ```
46
+
47
+ ### Step function for processing
48
+
49
+ Each webhook request is processed in its own step, giving you full Node.js access for validation, database writes, and responding to the caller:
50
+
51
+ ```typescript
52
+ import { type RequestWithResponse } from "workflow";
53
+
54
+ async function processEvent(
55
+ request: RequestWithResponse
56
+ ): Promise<{ type: string }> {
57
+ "use step";
58
+
59
+ const body = await request.json().catch(() => ({}));
60
+ const type = body?.type ?? "unknown";
61
+
62
+ // Validate, process, and respond inline
63
+ if (type === "payment.succeeded") {
64
+ // Record the payment in your database
65
+ await request.respondWith(Response.json({ ack: true, action: "captured" })); // [!code highlight]
66
+ } else if (type === "payment.failed") {
67
+ await request.respondWith(Response.json({ ack: true, action: "flagged" }));
68
+ } else {
69
+ await request.respondWith(Response.json({ ack: true, action: "ignored" }));
70
+ }
71
+
72
+ return { type };
73
+ }
74
+ ```
75
+
76
+ ## Pattern: Async request-reply with timeout
77
+
78
+ Submit a request to an external service, pass it your webhook URL, then race the callback against a deadline:
79
+
80
+ ```typescript
81
+ import { createWebhook, sleep, FatalError, type RequestWithResponse } from "workflow";
82
+
83
+ export async function asyncVerification(documentId: string) {
84
+ "use workflow";
85
+
86
+ const webhook = createWebhook({ respondWith: "manual" });
87
+
88
+ // Submit to vendor, passing our webhook URL for the callback
89
+ await submitToVendor(documentId, webhook.url);
90
+
91
+ // Race: wait for callback OR timeout after 30 seconds
92
+ const result = await Promise.race([ // [!code highlight]
93
+ (async () => {
94
+ for await (const request of webhook) {
95
+ const body = await processCallback(request);
96
+ return body;
97
+ }
98
+ throw new FatalError("Webhook closed without callback");
99
+ })(),
100
+ sleep("30s").then(() => ({ status: "timed_out" as const })), // [!code highlight]
101
+ ]);
102
+
103
+ return { documentId, ...result };
104
+ }
105
+
106
+ async function submitToVendor(documentId: string, callbackUrl: string): Promise<void> {
107
+ "use step";
108
+ await fetch("https://vendor.example.com/verify", {
109
+ method: "POST",
110
+ body: JSON.stringify({ documentId, callbackUrl }),
111
+ });
112
+ }
113
+
114
+ async function processCallback(
115
+ request: RequestWithResponse
116
+ ): Promise<{ status: string; details: string }> {
117
+ "use step";
118
+ const body = await request.json();
119
+ await request.respondWith(Response.json({ ack: true }));
120
+ return {
121
+ status: body.approved ? "verified" : "rejected",
122
+ details: body.details ?? body.reason ?? "",
123
+ };
124
+ }
125
+ ```
126
+
127
+ ## Pattern: Large payload by reference
128
+
129
+ When payloads are too large to serialize into the event log, pass a lightweight reference (a "claim check") instead. Use a hook to signal when the data is ready:
130
+
131
+ ```typescript
132
+ import { defineHook } from "workflow";
133
+
134
+ export const blobReady = defineHook<{ blobToken: string }>(); // [!code highlight]
135
+
136
+ export async function importLargeFile(importId: string) {
137
+ "use workflow";
138
+
139
+ // Suspend until the external system signals the blob is uploaded
140
+ const { blobToken } = await blobReady.create({ token: `upload:${importId}` }); // [!code highlight]
141
+
142
+ // Process by reference -- the full payload never enters the event log
143
+ await processBlob(blobToken);
144
+
145
+ return { importId, blobToken, status: "indexed" };
146
+ }
147
+
148
+ async function processBlob(blobToken: string): Promise<void> {
149
+ "use step";
150
+ // Fetch the blob using the token, process it
151
+ const res = await fetch(`https://storage.example.com/blobs/${blobToken}`);
152
+ const data = await res.arrayBuffer();
153
+ // Index, transform, or store the data
154
+ }
155
+ ```
156
+
157
+ Resume from an API route when the upload completes:
158
+
159
+ ```typescript
160
+ import { resumeHook } from "workflow/api";
161
+
162
+ // POST /api/upload-complete
163
+ export async function POST(request: Request) {
164
+ const { importId, blobToken } = await request.json();
165
+ await resumeHook(`upload:${importId}`, { blobToken }); // [!code highlight]
166
+ return Response.json({ ok: true });
167
+ }
168
+ ```
169
+
170
+ ## Tips
171
+
172
+ - **`respondWith: "manual"`** gives you control over the HTTP response from inside a step. Use this when you need to validate the request before responding.
173
+ - **`for await` on a webhook** lets you process multiple events from the same URL. Use `break` to stop listening after a terminal event.
174
+ - **Webhooks auto-generate URLs** at `/.well-known/workflow/v1/webhook/:token`. Pass this URL to external services.
175
+ - **Race webhooks against `sleep()`** for deadlines. If the callback doesn't arrive in time, the workflow can take a fallback action.
176
+ - **For large payloads**, use a hook + reference token instead of passing the data through the workflow. The event log serializes all step inputs/outputs, so large payloads hurt performance.
177
+
178
+ ## Key APIs
179
+
180
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
181
+ - [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions with full Node.js access
182
+ - [`createWebhook()`](/docs/api-reference/workflow/create-webhook) -- creates an HTTP endpoint the workflow can await
183
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook) -- creates a typed hook for signal-based patterns
184
+ - [`sleep()`](/docs/api-reference/workflow/sleep) -- durable timer for deadlines
185
+ - [`FatalError`](/docs/api-reference/workflow/fatal-error) -- prevents retry on permanent failures
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: Cookbook
3
+ description: Best-practice workflow patterns with copy-paste code examples.
4
+ type: overview
5
+ ---
6
+
7
+ A curated collection of workflow patterns with clean, copy-paste code examples for real use cases.
8
+
9
+ ## Common Patterns
10
+
11
+ - [**Saga**](/cookbook/common-patterns/saga) — Coordinate multi-step transactions with automatic rollback when a step fails
12
+ - [**Batching**](/cookbook/common-patterns/batching) — Process large collections in parallel batches with failure isolation
13
+ - [**Rate Limiting**](/cookbook/common-patterns/rate-limiting) — Handle 429 responses and transient failures with RetryableError and backoff
14
+ - [**Fan-Out**](/cookbook/common-patterns/fan-out) — Send to multiple channels in parallel with independent failure handling
15
+ - [**Scheduling**](/cookbook/common-patterns/scheduling) — Use durable sleep to schedule actions minutes, hours, or weeks ahead
16
+ - [**Idempotency**](/cookbook/common-patterns/idempotency) — Ensure side effects happen exactly once, even when steps retry
17
+ - [**Webhooks**](/cookbook/common-patterns/webhooks) — Receive HTTP callbacks from external services and process them durably
18
+ - [**Conditional Routing**](/cookbook/common-patterns/content-router) — Route payloads to different step handlers based on content
19
+ - [**Child Workflows**](/cookbook/common-patterns/child-workflows) — Spawn and orchestrate child workflows from a parent
20
+
21
+ ## Agent Patterns
22
+
23
+ - [**Durable Agent**](/cookbook/agent-patterns/durable-agent) — Replace a stateless AI agent with one that survives crashes and retries tool calls
24
+ - [**Tool Streaming**](/cookbook/agent-patterns/tool-streaming) — Stream real-time progress updates from tools to the UI
25
+ - [**Human-in-the-Loop**](/cookbook/agent-patterns/human-in-the-loop) — Pause an agent for human approval, then resume based on the decision
26
+ - [**Tool Orchestration**](/cookbook/agent-patterns/tool-orchestration) — Choose between step-level and workflow-level tools, or combine both
27
+ - [**Stop Workflow**](/cookbook/agent-patterns/stop-workflow) — Gracefully cancel a running agent workflow using a hook signal
28
+
29
+ ## Integrations
30
+
31
+ - [**AI SDK**](/cookbook/integrations/ai-sdk) — Use AI SDK model providers, tool calling, and streaming inside durable workflows
32
+ - [**Chat SDK**](/cookbook/integrations/chat-sdk) — Build durable chat sessions with workflow persistence and AI SDK chat primitives
33
+ - [**Sandbox**](/cookbook/integrations/sandbox) — Orchestrate Vercel Sandbox lifecycle inside durable workflows
34
+
35
+ ## Advanced
36
+
37
+ - [**Serializable Steps**](/cookbook/advanced/serializable-steps) — Wrap non-serializable objects so they cross the workflow boundary
38
+ - [**Durable Objects**](/cookbook/advanced/durable-objects) — Model long-lived stateful entities as workflows
39
+ - [**Isomorphic Packages**](/cookbook/advanced/isomorphic-packages) — Publish packages that work inside and outside the workflow runtime
40
+ - [**Custom Serialization**](/cookbook/advanced/custom-serialization) — Make custom classes survive workflow serialization
41
+ - [**Publishing Libraries**](/cookbook/advanced/publishing-libraries) — Ship npm packages that export reusable workflow functions