workflow 5.0.0-beta.3 → 5.0.0-beta.31

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 (176) hide show
  1. package/dist/api-workflow.d.ts +1 -1
  2. package/dist/api-workflow.d.ts.map +1 -1
  3. package/dist/api-workflow.js +1 -1
  4. package/dist/api.d.ts +5 -1
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/api.js +12 -2
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +6 -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/observability.d.ts +1 -1
  14. package/dist/observability.js +2 -2
  15. package/dist/runtime.d.ts +2 -1
  16. package/dist/runtime.d.ts.map +1 -1
  17. package/dist/runtime.js +5 -2
  18. package/docs/ai/chat-session-modeling.mdx +7 -3
  19. package/docs/ai/defining-tools.mdx +2 -2
  20. package/docs/ai/index.mdx +28 -23
  21. package/docs/ai/message-queueing.mdx +10 -10
  22. package/docs/ai/resumable-streams.mdx +9 -1
  23. package/docs/api-reference/index.mdx +24 -0
  24. package/docs/api-reference/meta.json +8 -0
  25. package/docs/api-reference/vitest/index.mdx +28 -7
  26. package/docs/api-reference/workflow/create-hook.mdx +38 -0
  27. package/docs/api-reference/workflow/create-webhook.mdx +1 -0
  28. package/docs/api-reference/workflow/fetch.mdx +5 -0
  29. package/docs/api-reference/workflow/index.mdx +3 -0
  30. package/docs/api-reference/workflow/set-attributes.mdx +65 -0
  31. package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -45
  32. package/docs/api-reference/workflow-ai/index.mdx +2 -2
  33. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +44 -1
  34. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +7 -0
  35. package/docs/api-reference/workflow-api/get-run.mdx +25 -0
  36. package/docs/api-reference/workflow-api/index.mdx +6 -8
  37. package/docs/api-reference/workflow-api/resume-hook.mdx +57 -0
  38. package/docs/api-reference/workflow-api/start.mdx +13 -5
  39. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  40. package/docs/api-reference/workflow-astro/meta.json +4 -0
  41. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  42. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  43. package/docs/api-reference/workflow-errors/index.mdx +85 -0
  44. package/docs/api-reference/workflow-errors/meta.json +5 -0
  45. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  46. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  47. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
  48. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  49. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  50. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
  51. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  52. package/docs/api-reference/workflow-nest/meta.json +9 -0
  53. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  54. package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
  55. package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
  56. package/docs/api-reference/workflow-next/with-workflow.mdx +56 -2
  57. package/docs/api-reference/workflow-nitro/index.mdx +60 -0
  58. package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
  59. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  60. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  61. package/docs/api-reference/workflow-observability/index.mdx +64 -0
  62. package/docs/api-reference/workflow-observability/meta.json +11 -0
  63. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  64. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  65. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  66. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  67. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  68. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  69. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
  70. package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
  71. package/docs/api-reference/workflow-runtime/index.mdx +43 -0
  72. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  73. package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
  74. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
  75. package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +5 -8
  76. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  77. package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +4 -2
  78. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +11 -4
  79. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
  80. package/docs/api-reference/workflow-serde/index.mdx +0 -1
  81. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
  82. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +1 -2
  83. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  84. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  85. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  86. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  87. package/docs/api-reference/workflow-vite/meta.json +4 -0
  88. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  89. package/docs/changelog/attributes-mvp.mdx +380 -0
  90. package/docs/changelog/eager-processing.mdx +269 -0
  91. package/docs/changelog/index.mdx +2 -1
  92. package/docs/changelog/lazy-event-creation.md +127 -0
  93. package/docs/changelog/meta.json +8 -1
  94. package/docs/changelog/resilient-start.mdx +31 -283
  95. package/docs/changelog/step-message-ownership.mdx +360 -0
  96. package/docs/changelog/turbo-mode.md +87 -0
  97. package/docs/configuration/build-and-diagnostics.mdx +51 -0
  98. package/docs/configuration/cli-and-web-ui.mdx +154 -0
  99. package/docs/configuration/framework-options.mdx +165 -0
  100. package/docs/configuration/index.mdx +32 -0
  101. package/docs/configuration/meta.json +12 -0
  102. package/docs/configuration/runtime-tuning.mdx +156 -0
  103. package/docs/configuration/worlds.mdx +228 -0
  104. package/docs/cookbook/advanced/child-workflows.mdx +199 -256
  105. package/docs/cookbook/advanced/meta.json +1 -1
  106. package/docs/cookbook/advanced/publishing-libraries.mdx +13 -12
  107. package/docs/cookbook/advanced/serializable-steps.mdx +8 -4
  108. package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
  109. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +31 -76
  110. package/docs/cookbook/agent-patterns/durable-agent.mdx +11 -143
  111. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +7 -3
  112. package/docs/cookbook/common-patterns/idempotency.mdx +36 -52
  113. package/docs/cookbook/common-patterns/rate-limiting.mdx +1 -1
  114. package/docs/cookbook/common-patterns/saga.mdx +2 -2
  115. package/docs/cookbook/common-patterns/scheduling.mdx +8 -0
  116. package/docs/cookbook/common-patterns/timeouts.mdx +2 -1
  117. package/docs/cookbook/common-patterns/workflow-composition.mdx +15 -16
  118. package/docs/cookbook/index.mdx +3 -3
  119. package/docs/cookbook/integrations/ai-sdk.mdx +60 -28
  120. package/docs/cookbook/integrations/chat-sdk.mdx +13 -0
  121. package/docs/cookbook/integrations/sandbox.mdx +13 -0
  122. package/docs/deploying/building-a-world.mdx +1 -1
  123. package/docs/deploying/index.mdx +1 -0
  124. package/docs/deploying/world/local-world.mdx +23 -6
  125. package/docs/deploying/world/postgres-world.mdx +79 -13
  126. package/docs/deploying/world/vercel-world.mdx +47 -12
  127. package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
  128. package/docs/errors/corrupted-event-log.mdx +5 -5
  129. package/docs/errors/hook-conflict.mdx +56 -4
  130. package/docs/errors/index.mdx +1 -35
  131. package/docs/errors/replay-divergence.mdx +27 -0
  132. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  133. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  134. package/docs/errors/step-not-registered.mdx +1 -1
  135. package/docs/foundations/cancellation.mdx +459 -0
  136. package/docs/foundations/errors-and-retries.mdx +7 -3
  137. package/docs/foundations/hooks.mdx +29 -0
  138. package/docs/foundations/idempotency.mdx +236 -11
  139. package/docs/foundations/index.mdx +1 -23
  140. package/docs/foundations/meta.json +3 -1
  141. package/docs/foundations/serialization.mdx +77 -41
  142. package/docs/foundations/starting-workflows.mdx +5 -1
  143. package/docs/foundations/streaming.mdx +14 -23
  144. package/docs/foundations/versioning.mdx +263 -0
  145. package/docs/getting-started/astro.mdx +6 -0
  146. package/docs/getting-started/index.mdx +6 -7
  147. package/docs/getting-started/meta.json +1 -0
  148. package/docs/getting-started/nestjs.mdx +9 -0
  149. package/docs/getting-started/next.mdx +5 -3
  150. package/docs/getting-started/nitro.mdx +22 -0
  151. package/docs/getting-started/sveltekit.mdx +6 -0
  152. package/docs/getting-started/tanstack-start.mdx +241 -0
  153. package/docs/how-it-works/cancellation.mdx +287 -0
  154. package/docs/how-it-works/code-transform.mdx +2 -2
  155. package/docs/how-it-works/encryption.mdx +2 -2
  156. package/docs/how-it-works/event-sourcing.mdx +2 -2
  157. package/docs/how-it-works/meta.json +2 -1
  158. package/docs/internal/index.mdx +21 -0
  159. package/docs/internal/meta.json +10 -0
  160. package/docs/internal/nitro-native-build.mdx +38 -0
  161. package/docs/internal/nitro-web-ui.mdx +24 -0
  162. package/docs/internal/serializable-abort-controller.mdx +148 -0
  163. package/docs/meta.json +1 -1
  164. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +7 -12
  165. package/docs/migration-guides/migrating-from-inngest.mdx +7 -17
  166. package/docs/migration-guides/migrating-from-temporal.mdx +6 -11
  167. package/docs/migration-guides/migrating-from-trigger-dev.mdx +8 -17
  168. package/docs/observability/attributes.mdx +81 -0
  169. package/docs/observability/index.mdx +18 -1
  170. package/docs/observability/meta.json +1 -1
  171. package/docs/observability/tracing.mdx +119 -0
  172. package/docs/testing/index.mdx +2 -2
  173. package/package.json +14 -13
  174. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  175. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  176. package/docs/cookbook/advanced/distributed-abort-controller.mdx +0 -318
