workflow 5.0.0-beta.1 → 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.js +1 -1
- 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 +1 -1
- package/dist/nitro.js +1 -1
- package/dist/nuxt.js +1 -1
- package/dist/observability.js +1 -1
- 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 +34 -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/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/world/local-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +1 -1
- 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/python.mdx +165 -0
- 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 -13
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Migrating from AWS Step Functions
|
|
3
|
+
description: Move an AWS Step Functions state machine to the Workflow SDK by replacing JSON state definitions, Task states, Choice/Wait/Parallel states, Retry/Catch blocks, and .waitForTaskToken callbacks with Workflows, Steps, Hooks, and idiomatic TypeScript control flow.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Translate an AWS Step Functions state machine into the Workflow SDK with side-by-side code examples.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/getting-started/next
|
|
8
|
+
- /docs/foundations/workflows-and-steps
|
|
9
|
+
related:
|
|
10
|
+
- /docs/foundations/starting-workflows
|
|
11
|
+
- /docs/foundations/errors-and-retries
|
|
12
|
+
- /docs/foundations/hooks
|
|
13
|
+
- /docs/foundations/streaming
|
|
14
|
+
- /docs/deploying/world/vercel-world
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
Move an AWS Step Functions state machine to the Workflow SDK by replacing JSON state definitions with TypeScript functions. This guide shows the direct mapping between ASL states and Workflow SDK primitives.
|
|
18
|
+
|
|
19
|
+
<Callout type="info">
|
|
20
|
+
Install the Workflow SDK migration skill:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx skills add https://github.com/vercel/workflow --skill migrating-to-workflow-sdk
|
|
24
|
+
```
|
|
25
|
+
</Callout>
|
|
26
|
+
|
|
27
|
+
## Why migrate to the Workflow SDK
|
|
28
|
+
|
|
29
|
+
- Orchestration code is TypeScript, not JSON ASL. Transitions are `await`, branches are `if`/`switch`, and parallelism is `Promise.all`.
|
|
30
|
+
- Streaming is built in. Write durable progress from steps with `getWritable()` and named streams. No DynamoDB or SNS glue to surface status to clients.
|
|
31
|
+
- Infrastructure lives in one deployment. No separate state machine, per-task Lambda, IAM role wiring, or callback SQS queues.
|
|
32
|
+
- Error handling is TypeScript-native: step-level retries, `RetryableError`, and `FatalError` replace per-state Retry/Catch blocks.
|
|
33
|
+
- Agent-first tooling: the `npx workflow` CLI, `@workflow/ai` integration, and the Claude skill are available out of the box.
|
|
34
|
+
|
|
35
|
+
## What changes when you leave Step Functions?
|
|
36
|
+
|
|
37
|
+
AWS Step Functions defines workflows as JSON state machines using Amazon States Language (ASL). Each state (Task, Choice, Wait, Parallel, Map) is a node in a declarative graph. Lambda functions handle tasks, Retry/Catch blocks configure per-state error handling, and `.waitForTaskToken` manages callbacks.
|
|
38
|
+
|
|
39
|
+
The Workflow SDK replaces that JSON DSL with TypeScript. `"use workflow"` functions orchestrate `"use step"` functions in the same file. Branching is `if`/`else`. Waiting is `sleep()`. Parallelism is `Promise.all()`. Retries move down to the step level.
|
|
40
|
+
|
|
41
|
+
The migration replaces declarative configuration with idiomatic TypeScript and collapses the orchestrator and compute split. Business logic stays the same.
|
|
42
|
+
|
|
43
|
+
## Concept mapping
|
|
44
|
+
|
|
45
|
+
| AWS Step Functions | Workflow SDK | Migration note |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| State machine (ASL JSON) | `"use workflow"` function | The workflow function is the state machine. |
|
|
48
|
+
| Task state / Lambda | `"use step"` function | Side effects go in steps. No separate Lambda. |
|
|
49
|
+
| Choice state | `if` / `else` / `switch` | Native TypeScript control flow. |
|
|
50
|
+
| Wait state | `sleep()` | Import `sleep` from `workflow`. |
|
|
51
|
+
| Parallel state | `Promise.all()` | Standard concurrency primitives. |
|
|
52
|
+
| Map state | Loop + `Promise.all()` or child workflows | Iterate with `for`/`map`. |
|
|
53
|
+
| Retry / Catch | Step retries, `RetryableError`, `FatalError` | Retry logic moves to step boundaries. |
|
|
54
|
+
| `.waitForTaskToken` | `createHook()` or `createWebhook()` | Hooks for typed signals; webhooks for HTTP. |
|
|
55
|
+
| Child state machine (`StartExecution`) | `"use step"` around `start()` / `getRun()` | Return the `Run` object, await its result from another step. |
|
|
56
|
+
| Execution event history | Workflow event log | Same durable replay model. |
|
|
57
|
+
| Progress via DynamoDB / SNS for client polling | `getWritable()` + named streams | Stream durable updates; clients read from the stream. |
|
|
58
|
+
|
|
59
|
+
<Callout type="info">
|
|
60
|
+
`.waitForTaskToken` becomes `createHook()` or `createWebhook()`. Choice states become `if`/`else`. Map states become `Promise.all()`. Retry policies move from per-state configuration to step-level defaults.
|
|
61
|
+
</Callout>
|
|
62
|
+
|
|
63
|
+
## Translate your first workflow
|
|
64
|
+
|
|
65
|
+
Start with a single Task state. In ASL, even "call one Lambda" requires a state machine shell:
|
|
66
|
+
|
|
67
|
+
```json title="stateMachine.asl.json (Step Functions)"
|
|
68
|
+
"LoadOrder": {
|
|
69
|
+
"Type": "Task",
|
|
70
|
+
"Resource": "arn:aws:states:::lambda:invoke",
|
|
71
|
+
"Parameters": { "FunctionName": "loadOrder", "Payload.$": "$" },
|
|
72
|
+
"End": true
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```typescript title="workflow/workflows/order.ts (Workflow SDK)"
|
|
77
|
+
export async function processOrder(orderId: string) {
|
|
78
|
+
'use workflow'; // [!code highlight]
|
|
79
|
+
return await loadOrder(orderId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function loadOrder(orderId: string) {
|
|
83
|
+
'use step'; // [!code highlight]
|
|
84
|
+
const res = await fetch(`https://example.com/api/orders/${orderId}`);
|
|
85
|
+
return res.json() as Promise<{ id: string }>;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
What changed: the ASL state machine and its Lambda collapse into two directive-tagged functions in one file.
|
|
90
|
+
|
|
91
|
+
### Adding a second step
|
|
92
|
+
|
|
93
|
+
In ASL, a second Task means a new state and a `"Next"` transition. In the Workflow SDK, it's another `await`:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
export async function processOrder(orderId: string) {
|
|
97
|
+
'use workflow';
|
|
98
|
+
const order = await loadOrder(orderId);
|
|
99
|
+
await reserveInventory(order.id); // [!code highlight]
|
|
100
|
+
return { orderId: order.id, status: 'reserved' };
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`await` replaces `"Next"`. Each new step is a new function with `"use step"`; no additional deployment.
|
|
105
|
+
|
|
106
|
+
### Starting from an API route
|
|
107
|
+
|
|
108
|
+
Step Functions starts a run via `StartExecution` (AWS SDK or API Gateway integration). The Workflow SDK starts a run with `start()` from a route handler:
|
|
109
|
+
|
|
110
|
+
```typescript title="app/api/orders/route.ts"
|
|
111
|
+
import { start } from 'workflow/api';
|
|
112
|
+
import { processOrder } from '@/workflows/order';
|
|
113
|
+
|
|
114
|
+
export async function POST(request: Request) {
|
|
115
|
+
const { orderId } = (await request.json()) as { orderId: string };
|
|
116
|
+
const run = await start(processOrder, [orderId]); // [!code highlight]
|
|
117
|
+
return Response.json({ runId: run.runId });
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Wait for an external signal
|
|
122
|
+
|
|
123
|
+
The minimal ASL for a callback is a Task with `.waitForTaskToken`:
|
|
124
|
+
|
|
125
|
+
```json title="approval.asl.json (Step Functions)"
|
|
126
|
+
"WaitForApproval": {
|
|
127
|
+
"Type": "Task",
|
|
128
|
+
"Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken",
|
|
129
|
+
"Parameters": {
|
|
130
|
+
"QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/approvals",
|
|
131
|
+
"MessageBody": {
|
|
132
|
+
"refundId.$": "$.refundId",
|
|
133
|
+
"TaskToken.$": "$$.Task.Token"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"End": true
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```typescript title="workflow/workflows/refund.ts (Workflow SDK)"
|
|
141
|
+
import { createHook } from 'workflow';
|
|
142
|
+
|
|
143
|
+
export async function refundWorkflow(refundId: string) {
|
|
144
|
+
'use workflow';
|
|
145
|
+
using approval = createHook<{ approved: boolean }>({ // [!code highlight]
|
|
146
|
+
token: `refund:${refundId}:approval`,
|
|
147
|
+
});
|
|
148
|
+
return await approval;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
What changed: no SQS queue, no task token, no callback Lambda. The hook suspends the workflow durably until it is resumed.
|
|
153
|
+
|
|
154
|
+
### Resuming the hook
|
|
155
|
+
|
|
156
|
+
Step Functions resumes by calling `SendTaskSuccess` with the task token. The Workflow SDK resumes by calling `resumeHook` with the hook's token:
|
|
157
|
+
|
|
158
|
+
```typescript title="app/api/refunds/[refundId]/approve/route.ts"
|
|
159
|
+
import { resumeHook } from 'workflow/api';
|
|
160
|
+
|
|
161
|
+
export async function POST(req: Request, { params }: { params: Promise<{ refundId: string }> }) {
|
|
162
|
+
const { refundId } = await params;
|
|
163
|
+
const { approved } = (await req.json()) as { approved: boolean };
|
|
164
|
+
await resumeHook(`refund:${refundId}:approval`, { approved }); // [!code highlight]
|
|
165
|
+
return Response.json({ ok: true });
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Branching on the result
|
|
170
|
+
|
|
171
|
+
In ASL, branching after the wait requires a Choice state. In TypeScript, it's just `if`/`else`:
|
|
172
|
+
|
|
173
|
+
```json title="approval.asl.json (Step Functions)"
|
|
174
|
+
"CheckApproval": {
|
|
175
|
+
"Type": "Choice",
|
|
176
|
+
"Choices": [
|
|
177
|
+
{ "Variable": "$.approved", "BooleanEquals": true, "Next": "Approved" }
|
|
178
|
+
],
|
|
179
|
+
"Default": "Rejected"
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
{/* @skip-typecheck: continuation snippet */}
|
|
184
|
+
```typescript title="workflow/workflows/refund.ts (Workflow SDK)"
|
|
185
|
+
const { approved } = await approval;
|
|
186
|
+
if (approved) return { refundId, status: 'approved' }; // [!code highlight]
|
|
187
|
+
return { refundId, status: 'rejected' };
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Spawn a child workflow
|
|
191
|
+
|
|
192
|
+
In ASL, a parent machine calls `StartExecution` (usually via `.sync` or `.waitForTaskToken`) to launch a child. In the Workflow SDK, `start()` and `getRun()` are runtime APIs, so wrap them in `"use step"` functions. Returning the `Run` object from the spawn step lets workflow observability deep-link to the child run.
|
|
193
|
+
|
|
194
|
+
### Parent starts a child
|
|
195
|
+
|
|
196
|
+
```typescript title="workflow/workflows/parent.ts"
|
|
197
|
+
import { start } from 'workflow/api';
|
|
198
|
+
|
|
199
|
+
async function spawnChild(item: string) {
|
|
200
|
+
'use step'; // [!code highlight]
|
|
201
|
+
return start(childWorkflow, [item]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function parentWorkflow(item: string) {
|
|
205
|
+
'use workflow';
|
|
206
|
+
const run = await spawnChild(item);
|
|
207
|
+
return { childRunId: run.runId };
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Awaiting the child's result
|
|
212
|
+
|
|
213
|
+
Add a second step that wraps `getRun()` and awaits `returnValue`:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { getRun } from 'workflow/api';
|
|
217
|
+
|
|
218
|
+
async function collectResult(runId: string) {
|
|
219
|
+
'use step'; // [!code highlight]
|
|
220
|
+
const run = getRun(runId);
|
|
221
|
+
return (await run.returnValue) as { item: string; result: string };
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Then in the workflow: `const result = await collectResult(run.runId);`. The child workflow itself (`childWorkflow`) is defined elsewhere with `"use workflow"`.
|
|
226
|
+
|
|
227
|
+
## What you stop operating
|
|
228
|
+
|
|
229
|
+
Moving off Step Functions removes these surfaces from the application:
|
|
230
|
+
|
|
231
|
+
- ASL state machine JSON and its reference syntax.
|
|
232
|
+
- Per-task Lambda functions, their IAM roles, and CloudFormation/CDK wiring.
|
|
233
|
+
- Task-token delivery infrastructure (SQS queues, callback Lambdas).
|
|
234
|
+
- Separate progress channels (DynamoDB, SNS) for client-visible updates.
|
|
235
|
+
- CloudWatch and X-Ray configuration for orchestrator observability.
|
|
236
|
+
|
|
237
|
+
Workflow and step functions live in the same deployment as the application. State transitions are `await` calls. Progress streaming, retries, and observability are built in.
|
|
238
|
+
|
|
239
|
+
## Step-by-step first migration
|
|
240
|
+
|
|
241
|
+
Pick one state machine and migrate it end-to-end before touching the rest. The steps below describe the smallest viable path.
|
|
242
|
+
|
|
243
|
+
### Step 1: Install the Workflow SDK
|
|
244
|
+
|
|
245
|
+
Add the `workflow` runtime package.
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
pnpm add workflow
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Step 2: Rewrite the state machine as a `"use workflow"` function
|
|
252
|
+
|
|
253
|
+
Transitions become `await` calls. Control flow (`Choice`, `Wait`, `Parallel`, `Map`) becomes `if`/`switch`, `sleep`, `Promise.all`, and loops.
|
|
254
|
+
|
|
255
|
+
```ts title="workflows/order.ts"
|
|
256
|
+
export async function processOrder(orderId: string) {
|
|
257
|
+
"use workflow"; // [!code highlight]
|
|
258
|
+
const order = await loadOrder(orderId);
|
|
259
|
+
if (order.total > 1000) await reviewManually(order);
|
|
260
|
+
await chargePayment(order);
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Step 3: Move each Lambda into a step function
|
|
265
|
+
|
|
266
|
+
Inline the Lambda body into a function with `"use step"` on the first line. Step functions keep full Node.js access, so existing SDK calls work unchanged.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
async function loadOrder(id: string) {
|
|
270
|
+
"use step"; // [!code highlight]
|
|
271
|
+
return fetch(`/api/orders/${id}`).then((r) => r.json());
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Step 4: Replace `.waitForTaskToken` with a hook
|
|
276
|
+
|
|
277
|
+
Swap the task-token callback Lambda for `createHook()`. Callers `resumeHook(token, payload)` instead of `SendTaskSuccess`.
|
|
278
|
+
|
|
279
|
+
### Step 5: Start runs from an API route
|
|
280
|
+
|
|
281
|
+
Delete the `StartExecution` call and IAM wiring. Launch runs directly from a route handler:
|
|
282
|
+
|
|
283
|
+
```ts title="app/api/orders/route.ts"
|
|
284
|
+
import { start } from "workflow/api";
|
|
285
|
+
import { processOrder } from "@/workflows/order";
|
|
286
|
+
|
|
287
|
+
export async function POST(req: Request) {
|
|
288
|
+
const { orderId } = await req.json();
|
|
289
|
+
const run = await start(processOrder, [orderId]);
|
|
290
|
+
return Response.json({ runId: run.runId });
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Step 6: Retire the Step Functions infrastructure
|
|
295
|
+
|
|
296
|
+
Delete the ASL JSON, per-task Lambda deployments, IAM roles, and callback queues. Remove CloudWatch and X-Ray wiring used for orchestrator observability. Verify the run in `npx workflow web` before shipping.
|
|
297
|
+
|
|
298
|
+
## Quick-start checklist
|
|
299
|
+
|
|
300
|
+
- Replace the ASL state machine with a single `"use workflow"` function. Transitions become `await` calls.
|
|
301
|
+
- Convert each Task / Lambda into a `"use step"` function in the same file.
|
|
302
|
+
- Replace Choice states with `if`/`else`/`switch`.
|
|
303
|
+
- Replace Wait states with `sleep()` from `workflow`.
|
|
304
|
+
- Replace Parallel states with `Promise.all()`.
|
|
305
|
+
- Replace Map states with loops or `Promise.all()`. For large fan-outs, wrap `start()` in a step.
|
|
306
|
+
- Replace `StartExecution` child machines with `"use step"` wrappers around `start()` and `getRun()`.
|
|
307
|
+
- Replace `.waitForTaskToken` with `createHook()` (internal callers) or `createWebhook()` (HTTP callers).
|
|
308
|
+
- Move Retry/Catch to step boundaries using `maxRetries`, `RetryableError`, and `FatalError`.
|
|
309
|
+
- Use `getStepMetadata().stepId` as the idempotency key for external side effects.
|
|
310
|
+
- Stream progress from steps with `getWritable()` instead of polling DynamoDB or SNS.
|
|
311
|
+
- Deploy and verify runs end-to-end with built-in observability.
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Migrating from Inngest
|
|
3
|
+
description: Move an Inngest TypeScript app to the Workflow SDK by replacing createFunction, step.run(), step.sleep(), step.waitForEvent(), and step.invoke() with Workflows, Steps, Hooks, and start()/getRun().
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Translate an Inngest app into the Workflow SDK with side-by-side code examples.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/getting-started/next
|
|
8
|
+
- /docs/foundations/workflows-and-steps
|
|
9
|
+
related:
|
|
10
|
+
- /docs/foundations/starting-workflows
|
|
11
|
+
- /docs/foundations/errors-and-retries
|
|
12
|
+
- /docs/foundations/hooks
|
|
13
|
+
- /docs/foundations/streaming
|
|
14
|
+
- /docs/deploying/world/vercel-world
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<Callout type="info">
|
|
18
|
+
Install the Workflow SDK migration skill:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx skills add https://github.com/vercel/workflow --skill migrating-to-workflow-sdk
|
|
22
|
+
```
|
|
23
|
+
</Callout>
|
|
24
|
+
|
|
25
|
+
## Why migrate to the Workflow SDK
|
|
26
|
+
|
|
27
|
+
- Streaming is built in. Durable progress writes go to named streams via `getWritable()`. There is no separate realtime publish channel or WebSocket layer to operate.
|
|
28
|
+
- Infrastructure and orchestration live in a single deployment. Workflows run where the app runs. There is no separate Inngest Dev Server or event bus to operate.
|
|
29
|
+
- TypeScript-first DX. Steps are named async functions marked with `"use step"`. No inline closures tied to a framework-specific lifecycle.
|
|
30
|
+
- Agent-first tooling: the `npx workflow` CLI, `@workflow/ai` integration for durable AI agents, and a Claude skill for generating workflows.
|
|
31
|
+
|
|
32
|
+
## What changes when you leave Inngest
|
|
33
|
+
|
|
34
|
+
Inngest defines functions with `inngest.createFunction()`, registers them through a `serve()` handler, and breaks work into steps with `step.run()`, `step.sleep()`, and `step.waitForEvent()`. The platform routes events, schedules steps, and applies retries.
|
|
35
|
+
|
|
36
|
+
The Workflow SDK replaces that with `"use workflow"` functions that orchestrate `"use step"` functions in plain TypeScript. There is no function registry, event dispatch layer, or SDK client. Durable replay, automatic retries, and step-level persistence are built into the runtime.
|
|
37
|
+
|
|
38
|
+
Migration collapses the SDK abstraction into plain async functions. Business logic stays the same.
|
|
39
|
+
|
|
40
|
+
## Concept mapping
|
|
41
|
+
|
|
42
|
+
| Inngest | Workflow SDK | Migration note |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `inngest.createFunction()` | `"use workflow"` function started with `start()` | No wrapper needed. |
|
|
45
|
+
| `step.run()` | `"use step"` function | Standalone async function with Node.js access. |
|
|
46
|
+
| `step.sleep()` / `step.sleepUntil()` | `sleep()` | Import from `workflow`. |
|
|
47
|
+
| `step.waitForEvent()` | `createHook()` or `createWebhook()` | Hooks for typed signals, webhooks for HTTP. |
|
|
48
|
+
| `step.invoke()` | `"use step"` wrappers around `start()` / `getRun()` | Spawn a child run, pass `runId` forward. |
|
|
49
|
+
| `inngest.send()` / event triggers | `start()` from your app boundary | Start workflows directly. |
|
|
50
|
+
| Retry configuration (`retries`) | `RetryableError`, `FatalError`, `maxRetries` | Retry logic lives at the step level. |
|
|
51
|
+
| `step.sendEvent()` | `"use step"` wrapper around `start()` | Fan out via `start()`, not an event bus. |
|
|
52
|
+
| Realtime / `step.realtime.publish()` | `getWritable()` / `getWritable({ namespace })` | Named streams are the canonical way for clients to read workflow status. No database or `getRun()` polling required. |
|
|
53
|
+
|
|
54
|
+
## Translate your first workflow
|
|
55
|
+
|
|
56
|
+
Start with the shell of a function. Inngest wraps it in `createFunction`; Workflow SDK marks it with a directive.
|
|
57
|
+
|
|
58
|
+
{/* @skip-typecheck: Inngest SDK types not available */}
|
|
59
|
+
```typescript title="inngest/functions/order.ts"
|
|
60
|
+
export const processOrder = inngest.createFunction(
|
|
61
|
+
{
|
|
62
|
+
id: 'process-order',
|
|
63
|
+
triggers: [{ event: 'order/created' }],
|
|
64
|
+
},
|
|
65
|
+
async ({ event, step }) => {
|
|
66
|
+
return { orderId: event.data.orderId, status: 'completed' };
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```typescript title="workflow/workflows/order.ts"
|
|
72
|
+
export async function processOrder(orderId: string) {
|
|
73
|
+
'use workflow'; // [!code highlight]
|
|
74
|
+
return { orderId, status: 'completed' };
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**What changed:** the factory + event binding collapses into a plain exported function with a `"use workflow"` directive.
|
|
79
|
+
|
|
80
|
+
### Add a step
|
|
81
|
+
|
|
82
|
+
`step.run()` closures become named `"use step"` functions.
|
|
83
|
+
|
|
84
|
+
```typescript title="workflow/workflows/order.ts"
|
|
85
|
+
async function loadOrder(orderId: string) {
|
|
86
|
+
'use step'; // [!code highlight]
|
|
87
|
+
const res = await fetch(`https://example.com/api/orders/${orderId}`);
|
|
88
|
+
return res.json() as Promise<{ id: string }>;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Call it from the workflow like any async function: `const order = await loadOrder(orderId)`. Additional side effects (`reserveInventory`, `chargePayment`) follow the same shape.
|
|
93
|
+
|
|
94
|
+
### Start the run
|
|
95
|
+
|
|
96
|
+
Inngest dispatches via `inngest.send({ name: 'order/created', data: { orderId } })`. Workflow SDK launches directly:
|
|
97
|
+
|
|
98
|
+
```typescript title="app/api/orders/route.ts"
|
|
99
|
+
import { start } from 'workflow/api';
|
|
100
|
+
import { processOrder } from '@/workflows/order';
|
|
101
|
+
|
|
102
|
+
const run = await start(processOrder, [orderId]); // [!code highlight]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
No event bus, no registry. `start()` returns a handle immediately.
|
|
106
|
+
|
|
107
|
+
## Wait for an external signal
|
|
108
|
+
|
|
109
|
+
`step.waitForEvent()` becomes `createHook()` plus `await`.
|
|
110
|
+
|
|
111
|
+
```typescript title="workflow/workflows/refund.ts (Inngest)"
|
|
112
|
+
const approval = await step.waitForEvent('wait-for-approval', {
|
|
113
|
+
event: 'refund/approved',
|
|
114
|
+
match: 'data.refundId',
|
|
115
|
+
timeout: '7d',
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
{/* @skip-typecheck: snippet without imports */}
|
|
120
|
+
```typescript title="workflow/workflows/refund.ts (Workflow SDK)"
|
|
121
|
+
using approval = createHook<{ approved: boolean }>({ // [!code highlight]
|
|
122
|
+
token: `refund:${refundId}:approval`,
|
|
123
|
+
});
|
|
124
|
+
const payload = await approval;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**What changed:** event name + match expression collapse into a single `token` string. The caller supplies that token directly, with no event schema.
|
|
128
|
+
|
|
129
|
+
### Resume from an API route
|
|
130
|
+
|
|
131
|
+
Inngest resumes with `inngest.send({ name: 'refund/approved', data: { refundId, approved } })`. The SDK equivalent is `resumeHook`:
|
|
132
|
+
|
|
133
|
+
```typescript title="app/api/refunds/[refundId]/approve/route.ts"
|
|
134
|
+
import { resumeHook } from 'workflow/api';
|
|
135
|
+
|
|
136
|
+
export async function POST(request: Request, { params }: { params: Promise<{ refundId: string }> }) {
|
|
137
|
+
const { refundId } = await params;
|
|
138
|
+
const { approved } = (await request.json()) as { approved: boolean };
|
|
139
|
+
await resumeHook(`refund:${refundId}:approval`, { approved }); // [!code highlight]
|
|
140
|
+
return Response.json({ ok: true });
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Add a timeout and branch on the payload
|
|
145
|
+
|
|
146
|
+
Inngest's `timeout: '7d'` option maps to a `Promise.race()` with `sleep()`:
|
|
147
|
+
|
|
148
|
+
{/* @skip-typecheck: continuation snippet */}
|
|
149
|
+
```typescript title="workflow/workflows/refund.ts"
|
|
150
|
+
const result = await Promise.race([
|
|
151
|
+
approval.then((p) => ({ type: 'decision' as const, approved: p.approved })),
|
|
152
|
+
sleep('7d').then(() => ({ type: 'timeout' as const })), // [!code highlight]
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
if (result.type === 'timeout') return { refundId, status: 'timed-out' };
|
|
156
|
+
if (!result.approved) return { refundId, status: 'rejected' };
|
|
157
|
+
return { refundId, status: 'approved' };
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
<Callout type="info">
|
|
161
|
+
Event matching disappears. A hook's token encodes the routing (for example, `refund:${refundId}:approval`), and the caller supplies that token to `resumeHook()`.
|
|
162
|
+
</Callout>
|
|
163
|
+
|
|
164
|
+
## Spawn a child workflow
|
|
165
|
+
|
|
166
|
+
`step.invoke()` splits into two steps: spawn and collect. `start()` and `getRun()` are runtime APIs, so wrap them in `"use step"` functions. Return the `Run` object from the spawn step so observability can deep-link into the child run.
|
|
167
|
+
|
|
168
|
+
{/* @skip-typecheck: snippet without imports */}
|
|
169
|
+
```typescript title="workflow/workflows/parent.ts"
|
|
170
|
+
async function spawnChild(item: string) {
|
|
171
|
+
'use step';
|
|
172
|
+
return start(childWorkflow, [item]); // [!code highlight]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Await the result in a second step, then orchestrate both from the parent:
|
|
177
|
+
|
|
178
|
+
{/* @skip-typecheck: snippet without imports */}
|
|
179
|
+
```typescript title="workflow/workflows/parent.ts"
|
|
180
|
+
async function collectResult(runId: string) {
|
|
181
|
+
'use step';
|
|
182
|
+
const run = getRun(runId);
|
|
183
|
+
return await run.returnValue; // [!code highlight]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function parentWorkflow(item: string) {
|
|
187
|
+
'use workflow';
|
|
188
|
+
const child = await spawnChild(item);
|
|
189
|
+
return await collectResult(child.runId);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## What you stop operating
|
|
194
|
+
|
|
195
|
+
Dropping the Inngest SDK removes several moving parts:
|
|
196
|
+
|
|
197
|
+
- **No SDK client or serve handler.** Workflow files carry directive annotations. No registry, no serve endpoint.
|
|
198
|
+
- **No event bus.** `start()` launches workflows directly from API routes, server actions, or other entry points. No event schemas or dispatch layer.
|
|
199
|
+
- **No inline step closures.** Steps are named async functions. They type-check and test like any other TypeScript function.
|
|
200
|
+
- **No separate streaming transport.** `getWritable()` delivers progress to clients without WebSockets or SSE glue.
|
|
201
|
+
- **No idle workers.** Workflows suspended on `sleep()` or a hook consume no compute until resumed.
|
|
202
|
+
|
|
203
|
+
## Step-by-step first migration
|
|
204
|
+
|
|
205
|
+
Pick one Inngest function and migrate it end-to-end before touching the rest. The steps below describe the smallest viable path.
|
|
206
|
+
|
|
207
|
+
### Step 1: Install the Workflow SDK
|
|
208
|
+
|
|
209
|
+
Add the runtime and the framework integration that matches the app.
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
pnpm add workflow
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Step 2: Convert `createFunction` to a `"use workflow"` export
|
|
216
|
+
|
|
217
|
+
Replace the factory call with a plain async export. Move the handler body up. The event-binding argument goes away.
|
|
218
|
+
|
|
219
|
+
```ts title="workflows/order.ts"
|
|
220
|
+
// Before (Inngest)
|
|
221
|
+
// export const processOrder = inngest.createFunction(
|
|
222
|
+
// { id: "process-order", triggers: [{ event: "order.created" }] },
|
|
223
|
+
// async ({ event, step }) => { ... }
|
|
224
|
+
// );
|
|
225
|
+
|
|
226
|
+
// After (Workflow SDK)
|
|
227
|
+
export async function processOrder(orderId: string) {
|
|
228
|
+
"use workflow"; // [!code highlight]
|
|
229
|
+
// ...
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Step 3: Convert `step.run` callbacks into named step functions
|
|
234
|
+
|
|
235
|
+
Each inline callback becomes a named function with `"use step"` on the first line. The workflow calls them with a plain `await`.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
async function loadOrder(id: string) {
|
|
239
|
+
"use step"; // [!code highlight]
|
|
240
|
+
return fetch(`/api/orders/${id}`).then((r) => r.json());
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Step 4: Replace `waitForEvent`, `sleep`, and `invoke`
|
|
245
|
+
|
|
246
|
+
- `step.waitForEvent(...)` → `createHook({ token })` + `await hook`. Resume it from an API route with `resumeHook(token, payload)`.
|
|
247
|
+
- `step.sleep(...)` → `sleep("5m")` from `workflow`.
|
|
248
|
+
- `step.invoke(child, { data })` → wrap `start(child, [data])` in a `"use step"` function that returns the `Run`, and optionally read its return value with `getRun(run.runId).returnValue`.
|
|
249
|
+
|
|
250
|
+
### Step 5: Start runs from the app
|
|
251
|
+
|
|
252
|
+
Delete the `serve()` handler and event dispatch. Launch runs directly from an API route:
|
|
253
|
+
|
|
254
|
+
```ts title="app/api/orders/route.ts"
|
|
255
|
+
import { start } from "workflow/api";
|
|
256
|
+
import { processOrder } from "@/workflows/order";
|
|
257
|
+
|
|
258
|
+
export async function POST(req: Request) {
|
|
259
|
+
const { orderId } = await req.json();
|
|
260
|
+
const run = await start(processOrder, [orderId]);
|
|
261
|
+
return Response.json({ runId: run.runId });
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Step 6: Retire the Inngest infrastructure
|
|
266
|
+
|
|
267
|
+
Remove the `inngest` client, the `serve()` route, event schemas, and the Inngest Dev Server from the app. Verify the run in `npx workflow web` before shipping.
|
|
268
|
+
|
|
269
|
+
## Quick-start checklist
|
|
270
|
+
|
|
271
|
+
- Replace `inngest.createFunction()` with a `"use workflow"` function; launch it with `start()`.
|
|
272
|
+
- Convert each `step.run()` callback into a named `"use step"` function.
|
|
273
|
+
- Swap `step.sleep()` / `step.sleepUntil()` for `sleep()` from `workflow`.
|
|
274
|
+
- Swap `step.waitForEvent()` for `createHook()` (internal) or `createWebhook()` (HTTP).
|
|
275
|
+
- Model `waitForEvent` timeouts as `Promise.race()` between the hook and `sleep()`.
|
|
276
|
+
- Replace `step.invoke()` with `"use step"` wrappers around `start()` and `getRun()`.
|
|
277
|
+
- Replace `step.sendEvent()` fan-out with `start()` called from a `"use step"` function.
|
|
278
|
+
- Remove the Inngest client, `serve()` handler, and event definitions.
|
|
279
|
+
- Push retry configuration down to step boundaries via `maxRetries`, `RetryableError`, and `FatalError`.
|
|
280
|
+
- Use `getStepMetadata().stepId` as the idempotency key for external side effects.
|
|
281
|
+
- Replace `step.realtime.publish()` with `getWritable()`.
|
|
282
|
+
- Deploy and verify end-to-end with the built-in observability UI.
|