@workflow/core 5.0.0-beta.4 → 5.0.0-beta.41

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 (322) hide show
  1. package/dist/attribute-changes.d.ts +7 -0
  2. package/dist/attribute-changes.d.ts.map +1 -0
  3. package/dist/attribute-changes.js +25 -0
  4. package/dist/capabilities.d.ts +33 -1
  5. package/dist/capabilities.d.ts.map +1 -1
  6. package/dist/capabilities.js +71 -4
  7. package/dist/capture-stack.d.ts +16 -0
  8. package/dist/capture-stack.d.ts.map +1 -0
  9. package/dist/capture-stack.js +21 -0
  10. package/dist/class-serialization.d.ts +32 -0
  11. package/dist/class-serialization.d.ts.map +1 -1
  12. package/dist/class-serialization.js +37 -1
  13. package/dist/classify-error.d.ts +26 -3
  14. package/dist/classify-error.d.ts.map +1 -1
  15. package/dist/classify-error.js +109 -6
  16. package/dist/context-errors.d.ts +27 -0
  17. package/dist/context-errors.d.ts.map +1 -0
  18. package/dist/context-errors.js +101 -0
  19. package/dist/context-violation-error.d.ts +97 -0
  20. package/dist/context-violation-error.d.ts.map +1 -0
  21. package/dist/context-violation-error.js +149 -0
  22. package/dist/create-hook.d.ts +62 -4
  23. package/dist/create-hook.d.ts.map +1 -1
  24. package/dist/create-hook.js +4 -3
  25. package/dist/define-hook.d.ts.map +1 -1
  26. package/dist/define-hook.js +20 -5
  27. package/dist/describe-error.d.ts +70 -0
  28. package/dist/describe-error.d.ts.map +1 -0
  29. package/dist/describe-error.js +189 -0
  30. package/dist/encryption.d.ts +37 -3
  31. package/dist/encryption.d.ts.map +1 -1
  32. package/dist/encryption.js +97 -31
  33. package/dist/events-consumer.d.ts +147 -1
  34. package/dist/events-consumer.d.ts.map +1 -1
  35. package/dist/events-consumer.js +444 -43
  36. package/dist/flushable-stream.d.ts +65 -10
  37. package/dist/flushable-stream.d.ts.map +1 -1
  38. package/dist/flushable-stream.js +131 -18
  39. package/dist/global.d.ts +37 -1
  40. package/dist/global.d.ts.map +1 -1
  41. package/dist/global.js +21 -3
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +2 -1
  45. package/dist/log-format.d.ts +25 -0
  46. package/dist/log-format.d.ts.map +1 -0
  47. package/dist/log-format.js +250 -0
  48. package/dist/logger.d.ts +29 -30
  49. package/dist/logger.d.ts.map +1 -1
  50. package/dist/logger.js +82 -32
  51. package/dist/private.d.ts +246 -8
  52. package/dist/private.d.ts.map +1 -1
  53. package/dist/private.js +345 -9
  54. package/dist/replay-payload-cache.d.ts +68 -0
  55. package/dist/replay-payload-cache.d.ts.map +1 -0
  56. package/dist/replay-payload-cache.js +160 -0
  57. package/dist/runtime/compute-instance.d.ts +12 -0
  58. package/dist/runtime/compute-instance.d.ts.map +1 -0
  59. package/dist/runtime/compute-instance.js +13 -0
  60. package/dist/runtime/constants.d.ts +251 -0
  61. package/dist/runtime/constants.d.ts.map +1 -1
  62. package/dist/runtime/constants.js +425 -16
  63. package/dist/runtime/count-step-started-events.d.ts +52 -0
  64. package/dist/runtime/count-step-started-events.d.ts.map +1 -0
  65. package/dist/runtime/count-step-started-events.js +72 -0
  66. package/dist/runtime/deployment-guard.d.ts +99 -0
  67. package/dist/runtime/deployment-guard.d.ts.map +1 -0
  68. package/dist/runtime/deployment-guard.js +154 -0
  69. package/dist/runtime/get-port-lazy.d.ts +25 -0
  70. package/dist/runtime/get-port-lazy.d.ts.map +1 -0
  71. package/dist/runtime/get-port-lazy.js +92 -0
  72. package/dist/runtime/get-world-lazy.d.ts +23 -0
  73. package/dist/runtime/get-world-lazy.d.ts.map +1 -0
  74. package/dist/runtime/get-world-lazy.js +46 -0
  75. package/dist/runtime/helpers.d.ts +327 -10
  76. package/dist/runtime/helpers.d.ts.map +1 -1
  77. package/dist/runtime/helpers.js +586 -40
  78. package/dist/runtime/quickjs-assets.generated.d.ts +14 -0
  79. package/dist/runtime/quickjs-assets.generated.d.ts.map +1 -0
  80. package/dist/runtime/quickjs-assets.generated.js +30 -0
  81. package/dist/runtime/quickjs-entrypoint.d.ts +124 -0
  82. package/dist/runtime/quickjs-entrypoint.d.ts.map +1 -0
  83. package/dist/runtime/quickjs-entrypoint.js +1437 -0
  84. package/dist/runtime/quickjs-runtime.d.ts +228 -0
  85. package/dist/runtime/quickjs-runtime.d.ts.map +1 -0
  86. package/dist/runtime/quickjs-runtime.js +2372 -0
  87. package/dist/runtime/quickjs-serde.d.ts +107 -0
  88. package/dist/runtime/quickjs-serde.d.ts.map +1 -0
  89. package/dist/runtime/quickjs-serde.js +2098 -0
  90. package/dist/runtime/replay-budget.d.ts +89 -0
  91. package/dist/runtime/replay-budget.d.ts.map +1 -0
  92. package/dist/runtime/replay-budget.js +139 -0
  93. package/dist/runtime/replay-recovery-reporter.d.ts +36 -0
  94. package/dist/runtime/replay-recovery-reporter.d.ts.map +1 -0
  95. package/dist/runtime/replay-recovery-reporter.js +64 -0
  96. package/dist/runtime/resume-hook.d.ts +27 -4
  97. package/dist/runtime/resume-hook.d.ts.map +1 -1
  98. package/dist/runtime/resume-hook.js +452 -76
  99. package/dist/runtime/run-id-time.d.ts +19 -0
  100. package/dist/runtime/run-id-time.d.ts.map +1 -0
  101. package/dist/runtime/run-id-time.js +42 -0
  102. package/dist/runtime/run.d.ts +8 -2
  103. package/dist/runtime/run.d.ts.map +1 -1
  104. package/dist/runtime/run.js +62 -15
  105. package/dist/runtime/runs.d.ts +54 -3
  106. package/dist/runtime/runs.d.ts.map +1 -1
  107. package/dist/runtime/runs.js +119 -13
  108. package/dist/runtime/start.d.ts +70 -1
  109. package/dist/runtime/start.d.ts.map +1 -1
  110. package/dist/runtime/start.js +313 -60
  111. package/dist/runtime/step-executor.d.ts +177 -0
  112. package/dist/runtime/step-executor.d.ts.map +1 -0
  113. package/dist/runtime/step-executor.js +946 -0
  114. package/dist/runtime/step-latency.d.ts +197 -0
  115. package/dist/runtime/step-latency.d.ts.map +1 -0
  116. package/dist/runtime/step-latency.js +207 -0
  117. package/dist/runtime/step-ownership.d.ts +72 -0
  118. package/dist/runtime/step-ownership.d.ts.map +1 -0
  119. package/dist/runtime/step-ownership.js +114 -0
  120. package/dist/runtime/step-single-flight.d.ts +12 -0
  121. package/dist/runtime/step-single-flight.d.ts.map +1 -0
  122. package/dist/runtime/step-single-flight.js +69 -0
  123. package/dist/runtime/suspension-handler.d.ts +109 -7
  124. package/dist/runtime/suspension-handler.d.ts.map +1 -1
  125. package/dist/runtime/suspension-handler.js +486 -140
  126. package/dist/runtime/vm-mode.d.ts +44 -0
  127. package/dist/runtime/vm-mode.d.ts.map +1 -0
  128. package/dist/runtime/vm-mode.js +62 -0
  129. package/dist/runtime/vm-serde-bundle.generated.d.ts +14 -0
  130. package/dist/runtime/vm-serde-bundle.generated.d.ts.map +1 -0
  131. package/dist/runtime/vm-serde-bundle.generated.js +16 -0
  132. package/dist/runtime/wait-continuation.d.ts +84 -0
  133. package/dist/runtime/wait-continuation.d.ts.map +1 -0
  134. package/dist/runtime/wait-continuation.js +105 -0
  135. package/dist/runtime/wait-until.d.ts +18 -0
  136. package/dist/runtime/wait-until.d.ts.map +1 -0
  137. package/dist/runtime/wait-until.js +42 -0
  138. package/dist/runtime/world-compatibility.d.ts +20 -0
  139. package/dist/runtime/world-compatibility.d.ts.map +1 -0
  140. package/dist/runtime/world-compatibility.js +32 -0
  141. package/dist/runtime/world-init.d.ts +50 -0
  142. package/dist/runtime/world-init.d.ts.map +1 -0
  143. package/dist/runtime/world-init.js +50 -0
  144. package/dist/runtime/world.d.ts +14 -2
  145. package/dist/runtime/world.d.ts.map +1 -1
  146. package/dist/runtime/world.js +101 -26
  147. package/dist/runtime.d.ts +17 -12
  148. package/dist/runtime.d.ts.map +1 -1
  149. package/dist/runtime.js +3135 -328
  150. package/dist/schemas.d.ts +1 -1
  151. package/dist/schemas.d.ts.map +1 -1
  152. package/dist/schemas.js +1 -1
  153. package/dist/sealed-box.d.ts +167 -0
  154. package/dist/sealed-box.d.ts.map +1 -0
  155. package/dist/sealed-box.js +571 -0
  156. package/dist/serialization/client.d.ts +17 -0
  157. package/dist/serialization/client.d.ts.map +1 -0
  158. package/dist/serialization/client.js +48 -0
  159. package/dist/serialization/codec-devalue-vm.d.ts +16 -0
  160. package/dist/serialization/codec-devalue-vm.d.ts.map +1 -0
  161. package/dist/serialization/codec-devalue-vm.js +148 -0
  162. package/dist/serialization/codec-devalue.d.ts +14 -0
  163. package/dist/serialization/codec-devalue.d.ts.map +1 -0
  164. package/dist/serialization/codec-devalue.js +105 -0
  165. package/dist/serialization/codec.d.ts +125 -0
  166. package/dist/serialization/codec.d.ts.map +1 -0
  167. package/dist/serialization/codec.js +17 -0
  168. package/dist/serialization/compression.d.ts +104 -0
  169. package/dist/serialization/compression.d.ts.map +1 -0
  170. package/dist/serialization/compression.js +260 -0
  171. package/dist/serialization/encryption.d.ts +134 -0
  172. package/dist/serialization/encryption.d.ts.map +1 -0
  173. package/dist/serialization/encryption.js +186 -0
  174. package/dist/serialization/errors.d.ts +34 -0
  175. package/dist/serialization/errors.d.ts.map +1 -0
  176. package/dist/serialization/errors.js +59 -0
  177. package/dist/serialization/format.d.ts +60 -0
  178. package/dist/serialization/format.d.ts.map +1 -0
  179. package/dist/serialization/format.js +97 -0
  180. package/dist/serialization/hardened.d.ts +155 -0
  181. package/dist/serialization/hardened.d.ts.map +1 -0
  182. package/dist/serialization/hardened.js +523 -0
  183. package/dist/serialization/index.d.ts +20 -0
  184. package/dist/serialization/index.d.ts.map +1 -0
  185. package/dist/serialization/index.js +22 -0
  186. package/dist/serialization/reducers/class-vm.d.ts +20 -0
  187. package/dist/serialization/reducers/class-vm.d.ts.map +1 -0
  188. package/dist/serialization/reducers/class-vm.js +77 -0
  189. package/dist/serialization/reducers/class.d.ts +11 -0
  190. package/dist/serialization/reducers/class.d.ts.map +1 -0
  191. package/dist/serialization/reducers/class.js +73 -0
  192. package/dist/serialization/reducers/common-vm.d.ts +15 -0
  193. package/dist/serialization/reducers/common-vm.d.ts.map +1 -0
  194. package/dist/serialization/reducers/common-vm.js +579 -0
  195. package/dist/serialization/reducers/common.d.ts +16 -0
  196. package/dist/serialization/reducers/common.d.ts.map +1 -0
  197. package/dist/serialization/reducers/common.js +478 -0
  198. package/dist/serialization/reducers/step-function-vm.d.ts +44 -0
  199. package/dist/serialization/reducers/step-function-vm.d.ts.map +1 -0
  200. package/dist/serialization/reducers/step-function-vm.js +97 -0
  201. package/dist/serialization/reducers/step-function.d.ts +35 -0
  202. package/dist/serialization/reducers/step-function.d.ts.map +1 -0
  203. package/dist/serialization/reducers/step-function.js +104 -0
  204. package/dist/serialization/step.d.ts +17 -0
  205. package/dist/serialization/step.d.ts.map +1 -0
  206. package/dist/serialization/step.js +48 -0
  207. package/dist/serialization/types.d.ts +273 -0
  208. package/dist/serialization/types.d.ts.map +1 -0
  209. package/dist/serialization/types.js +35 -0
  210. package/dist/serialization/workflow-vm.d.ts +29 -0
  211. package/dist/serialization/workflow-vm.d.ts.map +1 -0
  212. package/dist/serialization/workflow-vm.js +71 -0
  213. package/dist/serialization/workflow.d.ts +29 -0
  214. package/dist/serialization/workflow.d.ts.map +1 -0
  215. package/dist/serialization/workflow.js +54 -0
  216. package/dist/serialization-format.d.ts +60 -4
  217. package/dist/serialization-format.d.ts.map +1 -1
  218. package/dist/serialization-format.js +276 -56
  219. package/dist/serialization.d.ts +374 -221
  220. package/dist/serialization.d.ts.map +1 -1
  221. package/dist/serialization.js +2300 -783
  222. package/dist/set-attributes.d.ts +13 -0
  223. package/dist/set-attributes.d.ts.map +1 -0
  224. package/dist/set-attributes.js +60 -0
  225. package/dist/sleep.d.ts.map +1 -1
  226. package/dist/sleep.js +3 -2
  227. package/dist/source-map.d.ts +25 -0
  228. package/dist/source-map.d.ts.map +1 -1
  229. package/dist/source-map.js +148 -10
  230. package/dist/step/context-storage.d.ts +61 -2
  231. package/dist/step/context-storage.d.ts.map +1 -1
  232. package/dist/step/context-storage.js +7 -5
  233. package/dist/step/get-closure-vars.d.ts.map +1 -1
  234. package/dist/step/get-closure-vars.js +3 -2
  235. package/dist/step/get-step-metadata.d.ts.map +1 -1
  236. package/dist/step/get-step-metadata.js +3 -2
  237. package/dist/step/get-workflow-metadata.d.ts.map +1 -1
  238. package/dist/step/get-workflow-metadata.js +3 -2
  239. package/dist/step/writable-stream.d.ts.map +1 -1
  240. package/dist/step/writable-stream.js +71 -7
  241. package/dist/step.d.ts.map +1 -1
  242. package/dist/step.js +225 -29
  243. package/dist/symbols.d.ts +54 -0
  244. package/dist/symbols.d.ts.map +1 -1
  245. package/dist/symbols.js +55 -1
  246. package/dist/telemetry/semantic-conventions.d.ts +265 -2
  247. package/dist/telemetry/semantic-conventions.d.ts.map +1 -1
  248. package/dist/telemetry/semantic-conventions.js +190 -1
  249. package/dist/telemetry.d.ts +73 -0
  250. package/dist/telemetry.d.ts.map +1 -1
  251. package/dist/telemetry.js +153 -16
  252. package/dist/types.d.ts +6 -0
  253. package/dist/types.d.ts.map +1 -1
  254. package/dist/types.js +23 -1
  255. package/dist/util.d.ts +16 -6
  256. package/dist/util.d.ts.map +1 -1
  257. package/dist/util.js +25 -16
  258. package/dist/version.d.ts +1 -1
  259. package/dist/version.d.ts.map +1 -1
  260. package/dist/version.js +2 -2
  261. package/dist/vm/index.d.ts.map +1 -1
  262. package/dist/vm/index.js +85 -14
  263. package/dist/vm/script-cache.d.ts +28 -0
  264. package/dist/vm/script-cache.d.ts.map +1 -0
  265. package/dist/vm/script-cache.js +140 -0
  266. package/dist/workflow/abort-controller.d.ts +65 -0
  267. package/dist/workflow/abort-controller.d.ts.map +1 -0
  268. package/dist/workflow/abort-controller.js +312 -0
  269. package/dist/workflow/attribute-dispatcher.d.ts +6 -0
  270. package/dist/workflow/attribute-dispatcher.d.ts.map +1 -0
  271. package/dist/workflow/attribute-dispatcher.js +48 -0
  272. package/dist/workflow/create-hook.d.ts.map +1 -1
  273. package/dist/workflow/create-hook.js +25 -3
  274. package/dist/workflow/define-hook.d.ts +1 -1
  275. package/dist/workflow/define-hook.d.ts.map +1 -1
  276. package/dist/workflow/define-hook.js +8 -4
  277. package/dist/workflow/get-workflow-metadata.d.ts.map +1 -1
  278. package/dist/workflow/get-workflow-metadata.js +14 -3
  279. package/dist/workflow/hook.d.ts.map +1 -1
  280. package/dist/workflow/hook.js +302 -36
  281. package/dist/workflow/index.d.ts +1 -0
  282. package/dist/workflow/index.d.ts.map +1 -1
  283. package/dist/workflow/index.js +5 -3
  284. package/dist/workflow/set-attributes.d.ts +68 -0
  285. package/dist/workflow/set-attributes.d.ts.map +1 -0
  286. package/dist/workflow/set-attributes.js +60 -0
  287. package/dist/workflow/sleep.d.ts.map +1 -1
  288. package/dist/workflow/sleep.js +57 -8
  289. package/dist/workflow/world-init-stub.d.ts +15 -0
  290. package/dist/workflow/world-init-stub.d.ts.map +1 -0
  291. package/dist/workflow/world-init-stub.js +15 -0
  292. package/dist/workflow.d.ts +79 -3
  293. package/dist/workflow.d.ts.map +1 -1
  294. package/dist/workflow.js +831 -553
  295. package/docs/api-reference/create-hook.mdx +79 -0
  296. package/docs/api-reference/create-webhook.mdx +1 -0
  297. package/docs/api-reference/define-hook.mdx +26 -24
  298. package/docs/api-reference/fatal-error.mdx +29 -7
  299. package/docs/api-reference/fetch.mdx +8 -4
  300. package/docs/api-reference/index.mdx +3 -0
  301. package/docs/api-reference/set-attributes.mdx +61 -0
  302. package/docs/api-reference/sleep.mdx +1 -1
  303. package/docs/foundations/cancellation.mdx +459 -0
  304. package/docs/foundations/errors-and-retries.mdx +7 -3
  305. package/docs/foundations/hooks.mdx +29 -0
  306. package/docs/foundations/idempotency.mdx +243 -11
  307. package/docs/foundations/index.mdx +1 -23
  308. package/docs/foundations/meta.json +3 -1
  309. package/docs/foundations/serialization.mdx +77 -41
  310. package/docs/foundations/starting-workflows.mdx +79 -2
  311. package/docs/foundations/streaming.mdx +14 -23
  312. package/docs/foundations/versioning.mdx +263 -0
  313. package/docs/how-it-works/cancellation.mdx +287 -0
  314. package/docs/how-it-works/code-transform.mdx +21 -17
  315. package/docs/how-it-works/encryption.mdx +5 -5
  316. package/docs/how-it-works/event-sourcing.mdx +17 -9
  317. package/docs/how-it-works/framework-integrations.mdx +96 -337
  318. package/docs/how-it-works/meta.json +2 -1
  319. package/package.json +31 -13
  320. package/dist/runtime/step-handler.d.ts +0 -2
  321. package/dist/runtime/step-handler.d.ts.map +0 -1
  322. package/dist/runtime/step-handler.js +0 -678