@@ -1,23 +1,27 @@
1
1
  ---
2
2
  title: Idempotency
3
- description: Ensure operations can be safely retried without producing duplicate side effects.
3
+ description: Make step retries safe and coordinate duplicate workflow starts with hook tokens.
4
4
  type: conceptual
5
- summary: Prevent duplicate side effects when retrying operations in steps.
5
+ summary: Use step IDs for retry-safe external calls, and route duplicate workflow-start requests through deterministic hook tokens.
6
6
  prerequisites:
7
7
  - /docs/foundations/workflows-and-steps
8
8
  related:
9
9
  - /docs/foundations/errors-and-retries
10
+ - /docs/foundations/starting-workflows
11
+ - /docs/foundations/hooks
10
12
  ---
11
13
 
12
- Idempotency is a property of an operation that ensures it can be safely retried without producing duplicate side effects.
14
+ Idempotency is a property of an operation that ensures repeated attempts have the same effect as a single attempt.
15
+
16
+ In Workflow, idempotency shows up in two related places: step idempotency makes external calls safe when a step retries, and run idempotency coordinates duplicate requests that try to start the same workflow.
17
+
18
+ ## Step Idempotency
13
19
 
14
20
  In distributed systems (calling external APIs), it is not always possible to ensure an operation has only been performed once just by seeing if it succeeds.
