@workflow/core 5.0.0-beta.42 → 5.0.0-beta.44

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 (220) hide show
  1. package/dist/capabilities.d.ts +4 -4
  2. package/dist/capabilities.js +5 -5
  3. package/dist/capture-stack.d.ts +1 -1
  4. package/dist/capture-stack.js +2 -2
  5. package/dist/class-serialization.d.ts +8 -8
  6. package/dist/class-serialization.js +9 -9
  7. package/dist/classify-error.js +5 -5
  8. package/dist/context-violation-error.d.ts +5 -5
  9. package/dist/context-violation-error.d.ts.map +1 -1
  10. package/dist/context-violation-error.js +5 -5
  11. package/dist/create-hook.d.ts +10 -6
  12. package/dist/create-hook.d.ts.map +1 -1
  13. package/dist/create-hook.js +1 -1
  14. package/dist/define-hook.js +2 -2
  15. package/dist/describe-error.d.ts +7 -7
  16. package/dist/describe-error.js +8 -8
  17. package/dist/encryption.d.ts +5 -5
  18. package/dist/encryption.js +7 -7
  19. package/dist/events-consumer.d.ts +19 -7
  20. package/dist/events-consumer.d.ts.map +1 -1
  21. package/dist/events-consumer.js +33 -12
  22. package/dist/flushable-stream.d.ts +4 -4
  23. package/dist/flushable-stream.js +12 -12
  24. package/dist/global.d.ts +1 -1
  25. package/dist/global.js +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +2 -2
  28. package/dist/log-format.d.ts +2 -2
  29. package/dist/log-format.js +5 -5
  30. package/dist/logger.js +4 -4
  31. package/dist/private.d.ts +39 -64
  32. package/dist/private.d.ts.map +1 -1
  33. package/dist/private.js +294 -48
  34. package/dist/replay-payload-cache.d.ts +1 -1
  35. package/dist/replay-payload-cache.js +4 -4
  36. package/dist/runtime/compute-instance.d.ts +4 -3
  37. package/dist/runtime/compute-instance.d.ts.map +1 -1
  38. package/dist/runtime/compute-instance.js +5 -4
  39. package/dist/runtime/constants.d.ts +46 -19
  40. package/dist/runtime/constants.d.ts.map +1 -1
  41. package/dist/runtime/constants.js +76 -38
  42. package/dist/runtime/count-step-started-events.d.ts +7 -7
  43. package/dist/runtime/count-step-started-events.js +4 -4
  44. package/dist/runtime/deployment-guard.d.ts +7 -7
  45. package/dist/runtime/deployment-guard.js +8 -8
  46. package/dist/runtime/get-port-lazy.d.ts +2 -2
  47. package/dist/runtime/get-port-lazy.d.ts.map +1 -1
  48. package/dist/runtime/get-port-lazy.js +37 -30
  49. package/dist/runtime/get-world-lazy.d.ts +2 -2
  50. package/dist/runtime/get-world-lazy.js +3 -3
  51. package/dist/runtime/helpers.d.ts +18 -18
  52. package/dist/runtime/helpers.js +27 -27
  53. package/dist/runtime/quickjs-entrypoint.d.ts +23 -10
  54. package/dist/runtime/quickjs-entrypoint.d.ts.map +1 -1
  55. package/dist/runtime/quickjs-entrypoint.js +200 -81
  56. package/dist/runtime/quickjs-runtime.d.ts +31 -15
  57. package/dist/runtime/quickjs-runtime.d.ts.map +1 -1
  58. package/dist/runtime/quickjs-runtime.js +126 -77
  59. package/dist/runtime/quickjs-serde.d.ts +10 -10
  60. package/dist/runtime/quickjs-serde.js +20 -20
  61. package/dist/runtime/replay-budget.d.ts +4 -4
  62. package/dist/runtime/replay-budget.js +5 -5
  63. package/dist/runtime/replay-recovery-reporter.d.ts +1 -1
  64. package/dist/runtime/replay-recovery-reporter.js +2 -2
  65. package/dist/runtime/resume-hook.d.ts +2 -2
  66. package/dist/runtime/resume-hook.js +27 -27
  67. package/dist/runtime/resume-latency.d.ts +25 -25
  68. package/dist/runtime/resume-latency.d.ts.map +1 -1
  69. package/dist/runtime/resume-latency.js +4 -4
  70. package/dist/runtime/run-id-time.d.ts +2 -2
  71. package/dist/runtime/run-id-time.js +5 -5
  72. package/dist/runtime/run.d.ts +21 -4
  73. package/dist/runtime/run.d.ts.map +1 -1
  74. package/dist/runtime/run.js +154 -35
  75. package/dist/runtime/runs.d.ts +4 -4
  76. package/dist/runtime/runs.js +2 -2
  77. package/dist/runtime/start.d.ts +3 -3
  78. package/dist/runtime/start.d.ts.map +1 -1
  79. package/dist/runtime/start.js +27 -23
  80. package/dist/runtime/step-executor.d.ts +59 -11
  81. package/dist/runtime/step-executor.d.ts.map +1 -1
  82. package/dist/runtime/step-executor.js +116 -42
  83. package/dist/runtime/step-latency.d.ts +20 -14
  84. package/dist/runtime/step-latency.d.ts.map +1 -1
  85. package/dist/runtime/step-latency.js +13 -11
  86. package/dist/runtime/step-ownership.d.ts +6 -6
  87. package/dist/runtime/step-ownership.js +8 -8
  88. package/dist/runtime/step-single-flight.d.ts +1 -1
  89. package/dist/runtime/step-single-flight.d.ts.map +1 -1
  90. package/dist/runtime/step-single-flight.js +17 -11
  91. package/dist/runtime/suspension-handler.d.ts +99 -7
  92. package/dist/runtime/suspension-handler.d.ts.map +1 -1
  93. package/dist/runtime/suspension-handler.js +678 -57
  94. package/dist/runtime/unserializable-step.d.ts +34 -0
  95. package/dist/runtime/unserializable-step.d.ts.map +1 -0
  96. package/dist/runtime/unserializable-step.js +52 -0
  97. package/dist/runtime/vm-mode.d.ts +1 -1
  98. package/dist/runtime/vm-mode.js +2 -2
  99. package/dist/runtime/wait-continuation.d.ts +31 -9
  100. package/dist/runtime/wait-continuation.d.ts.map +1 -1
  101. package/dist/runtime/wait-continuation.js +44 -13
  102. package/dist/runtime/wait-until.d.ts +1 -1
  103. package/dist/runtime/wait-until.js +2 -2
  104. package/dist/runtime/world-compatibility.d.ts +20 -9
  105. package/dist/runtime/world-compatibility.d.ts.map +1 -1
  106. package/dist/runtime/world-compatibility.js +24 -13
  107. package/dist/runtime/world-init.d.ts +5 -5
  108. package/dist/runtime/world-init.js +6 -6
  109. package/dist/runtime/world.d.ts.map +1 -1
  110. package/dist/runtime/world.js +13 -3
  111. package/dist/runtime.d.ts.map +1 -1
  112. package/dist/runtime.js +417 -213
  113. package/dist/sealed-box.d.ts +7 -7
  114. package/dist/sealed-box.js +15 -15
  115. package/dist/serialization/client.js +2 -2
  116. package/dist/serialization/codec-devalue-vm.d.ts +1 -1
  117. package/dist/serialization/codec-devalue-vm.js +4 -4
  118. package/dist/serialization/codec-devalue.d.ts +1 -1
  119. package/dist/serialization/codec-devalue.js +2 -2
  120. package/dist/serialization/codec.d.ts +4 -4
  121. package/dist/serialization/codec.js +2 -2
  122. package/dist/serialization/compression.d.ts +12 -11
  123. package/dist/serialization/compression.d.ts.map +1 -1
  124. package/dist/serialization/compression.js +16 -15
  125. package/dist/serialization/encryption.d.ts +7 -7
  126. package/dist/serialization/encryption.d.ts.map +1 -1
  127. package/dist/serialization/encryption.js +8 -8
  128. package/dist/serialization/errors.js +2 -2
  129. package/dist/serialization/format.d.ts +6 -6
  130. package/dist/serialization/format.js +7 -7
  131. package/dist/serialization/hardened.d.ts +18 -18
  132. package/dist/serialization/hardened.d.ts.map +1 -1
  133. package/dist/serialization/hardened.js +41 -32
  134. package/dist/serialization/index.d.ts +1 -1
  135. package/dist/serialization/index.js +2 -2
  136. package/dist/serialization/reducers/class-vm.d.ts +1 -1
  137. package/dist/serialization/reducers/class-vm.js +2 -2
  138. package/dist/serialization/reducers/class.js +2 -2
  139. package/dist/serialization/reducers/common-vm.js +12 -12
  140. package/dist/serialization/reducers/common.js +12 -12
  141. package/dist/serialization/reducers/step-function-vm.d.ts +1 -1
  142. package/dist/serialization/reducers/step-function-vm.js +2 -2
  143. package/dist/serialization/reducers/step-function.js +2 -2
  144. package/dist/serialization/step.js +2 -2
  145. package/dist/serialization/types.d.ts +5 -5
  146. package/dist/serialization/types.js +2 -2
  147. package/dist/serialization/workflow-vm.d.ts +1 -1
  148. package/dist/serialization/workflow-vm.d.ts.map +1 -1
  149. package/dist/serialization/workflow-vm.js +13 -10
  150. package/dist/serialization-format.d.ts +11 -11
  151. package/dist/serialization-format.d.ts.map +1 -1
  152. package/dist/serialization-format.js +29 -23
  153. package/dist/serialization.d.ts +20 -12
  154. package/dist/serialization.d.ts.map +1 -1
  155. package/dist/serialization.js +141 -99
  156. package/dist/set-attributes.js +2 -2
  157. package/dist/source-map.d.ts +1 -1
  158. package/dist/source-map.d.ts.map +1 -1
  159. package/dist/source-map.js +18 -12
  160. package/dist/step/context-storage.d.ts +5 -5
  161. package/dist/step/context-storage.js +1 -1
  162. package/dist/step/writable-stream.d.ts +3 -3
  163. package/dist/step/writable-stream.js +7 -7
  164. package/dist/step.js +12 -12
  165. package/dist/symbols.d.ts +2 -2
  166. package/dist/symbols.js +3 -3
  167. package/dist/telemetry/semantic-conventions.d.ts +21 -13
  168. package/dist/telemetry/semantic-conventions.d.ts.map +1 -1
  169. package/dist/telemetry/semantic-conventions.js +21 -15
  170. package/dist/telemetry.d.ts +9 -3
  171. package/dist/telemetry.d.ts.map +1 -1
  172. package/dist/telemetry.js +40 -10
  173. package/dist/test-support/orchestrator-context.d.ts.map +1 -1
  174. package/dist/test-support/orchestrator-context.js +12 -2
  175. package/dist/version.d.ts +1 -1
  176. package/dist/version.js +2 -2
  177. package/dist/vm/index.js +4 -4
  178. package/dist/vm/script-cache.d.ts.map +1 -1
  179. package/dist/vm/script-cache.js +23 -17
  180. package/dist/vm/uint8array-base64.js +1 -1
  181. package/dist/workflow/abort-controller.d.ts +1 -1
  182. package/dist/workflow/abort-controller.js +12 -12
  183. package/dist/workflow/create-hook.js +3 -3
  184. package/dist/workflow/get-workflow-metadata.js +2 -2
  185. package/dist/workflow/hook.js +18 -18
  186. package/dist/workflow/set-attributes.d.ts +1 -1
  187. package/dist/workflow/set-attributes.js +2 -2
  188. package/dist/workflow/sleep.js +2 -2
  189. package/dist/workflow/world-init-stub.d.ts +1 -1
  190. package/dist/workflow/world-init-stub.js +2 -2
  191. package/dist/workflow.d.ts +2 -2
  192. package/dist/workflow.js +36 -14
  193. package/docs/api-reference/create-hook.mdx +15 -15
  194. package/docs/api-reference/create-webhook.mdx +15 -15
  195. package/docs/api-reference/define-hook.mdx +10 -10
  196. package/docs/api-reference/fatal-error.mdx +2 -2
  197. package/docs/api-reference/fetch.mdx +7 -7
  198. package/docs/api-reference/get-step-metadata.mdx +2 -2
  199. package/docs/api-reference/get-workflow-metadata.mdx +3 -3
  200. package/docs/api-reference/get-writable.mdx +7 -7
  201. package/docs/api-reference/index.mdx +1 -1
  202. package/docs/api-reference/retryable-error.mdx +1 -1
  203. package/docs/api-reference/set-attributes.mdx +2 -2
  204. package/docs/api-reference/sleep.mdx +3 -3
  205. package/docs/foundations/cancellation.mdx +31 -31
  206. package/docs/foundations/errors-and-retries.mdx +42 -11
  207. package/docs/foundations/hooks.mdx +37 -37
  208. package/docs/foundations/idempotency.mdx +9 -9
  209. package/docs/foundations/serialization.mdx +21 -22
  210. package/docs/foundations/starting-workflows.mdx +36 -37
  211. package/docs/foundations/streaming.mdx +68 -39
  212. package/docs/foundations/versioning.mdx +3 -3
  213. package/docs/foundations/workflows-and-steps.mdx +9 -9
  214. package/docs/how-it-works/cancellation.mdx +62 -62
  215. package/docs/how-it-works/code-transform.mdx +66 -54
  216. package/docs/how-it-works/encryption.mdx +25 -21
  217. package/docs/how-it-works/event-sourcing.mdx +84 -28
  218. package/docs/how-it-works/framework-integrations.mdx +12 -12
  219. package/docs/how-it-works/understanding-directives.mdx +21 -21
  220. package/package.json +7 -7