@@ -345,29 +345,19 @@ export async function batchProcessingWorkflow(items: string[]) {
345
345
  }
346
346
  ```
347
347
 
348
- ### Streaming AI Responses with `DurableAgent`
348
+ ### Streaming AI Responses with `WorkflowAgent`
349
349
 
350
- Stream AI-generated content using [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) from `@workflow/ai`. Tools can also emit progress updates to the same stream using [data chunks](https://ai-sdk.dev/docs/ai-sdk-ui/streaming-data#streaming-custom-data) with the [`UIMessageChunk`](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol) type from the AI SDK:
350
+ Stream AI-generated content using AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow`. The agent writes `ModelCallStreamPart` chunks to the workflow stream, and route handlers convert them to UI message chunks with `createModelCallToUIChunkTransform()` before returning the response:
351
351
 
352
352
  ```typescript title="workflows/ai-assistant.ts" lineNumbers
353
- import { DurableAgent } from "@workflow/ai/agent";
353
+ import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
354
+ import { tool } from "ai";
354
355
  import { getWritable } from "workflow";
355
356
  import { z } from "zod";
356
- import type { UIMessageChunk } from "ai";
357
357
 
358
358
  async function searchFlights({ query }: { query: string }) {
359
359
  "use step";
360
360
 
361
- // Tools can emit progress updates to the stream
362
- const writable = getWritable<UIMessageChunk>(); // [!code highlight]
363
- const writer = writable.getWriter(); // [!code highlight]
364
- await writer.write({ // [!code highlight]
365
- type: "data-progress", // [!code highlight]
366
- data: { message: `Searching flights for ${query}...` }, // [!code highlight]
367
- transient: true, // [!code highlight]
368
- }); // [!code highlight]
369
- writer.releaseLock(); // [!code highlight]
370
-
371
361
  // ... search logic ...
372
362
  return { flights: [/* results */] };
373
363
  }
@@ -375,27 +365,28 @@ async function searchFlights({ query }: { query: string }) {
375
365
  export async function aiAssistantWorkflow(userMessage: string) {
376
366
  "use workflow";
377
367
 
378
- const agent = new DurableAgent({
368
+ const agent = new WorkflowAgent({
379
369
  model: "anthropic/claude-haiku-4.5",
380
- system: "You are a helpful flight assistant.",
370
+ instructions: "You are a helpful flight assistant.",
381
371
  tools: {
382
- searchFlights: {
372
+ searchFlights: tool({
383
373
  description: "Search for flights",
384
374
  inputSchema: z.object({ query: z.string() }),
385
375
  execute: searchFlights,
386
- },
376
+ }),
387
377
  },
388
378
  });
389
379
 
390
380
  // LLM response will be streamed to the run's writable
391
381
  await agent.stream({
392
382
  messages: [{ role: "user", content: userMessage }],
393
- writable: getWritable<UIMessageChunk>(), // [!code highlight]
383
+ writable: getWritable<ModelCallStreamPart>(), // [!code highlight]
394
384
  });
395
385
  }
396
386
  ```
