workflow 5.0.0-beta.35 → 5.0.0-beta.37

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 (72) hide show
  1. package/dist/nest-builder.d.ts +2 -0
  2. package/dist/nest-builder.d.ts.map +1 -0
  3. package/dist/nest-builder.js +2 -0
  4. package/dist/nest-vercel-builder.d.ts +2 -0
  5. package/dist/nest-vercel-builder.d.ts.map +1 -0
  6. package/dist/nest-vercel-builder.js +2 -0
  7. package/dist/runtime.d.ts +1 -1
  8. package/dist/runtime.d.ts.map +1 -1
  9. package/dist/runtime.js +1 -1
  10. package/docs/ai/index.mdx +1 -1
  11. package/docs/api-reference/workflow/create-hook.mdx +43 -2
  12. package/docs/api-reference/workflow/define-hook.mdx +26 -24
  13. package/docs/api-reference/workflow/fatal-error.mdx +29 -7
  14. package/docs/api-reference/workflow/fetch.mdx +3 -4
  15. package/docs/api-reference/workflow/set-attributes.mdx +0 -4
  16. package/docs/api-reference/workflow/sleep.mdx +1 -1
  17. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +2 -0
  18. package/docs/api-reference/workflow-api/resume-hook.mdx +2 -0
  19. package/docs/api-reference/workflow-api/resume-webhook.mdx +6 -4
  20. package/docs/api-reference/workflow-api/start.mdx +1 -1
  21. package/docs/api-reference/workflow-globals.mdx +4 -1
  22. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +1 -1
  23. package/docs/api-reference/workflow-runtime/health-check.mdx +4 -4
  24. package/docs/api-reference/workflow-runtime/world/queue.mdx +4 -4
  25. package/docs/comparisons/index.mdx +66 -0
  26. package/docs/comparisons/meta.json +11 -0
  27. package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
  28. package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
  29. package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
  30. package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
  31. package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
  32. package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +103 -0
  33. package/docs/configuration/build-and-diagnostics.mdx +19 -0
  34. package/docs/configuration/runtime-tuning.mdx +30 -6
  35. package/docs/configuration/worlds.mdx +22 -8
  36. package/docs/cookbook/advanced/publishing-libraries.mdx +17 -13
  37. package/docs/cookbook/common-patterns/idempotency.mdx +1 -1
  38. package/docs/{deploying/index.mdx → deploying.mdx} +6 -8
  39. package/docs/foundations/hooks.mdx +1 -1
  40. package/docs/foundations/idempotency.mdx +16 -9
  41. package/docs/getting-started/astro.mdx +2 -2
  42. package/docs/getting-started/express.mdx +2 -2
  43. package/docs/getting-started/fastify.mdx +2 -2
  44. package/docs/getting-started/hono.mdx +2 -2
  45. package/docs/getting-started/index.mdx +3 -3
  46. package/docs/getting-started/meta.json +2 -1
  47. package/docs/getting-started/nestjs.mdx +64 -3
  48. package/docs/getting-started/next.mdx +3 -3
  49. package/docs/getting-started/nitro.mdx +2 -2
  50. package/docs/getting-started/nuxt.mdx +2 -2
  51. package/docs/getting-started/sveltekit.mdx +2 -2
  52. package/docs/getting-started/tanstack-start.mdx +1 -1
  53. package/docs/getting-started/vite.mdx +2 -2
  54. package/docs/how-it-works/cancellation.mdx +2 -2
  55. package/docs/how-it-works/code-transform.mdx +19 -15
  56. package/docs/how-it-works/encryption.mdx +3 -3
  57. package/docs/how-it-works/event-sourcing.mdx +6 -6
  58. package/docs/how-it-works/framework-integrations.mdx +96 -342
  59. package/docs/meta.json +1 -1
  60. package/package.json +14 -12
  61. package/docs/deploying/building-a-world.mdx +0 -251
  62. package/docs/deploying/meta.json +0 -4
  63. package/docs/deploying/world/local-world.mdx +0 -105
  64. package/docs/deploying/world/meta.json +0 -4
  65. package/docs/deploying/world/postgres-world.mdx +0 -288
  66. package/docs/deploying/world/vercel-world.mdx +0 -270
  67. package/docs/migration-guides/index.mdx +0 -34
  68. package/docs/migration-guides/meta.json +0 -9
  69. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -362
  70. package/docs/migration-guides/migrating-from-inngest.mdx +0 -308
  71. package/docs/migration-guides/migrating-from-temporal.mdx +0 -317
  72. package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -332