@@ -14,7 +14,7 @@ Workflow DevKit supports two cancellation mechanisms: **AbortSignal** for fine-g
14
14
 
15
15
  ## AbortSignal
16
16
 
17
- `AbortController` and `AbortSignal` work across workflow and step boundaries. Create an `AbortController` with `new AbortController()` in a workflow function, pass its signal to steps, and call `abort()` using the standard [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) API you already know.
17
+ `AbortController` and `AbortSignal` work across workflow and step boundaries. Create an `AbortController` with `new AbortController()` in a workflow function, pass its signal to steps, and call `abort()` using the standard [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) API.
18
18
 
19
19
  ```typescript lineNumbers
20
20
  import { sleep } from "workflow";
@@ -48,17 +48,17 @@ async function longRunningStep(signal: AbortSignal) {
48
48
  }
49
49
  ```
50
50
 
51
- No special imports, no wrapper functions just the standard `AbortController` API.
51
+ Use the standard `AbortController` API without special imports or wrapper functions.
52
52
 
53
53
  <Callout type="info">
54
- Cancellation is **cooperative**. Aborting a signal doesn't forcefully kill a step it's up to the step's code to check `signal.aborted` or pass the signal to APIs like `fetch` that respect it. If a step ignores the signal, it runs to completion.
54
+ Cancellation is **cooperative**. Aborting a signal doesn't forcefully kill a step. The step's code must check `signal.aborted` or pass the signal to APIs like `fetch` that respect it. If a step ignores the signal, it runs to completion.
55
55
  </Callout>