15
21
  Consider a payment API that charges the user $10, but due to network failures, the confirmation response is lost. When the step retries (because the previous attempt was considered a failure), it will charge the user again.
16
22
 
17
23
  To prevent this, many external APIs support idempotency keys. An idempotency key is a unique identifier for an operation that can be used to deduplicate requests.
18
24
 
19
- ## The core pattern: use the step ID as your idempotency key
20
-
21
25
  Every step invocation has a stable `stepId` that stays the same across retries.
22
26
  Use it as the idempotency key when calling third-party APIs.
23
27
 
@@ -27,7 +31,7 @@ import { getStepMetadata } from "workflow";
27
31
  async function chargeUser(userId: string, amount: number) {
28
32
  "use step";
29
33
 
30
- const { stepId } = getStepMetadata();
34
+ const { stepId } = getStepMetadata(); // [!code highlight]
31
35
 
32
36
  // Example: Stripe-style idempotency key
33
37
  // This guarantees only one charge is created even if the step retries
@@ -49,14 +53,235 @@ Why this works:
49
53
  - **Stable across retries**: `stepId` does not change between attempts.
50
54
  - **Globally unique per step**: Fulfills the uniqueness requirement for an idempotency key.
51
55
 
52
- ## Best practices
56
+ ## Run idempotency
57
+
58
+ Step idempotency protects side effects **inside** a workflow run. Run idempotency answers a different question: if the same API request is sent twice, should it create one workflow run or two?
59
+
60
+ Because [hooks](/docs/foundations/hooks) already ensure globally unique active tokens, Workflow can use the same mechanism to coordinate duplicate requests while a run is active.
61
+
62
+ Use a hook token as the idempotency key for an active workflow run. Hook tokens are globally unique while they are active: if another run tries to create a hook with the same token, the runtime records a conflict, `hook.getConflict()` resolves with a `Run` handle for the run that owns the token, and the hook rejects with [`HookConflictError`](/docs/errors/hook-conflict) when the workflow awaits or iterates its payload.
63
+
64
+ The token should come from your domain, such as an order ID, invoice ID, import ID, or request ID. Create the hook near the beginning of the workflow and check `await hook.getConflict()` before doing duplicate-sensitive work that depends on owning the active token. Calling `createHook()` alone does not register the hook — awaiting `getConflict()` suspends the workflow to commit the registration.
65
+
66
+ ```typescript lineNumbers
67
+ import { createHook } from "workflow";
68
+
69
+ type OrderRequest = { confirmed: boolean };
70
+ type OrderResult =
71
+ | { status: "processed" | "cancelled" }
72
+ | { status: "duplicate"; runId: string };
73
+ declare function chargeOrder(orderId: string): Promise<void>; // @setup
74
+
75
+ export async function processOrder(orderId: string): Promise<OrderResult> {
76
+ "use workflow";
77
+
78
+ using request = createHook<OrderRequest>({ // [!code highlight]
79
+ token: `order:${orderId}`, // [!code highlight]
80
+ }); // [!code highlight]
81
+
82
+ const conflict = await request.getConflict(); // [!code highlight]
83
+ if (conflict) { // [!code highlight]
84
+ // Another active run already owns this order's token. // [!code highlight]
85
+ return { status: "duplicate" as const, runId: conflict.runId }; // [!code highlight]
86
+ } // [!code highlight]
87
+
88
+ const { confirmed } = await request;
89
+
90
+ if (!confirmed) {
91
+ return { status: "cancelled" as const };
92
+ }
93
+
94
+ await chargeOrder(orderId);
95
+ return { status: "processed" as const };
96
+ }
97
+ ```
98
+
99
+ The runtime creates the hook atomically. At most one active hook can own `order:${orderId}`, so duplicate workflow runs converge on one active owner. A duplicate run observes `getConflict()` resolving with the owner's `Run` and returns before it reaches `chargeOrder()`. The conflicting run's accessors (`status`, `returnValue`, `cancel()`, …) are durable steps, so the duplicate run can do more than report the owner — see [conflict-handling strategies](#conflict-handling-strategies) below.
100
+
101
+ Outside the workflow, try to resume the hook first. If the hook is not registered yet, start the workflow and retry the resume until the new run creates the hook:
102
+
103
+ ```typescript lineNumbers
104
+ import { resumeHook, start } from "workflow/api";
105
+ import { HookNotFoundError } from "workflow/errors";
106
+ import { processOrder } from "./workflows/process-order";
107
+
108
+ type OrderRequest = { confirmed: boolean };
109
+
110
+ async function resumeOrder(token: string, payload: OrderRequest) {
111
+ for (let attempt = 0; attempt < 5; attempt++) {
112
+ try {
113
+ return await resumeHook(token, payload); // [!code highlight]
114
+ } catch (error) {
115
+ if (!HookNotFoundError.is(error)) throw error;
116
+ await new Promise((resolve) => setTimeout(resolve, 100));
117
+ }
118
+ }
119
+
120
+ throw new Error("Order workflow did not register its hook in time");
121
+ }
122
+
123
+ export async function POST(request: Request) {
124
+ const { orderId, confirmed } = await request.json();
125
+ const token = `order:${orderId}`;
126
+ const payload = { confirmed };
127
+
128
+ try {
129
+ const hook = await resumeHook(token, payload); // [!code highlight]
130
+ return Response.json({ runId: hook.runId, reused: true });
131
+ } catch (error) {
132
+ if (!HookNotFoundError.is(error)) throw error;
133
+ }
134
+
135
+ const run = await start(processOrder, [orderId]); // [!code highlight]
136
+ const resumed = await resumeOrder(token, payload);
137
+
138
+ // A concurrent request's run may have won the race between `start()` // [!code highlight]
139
+ // and hook registration. The resume always reaches the actual active // [!code highlight]
140
+ // owner, so compare run IDs instead of waiting for this run to finish. // [!code highlight]
141
+ return Response.json({ // [!code highlight]
142
+ runId: resumed.runId, // [!code highlight]
143
+ reused: resumed.runId !== run.runId, // [!code highlight]
144
+ }); // [!code highlight]
145
+ }
146
+ ```
147
+
148
+ <Callout type="warn">
149
+ This avoids creating a new run only after the first run has registered its hook. Because `start()` returns before the run body executes and calls `createHook()`, two concurrent requests can both observe "no hook yet" and each call `start()`. The race is resolved inside the workflow body, where the losing run observes `getConflict()` resolving with the active owner and returns without doing duplicate-sensitive work — and the route detects it by comparing the resumed hook's `runId` against the run it just started, without waiting for either run to finish. A native API for atomically starting a run and registering a hook is in the works. Until then, model recovery inside the workflow by checking `hook.getConflict()`.
150
+ </Callout>
151
+
152
+ This is active-run coordination. When the workflow completes and disposes the hook, the token can be used again. If a duplicate request after completion must return the original result instead of starting fresh work, persist that completed result under the same domain key.
153
+
154
+ ### Conflict-handling strategies
155
+
156
+ Some workflow systems resolve duplicate IDs with a fixed, pre-declared policy — typically a static choice between rejecting the new execution, deferring to the existing one, or terminating it. Workflow has no policy enum. `hook.getConflict()` hands the duplicate run the conflicting `Run` itself, and the policy is ordinary code — including policies that inspect state before deciding, which static configuration can't express.
157
+
158
+ The example above implements **reject the duplicate**: return the owner's `runId` and let the caller decide. Other common strategies:
159
+
160
+ **Adopt the owner's result.** Wait for the active run to finish and return its result, so callers cannot tell which run did the work:
161
+
162
+ ```typescript lineNumbers
163
+ import { createHook } from "workflow";
164
+
165
+ type OrderRequest = { confirmed: boolean };
166
+ declare function processOwnedOrder(orderId: string): Promise<{ status: string }>; // @setup
167
+
168
+ export async function processOrder(orderId: string) {
169
+ "use workflow";
170
+
171
+ using request = createHook<OrderRequest>({
172
+ token: `order:${orderId}`,
173
+ });
174
+
175
+ const conflict = await request.getConflict();
176
+ if (conflict) {
177
+ // Callers get the same result regardless of which run did the work.
178
+ return await conflict.returnValue; // [!code highlight]
179
+ }
180
+
181
+ return await processOwnedOrder(orderId);
182
+ }
183
+ ```
184
+
185
+ **Inspect the owner before deciding.** Branch on the owner's live state:
186
+
187
+ ```typescript lineNumbers
188
+ import { createHook } from "workflow";
189
+
190
+ type OrderRequest = { confirmed: boolean };
191
+ declare function processOwnedOrder(orderId: string): Promise<{ status: string }>; // @setup
192
+
193
+ export async function processOrder(orderId: string) {
194
+ "use workflow";
195
+
196
+ using request = createHook<OrderRequest>({
197
+ token: `order:${orderId}`,
198
+ });
199
+
200
+ const conflict = await request.getConflict();
201
+ if (conflict) {
202
+ const status = await conflict.status; // [!code highlight]
203
+ if (status === "running") {
204
+ return { status: "duplicate" as const, runId: conflict.runId };
205
+ }
206
+ // Owner already reached a terminal state; its hook will be released.
207
+ }
208
+
209
+ return await processOwnedOrder(orderId);
210
+ }
211
+ ```
212
+
213
+ **Signal the owner instead of doing the work.** The duplicate run knows the token, so it can deliver this run's input to the owner's hook from a step:
214
+
215
+ ```typescript lineNumbers
216
+ import { createHook } from "workflow";
217
+ import { resumeHook } from "workflow/api";
218
+
219
+ type OrderRequest = { confirmed: boolean };
220
+
221
+ async function forwardToOwner(token: string, payload: OrderRequest) {
222
+ "use step";
223
+ await resumeHook(token, payload); // [!code highlight]
224
+ }
225
+
226
+ export async function processOrder(orderId: string, confirmed: boolean) {
227
+ "use workflow";
228
+
229
+ const token = `order:${orderId}`;
230
+ using request = createHook<OrderRequest>({ token });
231
+
232
+ const conflict = await request.getConflict();
233
+ if (conflict) {
234
+ await forwardToOwner(token, { confirmed }); // [!code highlight]
235
+ return { status: "forwarded" as const, runId: conflict.runId };
236
+ }
237
+
238
+ // ... own the token and do the work
239
+ }
240
+ ```
241
+
242
+ **Supersede the owner.** Newest-wins: cancel the active run, then claim the released token. Cancellation disposes the owner's hooks; the retry loop covers the window where that disposal has not propagated yet:
243
+
244
+ ```typescript lineNumbers
245
+ import { createHook } from "workflow";
246
+
247
+ type OrderRequest = { confirmed: boolean };
248
+ declare function chargeOrder(orderId: string): Promise<void>; // @setup
249
+
250
+ export async function processOrderNewestWins(orderId: string) {
251
+ "use workflow";
252
+
253
+ const token = `order:${orderId}`;
254
+
255
+ for (let attempt = 0; attempt < 3; attempt++) {
256
+ using request = createHook<OrderRequest>({ token });
257
+
258
+ const conflict = await request.getConflict();
259
+ if (!conflict) {
260
+ // Token claimed — this run is now the owner.
261
+ const { confirmed } = await request;
262
+ if (confirmed) {
263
+ await chargeOrder(orderId);
264
+ }
265
+ return { status: "processed" as const };
266
+ }
267
+
268
+ await conflict.cancel(); // [!code highlight]
269
+ }
270
+
271
+ throw new Error(`Could not claim ${token} after cancelling the owner`);
272
+ }
273
+ ```
274
+
275
+ If duplicate requests should only reuse the active run without sending data, use [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) as an advisory pre-check before calling `start()`. The workflow should still check `hook.getConflict()`, because the lookup and `start()` are not atomic.
53
276
 
54
- - **Always provide idempotency keys to external side effects that are not idempotent** inside steps (payments, emails, SMS, queues).
55
- - **Prefer `stepId` as your key**; it is stable across retries and unique per step.
56
- - **Keep keys deterministic**; avoid including timestamps or attempt counters.
57
- - **Handle 409/conflict responses** gracefully; treat them as success if the prior attempt completed.
277
+ Because this pattern uses hooks for idempotency, duplicate requests can also inject additional data and steer the existing run. The route example above uses `resumeHook()` for that: if the hook already exists, the duplicate request resumes the active workflow; if the hook is not registered yet, the route starts the workflow and retries `resumeHook()` so the payload is not dropped.
58
278
 
59
279
  ## Related docs
60
280
 
61
281
  - Learn about retries in [Errors & Retrying](/docs/foundations/errors-and-retries)
62
282
  - API reference: [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata)
283
+ - API reference: [`createHook()`](/docs/api-reference/workflow/create-hook)
284
+ - API reference: [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token)
285
+ - API reference: [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook)
286
+ - API reference: [`start()`](/docs/api-reference/workflow-api/start)
287
+ - Learn about deterministic hook tokens in [Hooks](/docs/foundations/hooks)
@@ -10,26 +10,4 @@ related:
10
10
 
11
11
  Workflow programming can be a slight shift from how you traditionally write real-world applications. Learning the foundations now will go a long way toward helping you use workflows effectively.
12
12
 
13
- <Cards>
14
- <Card href="/docs/foundations/workflows-and-steps" title="Workflows and Steps">
15
- Learn about the building blocks of durability
16
- </Card>
17
- <Card href="/docs/foundations/starting-workflows" title="Starting Workflows">
18
- Trigger workflows and track their execution using the `start()` function.
19
- </Card>
20
- <Card href="/docs/foundations/errors-and-retries" title="Errors & Retrying">
21
- Types of errors and how retrying work in workflows.
22
- </Card>
23
- <Card href="/docs/foundations/hooks" title="Webhooks (and hooks)">
24
- Respond to external events in your workflow using hooks and webhooks.
25
- </Card>
26
- <Card href="/docs/foundations/streaming" title="Streaming">
27
- Stream data in real-time to clients without waiting for the workflow to complete.
28
- </Card>
29
- <Card href="/docs/foundations/serialization" title="Serialization">
30
- Understand which types can be passed between workflow and step functions.
31
- </Card>
32
- <Card href="/docs/foundations/idempotency" title="Idempotency">
33
- Prevent duplicate side effects when retrying operations.
34
- </Card>
35
- </Cards>
13
+ <AutoCards />
@@ -6,8 +6,10 @@
6
6
  "errors-and-retries",
7
7
  "hooks",
8
8
  "streaming",
9
+ "cancellation",
9
10
  "serialization",
10
- "idempotency"
11
+ "idempotency",
12
+ "versioning"
11
13
  ],
12
14
  "defaultOpen": true
13
15
  }
