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.
- package/README.md +4 -4
- package/dist/api-workflow.d.ts +1 -3
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +2 -6
- package/dist/api.js +1 -1
- package/dist/astro.js +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/builtins.js +1 -1
- package/dist/internal/class-serialization.js +1 -1
- package/dist/internal/errors.js +1 -1
- package/dist/nest.js +1 -1
- package/dist/next.cjs +4 -2
- package/dist/next.d.cts +1 -1
- package/dist/next.d.cts.map +1 -1
- package/dist/nitro.js +1 -1
- package/dist/nuxt.js +1 -1
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/dist/runtime.js +1 -1
- package/dist/stdlib.js +1 -1
- package/dist/sveltekit.js +1 -1
- package/dist/typescript-plugin.cjs +1 -1
- package/dist/vite.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/ai/resumable-streams.mdx +1 -1
- package/docs/api-reference/workflow/create-webhook.mdx +37 -18
- package/docs/api-reference/workflow/get-workflow-metadata.mdx +61 -0
- package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
- package/docs/api-reference/workflow-ai/index.mdx +0 -5
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
- package/docs/api-reference/workflow-api/get-world.mdx +6 -6
- package/docs/api-reference/workflow-api/index.mdx +1 -1
- package/docs/api-reference/workflow-api/world/index.mdx +2 -2
- package/docs/api-reference/workflow-api/world/observability.mdx +1 -1
- package/docs/api-reference/workflow-api/world/queue.mdx +1 -1
- package/docs/api-reference/workflow-api/world/storage.mdx +8 -8
- package/docs/api-reference/workflow-api/world/streams.mdx +38 -36
- package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
- package/docs/cookbook/advanced/durable-objects.mdx +148 -0
- package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
- package/docs/cookbook/advanced/meta.json +10 -0
- package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
- package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
- package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
- package/docs/cookbook/agent-patterns/meta.json +10 -0
- package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
- package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
- package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
- package/docs/cookbook/common-patterns/batching.mdx +179 -0
- package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
- package/docs/cookbook/common-patterns/content-router.mdx +207 -0
- package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
- package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
- package/docs/cookbook/common-patterns/meta.json +15 -0
- package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
- package/docs/cookbook/common-patterns/saga.mdx +152 -0
- package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
- package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
- package/docs/cookbook/index.mdx +41 -0
- package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
- package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
- package/docs/cookbook/integrations/meta.json +4 -0
- package/docs/cookbook/integrations/sandbox.mdx +128 -0
- package/docs/cookbook/meta.json +5 -0
- package/docs/deploying/building-a-world.mdx +45 -43
- package/docs/deploying/world/local-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +10 -5
- package/docs/deploying/world/vercel-world.mdx +1 -1
- package/docs/errors/start-invalid-workflow-function.mdx +1 -1
- package/docs/getting-started/index.mdx +8 -1
- package/docs/getting-started/meta.json +2 -1
- package/docs/getting-started/next.mdx +24 -0
- package/docs/getting-started/python.mdx +165 -0
- package/docs/how-it-works/code-transform.mdx +6 -5
- package/docs/meta.json +1 -0
- package/docs/migration-guides/index.mdx +34 -0
- package/docs/migration-guides/meta.json +9 -0
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
- package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
- package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
- package/package.json +13 -14
- package/dist/internal/private.d.ts +0 -6
- package/dist/internal/private.d.ts.map +0 -1
- 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
|