56
56
 
57
57
  <Callout type="info">
58
58
  To learn how `AbortController` works durably across workflow suspensions, replays, and step boundaries, see [How Cancellation Works](/docs/how-it-works/cancellation).
59
59
  </Callout>
60
60
 
61
- ### Timeout with Cancellation
61
+ ### Timeout with cancellation
62
62
 
63
63
  Race a step against a timeout, and cancel the step if the timeout wins:
64
64
 
@@ -90,7 +90,7 @@ async function fetchUrl(url: string, signal: AbortSignal) {
90
90
  }
91
91
  ```
92
92
 
93
- ### Cancelling Parallel Work
93
+ ### Cancelling parallel work
94
94
 
95
95
  When racing multiple steps, cancel the losers:
96
96
 
@@ -116,7 +116,7 @@ async function fetchUrl(url: string, signal: AbortSignal) {
116
116
  }
117
117
  ```
118
118
 
119
- ### Passing Signal Through a Pipeline
119
+ ### Passing signal through a pipeline
120
120
 
121
121
  Pass the same signal to a chain of steps. Aborting cancels whichever step is currently running:
122
122
 
@@ -175,7 +175,7 @@ async function uploadData(data: ArrayBuffer, signal: AbortSignal) {
175
175
  }
176
176
  ```
177
177
 
178
- ### Step-Initiated Abort
178
+ ### Step-initiated abort
179
179
 
180
180
  A step can receive the full `AbortController` and call `abort()` to cancel parallel work. This is useful for watchdog/monitor patterns where one step observes an external condition and cancels other in-flight steps:
181
181
 
@@ -220,7 +220,7 @@ async function monitorQuota(userId: string, controller: AbortController) {
220
220
  }
221
221
  ```
222
222
 
223
- ### User-Triggered Cancellation with Hooks
223
+ ### User-triggered cancellation with hooks
224
224
 
225
225
  Combine hooks with abort controllers to let users cancel in-flight work from an external API:
226
226
 
@@ -266,23 +266,23 @@ export async function POST(request: Request) {
266
266
  }
