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.
Files changed (68) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.js +1 -1
  3. package/dist/api.js +1 -1
  4. package/dist/astro.js +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/internal/builtins.js +1 -1
  7. package/dist/internal/class-serialization.js +1 -1
  8. package/dist/internal/errors.js +1 -1
  9. package/dist/nest.js +1 -1
  10. package/dist/next.cjs +1 -1
  11. package/dist/nitro.js +1 -1
  12. package/dist/nuxt.js +1 -1
  13. package/dist/observability.js +1 -1
  14. package/dist/runtime.js +1 -1
  15. package/dist/stdlib.js +1 -1
  16. package/dist/sveltekit.js +1 -1
  17. package/dist/typescript-plugin.cjs +1 -1
  18. package/dist/vite.js +1 -1
  19. package/dist/workflow.js +1 -1
  20. package/docs/ai/resumable-streams.mdx +1 -1
  21. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  22. package/docs/api-reference/workflow/get-workflow-metadata.mdx +34 -0
  23. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  24. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  25. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  26. package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
  27. package/docs/cookbook/advanced/durable-objects.mdx +148 -0
  28. package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
  29. package/docs/cookbook/advanced/meta.json +10 -0
  30. package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
  31. package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
  32. package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
  33. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
  34. package/docs/cookbook/agent-patterns/meta.json +10 -0
  35. package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
  36. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
  37. package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
  38. package/docs/cookbook/common-patterns/batching.mdx +179 -0
  39. package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
  40. package/docs/cookbook/common-patterns/content-router.mdx +207 -0
  41. package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
  42. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  43. package/docs/cookbook/common-patterns/meta.json +15 -0
  44. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  45. package/docs/cookbook/common-patterns/saga.mdx +152 -0
  46. package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
  47. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  48. package/docs/cookbook/index.mdx +41 -0
  49. package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
  50. package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
  51. package/docs/cookbook/integrations/meta.json +4 -0
  52. package/docs/cookbook/integrations/sandbox.mdx +128 -0
  53. package/docs/cookbook/meta.json +5 -0
  54. package/docs/deploying/world/local-world.mdx +1 -1
  55. package/docs/deploying/world/postgres-world.mdx +1 -1
  56. package/docs/deploying/world/vercel-world.mdx +1 -1
  57. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  58. package/docs/getting-started/index.mdx +8 -1
  59. package/docs/getting-started/meta.json +2 -1
  60. package/docs/getting-started/python.mdx +165 -0
  61. package/docs/meta.json +1 -0
  62. package/docs/migration-guides/index.mdx +34 -0
  63. package/docs/migration-guides/meta.json +9 -0
  64. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
  65. package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
  66. package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
  67. package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
  68. package/package.json +13 -13
