workflow 5.0.0-beta.0 → 5.0.0-beta.10

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 (125) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.d.ts +2 -4
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +3 -7
  5. package/dist/api.d.ts +5 -1
  6. package/dist/api.d.ts.map +1 -1
  7. package/dist/api.js +14 -2
  8. package/dist/astro.js +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/internal/builtins.d.ts +17 -0
  11. package/dist/internal/builtins.d.ts.map +1 -1
  12. package/dist/internal/builtins.js +65 -1
  13. package/dist/internal/class-serialization.js +1 -1
  14. package/dist/internal/errors.js +1 -1
  15. package/dist/nest.js +1 -1
  16. package/dist/next.cjs +4 -2
  17. package/dist/next.d.cts +1 -1
  18. package/dist/next.d.cts.map +1 -1
  19. package/dist/nitro.js +1 -1
  20. package/dist/nuxt.js +1 -1
  21. package/dist/observability.d.ts +2 -2
  22. package/dist/observability.js +3 -3
  23. package/dist/runtime.d.ts +1 -1
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/runtime.js +2 -2
  26. package/dist/stdlib.js +1 -1
  27. package/dist/sveltekit.js +1 -1
  28. package/dist/typescript-plugin.cjs +1 -1
  29. package/dist/vite.js +1 -1
  30. package/dist/workflow.js +1 -1
  31. package/docs/ai/index.mdx +6 -5
  32. package/docs/ai/resumable-streams.mdx +1 -1
  33. package/docs/api-reference/vitest/index.mdx +28 -1
  34. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  35. package/docs/api-reference/workflow/experimental-set-attributes.mdx +63 -0
  36. package/docs/api-reference/workflow/get-workflow-metadata.mdx +61 -0
  37. package/docs/api-reference/workflow/index.mdx +3 -0
  38. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  39. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  40. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  41. package/docs/api-reference/workflow-api/get-world.mdx +6 -6
  42. package/docs/api-reference/workflow-api/index.mdx +1 -1
  43. package/docs/api-reference/workflow-api/start.mdx +5 -4
  44. package/docs/api-reference/workflow-api/world/index.mdx +2 -2
  45. package/docs/api-reference/workflow-api/world/observability.mdx +1 -1
  46. package/docs/api-reference/workflow-api/world/queue.mdx +1 -1
  47. package/docs/api-reference/workflow-api/world/storage.mdx +8 -8
  48. package/docs/api-reference/workflow-api/world/streams.mdx +38 -36
  49. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
  50. package/docs/api-reference/workflow-next/with-workflow.mdx +34 -2
  51. package/docs/changelog/attributes-mvp.mdx +368 -0
  52. package/docs/changelog/eager-processing.mdx +595 -0
  53. package/docs/changelog/index.mdx +2 -1
  54. package/docs/cookbook/advanced/child-workflows.mdx +313 -0
  55. package/docs/cookbook/advanced/meta.json +9 -0
  56. package/docs/cookbook/advanced/publishing-libraries.mdx +336 -0
  57. package/docs/cookbook/advanced/serializable-steps.mdx +147 -0
  58. package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
  59. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +156 -0
  60. package/docs/cookbook/agent-patterns/durable-agent.mdx +154 -0
  61. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +255 -0
  62. package/docs/cookbook/agent-patterns/meta.json +4 -0
  63. package/docs/cookbook/common-patterns/batching.mdx +105 -0
  64. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  65. package/docs/cookbook/common-patterns/meta.json +15 -0
  66. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  67. package/docs/cookbook/common-patterns/saga.mdx +247 -0
  68. package/docs/cookbook/common-patterns/scheduling.mdx +129 -0
  69. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
  70. package/docs/cookbook/common-patterns/timeouts.mdx +99 -0
  71. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  72. package/docs/cookbook/common-patterns/workflow-composition.mdx +111 -0
  73. package/docs/cookbook/index.mdx +38 -0
  74. package/docs/cookbook/integrations/ai-sdk.mdx +383 -0
  75. package/docs/cookbook/integrations/chat-sdk.mdx +307 -0
  76. package/docs/cookbook/integrations/meta.json +4 -0
  77. package/docs/cookbook/integrations/sandbox.mdx +520 -0
  78. package/docs/cookbook/meta.json +5 -0
  79. package/docs/deploying/building-a-world.mdx +46 -44
  80. package/docs/deploying/world/local-world.mdx +1 -1
  81. package/docs/deploying/world/postgres-world.mdx +15 -8
  82. package/docs/deploying/world/vercel-world.mdx +3 -1
  83. package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
  84. package/docs/errors/hook-conflict.mdx +56 -4
  85. package/docs/errors/index.mdx +3 -0
  86. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  87. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  88. package/docs/foundations/cancellation.mdx +460 -0
  89. package/docs/foundations/errors-and-retries.mdx +7 -3
  90. package/docs/foundations/index.mdx +3 -3
  91. package/docs/foundations/meta.json +3 -2
  92. package/docs/foundations/serialization.mdx +78 -42
  93. package/docs/foundations/starting-workflows.mdx +6 -2
  94. package/docs/foundations/versioning.mdx +263 -0
  95. package/docs/getting-started/astro.mdx +6 -0
  96. package/docs/getting-started/index.mdx +13 -7
  97. package/docs/getting-started/meta.json +3 -1
  98. package/docs/getting-started/nestjs.mdx +8 -0
  99. package/docs/getting-started/next.mdx +29 -3
  100. package/docs/getting-started/nitro.mdx +22 -0
  101. package/docs/getting-started/python.mdx +165 -0
  102. package/docs/getting-started/sveltekit.mdx +6 -0
  103. package/docs/getting-started/tanstack-start.mdx +241 -0
  104. package/docs/how-it-works/cancellation.mdx +287 -0
  105. package/docs/how-it-works/code-transform.mdx +8 -7
  106. package/docs/how-it-works/event-sourcing.mdx +2 -2
  107. package/docs/how-it-works/meta.json +2 -1
  108. package/docs/internal/index.mdx +19 -0
  109. package/docs/internal/meta.json +5 -0
  110. package/docs/internal/serializable-abort-controller.mdx +148 -0
  111. package/docs/meta.json +1 -0
  112. package/docs/migration-guides/index.mdx +34 -0
  113. package/docs/migration-guides/meta.json +9 -0
  114. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +358 -0
  115. package/docs/migration-guides/migrating-from-inngest.mdx +304 -0
  116. package/docs/migration-guides/migrating-from-temporal.mdx +313 -0
  117. package/docs/migration-guides/migrating-from-trigger-dev.mdx +328 -0
  118. package/docs/observability/attributes.mdx +66 -0
  119. package/docs/observability/index.mdx +9 -1
  120. package/docs/observability/meta.json +1 -1
  121. package/package.json +14 -14
  122. package/dist/internal/private.d.ts +0 -6
  123. package/dist/internal/private.d.ts.map +0 -1
  124. package/dist/internal/private.js +0 -6
  125. package/docs/foundations/common-patterns.mdx +0 -265