@@ -55,6 +55,50 @@ These types have special handling and are explained in detail in the sections be
55
55
  - `Response`
56
56
  - `ReadableStream<Serializable>`
57
57
  - `WritableStream<Serializable>`
58
+ - `AbortController`
59
+ - `AbortSignal`
60
+
61
+ ## Pass-by-Value Semantics
62
+
63
+ **Parameters are passed by value, not by reference.** Steps receive deserialized copies of data. Mutations inside a step won't affect the original in the workflow.
64
+
65
+ **Incorrect:**
66
+
67
+ ```typescript title="workflows/incorrect-mutation.ts" lineNumbers
68
+ export async function updateUserWorkflow(userId: string) {
69
+ "use workflow";
70
+
71
+ let user = { id: userId, name: "John", email: "john@example.com" };
72
+ await updateUserStep(user);
73
+
74
+ // user.email is still "john@example.com" // [!code highlight]
75
+ console.log(user.email); // [!code highlight]
76
+ }
77
+
78
+ async function updateUserStep(user: { id: string; name: string; email: string }) {
79
+ "use step";
80
+ user.email = "newemail@example.com"; // Changes are lost // [!code highlight]
81
+ }
82
+ ```
83
+
84
+ **Correct - return the modified data:**
85
+
86
+ ```typescript title="workflows/correct-mutation.ts" lineNumbers
87
+ export async function updateUserWorkflow(userId: string) {
88
+ "use workflow";
89
+
90
+ let user = { id: userId, name: "John", email: "john@example.com" };
91
+ user = await updateUserStep(user); // Reassign the return value // [!code highlight]
92
+
93
+ console.log(user.email); // "newemail@example.com"
94
+ }
95
+
96
+ async function updateUserStep(user: { id: string; name: string; email: string }) {
97
+ "use step";
98
+ user.email = "newemail@example.com";
99
+ return user; // [!code highlight]
100
+ }
101
+ ```
58
102
 
