workflow 5.0.0-beta.9 → 5.0.1

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 (265) hide show
  1. package/README.md +68 -23
  2. package/dist/api-workflow.d.ts +3 -1
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +2 -1
  5. package/dist/api.d.ts +5 -4
  6. package/dist/api.d.ts.map +1 -1
  7. package/dist/api.js +6 -7
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -1
  11. package/dist/internal/builtins.d.ts +4 -4
  12. package/dist/internal/builtins.js +6 -6
  13. package/dist/internal/errors.d.ts +1 -1
  14. package/dist/internal/errors.d.ts.map +1 -1
  15. package/dist/internal/errors.js +2 -2
  16. package/dist/nest-builder.d.ts +2 -0
  17. package/dist/nest-builder.d.ts.map +1 -0
  18. package/dist/nest-builder.js +2 -0
  19. package/dist/nest-vercel-builder.d.ts +2 -0
  20. package/dist/nest-vercel-builder.d.ts.map +1 -0
  21. package/dist/nest-vercel-builder.js +2 -0
  22. package/dist/runtime.d.ts +2 -1
  23. package/dist/runtime.d.ts.map +1 -1
  24. package/dist/runtime.js +4 -1
  25. package/docs/advanced/dynamic-workflows.mdx +227 -0
  26. package/docs/advanced/index.mdx +13 -0
  27. package/docs/advanced/meta.json +5 -0
  28. package/docs/ai/chat-session-modeling.mdx +176 -422
  29. package/docs/ai/defining-tools.mdx +6 -7
  30. package/docs/ai/human-in-the-loop.mdx +16 -12
  31. package/docs/ai/index.mdx +67 -72
  32. package/docs/ai/message-queueing.mdx +71 -110
  33. package/docs/ai/meta.json +1 -0
  34. package/docs/ai/resumable-streams.mdx +40 -28
  35. package/docs/ai/sleep-and-delays.mdx +10 -10
  36. package/docs/ai/streaming-updates-from-tools.mdx +6 -6
  37. package/docs/api-reference/index.mdx +25 -1
  38. package/docs/api-reference/meta.json +8 -0
  39. package/docs/api-reference/vitest/index.mdx +68 -15
  40. package/docs/api-reference/workflow/create-hook.mdx +170 -10
  41. package/docs/api-reference/workflow/create-webhook.mdx +16 -15
  42. package/docs/api-reference/workflow/define-hook.mdx +41 -33
  43. package/docs/api-reference/workflow/fatal-error.mdx +30 -8
  44. package/docs/api-reference/workflow/fetch.mdx +14 -10
  45. package/docs/api-reference/workflow/get-step-metadata.mdx +2 -2
  46. package/docs/api-reference/workflow/get-workflow-metadata.mdx +3 -3
  47. package/docs/api-reference/workflow/get-writable.mdx +7 -7
  48. package/docs/api-reference/workflow/index.mdx +3 -3
  49. package/docs/api-reference/workflow/retryable-error.mdx +1 -1
  50. package/docs/api-reference/workflow/set-attributes.mdx +63 -0
  51. package/docs/api-reference/workflow/sleep.mdx +4 -4
  52. package/docs/api-reference/workflow-ai/durable-agent.mdx +63 -101
  53. package/docs/api-reference/workflow-ai/index.mdx +5 -5
  54. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +67 -24
  55. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +37 -15
  56. package/docs/api-reference/workflow-api/get-run.mdx +43 -8
  57. package/docs/api-reference/workflow-api/index.mdx +8 -9
  58. package/docs/api-reference/workflow-api/register-lifecycle-hooks.mdx +87 -0
  59. package/docs/api-reference/workflow-api/resume-hook.mdx +77 -12
  60. package/docs/api-reference/workflow-api/resume-webhook.mdx +15 -9
  61. package/docs/api-reference/workflow-api/start.mdx +107 -12
  62. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  63. package/docs/api-reference/workflow-astro/meta.json +4 -0
  64. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  65. package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +4 -4
  66. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  67. package/docs/api-reference/workflow-errors/hook-force-claimed-error.mdx +70 -0
  68. package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
  69. package/docs/api-reference/workflow-errors/index.mdx +91 -0
  70. package/docs/api-reference/workflow-errors/meta.json +7 -0
  71. package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -0
  72. package/docs/api-reference/workflow-errors/run-expired-error.mdx +2 -2
  73. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  74. package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +5 -5
  75. package/docs/api-reference/workflow-errors/throttle-error.mdx +2 -2
  76. package/docs/api-reference/workflow-errors/too-early-error.mdx +2 -2
  77. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  78. package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +5 -6
  79. package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +13 -6
  80. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +12 -5
  81. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  82. package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +4 -4
  83. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  84. package/docs/api-reference/workflow-errors/workflow-world-error.mdx +8 -8
  85. package/docs/api-reference/workflow-globals.mdx +19 -11
  86. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +37 -0
  87. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  88. package/docs/api-reference/workflow-nest/meta.json +9 -0
  89. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  90. package/docs/api-reference/workflow-nest/workflow-controller.mdx +46 -0
  91. package/docs/api-reference/workflow-nest/workflow-module.mdx +134 -0
  92. package/docs/api-reference/workflow-next/with-workflow.mdx +39 -17
  93. package/docs/api-reference/workflow-nitro/index.mdx +60 -0
  94. package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
  95. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  96. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  97. package/docs/api-reference/workflow-observability/index.mdx +62 -0
  98. package/docs/api-reference/workflow-observability/meta.json +11 -0
  99. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  100. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  101. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  102. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  103. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  104. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  105. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +11 -14
  106. package/docs/api-reference/workflow-runtime/health-check.mdx +51 -0
  107. package/docs/api-reference/workflow-runtime/index.mdx +41 -0
  108. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  109. package/docs/api-reference/workflow-runtime/set-world.mdx +51 -0
  110. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +43 -0
  111. package/docs/api-reference/workflow-runtime/world/analytics.mdx +315 -0
  112. package/docs/api-reference/workflow-runtime/world/index.mdx +60 -0
  113. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  114. package/docs/api-reference/workflow-runtime/world/queue.mdx +88 -0
  115. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +133 -35
  116. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +8 -8
  117. package/docs/api-reference/workflow-serde/index.mdx +1 -2
  118. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +3 -4
  119. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +8 -8
  120. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  121. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  122. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  123. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  124. package/docs/api-reference/workflow-vite/meta.json +4 -0
  125. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  126. package/docs/changelog/attributes-mvp.mdx +53 -41
  127. package/docs/changelog/batched-event-writes.mdx +79 -0
  128. package/docs/changelog/eager-processing.mdx +110 -436
  129. package/docs/changelog/index.mdx +4 -2
  130. package/docs/changelog/lazy-event-creation.md +127 -0
  131. package/docs/changelog/lazy-hook-resume.mdx +78 -0
  132. package/docs/changelog/meta.json +11 -1
  133. package/docs/changelog/resilient-resume.mdx +32 -0
  134. package/docs/changelog/resilient-start.mdx +33 -285
  135. package/docs/changelog/step-message-ownership.mdx +360 -0
  136. package/docs/changelog/turbo-mode.md +87 -0
  137. package/docs/comparisons/index.mdx +66 -0
  138. package/docs/comparisons/meta.json +11 -0
  139. package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
  140. package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
  141. package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
  142. package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
  143. package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
  144. package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +104 -0
  145. package/docs/configuration/build-and-diagnostics.mdx +89 -0
  146. package/docs/configuration/cli-and-web-ui.mdx +241 -0
  147. package/docs/configuration/framework-options.mdx +165 -0
  148. package/docs/configuration/index.mdx +32 -0
  149. package/docs/configuration/meta.json +12 -0
  150. package/docs/configuration/runtime-tuning.mdx +424 -0
  151. package/docs/configuration/worlds.mdx +341 -0
  152. package/docs/cookbook/advanced/child-workflows.mdx +33 -25
  153. package/docs/cookbook/advanced/publishing-libraries.mdx +65 -56
  154. package/docs/cookbook/advanced/serializable-steps.mdx +48 -68
  155. package/docs/cookbook/advanced/upgrading-workflows.mdx +35 -31
  156. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +78 -60
  157. package/docs/cookbook/agent-patterns/durable-agent.mdx +23 -135
  158. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +180 -195
  159. package/docs/cookbook/common-patterns/batching.mdx +20 -14
  160. package/docs/cookbook/common-patterns/idempotency.mdx +41 -53
  161. package/docs/cookbook/common-patterns/rate-limiting.mdx +8 -4
  162. package/docs/cookbook/common-patterns/saga.mdx +23 -19
  163. package/docs/cookbook/common-patterns/scheduling.mdx +30 -22
  164. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +29 -25
  165. package/docs/cookbook/common-patterns/timeouts.mdx +26 -21
  166. package/docs/cookbook/common-patterns/webhooks.mdx +12 -7
  167. package/docs/cookbook/common-patterns/workflow-composition.mdx +27 -17
  168. package/docs/cookbook/index.mdx +22 -22
  169. package/docs/cookbook/integrations/ai-sdk.mdx +63 -48
  170. package/docs/cookbook/integrations/chat-sdk.mdx +46 -33
  171. package/docs/cookbook/integrations/sandbox.mdx +58 -45
  172. package/docs/deploying.mdx +106 -0
  173. package/docs/errors/abort-signal-timeout-in-workflow.mdx +16 -12
  174. package/docs/errors/corrupted-event-log.mdx +39 -18
  175. package/docs/errors/deployment-mismatch.mdx +71 -0
  176. package/docs/errors/fetch-in-workflow.mdx +15 -14
  177. package/docs/errors/hook-conflict.mdx +38 -11
  178. package/docs/errors/hook-force-claimed.mdx +96 -0
  179. package/docs/errors/index.mdx +24 -37
  180. package/docs/errors/node-js-module-in-workflow.mdx +9 -5
  181. package/docs/errors/replay-divergence.mdx +27 -0
  182. package/docs/errors/run-expired.mdx +85 -0
  183. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  184. package/docs/errors/serialization-failed.mdx +44 -12
  185. package/docs/errors/start-invalid-workflow-function.mdx +9 -5
  186. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  187. package/docs/errors/step-not-registered.mdx +6 -6
  188. package/docs/errors/timeout-in-workflow.mdx +12 -8
  189. package/docs/errors/webhook-invalid-respond-with-value.mdx +18 -18
  190. package/docs/errors/webhook-response-not-sent.mdx +20 -16
  191. package/docs/errors/workflow-not-registered.mdx +5 -5
  192. package/docs/foundations/cancellation.mdx +31 -32
  193. package/docs/foundations/errors-and-retries.mdx +54 -11
  194. package/docs/foundations/hooks.mdx +187 -36
  195. package/docs/foundations/idempotency.mdx +267 -12
  196. package/docs/foundations/index.mdx +1 -26
  197. package/docs/foundations/serialization.mdx +22 -22
  198. package/docs/foundations/starting-workflows.mdx +104 -30
  199. package/docs/foundations/streaming.mdx +108 -60
  200. package/docs/foundations/versioning.mdx +4 -4
  201. package/docs/foundations/workflows-and-steps.mdx +10 -10
  202. package/docs/getting-started/astro.mdx +22 -18
  203. package/docs/getting-started/express.mdx +15 -11
  204. package/docs/getting-started/fastify.mdx +15 -11
  205. package/docs/getting-started/hono.mdx +15 -11
  206. package/docs/getting-started/index.mdx +10 -3
  207. package/docs/getting-started/meta.json +3 -1
  208. package/docs/getting-started/nestjs.mdx +264 -21
  209. package/docs/getting-started/next.mdx +18 -14
  210. package/docs/getting-started/nitro.mdx +22 -18
  211. package/docs/getting-started/nuxt.mdx +15 -11
  212. package/docs/getting-started/python.mdx +190 -41
  213. package/docs/getting-started/react-router/index.mdx +33 -0
  214. package/docs/getting-started/react-router/meta.json +5 -0
  215. package/docs/getting-started/react-router/v7.mdx +237 -0
  216. package/docs/getting-started/react-router/v8.mdx +232 -0
  217. package/docs/getting-started/sveltekit.mdx +20 -16
  218. package/docs/getting-started/tanstack-start.mdx +17 -13
  219. package/docs/getting-started/vite.mdx +15 -11
  220. package/docs/how-it-works/cancellation.mdx +63 -63
  221. package/docs/how-it-works/code-transform.mdx +82 -66
  222. package/docs/how-it-works/encryption.mdx +30 -26
  223. package/docs/how-it-works/event-sourcing.mdx +132 -35
  224. package/docs/how-it-works/framework-integrations.mdx +96 -337
  225. package/docs/how-it-works/understanding-directives.mdx +22 -22
  226. package/docs/internal/index.mdx +6 -4
  227. package/docs/internal/meta.json +6 -1
  228. package/docs/internal/nitro-native-build.mdx +38 -0
  229. package/docs/internal/nitro-web-ui.mdx +24 -0
  230. package/docs/internal/serializable-abort-controller.mdx +7 -7
  231. package/docs/meta.json +4 -2
  232. package/docs/observability/attributes.mdx +91 -21
  233. package/docs/observability/index.mdx +29 -15
  234. package/docs/observability/lifecycle-hooks.mdx +95 -0
  235. package/docs/observability/meta.json +1 -1
  236. package/docs/observability/retention.mdx +95 -0
  237. package/docs/observability/tracing.mdx +124 -0
  238. package/docs/testing/index.mdx +118 -38
  239. package/docs/testing/server-based.mdx +10 -10
  240. package/docs/whats-new.mdx +196 -0
  241. package/docs/worlds/building-a-world.mdx +600 -0
  242. package/docs/worlds/local.mdx +129 -0
  243. package/docs/worlds/meta.json +10 -0
  244. package/docs/worlds/postgres.mdx +428 -0
  245. package/docs/worlds/upgrading-to-v5.mdx +183 -0
  246. package/docs/worlds/vercel.mdx +389 -0
  247. package/package.json +17 -14
  248. package/docs/api-reference/workflow/experimental-set-attributes.mdx +0 -63
  249. package/docs/api-reference/workflow-api/world/index.mdx +0 -58
  250. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  251. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  252. package/docs/api-reference/workflow-api/world/queue.mdx +0 -86
  253. package/docs/deploying/building-a-world.mdx +0 -251
  254. package/docs/deploying/index.mdx +0 -95
  255. package/docs/deploying/meta.json +0 -4
  256. package/docs/deploying/world/local-world.mdx +0 -84
  257. package/docs/deploying/world/meta.json +0 -4
  258. package/docs/deploying/world/postgres-world.mdx +0 -224
  259. package/docs/deploying/world/vercel-world.mdx +0 -181
  260. package/docs/migration-guides/index.mdx +0 -34
  261. package/docs/migration-guides/meta.json +0 -9
  262. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -358
  263. package/docs/migration-guides/migrating-from-inngest.mdx +0 -304
  264. package/docs/migration-guides/migrating-from-temporal.mdx +0 -313
  265. package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -328
