workflow 5.0.0-beta.1 → 5.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/api-workflow.d.ts +1 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +2 -2
- 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/child-workflows.mdx +372 -0
- package/docs/cookbook/advanced/distributed-abort-controller.mdx +318 -0
- package/docs/cookbook/advanced/meta.json +9 -0
- package/docs/cookbook/advanced/publishing-libraries.mdx +336 -0
- package/docs/cookbook/advanced/serializable-steps.mdx +147 -0
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +205 -0
- package/docs/cookbook/agent-patterns/durable-agent.mdx +150 -0
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +255 -0
- package/docs/cookbook/agent-patterns/meta.json +4 -0
- package/docs/cookbook/common-patterns/batching.mdx +105 -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 +247 -0
- package/docs/cookbook/common-patterns/scheduling.mdx +125 -0
- package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
- package/docs/cookbook/common-patterns/timeouts.mdx +99 -0
- package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
- package/docs/cookbook/common-patterns/workflow-composition.mdx +118 -0
- package/docs/cookbook/index.mdx +38 -0
- package/docs/cookbook/integrations/ai-sdk.mdx +360 -0
- package/docs/cookbook/integrations/chat-sdk.mdx +303 -0
- package/docs/cookbook/integrations/meta.json +4 -0
- package/docs/cookbook/integrations/sandbox.mdx +516 -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/foundations/index.mdx +0 -3
- package/docs/foundations/meta.json +0 -1
- package/docs/foundations/serialization.mdx +1 -1
- package/docs/foundations/starting-workflows.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 +363 -0
- package/docs/migration-guides/migrating-from-inngest.mdx +314 -0
- package/docs/migration-guides/migrating-from-temporal.mdx +318 -0
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +337 -0
- package/package.json +13 -13
- package/docs/foundations/common-patterns.mdx +0 -265
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Migrating from trigger.dev
|
|
3
|
+
description: Move a trigger.dev v3 TypeScript app to the Workflow SDK by replacing task(), schemaTask(), wait.for / wait.forToken, triggerAndWait, and metadata streams with Workflows, Steps, Hooks, and start() / getRun().
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Translate a trigger.dev v3 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 via named streams (`getWritable()` and `getWritable({ namespace })`). Durable status writes replace `metadata.stream()` and `metadata.set()`, and clients read from the end of the stream for current status.
|
|
28
|
+
- Orchestration and infrastructure live in a single deployment. There is no separate trigger.dev cloud or self-hosted worker fleet to operate.
|
|
29
|
+
- TypeScript-first DX: plain async/await control flow, no `task()` factory, no `schemaTask()` wrapper for payloads you already type with TypeScript.
|
|
30
|
+
- Agent-first tooling: the `npx workflow` CLI, `@workflow/ai` for durable AI agents, and a Claude skill for generating workflows.
|
|
31
|
+
- Retry policy is per step. Throw `RetryableError` to retry with a delay, or `FatalError` to stop. There is no central task-level retry config.
|
|
32
|
+
|
|
33
|
+
## What changes when you leave trigger.dev?
|
|
34
|
+
|
|
35
|
+
trigger.dev v3 defines durable work with `task()` or `schemaTask()` from `@trigger.dev/sdk` (trigger.dev v3), deploys tasks to the trigger.dev cloud or a self-hosted instance, and triggers runs via `tasks.trigger()`. A separate worker fleet picks up runs, applies retry policies, and routes `wait.for`, `wait.forToken`, and `metadata.stream` calls through the platform.
|
|
36
|
+
|
|
37
|
+
The Workflow SDK replaces that with `"use workflow"` functions that orchestrate `"use step"` functions in plain TypeScript. There is no task registry, separate deploy target, or SDK client. Durable replay, retries, and event history ship with the runtime.
|
|
38
|
+
|
|
39
|
+
Migration collapses the task abstraction into plain async functions. Business logic stays the same.
|
|
40
|
+
|
|
41
|
+
## Concept mapping
|
|
42
|
+
|
|
43
|
+
| trigger.dev | Workflow SDK | Migration note |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `task({ id, run })` | `"use workflow"` function started with `start()` | No factory or id registry. |
|
|
46
|
+
| `schemaTask({ schema, run })` | Typed function + `"use workflow"` | Validate inputs at the call site. |
|
|
47
|
+
| Inline `run` body | `"use step"` function | Side effects move into named steps. |
|
|
48
|
+
| `logger` / `metadata.set` | `console` + `getWritable({ namespace: 'status' })` | Logs flow through the run timeline. Status writes go on a named stream. |
|
|
49
|
+
| `wait.for({ seconds \| minutes \| hours \| days })` / `wait.until({ date })` | `sleep()` | Import from `workflow`. |
|
|
50
|
+
| `wait.forToken({ timeout })` | `createHook()` + `Promise.race` with `sleep()` | Hooks carry a typed token. |
|
|
51
|
+
| `tasks.trigger()` / `triggerAndWait()` | `start()` and `getRun(runId).returnValue` | Wrap both in `"use step"` functions. |
|
|
52
|
+
| `batch.triggerAndWait()` | `Promise.all(runIds.map(collectResult))` | Fan out via standard concurrency. |
|
|
53
|
+
| `AbortTaskRunError` | `FatalError` | Stops retries immediately. |
|
|
54
|
+
| `retry.onThrow` / `retry.fetch` | `RetryableError`, `FatalError`, `maxRetries` | Retry count lives on the step via `myStep.maxRetries = N` (default 3). Control delay between attempts by throwing `new RetryableError(msg, { retryAfter: '5s' })` — there is no built-in exponential helper; compute the delay yourself based on `getStepMetadata().attempt` if you need one. |
|
|
55
|
+
| `metadata.stream()` / Realtime | `getWritable()` / `getWritable({ namespace })` | For granular business status (progress updates, current-stage messages), prefer writing to `getWritable({ namespace: 'status' })` from a step and reading from the end of the named stream on the client. Use `getRun(runId).status` for terminal/lifecycle state only. |
|
|
56
|
+
| Self-hosted worker + dashboard | Managed execution + built-in UI | No worker fleet to operate. |
|
|
57
|
+
|
|
58
|
+
## Translate your first workflow
|
|
59
|
+
|
|
60
|
+
<Callout type="warn">
|
|
61
|
+
trigger.dev's `task.run` body has full Node.js access. The SDK's `'use workflow'` body runs in a sandboxed VM — side effects (I/O, `Date.now()`, `Math.random()`, DB, fetch) must live inside `'use step'` functions. Orchestration stays in the workflow body.
|
|
62
|
+
</Callout>
|
|
63
|
+
|
|
64
|
+
Start with the shell. trigger.dev wraps the handler in `task()`; the Workflow SDK marks the function with a directive.
|
|
65
|
+
|
|
66
|
+
```typescript title="trigger/order.ts (trigger.dev)"
|
|
67
|
+
import { task } from '@trigger.dev/sdk';
|
|
68
|
+
|
|
69
|
+
export const processOrder = task({
|
|
70
|
+
id: 'process-order',
|
|
71
|
+
run: async (payload: { orderId: string }) => {
|
|
72
|
+
return { orderId: payload.orderId, status: 'completed' };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```typescript title="trigger/order.ts (Workflow SDK)"
|
|
78
|
+
export async function processOrder(orderId: string) {
|
|
79
|
+
'use workflow'; // [!code highlight]
|
|
80
|
+
return { orderId, status: 'completed' };
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**What changed:** the `task()` factory and its `id` field disappear. The function is a plain export tagged with `"use workflow"`.
|
|
85
|
+
|
|
86
|
+
### Add a step
|
|
87
|
+
|
|
88
|
+
The body of `run` becomes one or more `"use step"` functions.
|
|
89
|
+
|
|
90
|
+
```typescript title="workflow/workflows/order.ts"
|
|
91
|
+
async function loadOrder(orderId: string) {
|
|
92
|
+
'use step'; // [!code highlight]
|
|
93
|
+
const res = await fetch(`https://example.com/api/orders/${orderId}`);
|
|
94
|
+
return res.json() as Promise<{ id: string }>;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Call it from the workflow with a plain `await`. Each additional side effect (`reserveInventory`, `chargePayment`) follows the same shape.
|
|
99
|
+
|
|
100
|
+
### Start the run
|
|
101
|
+
|
|
102
|
+
trigger.dev dispatches with `tasks.trigger<typeof processOrder>('process-order', { orderId })`. The Workflow SDK calls `start()` directly:
|
|
103
|
+
|
|
104
|
+
```typescript title="app/api/orders/route.ts"
|
|
105
|
+
import { start } from 'workflow/api';
|
|
106
|
+
import { processOrder } from '@/workflows/order';
|
|
107
|
+
|
|
108
|
+
export async function POST(request: Request) {
|
|
109
|
+
const { orderId } = (await request.json()) as { orderId: string };
|
|
110
|
+
const run = await start(processOrder, [orderId]); // [!code highlight]
|
|
111
|
+
return Response.json({ runId: run.runId });
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
No id lookup, no API key, no separate worker. `start()` returns a handle immediately.
|
|
116
|
+
|
|
117
|
+
## Wait for an external signal
|
|
118
|
+
|
|
119
|
+
`wait.forToken()` becomes `createHook()` plus `await`.
|
|
120
|
+
|
|
121
|
+
{/* @skip-typecheck: trigger.dev SDK types not available */}
|
|
122
|
+
```typescript title="workflow/workflows/refund.ts (trigger.dev, abbreviated)"
|
|
123
|
+
// import { wait } from '@trigger.dev/sdk';
|
|
124
|
+
const token = await wait.createToken({ timeout: '7d' });
|
|
125
|
+
const approval = await wait.forToken<{ approved: boolean }>(token.id).unwrap();
|
|
126
|
+
// External system resumes with: await wait.completeToken(token.id, { approved: true });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
{/* @skip-typecheck: snippet without imports */}
|
|
130
|
+
```typescript title="workflow/workflows/refund.ts (Workflow SDK)"
|
|
131
|
+
using approval = createHook<{ approved: boolean }>({ // [!code highlight]
|
|
132
|
+
token: `refund:${refundId}:approval`,
|
|
133
|
+
});
|
|
134
|
+
const payload = await approval;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**What changed:** the platform-issued opaque token becomes an app-owned string. The caller that resumes the run supplies that same string, so there is no token lookup. (trigger.dev also exposes `token.url` for external callers; the SDK analog is `createWebhook().url`).
|
|
138
|
+
|
|
139
|
+
### Resume from an API route
|
|
140
|
+
|
|
141
|
+
There are two shapes of resume, and `wait.forToken` can map to either:
|
|
142
|
+
|
|
143
|
+
- **Server-side resume (known token):** `createHook<T>({ token: 'business-token' })` + `resumeHook(token, payload)` from an API route. Use this when your app knows the token shape and controls the resume call.
|
|
144
|
+
- **Third-party callback URL (generated token):** `createWebhook({ respondWith: 'default' })` + pass `webhook.url` to the external system. The external system hits the URL to resume.
|
|
145
|
+
|
|
146
|
+
See [`/docs/foundations/hooks`](/docs/foundations/hooks) for both surfaces.
|
|
147
|
+
|
|
148
|
+
trigger.dev completes a token with `wait.completeToken(tokenId, { approved })`. The SDK equivalent is `resumeHook`:
|
|
149
|
+
|
|
150
|
+
```typescript title="app/api/refunds/[refundId]/approve/route.ts"
|
|
151
|
+
import { resumeHook } from 'workflow/api';
|
|
152
|
+
|
|
153
|
+
export async function POST(request: Request, { params }: { params: Promise<{ refundId: string }> }) {
|
|
154
|
+
const { refundId } = await params;
|
|
155
|
+
const { approved } = (await request.json()) as { approved: boolean };
|
|
156
|
+
await resumeHook(`refund:${refundId}:approval`, { approved }); // [!code highlight]
|
|
157
|
+
return Response.json({ ok: true });
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Add a timeout and branch on the payload
|
|
162
|
+
|
|
163
|
+
trigger.dev's `timeout: '7d'` option maps to a `Promise.race()` with `sleep()`:
|
|
164
|
+
|
|
165
|
+
{/* @skip-typecheck: continuation snippet */}
|
|
166
|
+
```typescript title="workflow/workflows/refund.ts"
|
|
167
|
+
const result = await Promise.race([
|
|
168
|
+
approval.then((p) => ({ type: 'decision' as const, approved: p.approved })),
|
|
169
|
+
sleep('7d').then(() => ({ type: 'timeout' as const })), // [!code highlight]
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
if (result.type === 'timeout') return { refundId, status: 'timed-out' };
|
|
173
|
+
if (!result.approved) return { refundId, status: 'rejected' };
|
|
174
|
+
return { refundId, status: 'approved' };
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
<Callout type="info">
|
|
178
|
+
A hook is an inbound write channel. The caller that knows the token resumes the run with a typed payload. For granular business status (progress updates, current-stage messages), prefer writing to `getWritable({ namespace: 'status' })` from a step and reading from the end of the named stream on the client. Use `getRun(runId).status` for terminal/lifecycle state only.
|
|
179
|
+
</Callout>
|
|
180
|
+
|
|
181
|
+
## Spawn a child workflow
|
|
182
|
+
|
|
183
|
+
`triggerAndWait()` splits into two steps: spawn and collect. `start()` and `getRun()` are runtime APIs, so wrap them in `"use step"` functions. Return the full `Run` object from `spawnChild` so observability tooling can deep-link to the child run.
|
|
184
|
+
|
|
185
|
+
You can return either the full `Run` object (enables deep-linking) or just `run.runId` (simpler). The runtime serializes `Run` to its `runId` in the event log either way.
|
|
186
|
+
|
|
187
|
+
```typescript title="workflow/workflows/parent.ts"
|
|
188
|
+
import { start } from 'workflow/api';
|
|
189
|
+
|
|
190
|
+
async function spawnChild(item: string) {
|
|
191
|
+
'use step';
|
|
192
|
+
return start(childWorkflow, [item]); // [!code highlight]
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Await the result in a second step, then orchestrate both from the parent:
|
|
197
|
+
|
|
198
|
+
```typescript title="workflow/workflows/parent.ts"
|
|
199
|
+
import { getRun } from 'workflow/api';
|
|
200
|
+
|
|
201
|
+
async function collectResult(runId: string) {
|
|
202
|
+
'use step';
|
|
203
|
+
const run = getRun(runId);
|
|
204
|
+
return (await run.returnValue) as { item: string; result: string }; // [!code highlight]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function parentWorkflow(item: string) {
|
|
208
|
+
'use workflow';
|
|
209
|
+
const child = await spawnChild(item);
|
|
210
|
+
return await collectResult(child.runId);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
To fan out, call `spawnChild` inside a loop, then `Promise.all` the `collectResult` calls. That replaces `batch.triggerAndWait()`.
|
|
215
|
+
|
|
216
|
+
`Promise.all` rejects on first failure; use `Promise.allSettled` if you need batch-mode error tolerance similar to trigger.dev's `{ ok, output, error }` per-run result.
|
|
217
|
+
|
|
218
|
+
## What you stop operating
|
|
219
|
+
|
|
220
|
+
Dropping the trigger.dev SDK removes several moving parts:
|
|
221
|
+
|
|
222
|
+
- **No task registry or `id` strings.** Workflow files carry directive annotations and export plain functions.
|
|
223
|
+
- **No `@trigger.dev/sdk` client or API key.** `start()` launches runs directly from API routes or server actions.
|
|
224
|
+
- **No worker fleet or self-hosted instance.** The runtime schedules execution inside the app's deploy target.
|
|
225
|
+
- **No separate Realtime channel.** `getWritable()` streams updates from steps over the run's durable stream.
|
|
226
|
+
- **No dashboard account.** The built-in observability UI (`npx workflow web`) reads the same event log the runtime writes.
|
|
227
|
+
|
|
228
|
+
Workflows suspended on `sleep()` or a hook consume no compute until resumed.
|
|
229
|
+
|
|
230
|
+
## Step-by-step first migration
|
|
231
|
+
|
|
232
|
+
Pick one trigger.dev task and migrate it end-to-end before touching the rest. The steps below describe the smallest viable path.
|
|
233
|
+
|
|
234
|
+
### Step 1: Install the Workflow SDK
|
|
235
|
+
|
|
236
|
+
Add the runtime. Framework integrations (Next.js, Nitro, Nuxt, SvelteKit, Astro, Nest) are subpath exports of the same `workflow` package, e.g. `workflow/next` — no additional install needed.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
pnpm add workflow
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Step 2: Convert `task()` to a `"use workflow"` export
|
|
243
|
+
|
|
244
|
+
Drop the factory call and the `id`. Move the handler body up and replace the payload object with typed function arguments.
|
|
245
|
+
|
|
246
|
+
```ts title="workflows/order.ts"
|
|
247
|
+
// Before (trigger.dev)
|
|
248
|
+
// export const processOrder = task({
|
|
249
|
+
// id: "process-order",
|
|
250
|
+
// run: async ({ orderId }: { orderId: string }) => { ... },
|
|
251
|
+
// });
|
|
252
|
+
|
|
253
|
+
// After (Workflow SDK)
|
|
254
|
+
export async function processOrder(orderId: string) {
|
|
255
|
+
"use workflow"; // [!code highlight]
|
|
256
|
+
// ...
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Step 3: Convert the task body into `"use step"` named functions
|
|
261
|
+
|
|
262
|
+
Each side effect becomes a named function with `"use step"` on the first line. The workflow calls them with a plain `await`. `schemaTask()` validation moves to the call site: validate with zod before calling `start()`.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
async function loadOrder(id: string) {
|
|
266
|
+
"use step"; // [!code highlight]
|
|
267
|
+
return fetch(`/api/orders/${id}`).then((r) => r.json());
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Step 4: Replace `wait.*` with hooks and `sleep`
|
|
272
|
+
|
|
273
|
+
- `wait.for({ seconds | minutes | hours | days })` / `wait.until({ date })` → `sleep('5m')` or `sleep(date)` from `workflow`.
|
|
274
|
+
- `wait.forToken(token)` → `createHook({ token })` + `await`. Complete it with `resumeHook(token, payload)` from an API route.
|
|
275
|
+
- `wait.forToken({ timeout })` → `Promise.race([hook, sleep(timeout)])`.
|
|
276
|
+
- `triggerAndWait(payload)` → wrap `start(child, [payload])` in a `"use step"` function and return the `Run` object, then read the result with a second step that calls `getRun(runId).returnValue`.
|
|
277
|
+
|
|
278
|
+
### Step 5: Start runs from the app
|
|
279
|
+
|
|
280
|
+
Delete the trigger.dev client setup. Launch runs directly from an API route or server action:
|
|
281
|
+
|
|
282
|
+
```ts title="app/api/orders/route.ts"
|
|
283
|
+
import { start } from "workflow/api";
|
|
284
|
+
import { processOrder } from "@/workflows/order";
|
|
285
|
+
|
|
286
|
+
export async function POST(req: Request) {
|
|
287
|
+
const { orderId } = await req.json();
|
|
288
|
+
const run = await start(processOrder, [orderId]);
|
|
289
|
+
return Response.json({ runId: run.runId });
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Step 6: Retire trigger.dev infrastructure
|
|
294
|
+
|
|
295
|
+
Remove the `@trigger.dev/sdk` dependency, the `trigger.config.ts` file, the `trigger/` directory, and any self-hosted worker deployment. Delete dashboard API keys from the environment. Verify the run in `npx workflow web` before shipping.
|
|
296
|
+
|
|
297
|
+
## Retries on steps
|
|
298
|
+
|
|
299
|
+
Retry count lives on the step function itself. Set it as a property on the step:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
async function chargePayment(orderId: string) {
|
|
303
|
+
"use step";
|
|
304
|
+
// ...
|
|
305
|
+
}
|
|
306
|
+
chargePayment.maxRetries = 5;
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Throw `new RetryableError(msg, { retryAfter: '5s' })` to control delay between attempts, or `FatalError` to stop retries immediately. See [`/docs/foundations/errors-and-retries`](/docs/foundations/errors-and-retries).
|
|
310
|
+
|
|
311
|
+
## Features without a 1:1 equivalent
|
|
312
|
+
|
|
313
|
+
- **`schedules.task()` / cron triggers.** The SDK has no built-in scheduler. Trigger runs from Vercel Cron or a system cron calling `start()`.
|
|
314
|
+
- **Concurrency keys / queue concurrency limits.** No direct analog. Enforce limits inside steps (semaphores, external coordinator) or debounce at the publisher.
|
|
315
|
+
- **`machine` presets / custom images.** Machine specs are per-task in trigger.dev; in the SDK, function resources are per-deployment (configure via your hosting platform).
|
|
316
|
+
- **Realtime / `subscribeToRun`.** Use `getRun(runId).getReadable()` plus named `getWritable()` streams for live progress.
|
|
317
|
+
- **`onFailure` lifecycle hook.** No equivalent. Handle cleanup in the workflow body with a try/catch + compensation-stack pattern.
|
|
318
|
+
- **Trigger.dev dashboard.** Workflow SDK ships `npx workflow web` for local inspection and the Vercel Observability tab for deployed runs.
|
|
319
|
+
|
|
320
|
+
## Quick-start checklist
|
|
321
|
+
|
|
322
|
+
- Replace `task({ id, run })` with a `"use workflow"` function; launch it with `start()`.
|
|
323
|
+
- Convert each task body into named `"use step"` functions.
|
|
324
|
+
- Swap `wait.for` / `wait.until` for `sleep()` from `workflow`.
|
|
325
|
+
- Swap `wait.forToken` for `createHook()` (internal) or `createWebhook()` (HTTP).
|
|
326
|
+
- Model `wait.forToken` timeouts as `Promise.race()` between the hook and `sleep()`.
|
|
327
|
+
- Replace `triggerAndWait()` with `"use step"` wrappers around `start()` and `getRun()`.
|
|
328
|
+
- Replace `batch.triggerAndWait()` with `Promise.all` over the collected child `Run` handles.
|
|
329
|
+
- Move `schemaTask` validation to the call site; pass typed arguments into the workflow.
|
|
330
|
+
- Replace `AbortTaskRunError` with `FatalError`; model retries per step with `RetryableError` and `maxRetries`.
|
|
331
|
+
- Use `getStepMetadata().stepId` as the idempotency key for external side effects.
|
|
332
|
+
- Replace `metadata.stream()` and Realtime with `getWritable()`.
|
|
333
|
+
- Remove the `@trigger.dev/sdk` dependency, `trigger.config.ts`, and any self-hosted worker.
|
|
334
|
+
- Deploy and verify runs end-to-end with the built-in observability UI.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
*Verified against `workflow@5.0.0-beta.1` and `@trigger.dev/sdk` v3 on 2026-04-16.*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.3",
|
|
4
4
|
"description": "Workflow SDK - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -56,18 +56,18 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"ms": "2.1.3",
|
|
59
|
-
"@workflow/astro": "5.0.0-beta.
|
|
60
|
-
"@workflow/cli": "5.0.0-beta.
|
|
61
|
-
"@workflow/
|
|
62
|
-
"@workflow/
|
|
63
|
-
"@workflow/
|
|
64
|
-
"@workflow/
|
|
65
|
-
"@workflow/
|
|
66
|
-
"@workflow/nest": "5.0.0-beta.
|
|
67
|
-
"@workflow/nitro": "5.0.0-beta.
|
|
68
|
-
"@workflow/nuxt": "5.0.0-beta.
|
|
69
|
-
"@workflow/sveltekit": "5.0.0-beta.
|
|
70
|
-
"@workflow/rollup": "5.0.0-beta.
|
|
59
|
+
"@workflow/astro": "5.0.0-beta.3",
|
|
60
|
+
"@workflow/cli": "5.0.0-beta.3",
|
|
61
|
+
"@workflow/errors": "5.0.0-beta.1",
|
|
62
|
+
"@workflow/typescript-plugin": "5.0.0-beta.3",
|
|
63
|
+
"@workflow/core": "5.0.0-beta.3",
|
|
64
|
+
"@workflow/next": "5.0.0-beta.3",
|
|
65
|
+
"@workflow/utils": "5.0.0-beta.1",
|
|
66
|
+
"@workflow/nest": "5.0.0-beta.3",
|
|
67
|
+
"@workflow/nitro": "5.0.0-beta.3",
|
|
68
|
+
"@workflow/nuxt": "5.0.0-beta.3",
|
|
69
|
+
"@workflow/sveltekit": "5.0.0-beta.3",
|
|
70
|
+
"@workflow/rollup": "5.0.0-beta.3"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@types/ms": "2.1.0",
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Common Patterns
|
|
3
|
-
description: Implement distributed patterns using familiar async/await syntax with no new APIs to learn.
|
|
4
|
-
type: guide
|
|
5
|
-
summary: Apply sequential, parallel, timeout, and composition patterns in workflows.
|
|
6
|
-
prerequisites:
|
|
7
|
-
- /docs/foundations/workflows-and-steps
|
|
8
|
-
related:
|
|
9
|
-
- /docs/foundations/errors-and-retries
|
|
10
|
-
- /docs/foundations/hooks
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
Common distributed patterns are simple to implement in workflows and require learning no new syntax. You can just use familiar async/await patterns.
|
|
14
|
-
|
|
15
|
-
## Sequential Execution
|
|
16
|
-
|
|
17
|
-
The simplest way to orchestrate steps is to execute them one after another, where each step can be dependent on the previous step.
|
|
18
|
-
|
|
19
|
-
```typescript lineNumbers
|
|
20
|
-
declare function validateData(data: unknown): Promise<string>; // @setup
|
|
21
|
-
declare function processData(data: string): Promise<string>; // @setup
|
|
22
|
-
declare function storeData(data: string): Promise<string>; // @setup
|
|
23
|
-
|
|
24
|
-
export async function dataPipelineWorkflow(data: unknown) {
|
|
25
|
-
"use workflow";
|
|
26
|
-
|
|
27
|
-
const validated = await validateData(data);
|
|
28
|
-
const processed = await processData(validated);
|
|
29
|
-
const stored = await storeData(processed);
|
|
30
|
-
|
|
31
|
-
return stored;
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Parallel Execution
|
|
36
|
-
|
|
37
|
-
When you need to execute multiple steps in parallel, you can use `Promise.all` to run them all at the same time.
|
|
38
|
-
|
|
39
|
-
```typescript lineNumbers
|
|
40
|
-
declare function fetchUser(userId: string): Promise<{ name: string }>; // @setup
|
|
41
|
-
declare function fetchOrders(userId: string): Promise<{ items: string[] }>; // @setup
|
|
42
|
-
declare function fetchPreferences(userId: string): Promise<{ theme: string }>; // @setup
|
|
43
|
-
|
|
44
|
-
export async function fetchUserData(userId: string) {
|
|
45
|
-
"use workflow";
|
|
46
|
-
|
|
47
|
-
const [user, orders, preferences] = await Promise.all([ // [!code highlight]
|
|
48
|
-
fetchUser(userId), // [!code highlight]
|
|
49
|
-
fetchOrders(userId), // [!code highlight]
|
|
50
|
-
fetchPreferences(userId) // [!code highlight]
|
|
51
|
-
]); // [!code highlight]
|
|
52
|
-
|
|
53
|
-
return { user, orders, preferences };
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
This not only applies to steps - since [`sleep()`](/docs/api-reference/workflow/sleep) and [`webhook`](/docs/api-reference/workflow/create-webhook) are also just promises, we can await those in parallel too.
|
|
58
|
-
We can also use `Promise.race` instead of `Promise.all` to stop executing promises after the first one completes.
|
|
59
|
-
|
|
60
|
-
```typescript lineNumbers
|
|
61
|
-
import { sleep, createWebhook } from "workflow";
|
|
62
|
-
declare function executeExternalTask(webhookUrl: string): Promise<void>; // @setup
|
|
63
|
-
|
|
64
|
-
export async function runExternalTask(userId: string) {
|
|
65
|
-
"use workflow";
|
|
66
|
-
|
|
67
|
-
const webhook = createWebhook();
|
|
68
|
-
await executeExternalTask(webhook.url); // Send the webhook somewhere
|
|
69
|
-
|
|
70
|
-
// Wait for the external webhook to be hit, with a timeout of 1 day,
|
|
71
|
-
// whichever comes first
|
|
72
|
-
await Promise.race([ // [!code highlight]
|
|
73
|
-
webhook, // [!code highlight]
|
|
74
|
-
sleep("1 day"), // [!code highlight]
|
|
75
|
-
]); // [!code highlight]
|
|
76
|
-
|
|
77
|
-
console.log("Done")
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## A Full Example
|
|
82
|
-
|
|
83
|
-
Here's a simplified example taken from the [birthday card generator demo](https://github.com/vercel/workflow-examples/tree/main/birthday-card-generator), to illustrate how sequential and parallel execution can be combined.
|
|
84
|
-
|
|
85
|
-
```typescript lineNumbers
|
|
86
|
-
import { createWebhook, sleep, type Webhook } from "workflow"
|
|
87
|
-
declare function makeCardText(prompt: string): Promise<string>; // @setup
|
|
88
|
-
declare function makeCardImage(text: string): Promise<string>; // @setup
|
|
89
|
-
declare function sendRSVPEmail(friend: string, webhook: Webhook): Promise<void>; // @setup
|
|
90
|
-
declare function sendBirthdayCard(text: string, image: string, rsvps: unknown[], email: string): Promise<void>; // @setup
|
|
91
|
-
|
|
92
|
-
async function birthdayWorkflow(
|
|
93
|
-
prompt: string,
|
|
94
|
-
email: string,
|
|
95
|
-
friends: string[],
|
|
96
|
-
birthday: Date
|
|
97
|
-
) {
|
|
98
|
-
"use workflow";
|
|
99
|
-
|
|
100
|
-
// Generate a birthday card with sequential steps
|
|
101
|
-
const text = await makeCardText(prompt)
|
|
102
|
-
const image = await makeCardImage(text)
|
|
103
|
-
|
|
104
|
-
// Create webhooks for each friend who's invited to the birthday party
|
|
105
|
-
const webhooks = friends.map(_ => createWebhook())
|
|
106
|
-
|
|
107
|
-
// Send out all the RSVP invites in parallel steps
|
|
108
|
-
await Promise.all(
|
|
109
|
-
friends.map(
|
|
110
|
-
(friend, i) => sendRSVPEmail(friend, webhooks[i])
|
|
111
|
-
)
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
// Collect RSVPs as they are made without blocking the workflow
|
|
115
|
-
let rsvps = []
|
|
116
|
-
webhooks.map(
|
|
117
|
-
webhook => webhook
|
|
118
|
-
.then(req => req.json())
|
|
119
|
-
.then(( { rsvp } ) => rsvps.push(rsvp))
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
// Wait until the birthday
|
|
123
|
-
await sleep(birthday)
|
|
124
|
-
|
|
125
|
-
// Send birthday card with as many rsvps were collected
|
|
126
|
-
await sendBirthdayCard(text, image, rsvps, email)
|
|
127
|
-
|
|
128
|
-
return { text, image, status: "Sent" }
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Timeout Pattern
|
|
133
|
-
|
|
134
|
-
A common requirement is adding timeouts to operations that might take too long. Use `Promise.race` with `sleep()` to implement this pattern.
|
|
135
|
-
|
|
136
|
-
```typescript lineNumbers
|
|
137
|
-
import { sleep } from "workflow";
|
|
138
|
-
declare function processData(data: string): Promise<string>; // @setup
|
|
139
|
-
|
|
140
|
-
export async function processWithTimeout(data: string) {
|
|
141
|
-
"use workflow";
|
|
142
|
-
|
|
143
|
-
const result = await Promise.race([ // [!code highlight]
|
|
144
|
-
processData(data), // [!code highlight]
|
|
145
|
-
sleep("30s").then(() => "timeout" as const), // [!code highlight]
|
|
146
|
-
]); // [!code highlight]
|
|
147
|
-
|
|
148
|
-
if (result === "timeout") {
|
|
149
|
-
// In workflows, any thrown error exits the workflow (FatalError is for steps)
|
|
150
|
-
throw new Error("Processing timed out after 30 seconds");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return result;
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
This pattern works with any promise-returning operation including steps, hooks, and webhooks. For example, you can add a timeout to a webhook that waits for external input:
|
|
158
|
-
|
|
159
|
-
```typescript lineNumbers
|
|
160
|
-
import { sleep, createWebhook } from "workflow";
|
|
161
|
-
declare function sendApprovalRequest(requestId: string, webhookUrl: string): Promise<void>; // @setup
|
|
162
|
-
|
|
163
|
-
export async function waitForApproval(requestId: string) {
|
|
164
|
-
"use workflow";
|
|
165
|
-
|
|
166
|
-
const webhook = createWebhook<{ approved: boolean }>();
|
|
167
|
-
await sendApprovalRequest(requestId, webhook.url);
|
|
168
|
-
|
|
169
|
-
const result = await Promise.race([ // [!code highlight]
|
|
170
|
-
webhook.then((req) => req.json()), // [!code highlight]
|
|
171
|
-
sleep("7 days").then(() => ({ timedOut: true }) as const), // [!code highlight]
|
|
172
|
-
]); // [!code highlight]
|
|
173
|
-
|
|
174
|
-
if ("timedOut" in result) {
|
|
175
|
-
throw new Error("Approval request expired after 7 days");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return result.approved;
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## Workflow Composition
|
|
183
|
-
|
|
184
|
-
Workflows can call other workflows, enabling you to break complex processes into reusable building blocks. There are two approaches depending on your needs.
|
|
185
|
-
|
|
186
|
-
### Direct Await (Flattening)
|
|
187
|
-
|
|
188
|
-
Call a child workflow directly using `await`. This "flattens" the child workflow into the parent - the child's steps execute inline within the parent workflow's context.
|
|
189
|
-
|
|
190
|
-
```typescript lineNumbers
|
|
191
|
-
declare function sendEmail(userId: string): Promise<void>; // @setup
|
|
192
|
-
declare function sendPushNotification(userId: string): Promise<void>; // @setup
|
|
193
|
-
declare function createAccount(userId: string): Promise<void>; // @setup
|
|
194
|
-
declare function setupPreferences(userId: string): Promise<void>; // @setup
|
|
195
|
-
|
|
196
|
-
// Child workflow
|
|
197
|
-
export async function sendNotifications(userId: string) {
|
|
198
|
-
"use workflow";
|
|
199
|
-
|
|
200
|
-
await sendEmail(userId);
|
|
201
|
-
await sendPushNotification(userId);
|
|
202
|
-
return { notified: true };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Parent workflow calls child directly
|
|
206
|
-
export async function onboardUser(userId: string) {
|
|
207
|
-
"use workflow";
|
|
208
|
-
|
|
209
|
-
await createAccount(userId);
|
|
210
|
-
await sendNotifications(userId); // [!code highlight]
|
|
211
|
-
await setupPreferences(userId);
|
|
212
|
-
|
|
213
|
-
return { userId, status: "onboarded" };
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
With direct await, the parent workflow waits for the child to complete before continuing. The child's steps appear in the parent's event log as if they were called directly from the parent.
|
|
218
|
-
|
|
219
|
-
### Background Execution via Step
|
|
220
|
-
|
|
221
|
-
To run a child workflow independently without blocking the parent, use a step that calls [`start()`](/docs/api-reference/workflow-api/start). This launches the child workflow in the background.
|
|
222
|
-
|
|
223
|
-
```typescript lineNumbers
|
|
224
|
-
import { start } from "workflow/api";
|
|
225
|
-
declare function generateReport(reportId: string): Promise<void>; // @setup
|
|
226
|
-
declare function fulfillOrder(orderId: string): Promise<{ id: string }>; // @setup
|
|
227
|
-
declare function sendConfirmation(orderId: string): Promise<void>; // @setup
|
|
228
|
-
|
|
229
|
-
// Step that starts a workflow in the background
|
|
230
|
-
async function triggerReportGeneration(reportId: string) {
|
|
231
|
-
"use step";
|
|
232
|
-
|
|
233
|
-
const run = await start(generateReport, [reportId]); // [!code highlight]
|
|
234
|
-
return run.runId;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Parent workflow
|
|
238
|
-
export async function processOrder(orderId: string) {
|
|
239
|
-
"use workflow";
|
|
240
|
-
|
|
241
|
-
const order = await fulfillOrder(orderId);
|
|
242
|
-
|
|
243
|
-
// Fire off report generation without waiting
|
|
244
|
-
const reportRunId = await triggerReportGeneration(orderId); // [!code highlight]
|
|
245
|
-
|
|
246
|
-
// Continue immediately - report generates in background
|
|
247
|
-
await sendConfirmation(orderId);
|
|
248
|
-
|
|
249
|
-
return { orderId, reportRunId };
|
|
250
|
-
}
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
With background execution, the parent workflow continues immediately after starting the child. The child workflow runs independently with its own event log and can be monitored separately using the returned `runId`.
|
|
254
|
-
|
|
255
|
-
<Callout type="info">
|
|
256
|
-
If you want the child workflow to run on the latest deployment rather than the current one, you can pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) in the `start()` options. This is currently a Vercel-specific feature. Be aware that the child workflow's function name, file path, argument types, and return type must remain compatible across deployments — renaming the function or changing its location will change the workflow ID, and modifying expected inputs or outputs can cause serialization failures.
|
|
257
|
-
</Callout>
|
|
258
|
-
|
|
259
|
-
**Choose direct await when:**
|
|
260
|
-
- The parent needs the child's result before continuing
|
|
261
|
-
- You want a single, unified event log
|
|
262
|
-
|
|
263
|
-
**Choose background execution when:**
|
|
264
|
-
- The parent doesn't need to wait for the result
|
|
265
|
-
- You want separate workflow runs for observability
|