59
103
  **Custom Classes:**
60
104
 
@@ -125,6 +169,39 @@ export async function fetch(...args: Parameters<typeof globalThis.fetch>) {
125
169
 
126
170
  This allows you to make HTTP requests directly in workflow functions while maintaining deterministic replay behavior through automatic caching.
127
171
 
172
+ ## AbortController & AbortSignal
173
+
174
+ `AbortController` and `AbortSignal` are serializable types that enable cooperative cancellation across workflow and step boundaries. Inside a workflow function, `new AbortController()` creates a durable controller that works across suspensions and step boundaries:
175
+
176
+ ```typescript lineNumbers
177
+ import { sleep } from "workflow";
178
+
179
+ export async function cancellableWorkflow() {
180
+ "use workflow";
181
+
182
+ const controller = new AbortController(); // [!code highlight]
183
+
184
+ const result = await Promise.race([
185
+ fetchData(controller.signal), // [!code highlight]
186
+ sleep("10s").then(() => null),
187
+ ]);
188
+
189
+ if (result === null) {
190
+ controller.abort(); // [!code highlight]
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ async function fetchData(signal: AbortSignal) {
197
+ "use step";
198
+ const response = await fetch("https://api.example.com/data", { signal });
199
+ return response.json();
200
+ }
201
+ ```
202
+
203
+ For usage patterns including timeouts, parallel cancellation, user-triggered cancellation, and run cancellation, see the [Cancellation Guide](/docs/foundations/cancellation). For details on the hook and stream backing that makes this work, see [How Cancellation Works](/docs/how-it-works/cancellation).
204
+
128
205
  ## Custom Class Serialization
129
206
 
130
207
  By default, custom class instances cannot be serialized because the serialization system doesn't know how to reconstruct them. You can make your classes serializable by implementing two static methods using special symbols from the `@workflow/serde` package.
@@ -332,44 +409,3 @@ export async function processOrderWorkflow() {
332
409
  }
333
410
  ```
334
411
 
335
- ## Pass-by-Value Semantics
336
-
337
- **Parameters are passed by value, not by reference.** Steps receive deserialized copies of data. Mutations inside a step won't affect the original in the workflow.
338
-
339
- **Incorrect:**
340
-
341
- ```typescript title="workflows/incorrect-mutation.ts" lineNumbers
342
- export async function updateUserWorkflow(userId: string) {
343
- "use workflow";
344
-
345
- let user = { id: userId, name: "John", email: "john@example.com" };
346
- await updateUserStep(user);
347
-
348
- // user.email is still "john@example.com" // [!code highlight]
349
- console.log(user.email); // [!code highlight]
350
- }
351
-
352
- async function updateUserStep(user: { id: string; name: string; email: string }) {
353
- "use step";
354
- user.email = "newemail@example.com"; // Changes are lost // [!code highlight]
355
- }
356
- ```
357
-
358
- **Correct - return the modified data:**
359
-
360
- ```typescript title="workflows/correct-mutation.ts" lineNumbers
361
- export async function updateUserWorkflow(userId: string) {
362
- "use workflow";
363
-
364
- let user = { id: userId, name: "John", email: "john@example.com" };
365
- user = await updateUserStep(user); // Reassign the return value // [!code highlight]
366
-
367
- console.log(user.email); // "newemail@example.com"
368
- }
369
-
370
- async function updateUserStep(user: { id: string; name: string; email: string }) {
371
- "use step";
372
- user.email = "newemail@example.com";
373
- return user; // [!code highlight]
374
- }
375
- ```
@@ -13,7 +13,7 @@ Once you've defined your workflow functions, you need to trigger them to begin e
13
13
 
14
14
  ## The `start()` Function
15
15
 
16
- The [`start()`](/docs/api-reference/workflow-api/start) function is used to programmatically trigger workflow executions from runtime contexts like API routes, Server Actions, or any server-side code.
16
+ The [`start()`](/docs/api-reference/workflow-api/start) function is used to programmatically trigger workflow executions from runtime contexts like API routes, Server Actions, or any server-side code. In v5, you can also call `start()` from inside a workflow function when you want to spawn a child run or continue work in a new run.
17
17
 
18
18
  ```typescript lineNumbers
19
19
  import { start } from "workflow/api";
@@ -41,6 +41,10 @@ export async function POST(request: Request) {
41
41
 
42
42
  **Learn more**: [`start()` API Reference](/docs/api-reference/workflow-api/start)
43
43
 
44
+ <Callout type="info">
45
+ For parent-child workflow patterns, see [Workflow Composition](/cookbook/common-patterns/workflow-composition). For long-lived workflows that intentionally hand off to newer deployments with `deploymentId: "latest"`, see [Versioning](/docs/foundations/versioning).
46
+ </Callout>
47
+
44
48
  ## The `Run` Object
45
49
 
46
50
  When you call `start()`, it returns a [`Run`](/docs/api-reference/workflow-api/start#returns) object that provides access to the workflow's status and results.
@@ -345,29 +345,19 @@ export async function batchProcessingWorkflow(items: string[]) {
345
345
  }
346
346
  ```
347
347
 
348
- ### Streaming AI Responses with `DurableAgent`
348
+ ### Streaming AI Responses with `WorkflowAgent`
349
349
 
350
- Stream AI-generated content using [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) from `@workflow/ai`. Tools can also emit progress updates to the same stream using [data chunks](https://ai-sdk.dev/docs/ai-sdk-ui/streaming-data#streaming-custom-data) with the [`UIMessageChunk`](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol) type from the AI SDK:
350
+ Stream AI-generated content using AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow`. The agent writes `ModelCallStreamPart` chunks to the workflow stream, and route handlers convert them to UI message chunks with `createModelCallToUIChunkTransform()` before returning the response:
351
351
 
352
352
  ```typescript title="workflows/ai-assistant.ts" lineNumbers
353
- import { DurableAgent } from "@workflow/ai/agent";
353
+ import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
354
+ import { tool } from "ai";
354
355
  import { getWritable } from "workflow";
355
356
  import { z } from "zod";
356
- import type { UIMessageChunk } from "ai";
357
357
 
358
358
  async function searchFlights({ query }: { query: string }) {
359
359
  "use step";
360
360
 
361
- // Tools can emit progress updates to the stream
362
- const writable = getWritable<UIMessageChunk>(); // [!code highlight]
363
- const writer = writable.getWriter(); // [!code highlight]
364
- await writer.write({ // [!code highlight]
365
- type: "data-progress", // [!code highlight]
366
- data: { message: `Searching flights for ${query}...` }, // [!code highlight]
367
- transient: true, // [!code highlight]
368
- }); // [!code highlight]
369
- writer.releaseLock(); // [!code highlight]
370
-
371
361
  // ... search logic ...
372
362
  return { flights: [/* results */] };
373
363
  }
@@ -375,27 +365,28 @@ async function searchFlights({ query }: { query: string }) {
375
365
  export async function aiAssistantWorkflow(userMessage: string) {
376
366
  "use workflow";
377
367
 
378
- const agent = new DurableAgent({
368
+ const agent = new WorkflowAgent({
379
369
  model: "anthropic/claude-haiku-4.5",
380
- system: "You are a helpful flight assistant.",
370
+ instructions: "You are a helpful flight assistant.",
381
371
  tools: {
382
- searchFlights: {
372
+ searchFlights: tool({
383
373
  description: "Search for flights",
384
374
  inputSchema: z.object({ query: z.string() }),
385
375
  execute: searchFlights,
386
- },
376
+ }),
387
377
  },
388
378
  });
389
379
 
390
380
  // LLM response will be streamed to the run's writable
391
381
  await agent.stream({
392
382
  messages: [{ role: "user", content: userMessage }],
393
- writable: getWritable<UIMessageChunk>(), // [!code highlight]
383
+ writable: getWritable<ModelCallStreamPart>(), // [!code highlight]
394
384
  });
395
385
  }
396
386
  ```
397
387
 
398
388
  ```typescript title="app/api/ai-assistant/route.ts" lineNumbers
389
+ import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
399
390
  import { createUIMessageStreamResponse } from "ai";
400
391
  import { start } from "workflow/api";
401
392
  import { aiAssistantWorkflow } from "./workflows/ai";
@@ -406,13 +397,13 @@ export async function POST(request: Request) {
406
397
  const run = await start(aiAssistantWorkflow, [message]);
407
398
 
408
399
  return createUIMessageStreamResponse({
409
- stream: run.readable,
400
+ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()), // [!code highlight]
410
401
  });
411
402
  }
412
403
  ```
413
404
 
414
405
  <Callout type="info">
415
- For a complete implementation, see the [flight booking example](https://github.com/vercel/workflow-examples/tree/main/flight-booking-app) which demonstrates streaming AI responses with tool progress updates.
406
+ For the full agent API and migration notes, see the [`WorkflowAgent` documentation](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent).
416
407
  </Callout>
417
408
 
418
409
  ### Streaming Between Steps
@@ -593,8 +584,8 @@ Stream errors don't trigger automatic retries for the producer step. Design your
593
584
  - [`sleep()` API Reference](/docs/api-reference/workflow/sleep) - Pause workflow execution for a duration
594
585
  - [`start()` API Reference](/docs/api-reference/workflow-api/start) - Start workflows and access the `Run` object
595
586
  - [`getRun()` API Reference](/docs/api-reference/workflow-api/get-run) - Retrieve runs and their streams later
596
- - [world.streams](/docs/api-reference/workflow-api/world/streams) - Low-level stream read/write/close via World SDK
597
- - [DurableAgent](/docs/api-reference/workflow-ai/durable-agent) - AI agents with built-in streaming support
587
+ - [world.streams](/docs/api-reference/workflow-runtime/world/streams) - Low-level stream read/write/close via World SDK
588
+ - [WorkflowAgent](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI agents with durable, resumable streaming support
598
589
  - [Errors and Retries](/docs/foundations/errors-and-retries) - Understanding error handling and retry behavior
599
590
  - [Serialization](/docs/foundations/serialization) - Understanding what data types can be passed in workflows
600
591
  - [Workflows and Steps](/docs/foundations/workflows-and-steps) - Core concepts of workflow execution