@@ -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.
13
15
 
14
- 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.
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
19
+
20
+ In distributed systems (calling external APIs), it is not always possible to ensure an operation has only been performed once 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,265 @@ 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. Check it before calling any step: steps the workflow calls before it suspends are started alongside the hook's registration, so a duplicate run that only learns of the conflict later (for example, by awaiting the hook and letting `HookConflictError` end the run) may already have started them. See [Registering a hook before a step uses it](/docs/api-reference/workflow/create-hook#registering-a-hook-before-a-step-uses-it).
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 hook can own `order:${orderId}`, so duplicate workflow runs converge on one 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 coordinates active runs by default: the token becomes available when its workflow ends. Set `experimental_minRetention` to keep it unavailable to late duplicates. After the workflow ends, the Hook can still be found with `getHookByToken()` until retention ends, but it cannot be resumed. See [`createHook()` minimum retention](/docs/api-reference/workflow/create-hook#keep-a-token-unavailable-after-the-run-ends) for examples and supported values.
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.** Reuse a completed owner's result, but reject other duplicates:
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 === "completed") {
204
+ return await conflict.returnValue;
205
+ }
206
+ return { status: "duplicate" as const, runId: conflict.runId };
207
+ }
208
+
209
+ return await processOwnedOrder(orderId);
210
+ }
211
+ ```
212
+
213
+ **Signal the owner instead of doing the work.** A conflict can refer to a finished run when `experimental_minRetention` is set, so check its status before sending data to its Hook:
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 && ["pending", "running"].includes(await conflict.status)) {
234
+ await forwardToOwner(token, { confirmed }); // [!code highlight]
235
+ return { status: "forwarded" as const, runId: conflict.runId };
236
+ }
237
+ if (conflict) {
238
+ return { status: "duplicate" as const, runId: conflict.runId };
239
+ }
240
+
241
+ // ... own the token and do the work
242
+ }
243
+ ```
244
+
245
+ **Supersede the owner.** Create the Hook with [`experimental_force: true`](/docs/api-reference/workflow/create-hook#take-over-a-token-another-run-holds) so the newest run takes the token over from the active owner. This also works for a finished run holding the token under `experimental_minRetention`:
246
+
247
+ ```typescript lineNumbers
248
+ import { createHook } from "workflow";
249
+
250
+ type OrderRequest = { confirmed: boolean };
251
+ declare function chargeOrder(orderId: string): Promise<void>; // @setup
252
+
253
+ export async function processOrderNewestWins(orderId: string) {
254
+ "use workflow";
255
+
256
+ using request = createHook<OrderRequest>({
257
+ token: `order:${orderId}`,
258
+ experimental_force: true, // [!code highlight]
259
+ });
260
+
261
+ // This run now owns the token.
262
+ const { confirmed } = await request;
263
+ if (confirmed) {
264
+ await chargeOrder(orderId);
265
+ }
266
+ return { status: "processed" as const };
267
+ }
268
+ ```
269
+
270
+ The previous owner's `await request` rejects with [`HookForceClaimedError`](/docs/api-reference/workflow-errors/hook-force-claimed-error), and every `resumeHook()` for the token reaches the new run from then on. Any run of this workflow can be superseded by a later one, so catch the error and exit cleanly:
271
+
272
+ ```typescript lineNumbers
273
+ import { createHook } from "workflow";
274
+ import { HookForceClaimedError } from "workflow/errors";
275
+
276
+ type OrderRequest = { confirmed: boolean };
277
+ declare function chargeOrder(orderId: string): Promise<void>; // @setup
278
+
279
+ export async function processOrderNewestWins(orderId: string) {
280
+ "use workflow";
281
+
282
+ using request = createHook<OrderRequest>({
283
+ token: `order:${orderId}`,
284
+ experimental_force: true,
285
+ });
286
+
287
+ try {
288
+ const { confirmed } = await request;
289
+ if (confirmed) {
290
+ await chargeOrder(orderId);
291
+ }
292
+ return { status: "processed" as const };
293
+ } catch (error) {
294
+ if (HookForceClaimedError.is(error)) { // [!code highlight]
295
+ // A newer run for this order owns the token now. Stop here.
296
+ return { status: "superseded" as const, runId: error.claimedByRunId }; // [!code highlight]
297
+ }
298
+ throw error;
299
+ }
300
+ }
301
+ ```
302
+
303
+ A run started at a Workflow spec version below 8, including runs started by older SDK releases, can't be taken from. In that case the forced Hook rejects with `HookConflictError`, as it would without `experimental_force`.
304
+
305
+ 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
306
 
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.
307
+ 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
308
 
59
309
  ## Related docs
60
310
 
61
311
  - Learn about retries in [Errors & Retrying](/docs/foundations/errors-and-retries)
62
312
  - API reference: [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata)
313
+ - API reference: [`createHook()`](/docs/api-reference/workflow/create-hook)
314
+ - API reference: [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token)
315
+ - API reference: [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook)
316
+ - API reference: [`start()`](/docs/api-reference/workflow-api/start)
317
+ - Learn about deterministic hook tokens in [Hooks](/docs/foundations/hooks)
@@ -10,29 +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
- <Card href="/docs/foundations/versioning" title="Versioning">
36
- Understand how runs stay pinned to deployments and when to opt in to newer code.
37
- </Card>
38
- </Cards>
13
+ <AutoCards />
@@ -15,11 +15,11 @@ All function arguments and return values passed between workflow and step functi
15
15
  The serialization system ensures that all data persists correctly across workflow suspensions and resumptions, enabling durable execution.
16
16
  </Callout>
17
17
 
18
- ## Supported Serializable Types
18
+ ## Supported serializable types
19
19
 
20
20
  The following types can be serialized and passed through workflow functions:
21
21
 
22
- **Standard JSON Types:**
22
+ **Standard JSON types:**
23
23
 
24
24
  - `string`
25
25
  - `number`
@@ -28,12 +28,13 @@ The following types can be serialized and passed through workflow functions:
28
28
  - Arrays of serializable values
29
29
  - Objects with string keys and serializable values
30
30
 
31
- **Extended Types:**
31
+ **Extended types:**
32
32
 
33
33
  - `undefined`
34
34
  - `bigint`
35
35
  - `ArrayBuffer`
36
36
  - `BigInt64Array`, `BigUint64Array`
37
+ - `DataView`
37
38
  - `Date`
38
39
  - `Float32Array`, `Float64Array`
39
40
  - `Int8Array`, `Int16Array`, `Int32Array`
@@ -58,7 +59,7 @@ These types have special handling and are explained in detail in the sections be
58
59
  - `AbortController`
59
60
  - `AbortSignal`
60
61
 
61
- ## Pass-by-Value Semantics
62
+ ## Pass-by-value semantics
62
63
 
63
64
  **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
 
@@ -81,7 +82,7 @@ async function updateUserStep(user: { id: string; name: string; email: string })
81
82
  }