267
267
  ```
268
268
 
269
- ### How Steps Handle Abort
269
+ ### How steps handle abort
270
270
 
271
271
  When an `AbortSignal` is aborted, the behavior depends on how the step uses it:
272
272
 
273
- | Usage | Behavior on Abort |
273
+ | Usage | Behavior on abort |
274
274
  |-------|-------------------|
275
- | `fetch(url, { signal })` | Request is cancelled, throws `AbortError` |
275
+ | `fetch(url, { signal })` | Request is canceled, throws `AbortError` |
276
276
  | `signal.throwIfAborted()` | Throws the abort reason |
277
277
  | `signal.aborted` check | Returns `true`, step can exit gracefully |
278
278
  | `signal.addEventListener('abort', fn)` | Callback fires, step can clean up |
279
279
  | Ignored | Step runs to completion (abort is cooperative) |
280
280
 
281
- ### Abort Errors Skip Retries
281
+ ### Abort errors skip retries
282
282
 
283
- When a step throws due to an abort (e.g., `fetch` throws `AbortError`, or `signal.throwIfAborted()` throws), the error is automatically wrapped in a `FatalError`. This means the step **skips retries** and the error bubbles up to the workflow immediately.
283
+ When a step throws due to an abort (for example, `fetch` throws `AbortError`, or `signal.throwIfAborted()` throws), the runtime wraps the error in a `FatalError`. The step **skips retries**, and the error immediately bubbles up to the workflow.
284
284
 
285
- This is the correct behavior because an abort is an intentional cancellation retrying the step would just result in another abort. You don't need to manually wrap abort errors in `FatalError`.
285
+ An abort is an intentional cancellation, so retrying the step would result in another abort. You don't need to manually wrap abort errors in `FatalError`.
286
286
 
287
287
  ```typescript lineNumbers
288
288
  import { sleep } from "workflow";
@@ -299,7 +299,7 @@ export async function workflow() {
299
299
  if (result === null) controller.abort();
300
300
  return result;
301
301
  } catch (err) {
302
- // AbortError arrives as FatalError no retries attempted // [!code highlight]
302
+ // AbortError arrives as FatalError, with no retries attempted // [!code highlight]
303
303
  return { status: "cancelled" };
304
304
  }
305
305
  }
@@ -312,7 +312,7 @@ async function cancellableStep(signal: AbortSignal) {
312
312
  }
313
313
  ```
314
314
 
315
- ### Passing AbortSignal as Workflow Input
315
+ ### Passing AbortSignal as workflow input
316
316
 
317
317
  You can pass an `AbortSignal` from external code into a workflow via `start()`:
318
318
 
@@ -329,15 +329,15 @@ export async function POST(request: Request) {
329
329
  }
330
330
  ```
331
331
 
332
- When the signal is serialized at the `start()` boundary, an event listener is attached to the external signal that writes the cancellation packet to the backing stream. This means the external `abort()` propagates into the workflow — but only while the originating process is still alive (same constraint as passing a `ReadableStream` as input).
332
+ When the signal is serialized at the `start()` boundary, an event listener attaches to the external signal and writes the cancellation packet to the backing stream. The external `abort()` propagates into the workflow only while the originating process is still alive (the same constraint as passing a `ReadableStream` as input).
333
333
 
334
334
  <Callout type="info">