@@ -1,308 +0,0 @@
1
- ---
2
- title: Migrating from Inngest
3
- description: Move an Inngest TypeScript SDK v3 or v4 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
- <CopyPrompt
18
- text="Migrate this Inngest code to Workflow SDK. Run `npm i workflow`. Replace each `inngest.createFunction` handler with an exported async function that contains `&quot;use workflow&quot;`. Replace each `step.run()` callback with a helper function that contains `&quot;use step&quot;`. Replace `step.sleep()` with `sleep()` from `workflow`. Replace event waits with `defineHook()` or `createWebhook()` from `workflow`, and resume them from server routes with `resumeHook()` or webhook requests. Start runs from API routes or server code with `start(workflowFn, args)` from `workflow/api`, and use `getRun(runId)` where callers need status, streams, cancellation, or results. Preserve retries by throwing normal errors for retryable failures and `FatalError` for permanent failures. Add a local test or route call that starts the migrated workflow and verifies the same observable behavior as the Inngest function."
19
- />
20
-
21
- <Callout type="info">
22
- Install the Workflow SDK migration skill:
23
-
24
- ```bash
25
- npx skills add https://github.com/vercel/workflow --skill migrating-to-workflow-sdk
26
- ```
27
- </Callout>
28
-
29
- ## Why migrate to the Workflow SDK
30
-
31
- - 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.
32
- - 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.
33
- - TypeScript-first DX. Steps are named async functions marked with `"use step"`. No inline closures tied to a framework-specific lifecycle.
34
- - Agent-first tooling: the `npx workflow` CLI, `@workflow/ai` integration for durable AI agents, and a Claude skill for generating workflows.
35
-
36
- Inngest code samples in this guide use the v4 two-argument `createFunction({ id, triggers }, handler)` shape. If the app is still on Inngest v3, treat the three-argument form `createFunction({ id }, { event: '...' }, handler)` as equivalent — the `triggers` option in v4 replaces the second argument in v3.
37
-
38
- ## What changes when you leave Inngest
39
-
40
- 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.
41
-
42
- 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.
43
-
44
- Migration collapses the SDK abstraction into plain async functions. Business logic stays the same.
45
-
46
- <Callout type="info">
47
- Inngest's event-bus model is loosely coupled — publishers don't know consumers. `start()` requires the caller to import the workflow function directly, giving stronger type safety but tighter coupling. For event-bus-like fan-out, wrap `start()` in a shared publisher module.
48
- </Callout>
49
-
50
- ## Concept mapping
51
-
52
- | Inngest | Workflow SDK | Migration note |
53
- | --- | --- | --- |
54
- | `inngest.createFunction()` | `"use workflow"` function started with `start()` | No wrapper needed. |
55
- | `step.run()` | `"use step"` function | Standalone async function with Node.js access. |
56
- | `step.sleep()` / `step.sleepUntil()` | `sleep()` | `sleep('5m')` for a duration; `sleep(date)` for sleep-until. |
57
- | `step.waitForEvent()` | `createHook()` or `createWebhook()` | Hooks for typed signals, webhooks for HTTP. |
58
- | `step.invoke()` | `start()` plus a `"use step"` wrapper around `getRun()` | Spawn a child run, pass `runId` forward, and collect from a step when needed. |
59
- | `inngest.send()` / event triggers | `start()` from your app boundary | Start workflows directly. |
60
- | Retry configuration (`retries`) | `RetryableError`, `FatalError`, `maxRetries` | Retry logic lives at the step level. |
61
- | `step.sendEvent()` | `start()` from the workflow or app boundary | Fan out explicitly, not through an event bus. |
62
- | 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. |
63
-
64
- ## Translate your first workflow
65
-
66
- Start with the shell of a function. Inngest wraps it in `createFunction`; Workflow SDK marks it with a directive.
67
-
68
- {/* @skip-typecheck: Inngest SDK types not available */}
69
- ```typescript title="inngest/functions/order.ts"
70
- export const processOrder = inngest.createFunction(
71
- {
72
- id: 'process-order',
73
- triggers: { event: 'order/created' },
74
- },
75
- async ({ event, step }) => {
76
- return { orderId: event.data.orderId, status: 'completed' };
77
- }
78
- );
79
- ```
80
-
81
- ```typescript title="workflow/workflows/order.ts"
82
- export async function processOrder(orderId: string) {
83
- 'use workflow'; // [!code highlight]
84
- return { orderId, status: 'completed' };
85
- }
86
- ```
87
-
88
- **What changed:** the factory + event binding collapses into a plain exported function with a `"use workflow"` directive.
89
-
90
- ### Add a step
91
-
92
- `step.run()` closures become named `"use step"` functions.
93
-
94
- ```typescript title="workflow/workflows/order.ts"
95
- async function loadOrder(orderId: string) {
96
- 'use step'; // [!code highlight]
97
- const res = await fetch(`https://example.com/api/orders/${orderId}`);
98
- return res.json() as Promise<{ id: string }>;
99
- }
100
- ```
101
-
102
- Call it from the workflow like any async function: `const order = await loadOrder(orderId)`. Additional side effects (`reserveInventory`, `chargePayment`) follow the same shape.
103
-
104
- ### Start the run
105
-
106
- Inngest dispatches via `inngest.send({ name: 'order/created', data: { orderId } })`. Workflow SDK launches directly:
107
-
108
- ```typescript title="app/api/orders/route.ts"
109
- import { start } from 'workflow/api';
110
- import { processOrder } from '@/workflows/order';
111
-
112
- const run = await start(processOrder, [orderId]); // [!code highlight]
113
- ```
114
-
115
- No event bus, no registry. `start()` returns a handle immediately.
116
-
117
- ## Wait for an external signal
118
-
119
- `step.waitForEvent()` becomes `createHook()` plus `await`.
120
-
121
- ```typescript title="workflow/workflows/refund.ts (Inngest)"
122
- const approval = await step.waitForEvent('wait-for-approval', {
123
- event: 'refund/approved',
124
- match: 'data.refundId',
125
- timeout: '7d',
126
- });
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:** event name + match expression collapse into a single `token` string. The caller supplies that token directly, with no event schema.
138
-
139
- ### Resume from an API route
140
-
141
- Inngest resumes with `inngest.send({ name: 'refund/approved', data: { refundId, approved } })`. The SDK equivalent is `resumeHook`:
142
-
143
- ```typescript title="app/api/refunds/[refundId]/approve/route.ts"
144
- import { resumeHook } from 'workflow/api';
145
-
146
- export async function POST(request: Request, { params }: { params: Promise<{ refundId: string }> }) {
147
- const { refundId } = await params;
148
- const { approved } = (await request.json()) as { approved: boolean };
149
- await resumeHook(`refund:${refundId}:approval`, { approved }); // [!code highlight]
150
- return Response.json({ ok: true });
151
- }
152
- ```
153
-
154
- ### Add a timeout and branch on the payload
155
-
156
- Inngest's `timeout: '7d'` option maps to a `Promise.race()` with `sleep()`:
157
-
158
- {/* @skip-typecheck: continuation snippet */}
159
- ```typescript title="workflow/workflows/refund.ts"
160
- const result = await Promise.race([
161
- approval.then((p) => ({ type: 'decision' as const, approved: p.approved })),
162
- sleep('7d').then(() => ({ type: 'timeout' as const })), // [!code highlight]
163
- ]);
164
-
165
- if (result.type === 'timeout') return { refundId, status: 'timed-out' };
166
- if (!result.approved) return { refundId, status: 'rejected' };
167
- return { refundId, status: 'approved' };
168
- ```
169
-
170
- <Callout type="info">
171
- Event matching disappears. A hook's token encodes the routing (for example, `refund:${refundId}:approval`), and the caller supplies that token to `resumeHook()`.
172
- </Callout>
173
-
174
- ## Spawn a child workflow
175
-
176
- `step.invoke()` splits into spawn and collect. In v5, call `start()` directly from the workflow to spawn the child. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to collect the result. Returning the `Run` object from `start()` lets observability deep-link into the child run.
177
-
178
- You can return either the full `Run` object (enables deep-linking) or just `run.runId` (simpler).
179
-
180
- {/* @skip-typecheck: snippet without imports */}
181
- ```typescript title="workflow/workflows/parent.ts"
182
- async function collectResult(runId: string) {
183
- 'use step';
184
- const run = getRun(runId);
185
- return await run.returnValue; // [!code highlight]
186
- }
187
-
188
- export async function parentWorkflow(item: string) {
189
- 'use workflow';
190
- const child = await start(childWorkflow, [item]); // [!code highlight]
191
- return await collectResult(child.runId);
192
- }
193
- ```
194
-
195
- ## What you stop operating
196
-
197
- Dropping the Inngest SDK removes several moving parts:
198
-
199
- - **No SDK client or serve handler.** Workflow files carry directive annotations. No registry, no serve endpoint.
200
- - **No event bus.** `start()` launches workflows directly from API routes, server actions, or other entry points. No event schemas or dispatch layer.
201
- - **No inline step closures.** Steps are named async functions. They type-check and test like any other TypeScript function.
202
- - **No separate streaming transport.** `getWritable()` delivers progress to clients without WebSockets or SSE glue.
203
- - **No idle workers.** Workflows suspended on `sleep()` or a hook consume no compute until resumed.
204
-
205
- ## Step-by-step first migration
206
-
207
- Pick one Inngest function and migrate it end-to-end before touching the rest. The steps below describe the smallest viable path.
208
-
209
- ### Step 1: Install the Workflow SDK
210
-
211
- Install the SDK. Framework integrations (`workflow/next`, `workflow/nitro`, `workflow/nuxt`, etc.) are subpath exports of the same `workflow` package — no additional install needed.
212
-
213
- ```bash
214
- pnpm add workflow
215
- ```
216
-
217
- ### Step 2: Convert `createFunction` to a `"use workflow"` export
218
-
219
- Replace the factory call with a plain async export. Move the handler body up. The event-binding argument goes away.
220
-
221
- ```ts title="workflows/order.ts"
222
- // Before (Inngest)
223
- // export const processOrder = inngest.createFunction(
224
- // { id: "process-order", triggers: { event: "order/created" } },
225
- // async ({ event, step }) => { ... }
226
- // );
227
-
228
- // After (Workflow SDK)
229
- export async function processOrder(orderId: string) {
230
- "use workflow"; // [!code highlight]
231
- // ...
232
- }
233
- ```
234
-
235
- ### Step 3: Convert `step.run` callbacks into named step functions
236
-
237
- Each inline callback becomes a named function with `"use step"` on the first line. The workflow calls them with a plain `await`.
238
-
239
- ```ts
240
- async function loadOrder(id: string) {
241
- "use step"; // [!code highlight]
242
- return fetch(`https://example.com/api/orders/${id}`).then((r) => r.json());
243
- }
244
- ```
245
-
246
- Configure per-step retry counts by assigning `maxRetries` as a function property:
247
-
248
- ```ts
249
- async function callApi(endpoint: string) {
250
- "use step";
251
- const response = await fetch(endpoint);
252
- return response.json();
253
- }
254
- callApi.maxRetries = 5;
255
- ```
256
-
257
- See [Errors and retries](/docs/foundations/errors-and-retries) for full retry docs.
258
-
259
- ### Step 4: Replace `waitForEvent`, `sleep`, and `invoke`
260
-
261
- - `step.waitForEvent(...)` → `createHook({ token })` + `await hook`. Resume it from an API route with `resumeHook(token, payload)`.
262
- - `step.sleep(...)` → `sleep("5m")` from `workflow`.
263
- - `step.invoke(child, { data })` → call `start(child, [data])` from the workflow, and optionally read its return value from a step with `getRun(run.runId).returnValue`.
264
-
265
- ### Step 5: Start runs from the app
266
-
267
- Delete the `serve()` handler and event dispatch. Launch runs directly from an API route:
268
-
269
- ```ts title="app/api/orders/route.ts"
270
- import { start } from "workflow/api";
271
- import { processOrder } from "@/workflows/order";
272
-
273
- export async function POST(req: Request) {
274
- const { orderId } = await req.json();
275
- const run = await start(processOrder, [orderId]);
276
- return Response.json({ runId: run.runId });
277
- }
278
- ```
279
-
280
- ### Step 6: Retire the Inngest infrastructure
281
-
282
- 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.
283
-
284
- ## Features without a 1:1 equivalent
285
-
286
- - **Cron / scheduled functions (`triggers: { cron: '...' }`).** The SDK has no built-in scheduler. Trigger runs from Vercel Cron or a system cron calling `start()` from an API route.
287
- - **Concurrency, throttling, rate limiting, debounce, singleton, priority, and `batchEvents`.** These function-level settings have no direct analog. Enforce limits inside steps (semaphores, external rate-limiter service) or debounce at the publisher before calling `start()`.
288
- - **`EventSchemas` / typed events.** The event-bus indirection goes away; publishers import the workflow function directly, giving the same type safety through a different mechanism.
289
- - **Event-bus fan-out by name match.** `inngest.send()` that triggered multiple functions by event name must be replaced by explicit `start()` calls for each target workflow.
290
-
291
- ## Quick-start checklist
292
-
293
- - Replace `inngest.createFunction()` with a `"use workflow"` function; launch it with `start()`.
294
- - Convert each `step.run()` callback into a named `"use step"` function.
295
- - Swap `step.sleep()` / `step.sleepUntil()` for `sleep()` from `workflow`.
296
- - Swap `step.waitForEvent()` for `createHook()` (internal) or `createWebhook()` (HTTP).
297
- - Model `waitForEvent` timeouts as `Promise.race()` between the hook and `sleep()`.
298
- - Replace `step.invoke()` with direct `start()` calls and a `"use step"` wrapper around `getRun()` when collecting results.
299
- - Replace `step.sendEvent()` fan-out with explicit `start()` calls.
300
- - Remove the Inngest client, `serve()` handler, and event definitions.
301
- - Push retry configuration down to step boundaries via `maxRetries`, `RetryableError`, and `FatalError`.
302
- - Use `getStepMetadata().stepId` as the idempotency key for external side effects.
303
- - Replace `step.realtime.publish()` with `getWritable()`.
304
- - Deploy and verify end-to-end with the built-in observability UI.
305
-
306
- ---
307
-
308
- *Verified against `workflow@5.0.0-beta.1` and Inngest TypeScript SDK v4 on 2026-04-16.*
@@ -1,317 +0,0 @@
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
- <CopyPrompt
18
- text="Refactor this Temporal TypeScript code to Workflow SDK. Run `npm i workflow`. Replace Temporal workflow functions with exported async functions containing `&quot;use workflow&quot;`. Replace Activities with helper functions containing `&quot;use step&quot;`. Replace timers with `sleep()` from `workflow`. Replace Signals with `defineHook()` plus `resumeHook()` from `workflow/api`, or `createWebhook()` when an HTTP callback URL is needed. Replace Child Workflows with `start(childWorkflow, args)` and coordinate completion with hooks or `getRun()`. Remove Temporal workers; instead expose API routes or server functions that call `start()` and `getRun()` from `workflow/api`. Preserve idempotency and retries with normal errors, `RetryableError`, and `FatalError`, then add a verification path for each migrated workflow."
19
- />
20
-
21
- <Callout type="info">
22
- Install the Workflow SDK migration skill:
23
-
24
- ```bash
25
- npx skills add https://github.com/vercel/workflow --skill migrating-to-workflow-sdk
26
- ```
27
- </Callout>
28
-
29
- ## Why migrate to the Workflow SDK
30
-
31
- - 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.
32
- - 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.
33
- - TypeScript-first developer experience. Workflows and steps live in the same file with plain `await` control flow, `try/catch`, and `Promise.all`.
34
- - Agent-first tooling. A first-class CLI (`npx workflow`), [`@workflow/ai` integration for durable AI agents](/docs/ai), and a bundled Claude skill for AI-assisted authoring.
35
- - Per-step retry controls. `RetryableError`, `FatalError`, and `maxRetries` live at the step boundary instead of an Activity-level retry policy configured elsewhere.
36
-
37
- ## What changes when you leave Temporal
38
-
39
- 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.
40
-
41
- 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.
42
-
43
- Workflow functions must still be deterministic — no `Date.now()`, `Math.random()`, direct network I/O, or wall-clock branches inside `"use workflow"`. Move any such logic into a step, as you do today with Activities.
44
-
45
- Migration removes infrastructure and collapses indirection. Business logic stays as regular async TypeScript.
46
-
47
- ## Concept mapping
48
-
49
- | Temporal | Workflow SDK | Migration note |
50
- | --- | --- | --- |
51
- | Workflow Definition / Workflow Execution | `"use workflow"` function / run started with `start()` | Keep orchestration code in the workflow function. |
52
- | Activity | `"use step"` function | Put side effects and Node.js access in steps. |
53
- | Worker + Task Queue | Managed execution | No worker fleet or polling loop to operate. |
54
- | Signal | `createHook()` or `createWebhook()` | Use hooks for typed resume signals; webhooks for HTTP callbacks. |
55
- | Query | `getWritable({ namespace: 'status' })` stream | Durably stream status updates from the workflow. Clients read from the stream instead of polling a database. |
56
- | Update | `createHook()` + `resumeHook()` (one-way) | Temporal Updates return a value to the caller; hooks do not. If the Update returns data, either write the result to a named stream via `getWritable()` and have the caller read from it, or keep an HTTP read route that fetches the workflow's current state. |
57
- | Child Workflow | `start()` plus a `"use step"` wrapper around `getRun()` | Spawn a child run and return the `Run` object so observability can deep-link into child runs. |
58
- | Activity retry policy | Step retries, `RetryableError`, `FatalError`, `maxRetries` | Retries live at the step boundary. |
59
- | Event History | Workflow event log / run timeline | Same durable replay; built-in observability UI replaces Temporal Web. Search attributes and visibility APIs have no direct equivalent — filter by run status and timestamps instead. |
60
-
61
- ## Translate your first workflow
62
-
63
- ### Minimal translation
64
-
65
- Start with the directive change. The Temporal definition proxies activities through a module; the Workflow SDK version puts the directive inline.
66
-
67
- {/* @skip-typecheck: Temporal SDK types not available */}
68
- ```typescript title="workflows/order.ts (Temporal)"
69
- import * as wf from '@temporalio/workflow';
70
- import type * as activities from './activities';
71
-
72
- const { chargePayment } = wf.proxyActivities<typeof activities>({
73
- startToCloseTimeout: '5 minutes',
74
- });
75
-
76
- export async function processOrder(orderId: string) {
77
- await chargePayment(orderId);
78
- return { orderId, status: 'completed' };
79
- }
80
- ```
81
-
82
- ```typescript title="workflows/order.ts (Workflow SDK)"
83
- export async function processOrder(orderId: string) {
84
- 'use workflow'; // [!code highlight]
85
- await chargePayment(orderId);
86
- return { orderId, status: 'completed' };
87
- }
88
- ```
89
-
90
- **What changed:** `proxyActivities` and the activity module disappear. The orchestrator is plain async TypeScript marked with `"use workflow"`.
91
-
92
- ### Adding a step
93
-
94
- Side effects move into a colocated `"use step"` function:
95
-
96
- ```typescript title="workflow/workflows/order.ts"
97
- async function chargePayment(orderId: string) {
98
- 'use step'; // [!code highlight]
99
- await fetch(`https://example.com/api/orders/${orderId}/charge`, {
100
- method: 'POST',
101
- });
102
- }
103
- ```
104
-
105
- Additional steps (`loadOrder`, `reserveInventory`) follow the same shape and can be called from the workflow in sequence.
106
-
107
- ### Starting the run
108
-
109
- Replace Worker + Task Queue wiring with a single `start()` call from an API route:
110
-
111
- ```typescript title="app/api/orders/route.ts"
112
- import { start } from 'workflow/api';
113
- import { processOrder } from '@/workflows/order';
114
-
115
- export async function POST(request: Request) {
116
- const { orderId } = (await request.json()) as { orderId: string };
117
- const run = await start(processOrder, [orderId]); // [!code highlight]
118
- return Response.json({ runId: run.runId });
119
- }
120
- ```
121
-
122
- ## Wait for an external signal
123
-
124
- ### Minimal translation
125
-
126
- Temporal needs a signal definition, a handler, and a `condition()` guard. The Workflow SDK collapses all three into a single `createHook()` + `await`.
127
-
128
- {/* @skip-typecheck: Temporal SDK types not available */}
129
- ```typescript title="temporal/workflows/refund.ts"
130
- export const approveRefund = wf.defineSignal<[boolean]>('approveRefund');
131
-
132
- export async function refundWorkflow(refundId: string) {
133
- let approved: boolean | undefined;
134
- wf.setHandler(approveRefund, (v) => { approved = v; });
135
- await wf.condition(() => approved !== undefined);
136
- return { refundId, approved };
137
- }
138
- ```
139
-
140
- ```typescript title="workflow/workflows/refund.ts"
141
- import { createHook } from 'workflow';
142
-
143
- export async function refundWorkflow(refundId: string) {
144
- 'use workflow';
145
- using approval = createHook<{ approved: boolean }>({
146
- token: `refund:${refundId}:approval`, // [!code highlight]
147
- });
148
- const { approved } = await approval; // [!code highlight]
149
- return { refundId, approved };
150
- }
151
- ```
152
-
153
- The workflow suspends durably at `await approval` until resumed. No polling, no handler registration.
154
-
155
- Requires TypeScript 5.2+ for the `using` keyword. On older TypeScript, assign the hook to a `const` and call `resumeHook()` from the consuming code path.
156
-
157
- ### Resuming from an API route
158
-
159
- Any HTTP caller can resume by token:
160
-
161
- ```typescript title="app/api/refunds/[refundId]/approve/route.ts"
162
- import { resumeHook } from 'workflow/api';
163
-
164
- export async function POST(request: Request, { params }: { params: Promise<{ refundId: string }> }) {
165
- const { refundId } = await params;
166
- const body = (await request.json()) as { approved: boolean };
167
- await resumeHook(`refund:${refundId}:approval`, body); // [!code highlight]
168
- return Response.json({ ok: true });
169
- }
170
- ```
171
-
172
- <Callout type="info">
173
- 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.
174
- </Callout>
175
-
176
- ## Spawn a child workflow
177
-
178
- ### Minimal translation
179
-
180
- In v5, call `start()` directly from the workflow to spawn a child run. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to await the child result. Return the `Run` object (not a plain `runId` string) so workflow observability can deep-link into child runs.
181
-
182
- ```typescript title="workflow/workflows/parent.ts"
183
- import { start } from 'workflow/api';
184
-
185
- export async function parentWorkflow(item: string) {
186
- 'use workflow';
187
- const child = await start(childWorkflow, [item]); // [!code highlight]
188
- return { childRunId: child.runId };
189
- }
190
- ```
191
-
192
- ### Awaiting the child's return value
193
-
194
- A second step fetches the run and awaits `returnValue`:
195
-
196
- ```typescript title="workflow/workflows/parent.ts"
197
- import { getRun } from 'workflow/api';
198
-
199
- async function collectResult(runId: string) {
200
- 'use step';
201
- const run = getRun(runId);
202
- return (await run.returnValue) as { item: string; result: string }; // [!code highlight]
203
- }
204
- ```
205
-
206
- Call `start()` and then `collectResult()` from the parent in sequence: `const result = await collectResult(child.runId)`. To fan out, call `start()` inside a loop, then `Promise.all` the `collectResult` calls.
207
-
208
- <Callout type="warn">
209
- Activity retry policy moves to the step boundary. Use `maxRetries`, `RetryableError`, and `FatalError` on each step instead of a single workflow-wide retry block.
210
-
211
- Temporal's per-activity timeouts (`startToCloseTimeout`, `scheduleToCloseTimeout`, `heartbeatTimeout`) have no direct Workflow SDK equivalent. Enforce per-step deadlines inside the step using `AbortSignal.timeout(ms)` (e.g. on `fetch`), or wrap the call from the workflow in `Promise.race(step(), sleep('5m'))` to bail out after a bounded duration.
212
-
213
- Temporal's retry policy knobs (`initialInterval`, `backoffCoefficient`, `maximumInterval`, `nonRetryableErrorTypes`) don't port 1:1 — only `maxRetries` is configurable at the step boundary. Classify retryability with `RetryableError` (retryable) and `FatalError` (terminal) instead of listing error types, and control the delay between attempts with `new RetryableError(msg, { retryAfter: '5s' })`.
214
-
215
- Set `maxRetries` as a property assignment on the step function:
216
-
217
- ```typescript
218
- async function chargePayment(orderId: string) {
219
- "use step";
220
- // ...
221
- }
222
- chargePayment.maxRetries = 5;
223
- ```
224
-
225
- See [/docs/foundations/errors-and-retries](/docs/foundations/errors-and-retries) for full retry docs.
226
- </Callout>
227
-
228
- ## What you stop operating
229
-
230
- - **Temporal Server or Cloud.** Durable state lives in the managed event log.
231
- - **Worker fleet.** The runtime schedules execution; workflows run where the app runs.
232
- - **Task Queues.** No queue routing to configure or monitor.
233
- - **Activity modules and `proxyActivities`.** Steps live next to the workflow that calls them.
234
- - **Custom progress transport.** `getWritable()` streams updates from steps.
235
-
236
- Suspended workflows (on `sleep()` or a hook) consume no compute until resumed.
237
-
238
- ## Step-by-step first migration
239
-
240
- Pick one Temporal workflow and migrate it end-to-end before touching the rest. The steps below describe the smallest viable path.
241
-
242
- ### Step 1: Install the Workflow SDK
243
-
244
- Add the runtime. The Next.js integration ships as a subpath (`workflow/next`) of the same package.
245
-
246
- ```bash
247
- pnpm add workflow
248
- ```
249
-
250
- ### Step 2: Collapse Activities into step functions
251
-
252
- 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.
253
-
254
- ```ts title="workflows/order.ts"
255
- async function loadOrder(id: string) {
256
- "use step"; // [!code highlight]
257
- return fetch(`/api/orders/${id}`).then((r) => r.json());
258
- }
259
- ```
260
-
261
- ### Step 3: Mark the orchestrator with `"use workflow"`
262
-
263
- Keep the existing control flow: `await`, `try/catch`, `Promise.all`. The directive turns the function into a durable replay target.
264
-
265
- ```ts
266
- export async function processOrder(orderId: string) {
267
- "use workflow"; // [!code highlight]
268
- const order = await loadOrder(orderId);
269
- // ...
270
- }
271
- ```
272
-
273
- ### Step 4: Replace Signals with hooks
274
-
275
- Swap `defineSignal` + `setHandler` for `createHook()`. Callers `resumeHook(token, payload)` instead of `handle.signal(signalDef, payload)` on a `WorkflowHandle` obtained from `client.workflow.getHandle(workflowId)`.
276
-
277
- ### Step 5: Start runs from an API route or server action
278
-
279
- Delete the Worker bootstrap. Launch runs from an API route or server action with `start()`:
280
-
281
- ```ts title="app/api/orders/route.ts"
282
- import { start } from "workflow/api";
283
- import { processOrder } from "@/workflows/order";
284
-
285
- export async function POST(req: Request) {
286
- const { orderId } = await req.json();
287
- const run = await start(processOrder, [orderId]);
288
- return Response.json({ runId: run.runId });
289
- }
290
- ```
291
-
292
- ### Step 6: Retire the Temporal infrastructure
293
-
294
- 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.
295
-
296
- ## Features without a 1:1 equivalent
297
-
298
- - **Search attributes / visibility queries.** Temporal's search attribute system has no direct analog. Filter runs by status and timestamps via `getRun()` / observability UI.
299
- - **Event history archival.** Temporal archives histories to S3/GCS for long-term retention. Workflow SDK event logs are durable, but retention depends on the integration you are using. For example, see [Vercel Workflow Storage Retention](https://vercel.com/docs/workflows/pricing#storage-retention) for Vercel.
300
- - **Per-activity timeouts (`startToCloseTimeout`, `scheduleToCloseTimeout`, `heartbeatTimeout`).** Implement deadlines inside the step with `AbortSignal.timeout(ms)`, or wrap the call in `Promise.race(step(), sleep(...))` from the workflow.
301
- - **Rich retry policy (`initialInterval`, `backoffCoefficient`, `maximumInterval`, `nonRetryableErrorTypes`).** Only `maxRetries` is configurable. Classify retryability with `RetryableError`/`FatalError`; control delay between attempts via `new RetryableError(msg, { retryAfter: '5s' })`.
302
- - **Workers + task queues.** Managed deployments replace workers; self-hosted deployments still need a `World` implementation (see [/docs/deploying/building-a-world](/docs/deploying/building-a-world)).
303
-
304
- ## Quick-start checklist
305
-
306
- - Move orchestration into a `"use workflow"` function.
307
- - Convert each Activity into a `"use step"` function.
308
- - Remove Worker and Task Queue code. Start workflows from the app with `start()`.
309
- - Replace Signals with `createHook()` or `createWebhook()` for HTTP callers.
310
- - Use `start()` directly for child workflows, and wrap `getRun()` in a `"use step"` function when collecting results. Return the `Run` object from `start()` so observability can deep-link into child runs.
311
- - Set retry policy per step with `maxRetries`, `RetryableError`, and `FatalError`.
312
- - Use `getStepMetadata().stepId` as the idempotency key for external side effects.
313
- - Stream status and progress from steps with `getWritable({ namespace: 'status' })`, and have clients read from the stream instead of polling.
314
- - Deploy the app and verify runs end-to-end in the built-in observability UI.
315
-
316
- ---
317
- *Verified against `workflow@5.0.0-beta.1` and `@temporalio/workflow@1.16` on 2026-04-16.*