82
83
  ```
83
84
 
84
- **Correct - return the modified data:**
85
+ **Correct, return the modified data:**
85
86
 
86
87
  ```typescript title="workflows/correct-mutation.ts" lineNumbers
87
88
  export async function updateUserWorkflow(userId: string) {
@@ -100,7 +101,7 @@ async function updateUserStep(user: { id: string; name: string; email: string })
100
101
  }
101
102
  ```
102
103
 
103
- **Custom Classes:**
104
+ **Custom classes:**
104
105
 
105
106
  - Class instances that implement [`WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`](#custom-class-serialization)
106
107
 
@@ -110,7 +111,7 @@ async function updateUserStep(user: { id: string; name: string; email: string })
110
111
 
111
112
  For complete information about using streams in workflows, including patterns for AI streaming, file processing, and progress updates, see the [Streaming Guide](/docs/foundations/streaming).
112
113
 
113
- ## Request & Response
114
+ ## Request & response
114
115
 
115
116
  The Web API [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) APIs are supported by the serialization system,
116
117
  and can be passed around between workflow and step functions similarly to other data types.
@@ -140,7 +141,7 @@ export async function handleWebhookWorkflow() {
140
141
  }
141
142
  ```
142
143
 
143
- ### Using `fetch` in Workflows
144
+ ### Using `fetch` in workflows
144
145
 