@@ -0,0 +1,148 @@
1
+ ---
2
+ title: Serializable AbortController and AbortSignal
3
+ description: AbortController and AbortSignal now work across workflow and step boundaries using the standard Web API.
4
+ type: overview
5
+ ---
6
+
7
+ # Serializable AbortController and AbortSignal
8
+
9
+ <span className="text-sm text-fd-muted-foreground">March 12, 2026</span>
10
+
11
+ `AbortController` and `AbortSignal` now work natively in workflow functions. Create a controller, pass its signal to steps, and call `abort()` — no special imports or wrapper functions needed.
12
+
13
+ ## What's new
14
+
15
+ - **Standard API, zero boilerplate.** `new AbortController()` works inside `"use workflow"` functions. The controller and its signal are automatically serialized across workflow and step boundaries.
16
+ - **Dual hook + stream backing for durability.** Under the hood, each controller is backed by a durable [hook](/docs/foundations/hooks) (for replay correctness) and a [stream](/docs/foundations/streaming) (for real-time propagation to running steps). This means aborts survive cold starts, replays, and scale events.
17
+ - **Cooperative cancellation.** Steps receive the abort in real time and can respond by checking `signal.aborted`, calling `signal.throwIfAborted()`, or passing the signal to APIs like `fetch`.
18
+ - **Abort errors skip retries.** When a step throws due to an abort (e.g., `fetch` throws `AbortError`), the error is automatically wrapped in `FatalError` so it skips retries and bubbles up immediately.
19
+ - **`AbortSignal.timeout()` blocked in workflow VM.** Because it relies on real-time timers that break deterministic replay, `AbortSignal.timeout()` throws a helpful error pointing to the `sleep()` + `AbortController` pattern instead.
20
+ - **`Request.signal` preserved when it carries abort state.** A `Request`'s `.signal` is serialized when it's already aborted (so the cancellation that happened pre-serialization is preserved) or when it's a workflow-managed signal (so its hook + stream backing carries through). Plain non-aborted native signals — including the auto-generated signal on `new Request(url)` — are dropped to avoid minting stream infrastructure for every `Request`. To get cross-boundary cancellation through a `Request`, build it with the signal from a workflow-context `AbortController`.
21
+ - **Pending queue items drain on completion.** If you call `abort()` (or `dispose` a hook, or kick off a `void sleep('1d')`, or fire a `void someStep()`) without a suspension point between that call and the workflow's return, the runtime now treats end-of-run as a final suspension and commits all pending operations before the run is marked terminal. This matches normal JS semantics — `setTimeout` etc. continue running after the surrounding function returns. The most important case: `controller.abort()` called as the last statement of a workflow now actually propagates to in-flight steps on other compute instances.
22
+
23
+ ## Timeout with cancellation
24
+
25
+ Race a step against a durable `sleep()`, and cancel the step if the timeout wins:
26
+
27
+ ```typescript
28
+ import { sleep } from "workflow";
29
+
30
+ export async function fetchWithTimeout(url: string) {
31
+ "use workflow";
32
+
33
+ const controller = new AbortController();
34
+
35
+ const result = await Promise.race([
36
+ fetchUrl(url, controller.signal),
37
+ sleep("10s").then(() => null),
38
+ ]);
39
+
40
+ if (result === null) {
41
+ controller.abort();
42
+ throw new Error(`Request to ${url} timed out after 10s`);
43
+ }
44
+
45
+ return result;
46
+ }
47
+
48
+ async function fetchUrl(url: string, signal: AbortSignal) {
49
+ "use step";
50
+ const response = await fetch(url, { signal });
51
+ return response.json();
52
+ }
53
+ ```
54
+
55
+ ## Cancelling parallel work
56
+
57
+ When racing multiple steps, cancel the losers:
58
+
59
+ ```typescript
60
+ declare function fetchUrl(url: string, signal: AbortSignal): Promise<{ url: string; data: unknown }>; // @setup
61
+
62
+ export async function firstResponder(urls: string[]) {
63
+ "use workflow";
64
+
65
+ const controller = new AbortController();
66
+
67
+ const result = await Promise.race(
68
+ urls.map((url) => fetchUrl(url, controller.signal))
69
+ );
70
+
71
+ controller.abort(); // Cancel remaining fetches
72
+
73
+ return result;
74
+ }
75
+ ```
76
+
77
+ ## User-triggered cancellation with hooks
78
+
79
+ Combine hooks with abort controllers to let users cancel work from an external API:
80
+
81
+ ```typescript
82
+ declare function doExpensiveWork(signal: AbortSignal): Promise<unknown>; // @setup
83
+ import { createHook } from "workflow";
84
+
85
+ export async function userCancellableWorkflow(jobId: string) {
86
+ "use workflow";
87
+
88
+ using cancelHook = createHook<{ reason: string }>({
89
+ token: `cancel:${jobId}`,
90
+ });
91
+
92
+ const controller = new AbortController();
93
+ const workPromise = doExpensiveWork(controller.signal);
94
+
95
+ const result = await Promise.race([
96
+ workPromise.then((data) => ({ status: "completed", data })),
97
+ cancelHook.then((payload) => {
98
+ controller.abort();
99
+ return { status: "cancelled", reason: payload.reason };
100
+ }),
101
+ ]);
102
+
103
+ return result;
104
+ }
105
+ ```
106
+
107
+ ## Step-initiated abort
108
+
109
+ A step can receive the full `AbortController` and call `abort()` to cancel parallel work — useful for watchdog patterns like quota monitoring:
110
+
111
+ ```typescript
112
+ declare function processData(url: string, signal: AbortSignal): Promise<{ processed: boolean }>; // @setup
113
+
114
+ export async function processWithQuotaCheck(userId: string, dataUrl: string) {
115
+ "use workflow";
116
+
117
+ const controller = new AbortController();
118
+
119
+ const [result] = await Promise.all([
120
+ processData(dataUrl, controller.signal),
121
+ monitorQuota(userId, controller),
122
+ ]);
123
+
124
+ return result;
125
+ }
126
+
127
+ async function monitorQuota(userId: string, controller: AbortController) {
128
+ "use step";
129
+
130
+ while (!controller.signal.aborted) {
131
+ const quota = await fetch(`https://api.example.com/quota/${userId}`);
132
+ const { exceeded } = await quota.json();
133
+
134
+ if (exceeded) {
135
+ controller.abort("Quota exceeded"); // Cancels processData
136
+ return;
137
+ }
138
+
139
+ await new Promise((resolve) => setTimeout(resolve, 5000));
140
+ }
141
+ }
142
+ ```
143
+
144
+ ## Learn more
145
+
146
+ - [Cancellation](/docs/foundations/cancellation) — Full guide with all usage patterns
147
+ - [How Cancellation Works](/docs/how-it-works/cancellation) — Hook and stream internals
148
+ - [AbortSignal.timeout() in Workflow](/docs/errors/abort-signal-timeout-in-workflow) — Why `AbortSignal.timeout()` is blocked and what to use instead
package/docs/meta.json CHANGED
@@ -10,6 +10,7 @@
10
10
  "testing",
11
11
  "deploying",
12
12
  "errors",
13
+ "migration-guides",
13
14
  "api-reference"
14
15
  ]
15
16
  }
@@ -0,0 +1,34 @@
1
+ ---
2
+ title: Migration Guides
3
+ description: Move your existing durable workflow system to the Workflow SDK with side-by-side code comparisons and a realistic migration example.
4
+ type: overview
5
+ summary: Migrate from Temporal, Inngest, AWS Step Functions, or trigger.dev to the Workflow SDK.
6
+ related:
7
+ - /docs/foundations/workflows-and-steps
8
+ - /docs/getting-started
9
+ ---
10
+
11
+ <Callout type="info">
12
+ Install the Workflow SDK migration skill:
13
+
14
+ ```bash
15
+ npx skills add https://github.com/vercel/workflow --skill migrating-to-workflow-sdk
16
+ ```
17
+ </Callout>
18
+
19
+ Move an existing orchestration system to the Workflow SDK. Each guide pairs a concept-mapping table with side-by-side code, so you can translate one piece of your codebase at a time.
20
+
21
+ <Cards>
22
+ <Card href="/docs/migration-guides/migrating-from-temporal" title="Migrating from Temporal">
23
+ Map Activities, Workers, Signals, and Child Workflows onto workflows, steps, hooks, and `start()` / `getRun()`.
24
+ </Card>
25
+ <Card href="/docs/migration-guides/migrating-from-inngest" title="Migrating from Inngest">
26
+ Map `createFunction`, `step.run`, `step.sleep`, `step.waitForEvent`, and `step.invoke` onto workflows, steps, and hooks.
27
+ </Card>
28
+ <Card href="/docs/migration-guides/migrating-from-aws-step-functions" title="Migrating from AWS Step Functions">
29
+ Replace ASL JSON states, Task / Choice / Wait / Parallel states, and `.waitForTaskToken` callbacks with TypeScript.
30
+ </Card>
31
+ <Card href="/docs/migration-guides/migrating-from-trigger-dev" title="Migrating from trigger.dev">
32
+ Map `task()`, `schemaTask()`, `wait.for` / `wait.forToken`, `triggerAndWait`, and `metadata.stream` onto workflows, steps, hooks, and `start()` / `getRun()`.
33
+ </Card>
34
+ </Cards>
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "Migration Guides",
3
+ "pages": [
4
+ "migrating-from-temporal",
5
+ "migrating-from-inngest",
6
+ "migrating-from-aws-step-functions",
7
+ "migrating-from-trigger-dev"
8
+ ]
9
+ }
@@ -0,0 +1,358 @@
1
+ ---
2
+ title: Migrating from AWS Step Functions
3
+ description: Move an AWS Step Functions state machine to the Workflow SDK by replacing JSON state definitions, Task states, Choice/Wait/Parallel states, Retry/Catch blocks, and .waitForTaskToken callbacks with Workflows, Steps, Hooks, and idiomatic TypeScript control flow.
4
+ type: guide
5
+ summary: Translate an AWS Step Functions state machine into the Workflow SDK with side-by-side code examples.
6
+ prerequisites:
7
+ - /docs/getting-started/next
8
+ - /docs/foundations/workflows-and-steps
9
+ related:
10
+ - /docs/foundations/starting-workflows
11
+ - /docs/foundations/errors-and-retries
12
+ - /docs/foundations/hooks
13
+ - /docs/foundations/streaming
14
+ - /docs/deploying/world/vercel-world
15
+ ---
16
+
17
+ Move an AWS Step Functions state machine to the Workflow SDK by replacing JSON state definitions with TypeScript functions. This guide shows the direct mapping between ASL states and Workflow SDK primitives.
18
+
19
+ <Callout type="info">
20
+ Install the Workflow SDK migration skill:
21
+
22
+ ```bash
23
+ npx skills add https://github.com/vercel/workflow --skill migrating-to-workflow-sdk
24
+ ```
25
+ </Callout>
26
+
27
+ ## Why migrate to the Workflow SDK
28
+
29
+ - Orchestration code is TypeScript, not JSON ASL. Transitions are `await`, branches are `if`/`switch`, and parallelism is `Promise.all`.
30
+ - Streaming is built in. Write durable progress from steps with `getWritable()` and named streams. No DynamoDB or SNS glue to surface status to clients.
31
+ - Infrastructure lives in one deployment. No separate state machine, per-task Lambda, IAM role wiring, or callback SQS queues.
32
+ - Error handling is TypeScript-native: step-level retries, `RetryableError`, and `FatalError` replace per-state Retry/Catch blocks.
33
+ - The `npx workflow` CLI and `npx workflow web` observability UI ship out of the box.
34
+ - AI/agent helpers — `@workflow/ai` for AI-SDK integration and the Claude migration skill — are available as separate installs.
35
+
36
+ ## Before you migrate
37
+
38
+ This guide assumes **Standard** workflows. Express workflows have different semantics (at-least-once, 5-minute max duration, no execution history) and may need a different target — consider keeping them on Step Functions, moving them to a queue consumer, or ensuring your steps are idempotent before replaying the pattern here.
39
+
40
+ ## What changes when you leave Step Functions?
41
+
42
+ AWS Step Functions defines workflows as JSON state machines using Amazon States Language (ASL). Each state (Task, Choice, Wait, Parallel, Map) is a node in a declarative graph. Lambda functions handle tasks, Retry/Catch blocks configure per-state error handling, and `.waitForTaskToken` manages callbacks.
43
+
44
+ The Workflow SDK replaces that JSON DSL with TypeScript. `"use workflow"` functions orchestrate `"use step"` functions in the same file. Branching is `if`/`else`. Waiting is `sleep()`. Parallelism is `Promise.all()`. Retries move down to the step level.
45
+
46
+ The migration replaces declarative configuration with idiomatic TypeScript and collapses the orchestrator and compute split. Business logic stays the same.
47
+
48
+ ## Concept mapping
49
+
50
+ | AWS Step Functions | Workflow SDK | Migration note |
51
+ | --- | --- | --- |
52
+ | State machine (ASL JSON) | `"use workflow"` function | The workflow function is the state machine. |
53
+ | Task state / Lambda | `"use step"` function | Side effects go in steps. No separate Lambda. |
54
+ | Choice state | `if` / `else` / `switch` | Native TypeScript control flow. |
55
+ | Wait state | `sleep()` | Import `sleep` from `workflow`. |
56
+ | Parallel state | `Promise.all()` | Standard concurrency primitives. |
57
+ | Map state | Inline sequential → `for` loop; bounded parallel (`MaxConcurrency: N`) → batched `Promise.all` or a concurrency limiter like `p-limit`; Distributed Map / large fan-out → `start()` per item, then step-wrapped `getRun()` to collect. | Match the concurrency mode of the original Map. |
58
+ | Retry / Catch | Step retries, `RetryableError`, `FatalError` | Retry logic moves to step boundaries. |
59
+ | `Catch` to a compensation state | `try`/`catch` in the workflow function, calling compensation steps in reverse order (push/pop a rollback stack) | See [`/docs/foundations/errors-and-retries`](/docs/foundations/errors-and-retries) for the SAGA pattern. |
60
+ | `.waitForTaskToken` | `createHook()` or `createWebhook()` | Hooks for typed signals; webhooks for HTTP. |
61
+ | Child state machine (`StartExecution`) | `start()` plus a `"use step"` wrapper around `getRun()` | Return the `Run` object, await its result from another step. |
62
+ | Execution event history | Workflow event log | Same durable replay model. |
63
+ | Progress via DynamoDB / SNS for client polling | `getWritable()` + named streams | Stream durable updates; clients read from the stream. |
64
+
65
+ <Callout type="info">
66
+ `.waitForTaskToken` becomes `createHook()` or `createWebhook()`. Choice states become `if`/`else`. Map states become `Promise.all()`. Retry policies move from per-state configuration to step-level defaults.
67
+ </Callout>
68
+
69
+ ## Translate your first workflow
70
+
71
+ Start with a single Task state. In ASL, even "call one Lambda" requires a state machine shell:
72
+
73
+ ```json title="stateMachine.asl.json (Step Functions)"
74
+ "LoadOrder": {
75
+ "Type": "Task",
76
+ "Resource": "arn:aws:states:::lambda:invoke",
77
+ "Parameters": { "FunctionName": "loadOrder", "Payload.$": "$" },
78
+ "End": true
79
+ }
80
+ ```
81
+
82
+ <Callout type="info">
83
+ Examples use JSONPath mode. If your state machine sets `QueryLanguage: 'JSONata'`, the shape of `Arguments`/`Output` fields differs but the TypeScript translation is identical.
84
+ </Callout>
85
+
86
+ ```typescript title="workflow/workflows/order.ts (Workflow SDK)"
87
+ export async function processOrder(orderId: string) {
88
+ 'use workflow'; // [!code highlight]
89
+ return await loadOrder(orderId);
90
+ }
91
+
92
+ async function loadOrder(orderId: string) {
93
+ 'use step'; // [!code highlight]
94
+ const res = await fetch(`https://example.com/api/orders/${orderId}`);
95
+ return res.json() as Promise<{ id: string }>;
96
+ }
97
+ ```
98
+
99
+ What changed: the ASL state machine and its Lambda collapse into two directive-tagged functions in one file.
100
+
101
+ ### Adding a second step
102
+
103
+ In ASL, a second Task means a new state and a `"Next"` transition. In the Workflow SDK, it's another `await`:
104
+
105
+ ```typescript
106
+ export async function processOrder(orderId: string) {
107
+ 'use workflow';
108
+ const order = await loadOrder(orderId);
109
+ await reserveInventory(order.id); // [!code highlight]
110
+ return { orderId: order.id, status: 'reserved' };
111
+ }
112
+ ```
113
+
114
+ `await` replaces `"Next"`. Each new step is a new function with `"use step"`; no additional deployment. The second version also reshapes the return value; the workflow return type can be anything serializable.
115
+
116
+ ### Starting from an API route
117
+
118
+ Step Functions starts a run via `StartExecution` (AWS SDK or API Gateway integration). The Workflow SDK starts a run with `start()` from a route handler:
119
+
120
+ ```typescript title="app/api/orders/route.ts"
121
+ import { start } from 'workflow/api';
122
+ import { processOrder } from '@/workflows/order';
123
+
124
+ export async function POST(request: Request) {
125
+ const { orderId } = (await request.json()) as { orderId: string };
126
+ const run = await start(processOrder, [orderId]); // [!code highlight]
127
+ return Response.json({ runId: run.runId });
128
+ }
129
+ ```
130
+
131
+ ### Waiting for a fixed duration
132
+
133
+ A `Wait` state becomes `sleep()`:
134
+
135
+ ```json title="stateMachine.asl.json (Step Functions)"
136
+ { "Type": "Wait", "Seconds": 60, "Next": "Next" }
137
+ ```
138
+
139
+ {/* @skip-typecheck: one-line snippet fragment */}
140
+
141
+ ```typescript title="workflow/workflows/order.ts (Workflow SDK)"
142
+ await sleep('1m');
143
+ ```
144
+
145
+ ## Wait for an external signal
146
+
147
+ The minimal ASL for a callback is a Task with `.waitForTaskToken`:
148
+
149
+ ```json title="approval.asl.json (Step Functions)"
150
+ "WaitForApproval": {
151
+ "Type": "Task",
152
+ "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken",
153
+ "Parameters": {
154
+ "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/approvals",
155
+ "MessageBody": {
156
+ "refundId.$": "$.refundId",
157
+ "TaskToken.$": "$$.Task.Token"
158
+ }
159
+ },
160
+ "End": true
161
+ }
162
+ ```
163
+
164
+ ```typescript title="workflow/workflows/refund.ts (Workflow SDK)"
165
+ import { createHook } from 'workflow';
166
+
167
+ export async function refundWorkflow(refundId: string) {
168
+ 'use workflow';
169
+ using approval = createHook<{ approved: boolean }>({ // [!code highlight]
170
+ token: `refund:${refundId}:approval`,
171
+ });
172
+ return await approval;
173
+ }
174
+ ```
175
+
176
+ What changed: no SQS queue, no task token, no callback Lambda. The hook suspends the workflow durably until it is resumed.
177
+
178
+ ### Resuming the hook
179
+
180
+ Step Functions resumes by calling `SendTaskSuccess` with the task token. The Workflow SDK resumes by calling `resumeHook` with the hook's token:
181
+
182
+ ```typescript title="app/api/refunds/[refundId]/approve/route.ts"
183
+ import { resumeHook } from 'workflow/api';
184
+
185
+ export async function POST(req: Request, { params }: { params: Promise<{ refundId: string }> }) {
186
+ const { refundId } = await params;
187
+ const { approved } = (await req.json()) as { approved: boolean };
188
+ await resumeHook(`refund:${refundId}:approval`, { approved }); // [!code highlight]
189
+ return Response.json({ ok: true });
190
+ }
191
+ ```
192
+
193
+ ### Branching on the result
194
+
195
+ In ASL, branching after the wait requires a Choice state. In TypeScript, it's just `if`/`else`:
196
+
197
+ ```json title="approval.asl.json (Step Functions)"
198
+ "CheckApproval": {
199
+ "Type": "Choice",
200
+ "Choices": [
201
+ { "Variable": "$.approved", "BooleanEquals": true, "Next": "Approved" }
202
+ ],
203
+ "Default": "Rejected"
204
+ }
205
+ ```
206
+
207
+ {/* @skip-typecheck: continuation snippet */}
208
+ ```typescript title="workflow/workflows/refund.ts (Workflow SDK)"
209
+ const { approved } = await approval;
210
+ if (approved) return { refundId, status: 'approved' }; // [!code highlight]
211
+ return { refundId, status: 'rejected' };
212
+ ```
213
+
214
+ ## Spawn a child workflow
215
+
216
+ In ASL, a parent machine calls `StartExecution` (usually via `.sync` or `.waitForTaskToken`) to launch a child. In v5, call `start()` directly from the workflow to launch a child. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to await the child result.
217
+
218
+ ### Parent starts a child
219
+
220
+ ```typescript title="workflow/workflows/parent.ts"
221
+ import { start } from 'workflow/api';
222
+
223
+ export async function parentWorkflow(item: string) {
224
+ 'use workflow';
225
+ const run = await start(childWorkflow, [item]); // [!code highlight]
226
+ return { childRunId: run.runId };
227
+ }
228
+ ```
229
+
230
+ ### Awaiting the child's result
231
+
232
+ Add a second step that wraps `getRun()` and awaits `returnValue`:
233
+
234
+ ```typescript
235
+ import { getRun } from 'workflow/api';
236
+
237
+ async function collectResult(runId: string) {
238
+ 'use step'; // [!code highlight]
239
+ const run = getRun(runId);
240
+ return (await run.returnValue) as { item: string; result: string };
241
+ }
242
+ ```
243
+
244
+ Then in the workflow: `const result = await collectResult(run.runId);`. The child workflow itself (`childWorkflow`) is defined elsewhere with `"use workflow"`.
245
+
246
+ ## What you stop operating
247
+
248
+ Moving off Step Functions removes these surfaces from the application:
249
+
250
+ - ASL state machine JSON and its reference syntax.
251
+ - Per-task Lambda functions, their IAM roles, and CloudFormation/CDK wiring.
252
+ - Task-token delivery infrastructure (SQS queues, callback Lambdas).
253
+ - Separate progress channels (DynamoDB, SNS) for client-visible updates.
254
+ - Remove CloudWatch and X-Ray wiring that was specific to orchestrator state transitions. Keep (or re-wire) any application-level CloudWatch alarms, log retention policies, or X-Ray propagation that the rest of your AWS footprint still depends on. Workflow SDK exports OTEL traces, so existing OTEL-compatible backends can continue to ingest them.
255
+
256
+ Workflow and step functions live in the same deployment as the application. State transitions are ordinary control flow (`await`, `if`, `Promise.all`, `for`). Progress streaming, retries, and observability are built in.
257
+
258
+ ### What you take on
259
+
260
+ Steps that previously invoked AWS services via optimized integrations (EventBridge, DynamoDB, Bedrock, ECS.RunTask.sync, etc.) become ordinary SDK calls inside `'use step'` functions. Credentials and retries move into the step, and `.sync`-style waits for long-running jobs become explicit polling loops or hook-based callbacks.
261
+
262
+ ## Step-by-step first migration
263
+
264
+ Pick one state machine and migrate it end-to-end before touching the rest. The steps below describe the smallest viable path.
265
+
266
+ ### Step 1: Install the Workflow SDK
267
+
268
+ Add the `workflow` runtime package.
269
+
270
+ ```bash
271
+ pnpm add workflow
272
+ ```
273
+
274
+ ### Step 2: Rewrite the state machine as a `"use workflow"` function
275
+
276
+ Transitions become `await` calls. Control flow (`Choice`, `Wait`, `Parallel`, `Map`) becomes `if`/`switch`, `sleep`, `Promise.all`, and loops.
277
+
278
+ ```ts title="workflows/order.ts"
279
+ export async function processOrder(orderId: string) {
280
+ "use workflow"; // [!code highlight]
281
+ const order = await loadOrder(orderId);
282
+ if (order.total > 1000) await reviewManually(order);
283
+ await chargePayment(order);
284
+ }
285
+ ```
286
+
287
+ ### Step 3: Move each Lambda into a step function
288
+
289
+ Inline the Lambda body into a function with `"use step"` on the first line. Step functions keep full Node.js access, so existing SDK calls work unchanged.
290
+
291
+ ```ts
292
+ async function loadOrder(id: string) {
293
+ "use step"; // [!code highlight]
294
+ return fetch(`/api/orders/${id}`).then((r) => r.json());
295
+ }
296
+ ```
297
+
298
+ ### Step 4: Replace `.waitForTaskToken` with a hook
299
+
300
+ Swap the task-token callback Lambda for `createHook()`. Callers `resumeHook(token, payload)` instead of `SendTaskSuccess`.
301
+
302
+ Move Retry/Catch off per-state configuration and onto step boundaries. Set `maxRetries` as a function property; throw `RetryableError` or `FatalError` to control retry behavior:
303
+
304
+ ```typescript
305
+ async function chargePayment(orderId: string) {
306
+ "use step";
307
+ // ...
308
+ }
309
+ chargePayment.maxRetries = 5;
310
+ ```
311
+
312
+ See [`/docs/foundations/errors-and-retries`](/docs/foundations/errors-and-retries) for the full retry and SAGA compensation patterns.
313
+
314
+ ### Step 5: Start runs from an API route
315
+
316
+ Delete the `StartExecution` call and IAM wiring. Launch runs directly from a route handler:
317
+
318
+ ```ts title="app/api/orders/route.ts"
319
+ import { start } from "workflow/api";
320
+ import { processOrder } from "@/workflows/order";
321
+
322
+ export async function POST(req: Request) {
323
+ const { orderId } = await req.json();
324
+ const run = await start(processOrder, [orderId]);
325
+ return Response.json({ runId: run.runId });
326
+ }
327
+ ```
328
+
329
+ ### Step 6: Retire the Step Functions infrastructure
330
+
331
+ Delete the ASL JSON, per-task Lambda deployments, IAM roles, and callback queues. Remove CloudWatch and X-Ray wiring that was specific to orchestrator state transitions — keep alarms, log retention, and traces for resources you still depend on. Verify the run in `npx workflow web` before shipping.
332
+
333
+ ## Features without a 1:1 equivalent
334
+
335
+ - **Express workflows.** At-least-once semantics and 5-minute duration make them a poor fit for the SDK's durable replay model. Consider keeping them on Step Functions or migrating to a queue consumer.
336
+ - **Distributed Map state.** Up to 10,000 concurrent child executions with S3 item sources has no 1:1 analog; fan out with `start()` per item, then `Promise.all` with `p-limit` to bound concurrency.
337
+ - **Optimized AWS service integrations (`arn:aws:states:::dynamodb:*`, `eventbridge:*`, `bedrock:*`, `ecs:runTask.sync`, etc.).** These become regular SDK calls inside `'use step'` functions — credentials, retries, and polling move into the step.
338
+ - **Per-state IAM roles.** ASL lets each state run under its own IAM role. In the SDK, all steps share the deployment's credentials; scope secrets and roles at deployment time.
339
+ - **CloudWatch alarms / X-Ray cross-service traces / CloudWatch Logs retention.** The SDK event log + observability UI replaces orchestrator state transitions, not AWS-wide observability. Keep alarms and traces for other resources.
340
+ - **`JSONata` `QueryLanguage` mode.** Valid at the source; the TS translation is identical regardless of mode.
341
+
342
+ ## Quick-start checklist
343
+
344
+ - Replace the ASL state machine with a single `"use workflow"` function. Transitions become `await` calls.
345
+ - Convert each Task / Lambda into a `"use step"` function in the same file.
346
+ - Replace Choice states with `if`/`else`/`switch`.
347
+ - Replace Wait states with `sleep()` from `workflow`.
348
+ - Replace Parallel states with `Promise.all()`.
349
+ - Replace Map states based on their concurrency mode: inline sequential → `for` loop; bounded parallel (`MaxConcurrency: N`) → batched `Promise.all` or a concurrency limiter like `p-limit`; Distributed Map / large fan-out → `start()` per item, then step-wrapped `getRun()` to collect.
350
+ - Replace `StartExecution` child machines with direct `start()` calls and a `"use step"` wrapper around `getRun()` when collecting results.
351
+ - Replace `.waitForTaskToken` with `createHook()` (internal callers) or `createWebhook()` (HTTP callers).
352
+ - Move Retry/Catch to step boundaries using `maxRetries`, `RetryableError`, and `FatalError`.
353
+ - Use `getStepMetadata().stepId` as the idempotency key for external side effects.
354
+ - Stream progress from steps with `getWritable()` instead of polling DynamoDB or SNS.
355
+ - Deploy and verify runs end-to-end with built-in observability.
356
+
357
+ ---
358
+ *Verified against `workflow@5.0.0-beta.1` and the AWS Step Functions Amazon States Language spec on 2026-04-16.*