@@ -0,0 +1,284 @@
1
+ ---
2
+ title: Migrating from Temporal
3
+ description: Move a Temporal TypeScript workflow to the Workflow SDK by replacing Activities, Workers, Signals, and Child Workflows with Workflows, Steps, Hooks, and start()/getRun().
4
+ type: guide
5
+ summary: Translate a Temporal 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({ namespace })`, and clients read them directly. No separate WebSocket, SSE, or progress-polling layer to operate.
28
+ - Infrastructure and orchestration live in a single deployment. There is no separate Worker fleet or Temporal Server to run. The runtime, step logic, and orchestration share the app's observability and log aggregation.
29
+ - TypeScript-first developer experience. Workflows and steps live in the same file with plain `await` control flow, `try/catch`, and `Promise.all`.
30
+ - Agent-first tooling. A first-class CLI (`npx workflow`), `@workflow/ai` integration for durable AI agents, and a bundled Claude skill for AI-assisted authoring.
31
+ - Per-step retry controls. `RetryableError`, `FatalError`, and `maxRetries` live at the step boundary instead of an Activity-level retry policy configured elsewhere.
32
+
33
+ ## What changes when you leave Temporal
34
+
35
+ Temporal requires operating a control plane (Temporal Server or Cloud), a Worker fleet, Activity modules wired through `proxyActivities`, and Task Queues. The workflow code is durable; the surrounding infrastructure is substantial.
36
+
37
+ The Workflow SDK runs on managed infrastructure. Write `"use workflow"` functions that orchestrate `"use step"` functions in the same file, in plain TypeScript. There are no Workers, Task Queues, or separate Activity modules. Durable replay, automatic retries, and event history are handled by the runtime.
38
+
39
+ Migration removes infrastructure and collapses indirection. Business logic stays as regular async TypeScript.
40
+
41
+ ## Concept mapping
42
+
43
+ | Temporal | Workflow SDK | Migration note |
44
+ | --- | --- | --- |
45
+ | Workflow Definition / Workflow Execution | `"use workflow"` function / run started with `start()` | Keep orchestration code in the workflow function. |
46
+ | Activity | `"use step"` function | Put side effects and Node.js access in steps. |
47
+ | Worker + Task Queue | Managed execution | No worker fleet or polling loop to operate. |
48
+ | Signal | `createHook()` or `createWebhook()` | Use hooks for typed resume signals; webhooks for HTTP callbacks. |
49
+ | Query | `getWritable({ namespace: 'status' })` stream | Durably stream status updates from the workflow. Clients read from the stream instead of polling a database. |
50
+ | Update | `createHook()` + `resumeHook()` | Writes go through hooks. |
51
+ | Child Workflow | `"use step"` wrappers around `start()` / `getRun()` | Spawn from a step and return the `Run` object so observability can deep-link into child runs. |
52
+ | Activity retry policy | Step retries, `RetryableError`, `FatalError`, `maxRetries` | Retries live at the step boundary. |
53
+ | Event History | Workflow event log / run timeline | Same durable replay, fewer surfaces to manage. |
54
+
55
+ ## Translate your first workflow
56
+
57
+ ### Minimal translation
58
+
59
+ Start with the directive change. The Temporal definition proxies activities through a module; the Workflow SDK version puts the directive inline.
60
+
61
+ {/* @skip-typecheck: Temporal SDK types not available */}
62
+ ```typescript title="workflows/order.ts (Temporal)"
63
+ const { chargePayment } = wf.proxyActivities<typeof activities>({
64
+ startToCloseTimeout: '5 minutes',
65
+ });
66
+
67
+ export async function processOrder(orderId: string) {
68
+ await chargePayment(orderId);
69
+ return { orderId, status: 'completed' };
70
+ }
71
+ ```
72
+
73
+ ```typescript title="workflows/order.ts (Workflow SDK)"
74
+ export async function processOrder(orderId: string) {
75
+ 'use workflow'; // [!code highlight]
76
+ await chargePayment(orderId);
77
+ return { orderId, status: 'completed' };
78
+ }
79
+ ```
80
+
81
+ **What changed:** `proxyActivities` and the activity module disappear. The orchestrator is plain async TypeScript marked with `"use workflow"`.
82
+
83
+ ### Adding a step
84
+
85
+ Side effects move into a colocated `"use step"` function:
86
+
87
+ ```typescript title="workflow/workflows/order.ts"
88
+ async function chargePayment(orderId: string) {
89
+ 'use step'; // [!code highlight]
90
+ await fetch(`https://example.com/api/orders/${orderId}/charge`, {
91
+ method: 'POST',
92
+ });
93
+ }
94
+ ```
95
+
96
+ Additional steps (`loadOrder`, `reserveInventory`) follow the same shape and can be called from the workflow in sequence.
97
+
98
+ ### Starting the run
99
+
100
+ Replace Worker + Task Queue wiring with a single `start()` call from an API route:
101
+
102
+ ```typescript title="app/api/orders/route.ts"
103
+ import { start } from 'workflow/api';
104
+ import { processOrder } from '@/workflows/order';
105
+
106
+ export async function POST(request: Request) {
107
+ const { orderId } = (await request.json()) as { orderId: string };
108
+ const run = await start(processOrder, [orderId]); // [!code highlight]
109
+ return Response.json({ runId: run.runId });
110
+ }
111
+ ```
112
+
113
+ ## Wait for an external signal
114
+
115
+ ### Minimal translation
116
+
117
+ Temporal needs a signal definition, a handler, and a `condition()` guard. The Workflow SDK collapses all three into a single `createHook()` + `await`.
118
+
119
+ {/* @skip-typecheck: Temporal SDK types not available */}
120
+ ```typescript title="temporal/workflows/refund.ts"
121
+ export const approveRefund = wf.defineSignal<[boolean]>('approveRefund');
122
+
123
+ export async function refundWorkflow(refundId: string) {
124
+ let approved: boolean | undefined;
125
+ wf.setHandler(approveRefund, (v) => { approved = v; });
126
+ await wf.condition(() => approved !== undefined);
127
+ return { refundId, approved };
128
+ }
129
+ ```
130
+
131
+ ```typescript title="workflow/workflows/refund.ts"
132
+ import { createHook } from 'workflow';
133
+
134
+ export async function refundWorkflow(refundId: string) {
135
+ 'use workflow';
136
+ using approval = createHook<{ approved: boolean }>({
137
+ token: `refund:${refundId}:approval`, // [!code highlight]
138
+ });
139
+ const { approved } = await approval; // [!code highlight]
140
+ return { refundId, approved };
141
+ }
142
+ ```
143
+
144
+ The workflow suspends durably at `await approval` until resumed. No polling, no handler registration.
145
+
146
+ ### Resuming from an API route
147
+
148
+ Any HTTP caller can resume by token:
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 body = (await request.json()) as { approved: boolean };
156
+ await resumeHook(`refund:${refundId}:approval`, body); // [!code highlight]
157
+ return Response.json({ ok: true });
158
+ }
159
+ ```
160
+
161
+ <Callout type="info">
162
+ Temporal Queries expose in-memory workflow state on demand. In the Workflow SDK, the equivalent is a durable stream: call `getWritable({ namespace: 'status' })` from inside the workflow to write status updates, and have clients read from the end of that stream to get the current state. Hooks are the write channel for resuming a paused workflow with new data.
163
+ </Callout>
164
+
165
+ ## Spawn a child workflow
166
+
167
+ ### Minimal translation
168
+
169
+ `start()` and `getRun()` are runtime APIs, so wrap them in `"use step"` functions. Return the `Run` object (not a plain `runId` string) so workflow observability can deep-link into child runs.
170
+
171
+ ```typescript title="workflow/workflows/parent.ts"
172
+ import { start } from 'workflow/api';
173
+
174
+ async function spawnChild(item: string) {
175
+ 'use step'; // [!code highlight]
176
+ return start(childWorkflow, [item]); // [!code highlight]
177
+ }
178
+
179
+ export async function parentWorkflow(item: string) {
180
+ 'use workflow';
181
+ const child = await spawnChild(item); // [!code highlight]
182
+ return { childRunId: child.runId };
183
+ }
184
+ ```
185
+
186
+ ### Awaiting the child's return value
187
+
188
+ A second step fetches the run and awaits `returnValue`:
189
+
190
+ ```typescript title="workflow/workflows/parent.ts"
191
+ import { getRun } from 'workflow/api';
192
+
193
+ async function collectResult(runId: string) {
194
+ 'use step';
195
+ const run = getRun(runId);
196
+ return (await run.returnValue) as { item: string; result: string }; // [!code highlight]
197
+ }
198
+ ```
199
+
200
+ Call both steps from the parent in sequence: `const result = await collectResult(child.runId)`. To fan out, call `spawnChild` inside a loop, then `Promise.all` the `collectResult` calls.
201
+
202
+ <Callout type="warn">
203
+ Activity retry policy moves to the step boundary. Use `maxRetries`, `RetryableError`, and `FatalError` on each step instead of a single workflow-wide retry block.
204
+ </Callout>
205
+
206
+ ## What you stop operating
207
+
208
+ - **Temporal Server or Cloud.** Durable state lives in the managed event log.
209
+ - **Worker fleet.** The runtime schedules execution; workflows run where the app runs.
210
+ - **Task Queues.** No queue routing to configure or monitor.
211
+ - **Activity modules and `proxyActivities`.** Steps live next to the workflow that calls them.
212
+ - **Custom progress transport.** `getWritable()` streams updates from steps.
213
+
214
+ Suspended workflows (on `sleep()` or a hook) consume no compute until resumed.
215
+
216
+ ## Step-by-step first migration
217
+
218
+ Pick one Temporal workflow and migrate it end-to-end before touching the rest. The steps below describe the smallest viable path.
219
+
220
+ ### Step 1: Install the Workflow SDK
221
+
222
+ Add the runtime. The Next.js integration ships as a subpath (`workflow/next`) of the same package.
223
+
224
+ ```bash
225
+ pnpm add workflow
226
+ ```
227
+
228
+ ### Step 2: Collapse Activities into step functions
229
+
230
+ Delete the `activities/` module and the `proxyActivities` call. Each former Activity becomes a plain async function with `"use step"` on the first line, living next to the orchestrator.
231
+
232
+ ```ts title="workflows/order.ts"
233
+ async function loadOrder(id: string) {
234
+ "use step"; // [!code highlight]
235
+ return fetch(`/api/orders/${id}`).then((r) => r.json());
236
+ }
237
+ ```
238
+
239
+ ### Step 3: Mark the orchestrator with `"use workflow"`
240
+
241
+ Keep the existing control flow: `await`, `try/catch`, `Promise.all`. The directive turns the function into a durable replay target.
242
+
243
+ ```ts
244
+ export async function processOrder(orderId: string) {
245
+ "use workflow"; // [!code highlight]
246
+ const order = await loadOrder(orderId);
247
+ // ...
248
+ }
249
+ ```
250
+
251
+ ### Step 4: Replace Signals with hooks
252
+
253
+ Swap `defineSignal` + `setHandler` for `createHook()`. Callers `resumeHook(token, payload)` instead of `client.workflow.signal(...)`.
254
+
255
+ ### Step 5: Start runs from an API route or server action
256
+
257
+ Delete the Worker bootstrap. Launch runs from an API route or server action with `start()`:
258
+
259
+ ```ts title="app/api/orders/route.ts"
260
+ import { start } from "workflow/api";
261
+ import { processOrder } from "@/workflows/order";
262
+
263
+ export async function POST(req: Request) {
264
+ const { orderId } = await req.json();
265
+ const run = await start(processOrder, [orderId]);
266
+ return Response.json({ runId: run.runId });
267
+ }
268
+ ```
269
+
270
+ ### Step 6: Retire the Temporal infrastructure
271
+
272
+ Remove the Worker process, `@temporalio/*` dependencies, and the Temporal Server or Cloud connection. Verify the run in the built-in observability UI (`npx workflow web`) before shipping.
273
+
274
+ ## Quick-start checklist
275
+
276
+ - Move orchestration into a `"use workflow"` function.
277
+ - Convert each Activity into a `"use step"` function.
278
+ - Remove Worker and Task Queue code. Start workflows from the app with `start()`.
279
+ - Replace Signals with `createHook()` or `createWebhook()` for HTTP callers.
280
+ - Wrap `start()` and `getRun()` in `"use step"` functions for child workflows. Return the `Run` object from `start()` so observability can deep-link into child runs.
281
+ - Set retry policy per step with `maxRetries`, `RetryableError`, and `FatalError`.
282
+ - Use `getStepMetadata().stepId` as the idempotency key for external side effects.
283
+ - Stream status and progress from steps with `getWritable({ namespace: 'status' })`, and have clients read from the stream instead of polling.
284
+ - Deploy the app and verify runs end-to-end in the built-in observability UI.
@@ -0,0 +1,296 @@
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/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 })` / `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 lives on the step. Exponential backoff is a step `maxRetries` config, not a helper. |
55
+ | `metadata.stream()` / Realtime | `getWritable()` / `getWritable({ namespace })` | Named streams are the canonical read channel. Clients read from the end of the stream for current status. Do not poll `getRun()` or persist status to a database for client reads. |
56
+ | Self-hosted worker + dashboard | Managed execution + built-in UI | No worker fleet to operate. |
57
+
58
+ ## Translate your first workflow
59
+
60
+ Start with the shell. trigger.dev wraps the handler in `task()`; the Workflow SDK marks the function with a directive.
61
+
62
+ ```typescript title="trigger/order.ts (trigger.dev)"
63
+ import { task } from '@trigger.dev/sdk/v3';
64
+
65
+ export const processOrder = task({
66
+ id: 'process-order',
67
+ run: async (payload: { orderId: string }) => {
68
+ return { orderId: payload.orderId, status: 'completed' };
69
+ },
70
+ });
71
+ ```
72
+
73
+ ```typescript title="trigger/order.ts (Workflow SDK)"
74
+ export async function processOrder(orderId: string) {
75
+ 'use workflow'; // [!code highlight]
76
+ return { orderId, status: 'completed' };
77
+ }
78
+ ```
79
+
80
+ **What changed:** the `task()` factory and its `id` field disappear. The function is a plain export tagged with `"use workflow"`.
81
+
82
+ ### Add a step
83
+
84
+ The body of `run` becomes one or more `"use step"` functions.
85
+
86
+ ```typescript title="workflow/workflows/order.ts"
87
+ async function loadOrder(orderId: string) {
88
+ 'use step'; // [!code highlight]
89
+ const res = await fetch(`https://example.com/api/orders/${orderId}`);
90
+ return res.json() as Promise<{ id: string }>;
91
+ }
92
+ ```
93
+
94
+ Call it from the workflow with a plain `await`. Each additional side effect (`reserveInventory`, `chargePayment`) follows the same shape.
95
+
96
+ ### Start the run
97
+
98
+ trigger.dev dispatches with `tasks.trigger<typeof processOrder>('process-order', { orderId })`. The Workflow SDK calls `start()` directly:
99
+
100
+ ```typescript title="app/api/orders/route.ts"
101
+ import { start } from 'workflow/api';
102
+ import { processOrder } from '@/workflows/order';
103
+
104
+ export async function POST(request: Request) {
105
+ const { orderId } = (await request.json()) as { orderId: string };
106
+ const run = await start(processOrder, [orderId]); // [!code highlight]
107
+ return Response.json({ runId: run.runId });
108
+ }
109
+ ```
110
+
111
+ No id lookup, no API key, no separate worker. `start()` returns a handle immediately.
112
+
113
+ ## Wait for an external signal
114
+
115
+ `wait.forToken()` becomes `createHook()` plus `await`.
116
+
117
+ {/* @skip-typecheck: trigger.dev SDK types not available */}
118
+ ```typescript title="workflow/workflows/refund.ts (trigger.dev, abbreviated)"
119
+ // import { wait } from '@trigger.dev/sdk/v3';
120
+ const token = await wait.createToken({ timeout: '7d' });
121
+ const approval = await wait.forToken<{ approved: boolean }>(token.id).unwrap();
122
+ // External system resumes with: await wait.completeToken(token.id, { approved: true });
123
+ ```
124
+
125
+ {/* @skip-typecheck: snippet without imports */}
126
+ ```typescript title="workflow/workflows/refund.ts (Workflow SDK)"
127
+ using approval = createHook<{ approved: boolean }>({ // [!code highlight]
128
+ token: `refund:${refundId}:approval`,
129
+ });
130
+ const payload = await approval;
131
+ ```
132
+
133
+ **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.
134
+
135
+ ### Resume from an API route
136
+
137
+ trigger.dev completes a token with `wait.completeToken(tokenId, { approved })`. The SDK equivalent is `resumeHook`:
138
+
139
+ ```typescript title="app/api/refunds/[refundId]/approve/route.ts"
140
+ import { resumeHook } from 'workflow/api';
141
+
142
+ export async function POST(request: Request, { params }: { params: Promise<{ refundId: string }> }) {
143
+ const { refundId } = await params;
144
+ const { approved } = (await request.json()) as { approved: boolean };
145
+ await resumeHook(`refund:${refundId}:approval`, { approved }); // [!code highlight]
146
+ return Response.json({ ok: true });
147
+ }
148
+ ```
149
+
150
+ ### Add a timeout and branch on the payload
151
+
152
+ trigger.dev's `timeout: '7d'` option maps to a `Promise.race()` with `sleep()`:
153
+
154
+ {/* @skip-typecheck: continuation snippet */}
155
+ ```typescript title="workflow/workflows/refund.ts"
156
+ const result = await Promise.race([
157
+ approval.then((p) => ({ type: 'decision' as const, approved: p.approved })),
158
+ sleep('7d').then(() => ({ type: 'timeout' as const })), // [!code highlight]
159
+ ]);
160
+
161
+ if (result.type === 'timeout') return { refundId, status: 'timed-out' };
162
+ if (!result.approved) return { refundId, status: 'rejected' };
163
+ return { refundId, status: 'approved' };
164
+ ```
165
+
166
+ <Callout type="info">
167
+ A hook is an inbound write channel. The caller that knows the token resumes the run with a typed payload. To expose in-flight state to a dashboard, write updates from a step with `getWritable()` (or `getWritable({ namespace: 'status' })`), and have the client read from the end of that named stream.
168
+ </Callout>
169
+
170
+ ## Spawn a child workflow
171
+
172
+ `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.
173
+
174
+ ```typescript title="workflow/workflows/parent.ts"
175
+ import { start } from 'workflow/api';
176
+
177
+ async function spawnChild(item: string) {
178
+ 'use step';
179
+ return start(childWorkflow, [item]); // [!code highlight]
180
+ }
181
+ ```
182
+
183
+ Await the result in a second step, then orchestrate both from the parent:
184
+
185
+ ```typescript title="workflow/workflows/parent.ts"
186
+ import { getRun } from 'workflow/api';
187
+
188
+ async function collectResult(runId: string) {
189
+ 'use step';
190
+ const run = getRun(runId);
191
+ return (await run.returnValue) as { item: string; result: string }; // [!code highlight]
192
+ }
193
+
194
+ export async function parentWorkflow(item: string) {
195
+ 'use workflow';
196
+ const child = await spawnChild(item);
197
+ return await collectResult(child.runId);
198
+ }
199
+ ```
200
+
201
+ To fan out, call `spawnChild` inside a loop, then `Promise.all` the `collectResult` calls. That replaces `batch.triggerAndWait()`.
202
+
203
+ ## What you stop operating
204
+
205
+ Dropping the trigger.dev SDK removes several moving parts:
206
+
207
+ - **No task registry or `id` strings.** Workflow files carry directive annotations and export plain functions.
208
+ - **No `@trigger.dev/sdk/v3` client or API key.** `start()` launches runs directly from API routes or server actions.
209
+ - **No worker fleet or self-hosted instance.** The runtime schedules execution inside the app's deploy target.
210
+ - **No separate Realtime channel.** `getWritable()` streams updates from steps over the run's durable stream.
211
+ - **No dashboard account.** The built-in observability UI (`npx workflow web`) reads the same event log the runtime writes.
212
+
213
+ Workflows suspended on `sleep()` or a hook consume no compute until resumed.
214
+
215
+ ## Step-by-step first migration
216
+
217
+ Pick one trigger.dev task and migrate it end-to-end before touching the rest. The steps below describe the smallest viable path.
218
+
219
+ ### Step 1: Install the Workflow SDK
220
+
221
+ Add the runtime. The Next.js integration ships as the `workflow/next` subpath of the same package.
222
+
223
+ ```bash
224
+ pnpm add workflow
225
+ ```
226
+
227
+ ### Step 2: Convert `task()` to a `"use workflow"` export
228
+
229
+ Drop the factory call and the `id`. Move the handler body up and replace the payload object with typed function arguments.
230
+
231
+ ```ts title="workflows/order.ts"
232
+ // Before (trigger.dev)
233
+ // export const processOrder = task({
234
+ // id: "process-order",
235
+ // run: async ({ orderId }: { orderId: string }) => { ... },
236
+ // });
237
+
238
+ // After (Workflow SDK)
239
+ export async function processOrder(orderId: string) {
240
+ "use workflow"; // [!code highlight]
241
+ // ...
242
+ }
243
+ ```
244
+
245
+ ### Step 3: Convert the task body into `"use step"` named functions
246
+
247
+ 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()`.
248
+
249
+ ```ts
250
+ async function loadOrder(id: string) {
251
+ "use step"; // [!code highlight]
252
+ return fetch(`/api/orders/${id}`).then((r) => r.json());
253
+ }
254
+ ```
255
+
256
+ ### Step 4: Replace `wait.*` with hooks and `sleep`
257
+
258
+ - `wait.for({ seconds })` / `wait.until({ date })` → `sleep('5m')` or `sleep(date)` from `workflow`.
259
+ - `wait.forToken(token)` → `createHook({ token })` + `await`. Complete it with `resumeHook(token, payload)` from an API route.
260
+ - `wait.forToken({ timeout })` → `Promise.race([hook, sleep(timeout)])`.
261
+ - `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`.
262
+
263
+ ### Step 5: Start runs from the app
264
+
265
+ Delete the trigger.dev client setup. Launch runs directly from an API route or server action:
266
+
267
+ ```ts title="app/api/orders/route.ts"
268
+ import { start } from "workflow/api";
269
+ import { processOrder } from "@/workflows/order";
270
+
271
+ export async function POST(req: Request) {
272
+ const { orderId } = await req.json();
273
+ const run = await start(processOrder, [orderId]);
274
+ return Response.json({ runId: run.runId });
275
+ }
276
+ ```
277
+
278
+ ### Step 6: Retire trigger.dev infrastructure
279
+
280
+ 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.
281
+
282
+ ## Quick-start checklist
283
+
284
+ - Replace `task({ id, run })` with a `"use workflow"` function; launch it with `start()`.
285
+ - Convert each task body into named `"use step"` functions.
286
+ - Swap `wait.for` / `wait.until` for `sleep()` from `workflow`.
287
+ - Swap `wait.forToken` for `createHook()` (internal) or `createWebhook()` (HTTP).
288
+ - Model `wait.forToken` timeouts as `Promise.race()` between the hook and `sleep()`.
289
+ - Replace `triggerAndWait()` with `"use step"` wrappers around `start()` and `getRun()`.
290
+ - Replace `batch.triggerAndWait()` with `Promise.all` over the collected child `Run` handles.
291
+ - Move `schemaTask` validation to the call site; pass typed arguments into the workflow.
292
+ - Replace `AbortTaskRunError` with `FatalError`; model retries per step with `RetryableError` and `maxRetries`.
293
+ - Use `getStepMetadata().stepId` as the idempotency key for external side effects.
294
+ - Replace `metadata.stream()` and Realtime with `getWritable()`.
295
+ - Remove the `@trigger.dev/sdk` dependency, `trigger.config.ts`, and any self-hosted worker.
296
+ - Deploy and verify runs end-to-end with the built-in observability UI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow",
3
- "version": "5.0.0-beta.1",
3
+ "version": "5.0.0-beta.2",
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.1",
60
- "@workflow/cli": "5.0.0-beta.1",
61
- "@workflow/core": "5.0.0-beta.1",
62
- "@workflow/errors": "5.0.0-beta.0",
63
- "@workflow/typescript-plugin": "5.0.0-beta.1",
64
- "@workflow/utils": "5.0.0-beta.0",
65
- "@workflow/next": "5.0.0-beta.1",
66
- "@workflow/nest": "5.0.0-beta.1",
67
- "@workflow/nitro": "5.0.0-beta.1",
68
- "@workflow/nuxt": "5.0.0-beta.1",
69
- "@workflow/sveltekit": "5.0.0-beta.1",
70
- "@workflow/rollup": "5.0.0-beta.1"
59
+ "@workflow/astro": "5.0.0-beta.2",
60
+ "@workflow/core": "5.0.0-beta.2",
61
+ "@workflow/errors": "5.0.0-beta.1",
62
+ "@workflow/cli": "5.0.0-beta.2",
63
+ "@workflow/typescript-plugin": "5.0.0-beta.2",
64
+ "@workflow/next": "5.0.0-beta.2",
65
+ "@workflow/utils": "5.0.0-beta.1",
66
+ "@workflow/nuxt": "5.0.0-beta.2",
67
+ "@workflow/nest": "5.0.0-beta.2",
68
+ "@workflow/nitro": "5.0.0-beta.2",
69
+ "@workflow/sveltekit": "5.0.0-beta.2",
70
+ "@workflow/rollup": "5.0.0-beta.2"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/ms": "2.1.0",