145
146
  Because `Request` and `Response` are serializable, Workflow SDK provides a `fetch` function that can be used directly in workflow functions:
146
147
 
@@ -158,7 +159,7 @@ export async function apiWorkflow() {
158
159
  }
159
160
  ```
160
161
 
161
- The implementation is straightforward - `fetch` from workflow is a step function that wraps the standard `fetch`:
162
+ The `fetch` implementation from `workflow` is a step function that wraps the standard `fetch`:
162
163
 
163
164
  ```typescript title="Implementation" lineNumbers
164
165
  export async function fetch(...args: Parameters<typeof globalThis.fetch>) {
@@ -202,11 +203,11 @@ async function fetchData(signal: AbortSignal) {
202
203
 
203
204
  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
205
 
205
- ## Custom Class Serialization
206
+ ## Custom class serialization
206
207
 
207
208
  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.
208
209
 
209
- ### Basic Example
210
+ ### Basic example
210
211
 
211
212
  {/* @expect-error:2351 */}
212
213
 
@@ -256,13 +257,13 @@ async function doublePoint(point: Point) {
256
257
  }
257
258
  ```
258
259
 
259
- ### How It Works
260
+ ### How it works
260
261
 
261
262
  1. **`WORKFLOW_SERIALIZE`**: A static method that receives a class instance and returns serializable data (primitives, plain objects, arrays, etc.)
262
263
 
263
264
  2. **`WORKFLOW_DESERIALIZE`**: A static method that receives the serialized data and returns a new class instance
264
265
 
265
- 3. **Automatic Registration**: The SWC compiler plugin automatically detects classes that implement these symbols and registers them for serialization. Each class receives a deterministic `classId` derived from its file path and class name, and is registered into the global `Symbol.for("workflow-class-registry")` registry at build time — no manual registration step is required
266
+ 3. **Automatic Registration**: The SWC compiler plugin automatically detects classes that implement these symbols and registers them for serialization. Each class receives a deterministic `classId` derived from its file path and class name, and is registered into the global `Symbol.for("workflow-class-registry")` registry at build time. No manual registration step is required
266
267
 
267
268
  ### Requirements
268
269
 
@@ -279,12 +280,12 @@ The `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE` methods run inside the workf
279
280
  - No non-deterministic operations (like `Math.random()` or `Date.now()`)
280
281
  - No external network calls
281
282
 
282
- Keep these methods simple and focused on data transformation only.
283
+ Keep these methods focused on data transformation only.
283
284
  </Callout>
284
285
 
285
- ### Instance Methods as Steps
286
+ ### Instance methods as steps
286
287
 
287
- In practice, many classes have methods that need Node.js APIs, perform network calls, or interact with databases — operations that are not allowed in the `"use workflow"` execution context. You can make these methods workflow-compatible by adding `"use step"` to them. The SWC compiler will strip the method bodies from the workflow bundle and replace them with proxy functions that invoke the method as a step — with full Node.js runtime access. The `this` context (the class instance) is automatically serialized and deserialized across the workflow/step boundary.
288
+ In practice, many classes have methods that need Node.js APIs, perform network calls, or interact with databases, operations that are not allowed in the `"use workflow"` execution context. You can make these methods workflow-compatible by adding `"use step"` to them. The SWC compiler will strip the method bodies from the workflow bundle and replace them with proxy functions that invoke the method as a step, with full Node.js runtime access. The `this` context (the class instance) is automatically serialized and deserialized across the workflow/step boundary.
288
289
 
289
290
  This requires the class to implement `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`, so that the instance can be passed to the step execution context.
290
291
 
@@ -301,7 +302,7 @@ class Order {
301
302
  public createdAt: Date
302
303
  ) {}
303
304
 
304
- // Custom serialization — data must be serializable types
305
+ // Custom serialization: data must be serializable types
305
306
  static [WORKFLOW_SERIALIZE](instance: Order) { // [!code highlight]
306
307
  return { // [!code highlight]
307
308
  id: instance.id, // [!code highlight]
@@ -329,7 +330,7 @@ class Order {
329
330
  }
330
331
 
331
332
  // Instance methods with "use step" run as step functions
332
- // with full Node.js access — `this` is automatically serialized
333
+ // with full Node.js access; `this` is automatically serialized
333
334
  async save(): Promise<void> {
334
335
  "use step"; // [!code highlight]
335
336
  await db.orders.insert({ // [!code highlight]
@@ -355,7 +356,7 @@ class Order {
355
356
  }
356
357
  ```
357
358
 
358
- The class can then be used naturally inside a workflow function. Instance methods marked with `"use step"` are each executed as a step — with automatic caching, retry semantics, and full Node.js runtime access. Methods _without_ `"use step"` run directly in the workflow context, so they must follow the same constraints as workflow functions:
359
+ The class can then be used naturally inside a workflow function. Instance methods marked with `"use step"` are each executed as a step, with automatic caching, retry semantics, and full Node.js runtime access. Methods _without_ `"use step"` run directly in the workflow context, so they must follow the same constraints as workflow functions:
359
360
 
360
361
  {/* @expect-error:2693 */}
361
362
 
@@ -369,7 +370,7 @@ export async function processOrderWorkflow(
369
370
 
370
371
  const order = new Order(orderId, items, new Date()); // [!code highlight]
371
372
 
372
- // Runs in the workflow context — no "use step" needed
373
+ // Runs in the workflow context; no "use step" needed
373
374
  const itemCount = order.total(); // [!code highlight]
374
375
 
375
376
  // Each "use step" instance method call runs as a separate step
@@ -380,7 +381,7 @@ export async function processOrderWorkflow(
380
381
  }
381
382
  ```
382
383
 
383
- Note that [pass-by-value semantics](#pass-by-value-semantics) also apply to the `this` context of `"use step"` instance methods. Modifying instance properties inside a step method will not affect the original instance in the workflow. If you need to update instance state, return `this` from the step method and re-assign the variable in the workflow:
384
+ [Pass-by-value semantics](#pass-by-value-semantics) also apply to the `this` context of `"use step"` instance methods. Modifying instance properties inside a step method will not affect the original instance in the workflow. If you need to update instance state, return `this` from the step method and re-assign the variable in the workflow:
384
385
 
385
386
  {/* @expect-error:2351 */}
386
387
 
@@ -408,4 +409,3 @@ export async function processOrderWorkflow() {
408
409
  order = await order.addItem("Widget", 3); // [!code highlight]
409
410
  }
410
411
  ```
411
-