335
335
  For reliable external cancellation that works regardless of process lifetime, prefer the [User-Triggered Cancellation with Hooks](#user-triggered-cancellation-with-hooks) pattern. Hooks are durable and don't depend on the caller's process staying alive.
336
336
  </Callout>
337
337
 
338
- ## Run Cancellation
338
+ ## Run cancellation
339
339
 
340
- Run cancellation stops an entire workflow at the next suspension point. Unlike `AbortSignal`, it is not cooperative the workflow does not continue executing after cancellation.
340
+ Run cancellation stops an entire workflow at the next suspension point. Unlike `AbortSignal`, it is not cooperative. The workflow does not continue executing after cancellation.
341
341
 
342
342
  ```typescript title="app/api/cancel-run/route.ts" lineNumbers
343
343
  import { getRun } from "workflow/api";
@@ -353,10 +353,10 @@ export async function POST(request: Request) {
353
353
  ```
354
354
 
355
355
  <Callout type="info">
356
- Calling `run.cancel()` is the same action as clicking the **Cancel** button on a run in the observability UI both produce identical `run_cancelled` events in the event log.
356
+ Calling `run.cancel()` is the same action as clicking the **Cancel** button on a run in the observability UI. Both produce identical `run_cancelled` events in the event log.
357
357
  </Callout>
358
358
 
359
- When a run is cancelled:
359
+ When a run is canceled:
360
360
  - The workflow stops at its next suspension point (step call, hook await, or sleep)
361
361
  - A `run_cancelled` event is recorded in the [event log](/docs/how-it-works/event-sourcing)
362
362
  - All associated hooks are disposed and their tokens released
@@ -366,19 +366,19 @@ When a run is cancelled:
366
366
  Run cancellation does **not** automatically abort any outstanding `AbortSignal`s. Steps that are currently executing will run to completion. If you need in-flight cancellation of specific operations, use `AbortSignal`.
367
367
  </Callout>
368
368
 
369
- ## AbortSignal vs. Run Cancellation
369
+ ## AbortSignal vs. run cancellation
370
370
 
371
371
  | | AbortSignal | Run Cancellation |
372
372
  |---|---|---|
373
373
  | **Scope** | Individual operations within a step | Entire workflow run |
374
374
  | **Triggered by** | Your code (`controller.abort()`) | External API (`run.cancel()`) |
375
- | **Cooperative** | Yes steps must check the signal | No workflow stops at the next suspension point |
375
+ | **Cooperative** | Yes. Steps must check the signal | No. The workflow stops at the next suspension point |
376
376
  | **Granularity** | Can target specific steps or operations | All-or-nothing |
377
377
  | **In-flight steps** | Aborted immediately if using the signal | Run to completion |
378
378
 
379
379
  Use `AbortSignal` when you need fine-grained, in-flight cancellation of specific operations. Use run cancellation when you want to stop the entire workflow.
380
380
 
381
- ## Best Practices
381
+ ## Best practices
382
382
 
383
383
  **Use `throwIfAborted()` before expensive work.** This throws the signal's abort reason if the signal is already aborted, preventing wasted compute:
384
384
 
@@ -448,12 +448,12 @@ export async function workflow() {
448
448
  }
449
449
  ```
450
450
 
451
- This is safe even if both steps have already completed aborting a finished operation is a no-op.
451
+ This is safe even if both steps have already completed because aborting a finished operation is a no-op.
452
452
 
453
- ## Related Documentation
453
+ ## Related documentation
454
454
 
455
- - [How Cancellation Works](/docs/how-it-works/cancellation) Hook and stream backing, serialization internals
456
- - [Serialization](/docs/foundations/serialization) Understanding serializable types
457
- - [Cookbook](/v5/cookbook) Timeout, race, and other reliability patterns
458
- - [Hooks](/docs/foundations/hooks) Pausing workflows for external events
459
- - [Errors and Retries](/docs/foundations/errors-and-retries) Handling step failures
455
+ - [How Cancellation Works](/docs/how-it-works/cancellation): Hook and stream backing, serialization internals
456
+ - [Serialization](/docs/foundations/serialization): Understanding serializable types
457
+ - [Cookbook](/cookbook): Timeout, race, and other reliability patterns
458
+ - [Hooks](/docs/foundations/hooks): Pausing workflows for external events
459
+ - [Errors and Retries](/docs/foundations/errors-and-retries): Handling step failures
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Errors & Retrying
3
- description: Customize retry behavior with FatalError and RetryableError for robust error handling.
3
+ description: Customize retry behavior with FatalError and RetryableError.
4
4
  type: conceptual
5
5
  summary: Control how steps handle failures and customize retry behavior.
6
6
  prerequisites:
@@ -12,7 +12,7 @@ related:
12
12
 
13
13
  By default, errors thrown inside steps are retried. Additionally, Workflow SDK provides two new types of errors you can use to customize retries.
14
14
 
15
- ## Default Retrying
15
+ ## Default retrying
16
16
 
17
17
  By default, steps retry up to 3 times on arbitrary errors. You can customize the number of retries by adding a `maxRetries` property to the step function.
18
18
 
@@ -42,9 +42,9 @@ Steps get enqueued immediately after a failure. Read on to see how this can be c
42
42
  more information.
43
43
  </Callout>
44
44
 
45
- ## Intentional Errors
45
+ ## Intentional errors
46
46
 
47
- When your step needs to intentionally throw an error and skip retrying, simply throw a [`FatalError`](/docs/api-reference/workflow/fatal-error).
47
+ When your step needs to intentionally throw an error and skip retrying, throw a [`FatalError`](/docs/api-reference/workflow/fatal-error).
48
48
 
49
49
  ```typescript lineNumbers
50
50
  import { FatalError } from "workflow";
@@ -67,7 +67,7 @@ async function callApi(endpoint: string) {
67
67
  }
68
68
  ```
69
69
 
70
- ## Customize Retry Behavior
70
+ ## Customize retry behavior
71
71
 
72
72
  When you need to customize the delay on a retry, use [`RetryableError`](/docs/api-reference/workflow/retryable-error) and set the `retryAfter` property.
73
73
 
@@ -97,7 +97,7 @@ async function callApi(endpoint: string) {
97
97
  }
98
98
  ```
99
99
 
100
- ## Advanced Example
100
+ ## Advanced example
101
101
 
102
102
  This final example combines everything we've learned, along with [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata).
103
103
 
@@ -139,7 +139,32 @@ callApi.maxRetries = 5; // Retry up to 5 times on failure (6 total attempts)
139
139
  step can run up to 4 times total (1 initial attempt + 3 retries).
140
140
  </Callout>
141
141
 
142
- ## Error Codes
142
+ ## Serialization failures
143
+
144
+ A step whose arguments or return value cannot be [serialized](/docs/foundations/serialization) fails like a step whose body threw a `FatalError`: the failure is deterministic, so it skips the retry loop, and a `try/catch` around the step call observes the `SerializationError`:
145
+
146
+ ```typescript lineNumbers
147
+ async function someStep(input: unknown) {
148
+ "use step";
149
+ return input;
150
+ }
151
+
152
+ export async function myWorkflow(input: unknown) {
153
+ "use workflow";
154
+
155
+ try {
156
+ await someStep(input);
157
+ } catch (err) {
158
+ if ((err as Error).name === "SerializationError") {
159
+ // e.g. `Failed to serialize step arguments at path "..."`
160
+ }
161
+ }
162
+ }
163
+ ```
164
+
165
+ Uncaught, the run fails immediately with the `USER_ERROR` code, without retrying. See [serialization-failed](/docs/errors/serialization-failed) for common causes and fixes.
166
+
167
+ ## Error codes
143
168
 
144
169
  When a workflow run fails, the error includes an `errorCode` that classifies the failure, alongside the original thrown value (preserved as `cause`):
145
170
 
@@ -153,7 +178,7 @@ try {
153
178
  const result = await run.returnValue;
154
179
  } catch (err) {
155
180
  if (WorkflowRunFailedError.is(err)) {
156
- console.log(err.errorCode); // "USER_ERROR", "RUNTIME_ERROR", or undefined
181
+ console.log(err.errorCode); // e.g. "USER_ERROR", "MAX_EVENTS_EXCEEDED", or undefined
157
182
  // `cause` is the original thrown value, hydrated through the workflow
158
183
  // serialization pipeline. It can be any thrown value, so check shape.
159
184
  if (err.cause instanceof Error) {
@@ -166,13 +191,19 @@ try {
166
191
  | Code | Meaning |
167
192
  | --- | --- |
168
193
  | `USER_ERROR` | An error thrown in your workflow or step code (including propagated step failures like `FatalError`) |
169
- | `RUNTIME_ERROR` | An internal runtime error such as a corrupted event log or missing data. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) |
194
+ | `MAX_EVENTS_EXCEEDED` | The run reached the World's per-run event ceiling (25,000 on the Local and Vercel Worlds). Split unbounded loops into [child workflows](/cookbook/advanced/child-workflows); see [Limits](/docs/configuration/runtime-tuning#limits) |
195
+ | `MAX_DELIVERIES_EXCEEDED` | The run exceeded the maximum number of queue deliveries |
196
+ | `REPLAY_TIMEOUT` | A workflow replay exceeded the maximum allowed duration |
197
+ | `REPLAY_DIVERGENCE` | A replay could not consume the event log deterministically, usually because of non-deterministic workflow code. |
198
+ | `CORRUPTED_EVENT_LOG` | The event log contains orphaned or mismatched events and cannot be replayed. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) |
199
+ | `WORLD_CONTRACT_ERROR` | A World response violated the SDK contract; points at a World implementation bug |
200
+ | `RUNTIME_ERROR` | An internal runtime error. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) |
170
201
 
171
202
  <Callout type="info">
172
- The error code is also available on the run entity via the CLI (`npx workflow inspect runs <runId>`) in the `error.code` field, and as an OTEL span attribute (`workflow.error.code`) for observability.
203
+ The error code is also available on the run entity through the Workflow CLI (`npx workflow inspect runs <runId>`) in the `error.code` field, and as an OpenTelemetry span attribute (`workflow.error.code`) for observability.
173
204
  </Callout>
174
205
 
175
- ## Rolling Back Failed Steps
206
+ ## Rolling back failed steps
176
207
 
177
208
  When a workflow fails partway through, it can leave the system in an inconsistent state.
178
209
  A common pattern to address this is "rollbacks": for each successful step, record a corresponding rollback action that can undo it.
@@ -11,9 +11,9 @@ related:
11
11
  - /docs/ai/human-in-the-loop
12
12
  ---
13
13
 
14
- Hooks provide a powerful mechanism for pausing workflow execution and resuming it later with external data. They enable workflows to wait for external events, user interactions (also known as "human in the loop"), or HTTP requests. This guide will teach you the core concepts, starting with the low-level Hook primitive and building up to the higher-level Webhook abstraction.
14
+ Hooks pause workflow execution and resume it later with external data. Workflows can wait for external events, user interactions (also known as "human in the loop"), or HTTP requests.
15
15
 
16
- ## Understanding Hooks
16
+ ## Understanding hooks
17
17
 
18
18
  At their core, **Hooks** are a low-level primitive that allows you to pause a workflow and resume it later with arbitrary [serializable data](/docs/foundations/serialization). Think of them as suspension points in your workflow where you're waiting for external input.
19
19
 
@@ -23,9 +23,9 @@ When you create a hook, it generates a unique token that external systems can us
23
23
  - Receiving data from an external system or service
24
24
  - Implementing event-driven workflows that react to multiple events over time
25
25
 
26
- ### Creating Your First Hook
26
+ ### Creating your first hook
27
27
 
28
- Let's start with a simple example. Here's a workflow that creates a hook and waits for external data:
28
+ This workflow creates a hook and waits for external data:
29
29
 
30
30
  ```typescript lineNumbers
31
31
  import { createHook } from "workflow";
@@ -59,7 +59,7 @@ We recommend using the `using` keyword which implements the [TC39 Explicit Resou
59
59
  See the full API reference for [`createHook()`](/docs/api-reference/workflow/create-hook) for all available options.
60
60
  </Callout>
61
61
 
62
- ### Resuming a Hook
62
+ ### Resuming a hook
63
63
 
64
64
  To send data to a waiting workflow, use [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) from an API route, server action, or any other external context:
65
65
 
@@ -85,7 +85,7 @@ The key points:
85
85
  - You need the hook's `token` to resume it
86
86
  - The workflow will resume execution right where it left off
87
87
 
88
- ### Checking for Token Conflicts
88
+ ### Checking for token conflicts
89
89
 
90
90
  Sometimes you need to know that a hook token has been claimed, but you do not want to wait for external data yet. Await `hook.getConflict()` for that:
91
91
 
@@ -112,11 +112,11 @@ export async function orderWorkflow(orderId: string) {
112
112
  }
113
113
  ```
114
114
 
115
- Calling `createHook()` on its own does not register the hook registration is only committed when the workflow suspends. Awaiting `hook.getConflict()` suspends the workflow to commit the hook registration, then resolves with `null` once the hook is registered and ready to receive payloads, or with a `Run` handle for the run that owns the token (see [`HookConflictError`](/docs/errors/hook-conflict)). For `hook_conflict` events persisted by older worlds that did not record the owning run's ID, `getConflict()` rejects with `HookConflictError` instead of resolving with an incomplete handle. The conflicting run's accessors are durable steps, so the workflow can inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()` see [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies.
115
+ Calling `createHook()` on its own does not register the hook; registration is only committed when the workflow suspends. Awaiting `hook.getConflict()` suspends the workflow to commit the hook registration, then resolves with `null` once the hook is registered and ready to receive payloads, or with a `Run` handle for the run that owns the token (see [`HookConflictError`](/docs/errors/hook-conflict)). For `hook_conflict` events persisted by older worlds that did not record the owning run's ID, `getConflict()` rejects with `HookConflictError` instead of resolving with an incomplete handle. The conflicting run's accessors are durable steps, so the workflow can inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()`. See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies.
116
116
 
117
- ### Custom Tokens for Deterministic Hooks
117
+ ### Custom tokens for deterministic hooks
118
118
 
119
- By default, hooks generate a random token. However, you often want to use a **custom token** that external systems can reconstruct. This is especially useful for long-running workflows where the same workflow instance should handle multiple events.
119
+ By default, hooks generate their own token. However, you often want to use a **custom token** that external systems can reconstruct. This is especially useful for long-running workflows where the same workflow instance should handle multiple events.
120
120
 
121
121
  For example, imagine a Slack bot where each channel should have its own workflow instance:
122
122
 
@@ -168,9 +168,9 @@ export async function POST(request: Request) {
168
168
  }
169
169
  ```
170
170
 
171
- ### Receiving Multiple Events
171
+ ### Receiving multiple events
172
172
 
173
- Hooks are _reusable_ - they implement `AsyncIterable`, which means you can use `for await...of` to receive multiple events over time:
173
+ Hooks are _reusable_. They implement `AsyncIterable`, which means you can use `for await...of` to receive multiple events over time:
174
174
 
175
175
  ```typescript lineNumbers
176
176
  import { createHook } from "workflow";
@@ -198,7 +198,7 @@ export async function dataCollectionWorkflow() {
198
198
 
199
199
  Each time you call `resumeHook()` with the same token, the loop receives another value.
200
200
 
201
- ### Disposing Hooks Early
201
+ ### Disposing hooks early
202
202
 
203
203
  When a workflow ends, hooks are automatically disposed. However, you may want to release a hook token early so another workflow can use it while your workflow continues running. Use a block scope with `using` to control when disposal happens:
204
204
 
@@ -240,9 +240,9 @@ hook.dispose(); // Manually release the token
240
240
  After disposal, the hook will no longer receive events and the async iterator will stop yielding values.
241
241
  </Callout>
242
242
 
243
- ## Understanding Webhooks
243
+ ## Understanding webhooks
244
244
 
245
- While hooks are powerful, they require you to manually handle HTTP requests and route them to workflows. **Webhooks** solve this by providing a higher-level abstraction built on top of hooks that:
245
+ Hooks require you to manually handle HTTP requests and route them to workflows. **Webhooks** provide a higher-level abstraction built on top of hooks that:
246
246
 
247
247
  1. Automatically serializes the entire HTTP [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
248
248
  2. Provides an automatically addressable `url` property pointing to the generated webhook endpoint
@@ -251,16 +251,16 @@ While hooks are powerful, they require you to manually handle HTTP requests and
251
251
  When using Workflow SDK, webhooks are automatically wired up at `/.well-known/workflow/v1/webhook/:token` without any additional setup.
252
252
 
253
253
  <Callout type="warn">
254
- `createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes and a simple developer experience because you can share the webhook URL (endpoint) without creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
254
+ `createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes because you can share the webhook URL (endpoint) without creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
255
255
  </Callout>
256
256
 
257
257
  <Callout type="info">
258
258
  See the full API reference for [`createWebhook()`](/docs/api-reference/workflow/create-webhook) for all available options.
259
259
  </Callout>
260
260
 
261
- ### Creating Your First Webhook
261
+ ### Creating your first webhook
262
262
 
263
- Here's a simple webhook that receives HTTP requests. Like hooks, webhooks support the `using` keyword for automatic cleanup:
263
+ Here's a webhook that receives HTTP requests. Like hooks, webhooks support the `using` keyword for automatic cleanup:
264
264
 
265
265
  ```typescript lineNumbers
266
266
  import { createWebhook } from "workflow";
@@ -284,13 +284,13 @@ export async function webhookWorkflow() {
284
284
  }
285
285
  ```
286
286
 
287
- The webhook will automatically respond with a `202 Accepted` status by default. External systems can simply make an HTTP request to the `webhook.url` to resume your workflow.
287
+ The webhook will automatically respond with a `202 Accepted` status by default. External systems can make an HTTP request to the `webhook.url` to resume your workflow.
288
288
 
289
- ### Sending Custom Responses
289
+ ### Sending custom responses
290
290
 
291
291
  Webhooks provide two ways to send custom HTTP responses: **static responses** and **dynamic responses**.
292
292
 
293
- #### Static Responses
293
+ #### Static responses
294
294
 
295
295
  Use the `respondWith` option to provide a static response that will be sent automatically for every request:
296
296
 
@@ -319,7 +319,7 @@ async function processData(data: any) {
319
319
  }
320
320
  ```
321
321
 
322
- #### Dynamic Responses (Manual Mode)
322
+ #### Dynamic responses (manual mode)
323
323
 
324
324
  For dynamic responses based on the request content, set `respondWith: "manual"` and call the `respondWith()` method on the request:
325
325
 
@@ -365,7 +365,7 @@ export async function webhookWithDynamicResponse() {
365
365
  When using `respondWith: "manual"`, the `respondWith()` method **must** be called from within a step function due to serialization requirements. This requirement may be removed in the future.
366
366
  </Callout>
367
367
 
368
- ### Handling Multiple Webhook Requests
368
+ ### Handling multiple webhook requests
369
369
 
370
370
  Like hooks, webhooks support iteration:
371
371
 
@@ -405,7 +405,7 @@ export async function eventCollectorWorkflow() {
405
405
  }
406
406
  ```
407
407
 
408
- ## Hooks vs. Webhooks: When to Use Each
408
+ ## Hooks vs. webhooks: when to use each
409
409
 
410
410
  | Feature | Hooks | Webhooks |
411
411
  |---------|-------|----------|
@@ -415,19 +415,19 @@ export async function eventCollectorWorkflow() {
415
415
  | **Use Case** | Custom integrations, type-safe payloads | HTTP webhooks, standard REST APIs |
416
416
  | **Resuming** | [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) | Automatic via HTTP, or [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook) |
417
417
 
418
- **Use Hooks when:**
418
+ **Use hooks when:**
419
419
  - You need full control over the payload structure
420
420
  - You're integrating with custom event sources
421
421
  - You want strong TypeScript typing with [`defineHook()`](/docs/api-reference/workflow/define-hook)
422
422
 
423
- **Use Webhooks when:**
423
+ **Use webhooks when:**
424
424
  - You're receiving HTTP requests from external services
425
425
  - You need to send HTTP responses back to the caller
426
426
  - You want automatic URL routing without writing API handlers
427
427
 
428
- ## Advanced Patterns
428
+ ## Advanced patterns
429
429
 
430
- ### Type-Safe Hooks with `defineHook()`
430
+ ### Type-safe hooks with `defineHook()`
431
431
 
432
432
  The [`defineHook()`](/docs/api-reference/workflow/define-hook) helper provides type safety and runtime validation between creating and resuming hooks using [Standard Schema v1](https://standardschema.dev). Use any compliant validator like Zod or Valibot:
433
433
 
@@ -476,25 +476,25 @@ export async function POST(request: Request) {
476
476
 
477
477
  This pattern is especially valuable in larger applications where the workflow and API code are in separate files, providing both compile-time type safety and runtime validation.
478
478
 
479
- ## Best Practices
479
+ ## Best practices
480
480
 
481
- ### Token Design
481
+ ### Token design
482
482
 
483
- Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always use randomly generated tokens to prevent unauthorized access to public webhook endpoints.
483
+ Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always generate their own unique tokens. A generated token is not trivial to guess, but it is not a strong security contract either, so anyone who obtains the URL can invoke an unintended webhook resumption. To prevent unauthenticated run resumptions entirely, prefer a **hook** over the **webhook** convenience and implement your own authentication on the route that calls `resumeHook()`.
484
484
 
485
485
  When using custom tokens with `createHook()`:
486
486
 
487
- - **Make them deterministic**: Base them on data the external system can reconstruct (like channel IDs, user IDs, etc.)
488
- - **Use namespacing**: Prefix tokens to avoid conflicts (e.g., `slack:${channelId}`, `github:${repoId}`)
489
- - **Include routing information**: Ensure the token contains enough information to identify the correct workflow instance
487
+ - **Make them deterministic**: Base them on data the external system can reconstruct, such as channel IDs or user IDs.
488
+ - **Use namespacing**: Prefix tokens to avoid conflicts, such as `slack:${channelId}` or `github:${repoId}`.
489
+ - **Include routing information**: Ensure the token contains enough information to identify the correct workflow instance.
490
490
 
491
- ### Response Handling in Webhooks
491
+ ### Response handling in webhooks
492
492
 
493
- - Use **static responses** (`respondWith: Response`) for simple acknowledgments
493
+ - Use **static responses** (`respondWith: Response`) for acknowledgments
494
494
  - Use **manual mode** (`respondWith: "manual"`) when responses depend on request processing
495
495
  - Remember that `respondWith()` must be called from within a step function
496
496
 
497
- ### Iterating Over Events
497
+ ### Iterating over events
498
498
 
499
499
  Both hooks and webhooks support iteration, making them perfect for long-running event loops:
500
500
 
@@ -513,7 +513,7 @@ for await (const event of hook) {
513
513
 
514
514
  This pattern allows a single workflow instance to handle multiple events over time, maintaining state between events.
515
515
 
516
- ## Related Documentation
516
+ ## Related documentation
517
517
 
518
518
  - [Serialization](/docs/foundations/serialization) - Understanding what data can be passed through hooks
519
519
  - [`createHook()` API Reference](/docs/api-reference/workflow/create-hook)
@@ -15,9 +15,9 @@ Idempotency is a property of an operation that ensures repeated attempts have th
15
15
 
16
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
17
 
18
- ## Step Idempotency
18
+ ## Step idempotency
19
19
 
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.
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.
21
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.
22
22
 
23
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.
@@ -61,7 +61,7 @@ Because [hooks](/docs/foundations/hooks) already ensure globally unique active t
61
61
 
62
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
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.
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
65
 
66
66
  ```typescript lineNumbers
67
67
  import { createHook } from "workflow";
@@ -96,7 +96,7 @@ export async function processOrder(orderId: string): Promise<OrderResult> {
96
96
  }
97
97
  ```
98
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.
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
100
 
101
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
102
 
@@ -146,14 +146,14 @@ export async function POST(request: Request) {
146
146
  ```
147
147
 
148
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()`.
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
150
  </Callout>
151
151
 
152
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
153
 
154
154
  ### Conflict-handling strategies
155
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.
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
157
 
158
158
  The example above implements **reject the duplicate**: return the owner's `runId` and let the caller decide. Other common strategies:
159
159
 
@@ -260,7 +260,7 @@ export async function processOrderNewestWins(orderId: string) {
260
260
 
261
261
  const conflict = await request.getConflict();
262
262
  if (!conflict) {
263
- // Token claimed this run is now the owner.
263
+ // Token claimed: this run is now the owner.
264
264
  const { confirmed } = await request;
265
265
  if (confirmed) {
266
266
  await chargeOrder(orderId);
@@ -271,12 +271,12 @@ export async function processOrderNewestWins(orderId: string) {
271
271
  await conflict.cancel(); // [!code highlight]
272
272
  }
273
273
 
274
- throw new Error(`Could not claim ${token} after cancelling the owner`);
274
+ throw new Error(`Could not claim ${token} after canceling the owner`);
275
275
  }
276
276
  ```
277
277
 
278
278
  <Callout type="warn">
279
- This pattern does not work with `experimental_minRetention`: cancelling the old run does not make its token available early.
279
+ This pattern does not work with `experimental_minRetention`: canceling the old run does not make its token available early.
280
280
  </Callout>
281
281
 
282
282
  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.