397
387
 
398
388
  ```typescript title="app/api/ai-assistant/route.ts" lineNumbers
389
+ import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
399
390
  import { createUIMessageStreamResponse } from "ai";
400
391
  import { start } from "workflow/api";
401
392
  import { aiAssistantWorkflow } from "./workflows/ai";
@@ -406,13 +397,13 @@ export async function POST(request: Request) {
406
397
  const run = await start(aiAssistantWorkflow, [message]);
407
398
 
408
399
  return createUIMessageStreamResponse({
409
- stream: run.readable,
400
+ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()), // [!code highlight]
410
401
  });
411
402
  }
412
403
  ```
413
404
 
414
405
  <Callout type="info">
415
- For a complete implementation, see the [flight booking example](https://github.com/vercel/workflow-examples/tree/main/flight-booking-app) which demonstrates streaming AI responses with tool progress updates.
406
+ For the full agent API and migration notes, see the [`WorkflowAgent` documentation](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent).
416
407
  </Callout>
417
408
 
418
409
  ### Streaming Between Steps
@@ -593,8 +584,8 @@ Stream errors don't trigger automatic retries for the producer step. Design your
593
584
  - [`sleep()` API Reference](/docs/api-reference/workflow/sleep) - Pause workflow execution for a duration
594
585
  - [`start()` API Reference](/docs/api-reference/workflow-api/start) - Start workflows and access the `Run` object
595
586
  - [`getRun()` API Reference](/docs/api-reference/workflow-api/get-run) - Retrieve runs and their streams later
596
- - [world.streams](/docs/api-reference/workflow-api/world/streams) - Low-level stream read/write/close via World SDK
597
- - [DurableAgent](/docs/api-reference/workflow-ai/durable-agent) - AI agents with built-in streaming support
587
+ - [world.streams](/docs/api-reference/workflow-runtime/world/streams) - Low-level stream read/write/close via World SDK
588
+ - [WorkflowAgent](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI agents with durable, resumable streaming support
598
589
  - [Errors and Retries](/docs/foundations/errors-and-retries) - Understanding error handling and retry behavior
599
590
  - [Serialization](/docs/foundations/serialization) - Understanding what data types can be passed in workflows
600
591
  - [Workflows and Steps](/docs/foundations/workflows-and-steps) - Core concepts of workflow execution
@@ -0,0 +1,263 @@
1
+ ---
2
+ title: Versioning
3
+ description: Understand how workflow runs are pinned to deployments, how to recover runs after a fix, and how to opt in to newer code explicitly.
4
+ type: guide
5
+ summary: Keep in-flight runs stable by default, then choose explicit upgrade boundaries when you need them.
6
+ prerequisites:
7
+ - /docs/foundations/starting-workflows
8
+ related:
9
+ - /docs/api-reference/workflow-api/start
10
+ - /docs/foundations/cancellation
11
+ - /cookbook/common-patterns/workflow-composition
12
+ ---
13
+
14
+ Workflow runs are pinned to the deployment that starts them. When a run begins, Workflow SDK records the deployment for that run and continues executing the run on that same copy of your code.
15
+
16
+ That default is intentional. Durable workflows can pause for minutes, days, or months. If the code underneath a paused run changed every time you deployed, an in-flight run could resume into a different function body, different step names, or different input types than the ones it started with. That can make type safety fragile and can break long-running work in hard-to-debug ways.
17
+
18
+ With Workflow SDK, you can keep shipping. New runs use new deployments, while existing runs keep the version they already understand.
19
+
20
+ ## Default behavior
21
+
22
+ Start a workflow normally:
23
+
24
+ ```typescript title="app/api/orders/route.ts" lineNumbers
25
+ import { start } from "workflow/api";
26
+ import { fulfillOrder } from "@/workflows/fulfill-order";
27
+
28
+ export async function POST(request: Request) {
29
+ const { orderId } = await request.json();
30
+
31
+ const run = await start(fulfillOrder, [orderId]); // [!code highlight]
32
+
33
+ return Response.json({ runId: run.runId });
34
+ }
35
+ ```
36
+
37
+ The run is tied to the deployment that handled this request. If you deploy a new version while the workflow is [sleeping](/docs/api-reference/workflow/sleep), [waiting on a hook](/docs/foundations/hooks), [retrying a step](/docs/foundations/errors-and-retries), or processing later queue messages, that existing run still resumes on the original deployment.
38
+
39
+ ```typescript title="workflows/fulfill-order.ts" lineNumbers
40
+ import { sleep } from "workflow";
41
+
42
+ export async function fulfillOrder(orderId: string) {
43
+ "use workflow";
44
+
45
+ await reserveInventory(orderId);
46
+ await sleep("2d");
47
+ await chargeCustomer(orderId);
48
+ await shipOrder(orderId);
49
+ }
50
+
51
+ async function reserveInventory(orderId: string) {
52
+ "use step";
53
+ // ...
54
+ }
55
+
56
+ async function chargeCustomer(orderId: string) {
57
+ "use step";
58
+ // ...
59
+ }
60
+
61
+ async function shipOrder(orderId: string) {
62
+ "use step";
63
+ // ...
64
+ }
65
+ ```
66
+
67
+ If you deploy a change to `chargeCustomer()` while a run is in the two-day sleep, the existing run does not suddenly resume into the new implementation. It continues on the deployment it started on. The next order starts on the latest deployment and uses the new code from the beginning.
68
+
69
+ ## Fixing in-flight runs
70
+
71
+ Sometimes you deploy because the old code had a bug. The safest fix is usually explicit:
72
+
73
+ 1. Deploy the fixed code.
74
+ 2. Find the affected runs in [observability](/docs/observability) or with the CLI.
75
+ 3. Cancel the old runs if they are still running.
76
+ 4. Rerun them on the latest deployment with the same inputs.
77
+
78
+ This keeps the version boundary visible. The old run ends as cancelled or failed, and the replacement run starts fresh on the fixed deployment. This is a good fit for one-off, ad-hoc upgrades where you explicitly opt in to moving affected runs onto a new version.
79
+
80
+ ```bash
81
+ # Inspect affected runs and copy the exact workflowName value.
82
+ npx workflow inspect runs \
83
+ --backend vercel \
84
+ --status running
85
+
86
+ # Cancel one run.
87
+ npx workflow cancel <run-id> \
88
+ --backend vercel
89
+
90
+ # Or bulk-cancel matching running runs.
91
+ npx workflow cancel \
92
+ --status running \
93
+ --workflowName "workflow//./workflows/fulfill-order//fulfillOrder" \
94
+ --backend vercel
95
+ ```
96
+
97
+ The `--workflowName` filter expects the generated workflow ID, not only the exported function's short name. Use the `workflowName` value from `workflow inspect runs`, and use [`parseWorkflowName()`](/docs/api-reference/workflow-observability/parse-workflow-name) when you need display-friendly names.
98
+
99
+ In the [observability UI](/docs/observability), use **Rerun on latest** to enqueue the workflow again with the same inputs against the latest deployment.
100
+
101
+ If you are writing your own recovery route, call `start()` with the same arguments and `deploymentId: "latest"`:
102
+
103
+ ```typescript title="app/api/orders/rerun/route.ts" lineNumbers
104
+ import { start } from "workflow/api";
105
+ import { fulfillOrder } from "@/workflows/fulfill-order";
106
+
107
+ export async function POST(request: Request) {
108
+ const { orderId } = await request.json();
109
+
110
+ const run = await start(fulfillOrder, [orderId], {
111
+ deploymentId: "latest", // [!code highlight]
112
+ });
113
+
114
+ return Response.json({ runId: run.runId });
115
+ }
116
+ ```
117
+
118
+ <Callout type="warn">
119
+ `deploymentId: "latest"` is currently a Vercel-specific feature. Other Worlds may implement this option differently to match their own deployment runtimes, and the World spec may rename it from `deploymentId` to `version` in a future SDK version. On Vercel, `"latest"` resolves to the most recent deployment matching your current environment. Because the caller and target deployment can be different, keep the [workflow function name and file path](/docs/errors/workflow-not-registered), arguments, and return value backward-compatible across the deployments you plan to bridge.
120
+ </Callout>
121
+
122
+ ## Self upgrading workflows
123
+
124
+ Some workflows are expected to run for a very long time. Scheduled loops, recurring jobs, agents, and chat sessions often should not stay on one deployment forever.
125
+
126
+ Model those as a sequence of runs. Each run does a bounded piece of work, then starts the next run on the latest deployment and exits. This is similar to `continueAsNew` in other durable execution systems, but in Workflow SDK it is just [explicit recursion through `start()`](/cookbook/common-patterns/workflow-composition).
127
+
128
+ ```typescript title="workflows/daily-digest.ts" lineNumbers
129
+ import { sleep } from "workflow";
130
+ import { start } from "workflow/api";
131
+
132
+ type DigestState = {
133
+ userId: string;
134
+ lastSentAt?: string;
135
+ };
136
+
137
+ export async function dailyDigest(state: DigestState) {
138
+ "use workflow";
139
+
140
+ const sentAt = await sendDigest(state.userId);
141
+ await sleep("1d");
142
+
143
+ const run = await start(
144
+ dailyDigest,
145
+ [{ ...state, lastSentAt: sentAt }],
146
+ {
147
+ deploymentId: "latest", // [!code highlight]
148
+ }
149
+ );
150
+
151
+ return { continuedAs: run.runId };
152
+ }
153
+
154
+ async function sendDigest(userId: string) {
155
+ "use step";
156
+ // ...
157
+ return new Date().toISOString();
158
+ }
159
+ ```
160
+
161
+ This pattern gives every run a clear lifecycle:
162
+
163
+ - The current run stays on its original deployment.
164
+ - The next run starts on the latest deployment.
165
+ - The [serialized `state`](/docs/foundations/serialization) is the migration boundary between versions.
166
+ - Observability can link parent and child runs when a workflow starts another run.
167
+
168
+ ## Carrying context forward
169
+
170
+ Anything that is [serializable by Workflow SDK](/docs/foundations/serialization) can be passed from one run to the next as an argument. That includes plain state objects, `ReadableStream`, `WritableStream`, `AbortSignal`, and other supported serialized values.
171
+
172
+ For example, a long export can register its [output stream](/docs/foundations/streaming) once, write progress from each run, and pass the same stream plus updated state into the next run:
173
+
174
+ ```typescript title="workflows/export-report.ts" lineNumbers
175
+ import { getWritable } from "workflow";
176
+ import { start } from "workflow/api";
177
+
178
+ type ExportState = {
179
+ exportId: string;
180
+ page: number;
181
+ };
182
+
183
+ export async function exportReport(
184
+ state: ExportState,
185
+ progress?: WritableStream<string>
186
+ ) {
187
+ "use workflow";
188
+
189
+ // Register the stream once. Continuation runs receive this same stream
190
+ // as an argument and keep writing to it.
191
+ const stream =
192
+ progress !== undefined ? progress : getWritable<string>();
193
+
194
+ const hasMore = await exportPage(state, stream);
195
+
196
+ if (!hasMore) {
197
+ await writeProgress(stream, { type: "done", totalPages: state.page });
198
+ return { totalPages: state.page };
199
+ }
200
+
201
+ const run = await start(exportReport, [
202
+ { ...state, page: state.page + 1 },
203
+ stream,
204
+ ], {
205
+ deploymentId: "latest", // [!code highlight]
206
+ });
207
+
208
+ return { continuedAs: run.runId };
209
+ }
210
+
211
+ async function exportPage(
212
+ state: ExportState,
213
+ stream: WritableStream<string>
214
+ ) {
215
+ "use step";
216
+
217
+ // Do work for this version boundary.
218
+ const hasMore = state.page < 10;
219
+ const writer = stream.getWriter();
220
+
221
+ try {
222
+ await writer.write(
223
+ JSON.stringify({ type: "page", page: state.page }) + "\n"
224
+ );
225
+ return hasMore;
226
+ } finally {
227
+ writer.releaseLock();
228
+ }
229
+ }
230
+
231
+ async function writeProgress(
232
+ stream: WritableStream<string>,
233
+ event: { type: "done"; totalPages: number }
234
+ ) {
235
+ "use step";
236
+
237
+ const writer = stream.getWriter();
238
+ try {
239
+ await writer.write(JSON.stringify(event) + "\n");
240
+ } finally {
241
+ writer.releaseLock();
242
+ }
243
+ }
244
+ ```
245
+
246
+ ```typescript title="app/api/export/route.ts" lineNumbers
247
+ import { start } from "workflow/api";
248
+ import { exportReport } from "@/workflows/export-report";
249
+
250
+ export async function POST(request: Request) {
251
+ const { exportId } = await request.json();
252
+
253
+ const run = await start(exportReport, [{ exportId, page: 1 }]);
254
+
255
+ // Linked continuation runs keep writing to the stream registered by
256
+ // the parent run, because that stream is passed forward as an argument.
257
+ return new Response(run.readable, {
258
+ headers: { "Content-Type": "application/jsonl" },
259
+ });
260
+ }
261
+ ```
262
+
263
+ Each run still has one clear version boundary: the current run stays on its original deployment, the next run starts on the latest deployment, and only the explicit state and stream handle are carried forward.
@@ -0,0 +1,287 @@
1
+ ---
2
+ title: How Cancellation Works
3
+ description: Learn how AbortController is made durable using hooks and streams under the hood.
4
+ type: conceptual
5
+ summary: Understand the hook and stream backing that makes AbortSignal work across workflow boundaries.
6
+ prerequisites:
7
+ - /docs/foundations/cancellation
8
+ - /docs/how-it-works/event-sourcing
9
+ related:
10
+ - /docs/foundations/hooks
11
+ - /docs/foundations/streaming
12
+ - /docs/foundations/serialization
13
+ ---
14
+
15
+ <Callout>
16
+ This guide explains how cancellation works internally. Understanding these details is helpful for debugging and advanced use cases, but is not required to use `AbortController` in workflows. For usage patterns, see the [Cancellation](/docs/foundations/cancellation) guide.
17
+ </Callout>
18
+
19
+ When you write `new AbortController()` in a workflow function, Workflow DevKit creates a durable controller backed by two existing primitives: a [hook](/docs/foundations/hooks) and a [stream](/docs/foundations/streaming). This page explains why both are needed and how they work together.
20
+
21
+ ## The Problem
22
+
23
+ `AbortController` and `AbortSignal` are inherently stateful — an abort happens once and is permanent. In a durable workflow, this state must:
24
+
25
+ 1. **Survive replay** — If `abort()` was called, `signal.aborted` must return `true` on every subsequent replay of the workflow.
26
+ 2. **Propagate in real-time** — A running step on a different compute instance must receive the abort immediately, not on the next replay.
27
+
28
+ No single primitive solves both. Hooks provide durable event log state but can't reach into a running step. Streams provide real-time cross-process communication but aren't part of the event log. The solution is to use both.
29
+
30
+ ## Dual Backing: Hook + Stream
31
+
32
+ Every `AbortController` in the workflow context is backed by:
33
+
34
+ ### Hook (Durable State)
35
+
36
+ When `new AbortController()` is called in a workflow, an internal hook is created — similar to calling `createHook()`. This hook is registered in the workflow's invocations queue and produces events in the [event log](/docs/how-it-works/event-sourcing):
37
+
38
+ - **On creation**: A `hook_created` event records that the controller exists
39
+ - **On abort**: The hook is resumed (producing a `hook_received` event), recording the abort permanently
40
+ - **On replay**: The event consumer processes the `hook_received` event and updates `signal.aborted` to `true` at the same point in the replay as the original abort
41
+
42
+ This gives the workflow deterministic access to the abort state — `controller.signal.aborted` always returns the correct value, even after cold starts.
43
+
44
+ ### Stream (Real-Time Propagation)
45
+
46
+ When `controller.signal` is serialized as a step argument, a stream name is included in the serialized form. Inside the step, the deserialized `AbortSignal` listens on this stream:
47
+
48
+ - **On abort**: A cancellation packet is written to the stream
49
+ - **In the step**: A background reader receives the packet and calls `abort()` on the local `AbortController`, firing the signal immediately
50
+
51
+ This gives steps real-time cancellation without waiting for the workflow to replay.
52
+
53
+ ### Why Both?
54
+
55
+ | Mechanism | Solves | Doesn't Solve |
56
+ |---|---|---|
57
+ | Hook only | Deterministic replay, event log consistency | Can't reach into a running step on another instance |
58
+ | Stream only | Real-time propagation to running steps | Not part of the event log, lost on replay |
59
+ | Hook + Stream | Both | — |
60
+
61
+ ## Lifecycle
62
+
63
+ ### 1. Controller Created in Workflow
64
+
65
+ ```
66
+ new AbortController()
67
+
68
+ ├─→ Internal hook created (registered in invocations queue)
69
+ └─→ Stream name generated (deterministic ULID)
70
+ ```
71
+
72
+ ### 2. Signal Passed to Step
73
+
74
+ ```
75
+ stepFunction(controller.signal)
76
+
77
+ ├─→ Signal serialized as { streamName, hookToken, aborted }
78
+ └─→ In the step: deserialized as real AbortSignal
79
+
80
+ └─→ Background reader listens on stream for abort packet
81
+ ```
82
+
83
+ ### 3. abort() Called in Workflow
84
+
85
+ ```
86
+ controller.abort()
87
+
88
+ ├─→ signal.aborted set to true (synchronous, local state)
89
+ ├─→ Hook marked for resumption in invocations queue
90
+ └─→ Workflow suspends (reaches next step/sleep/hook await)
91
+
92
+ ├─→ Suspension handler creates hook_received event
93
+ ├─→ Suspension handler writes cancellation packet to stream
94
+ │ │
95
+ │ └─→ Step receives packet → local signal fires → fetch cancelled
96
+ └─→ Workflow re-enqueued for replay
97
+ ```
98
+
99
+ ### 4. Workflow Replays After Abort
100
+
101
+ ```
102
+ Replay starts → events loaded
103
+
104
+ ├─→ new AbortController() → hook created → event consumer subscribes
105
+ ├─→ hook_created event consumed
106
+ ├─→ hook_received event consumed → signal.aborted re-asserted as true
107
+ └─→ Workflow code sees signal.aborted === true at the correct point in replay
108
+ ```
109
+
110
+ On replay, the events consumer re-applies the abort by calling `_setAborted` when it encounters the `hook_received` event in the log — at the same point in execution where the original `abort()` happened. This is what makes the abort deterministic across replays.
111
+
112
+ ## Where the Hook Is Created
113
+
114
+ The backing hook is set up whenever an `AbortController` or `AbortSignal` enters the workflow context:
115
+
116
+ **`new AbortController()` in a workflow function** — The workflow VM provides a durable `AbortController` implementation (similar to how it provides deterministic `Date` and serializable `Request`/`Response`). The hook is created in the constructor using the orchestrator context injected via VM globals.
117
+
118
+ **Returned from a step** — A step can create a plain `new AbortController()` and return it. The step-side serializer generates a stream name and hook token (using a random ULID) and includes them in the serialized payload. When the return value is deserialized into the workflow via `hydrateStepReturnValue`, the workflow reviver reads the token from the payload and sets up the hook with that token. Since the serialized payload is stored in the event log (as part of the `step_completed` event), the same token is used on every replay — no deterministic generation needed in the workflow.
119
+
120
+ **Passed as workflow input** — Conceptually the same as "returned from a step". The **external reducer** handles it at serialization time:
121
+
122
+ 1. Generates a stream name and hook token (random ULID)
123
+ 2. Attaches an `abort` event listener on the source signal: when the external code calls `controller.abort()`, the listener writes the cancellation packet to the stream
124
+ 3. Pushes the listener's async work into `ops` (awaited via `waitUntil`)
125
+ 4. Serializes the reference as `{ streamName, hookToken, aborted }`
126
+
127
+ The serialized payload (including the generated token) is stored in the event log as part of the workflow's input. When the workflow deserializes the input, the reviver reads the token from the payload and creates the hook — identical to the "returned from a step" case. On replay, the same token is read from the event log, so the hook matches the same events.
128
+
129
+ If the external code calls `abort()` while the process is still alive (within the `waitUntil` window), the stream packet arrives in the workflow, and the workflow can resume the hook to record it in the event log.
130
+
131
+ <Callout type="info">
132
+ Since the external `AbortController` is a plain JavaScript object (not the workflow VM's durable version), the stream write depends on the originating process still being alive. This is the same constraint that applies to passing a `ReadableStream` as a workflow argument — the stream pipe runs via `waitUntil` and requires the process to remain active until the data is written.
133
+ </Callout>
134
+
135
+ ## Serialization & Deserialization
136
+
137
+ ### Serialized Form
138
+
139
+ An `AbortController` or `AbortSignal` is serialized as:
140
+
141
+ {/* @skip-typecheck: type definition, not runnable code */}
142
+ ```typescript
143
+ {
144
+ streamName: string; // e.g., "abrt_01HWKZ..."
145
+ hookToken: string; // Generated at serialization time, used by workflow reviver to create the hook
146
+ aborted: boolean; // Current state at serialization time
147
+ reason?: unknown; // The abort reason, if any
148
+ }
149
+ ```
150
+
151
+ The `streamName` and `hookToken` are generated once at serialization time (in the step or external context) and stored in the event log as part of the serialized payload. On replay, the workflow reviver reads them from the payload — it never generates them itself. This is the same pattern used by `ReadableStream` and `WritableStream` serialization.
152
+
153
+ ### Reducers (Serialization)
154
+
155
+ **In step context** (`getStepReducers`): When a step returns an `AbortController`, the reducer captures the stream name. If `abort()` was called in the step, `aborted: true` is recorded.
156
+
157
+ **In workflow context** (`getWorkflowReducers`): The reducer captures the stream name and hook token. These are handles — no I/O happens during serialization in the workflow.
158
+
159
+ **In external context** (`getExternalReducers`): When an `AbortController` is passed as a workflow argument from outside, the reducer creates the backing stream and serializes the reference.
160
+
161
+ ### Revivers (Deserialization)
162
+
163
+ **Into step context** (`getStepRevivers`): Creates a real `AbortController`. If `aborted: true`, calls `abort()` immediately. Otherwise, pushes a stream reader into the step's `ops` array that listens for the cancellation packet and calls `abort()` when received.
164
+
165
+ **Into workflow context** (`getWorkflowRevivers`): Creates the durable AbortController with hook backing. Subscribes to the events consumer for the hook's correlation ID. If the event log contains a `hook_received` event, `signal.aborted` is `true`.
166
+
167
+ ### abort() in a Step
168
+
169
+ When `abort()` is called on a deserialized `AbortController` inside a step:
170
+
171
+ 1. The local signal is aborted synchronously (standard behavior)
172
+ 2. The stream write (cancellation packet) is pushed into `ctx.ops`
173
+ 3. The hook resume (`resumeHook`) is pushed into `ctx.ops`
174
+
175
+ The step's `ops` array is awaited via `waitUntil(Promise.all(ops))` after the step function returns — the same mechanism used by [`getWritable()`](/docs/api-reference/workflow/get-writable). This keeps `abort()` synchronous from the caller's perspective while ensuring the async work completes.
176
+
177
+ ### Abort Errors Are Wrapped in FatalError
178
+
179
+ When a step throws due to an abort — whether from `fetch` throwing `AbortError`, `signal.throwIfAborted()`, or any other abort-induced error — the step executor wraps the error in `FatalError` before recording it in the event log. This ensures:
180
+
181
+ - **No retries**: An abort is intentional cancellation, not a transient failure. Retrying would just abort again.
182
+ - **Immediate propagation**: The error bubbles up to the workflow as a `FatalError`, which the workflow can catch with `FatalError.is(err)`.
183
+
184
+ The wrapping happens in `runtime/step-executor.ts` during error hydration. When the step's thrown error is an `AbortError` (checked via `err.name === 'AbortError'`), it is treated as fatal regardless of the step's `maxRetries` configuration.
185
+
186
+ ### abort() in the Workflow
187
+
188
+ When `abort()` is called in the workflow context:
189
+
190
+ 1. `signal.aborted` is updated to `true` immediately (so subsequent reads and serialization capture the correct state)
191
+ 2. The internal hook is marked for resumption in the invocations queue (same pattern as `hook.dispose()`)
192
+ 3. The workflow continues until it reaches the next suspension point (step call, hook await, or sleep) or completes
193
+ 4. The pending queue items are processed:
194
+ - Creates a `hook_received` event in the event log
195
+ - Writes the cancellation packet to the stream (for real-time step propagation)
196
+ - Re-enqueues the workflow for replay
197
+ 4. On replay, the event consumer processes the `hook_received` event, updating `signal.aborted` to `true` at the deterministically correct point
198
+
199
+ `signal.aborted` is updated synchronously so that the workflow can immediately check the state and serialization captures `aborted: true` when passing the signal to steps. On replay, the event consumer also processes the `hook_received` event, ensuring the state is consistent.
200
+
201
+ For abort specifically, this ensures that:
202
+
203
+ - The abort's `hook_received` event is created in the event log
204
+ - The cancellation stream packet is written to propagate to running steps
205
+
206
+ ## Race Conditions
207
+
208
+ ### Abort Before Hook Exists
209
+
210
+ When an `AbortSignal` is passed as a workflow argument via `start()`, the external reducer attaches a listener at serialization time. If the external code calls `abort()` before the workflow has started and created the internal hook, the stream packet is written but the hook doesn't exist yet.
211
+
212
+ This is resolved through eventual consistency:
213
+
214
+ 1. The stream packet is durable — it persists in storage
215
+ 2. When the workflow runs and passes the signal to a step, the step's reviver reads from the stream starting at index 0
216
+ 3. The step sees the existing packet, aborts locally, and resumes the hook (via `ops`)
217
+ 4. On the next workflow replay, the hook event is in the log and `signal.aborted` is `true`
218
+
219
+ **Important:** There is a window where the workflow's `signal.aborted` returns `false` even though the external code has already called `abort()`. This lasts until a step processes the stream packet and resumes the hook. This is analogous to hooks — `resumeHook()` doesn't take effect until the workflow replays.
220
+
221
+ ### Abort at Serialization Time
222
+
223
+ To prevent a micro-window where `abort()` is called between checking `signal.aborted` and attaching the listener, the external reducer uses this order:
224
+
225
+ 1. Attach the `abort` event listener first
226
+ 2. Then check `signal.aborted` — if already `true`, the listener won't fire, so handle immediately
227
+
228
+ This ensures no abort events are missed regardless of timing.
229
+
230
+ ## Stream/Hook Consistency
231
+
232
+ Since abort involves two operations (stream write + hook resume), partial failure is possible:
233
+
234
+ ### Stream Succeeds, Hook Fails
235
+
236
+ - Steps see the abort and throw `AbortError` (stream worked)
237
+ - Workflow doesn't see `signal.aborted === true` on the next replay (hook not resumed)
238
+ - The workflow sees the step failure as an error, which it can handle with try/catch
239
+ - **Recovery:** The step-side `resumeHook` call is best-effort — if it throws, the failure is swallowed. Convergence comes from the next replay: when the step's reviver re-reads the stream, it sees the abort packet and calls `resumeHook` again. There's no in-process retry loop; the dual-mechanism design relies on either the stream or the hook eventually landing.
240
+
241
+ ### Hook Succeeds, Stream Fails
242
+
243
+ - Workflow sees `signal.aborted === true` on replay (hook worked)
244
+ - Steps don't receive real-time cancellation (stream failed) — they run to completion
245
+ - On the next suspension, the workflow knows the abort happened and can stop calling more steps
246
+ - **Recovery:** Natural convergence — no active harm, just missed real-time cancellation for in-flight steps.
247
+
248
+ ### Both Fail
249
+
250
+ - Abort is lost — no propagation
251
+ - No crash or corruption — the system continues as if abort was never called
252
+ - **Recovery:** The caller can retry the abort. If using a hook for external cancellation, the hook's retry semantics apply.
253
+
254
+ The dual mechanism provides natural resilience — if either one succeeds, the system converges on the correct state.
255
+
256
+ ## `AbortSignal.timeout()` in Workflow VM
257
+
258
+ `AbortSignal.timeout()` is blocked in the workflow VM because it depends on real-time timers, which break deterministic replay. Calling it throws an error with a suggestion to use `sleep()` + `AbortController` instead. See [AbortSignal.timeout() in Workflow](/docs/errors/abort-signal-timeout-in-workflow) for details.
259
+
260
+ `AbortSignal.timeout()` works normally in step functions, which have full Node.js runtime access.
261
+
262
+ ## Request.signal
263
+
264
+ A `Request`'s `.signal` is forwarded by the `Request` reducer in two cases:
265
+
266
+ 1. **The signal is already aborted.** The serialized payload preserves `aborted: true` and the abort `reason`, so the deserialized step sees the cancellation that happened before the boundary.
267
+ 2. **The signal is workflow-managed** (i.e., it has the `ABORT_STREAM_NAME` symbol — produced by a workflow-context `AbortController`). Its hook + stream backing carries through, and the deserialized step listens on the stream as usual.
268
+
269
+ Plain non-aborted native signals are intentionally dropped, including the auto-generated signal that `new Request(url)` synthesizes when no `signal` is passed. Forwarding every Request signal would mint stream infrastructure for the throwaway auto-signals on every Request, even ones the caller never intended to use for cancellation.
270
+
271
+ If you want cross-boundary cancellation through a `Request`, build it with a signal from a workflow-context `AbortController`:
272
+
273
+ {/* @skip-typecheck: conceptual snippet */}
274
+ ```typescript
275
+ const controller = new AbortController(); // in workflow function
276
+ const req = new Request(url, { signal: controller.signal });
277
+ await fetchStep(req); // signal carries through
278
+ controller.abort(); // step-side fetch sees the abort
279
+ ```
280
+
281
+ ## Related Documentation
282
+
283
+ - [Cancellation](/docs/foundations/cancellation) — Usage patterns and API
284
+ - [Event Sourcing](/docs/how-it-works/event-sourcing) — How the event log works
285
+ - [Hooks](/docs/foundations/hooks) — The hook primitive
286
+ - [Streaming](/docs/foundations/streaming) — The stream primitive
287
+ - [Serialization](/docs/foundations/serialization) — Serializable types