workflow 5.0.0-beta.5 → 5.0.0-beta.50

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 (256) hide show
  1. package/README.md +68 -23
  2. package/dist/api-workflow.d.ts +1 -1
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +1 -1
  5. package/dist/api.d.ts +3 -3
  6. package/dist/api.d.ts.map +1 -1
  7. package/dist/api.js +5 -7
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -1
  11. package/dist/internal/builtins.d.ts +20 -3
  12. package/dist/internal/builtins.d.ts.map +1 -1
  13. package/dist/internal/builtins.js +68 -4
  14. package/dist/internal/errors.d.ts +1 -1
  15. package/dist/internal/errors.d.ts.map +1 -1
  16. package/dist/internal/errors.js +2 -2
  17. package/dist/nest-builder.d.ts +2 -0
  18. package/dist/nest-builder.d.ts.map +1 -0
  19. package/dist/nest-builder.js +2 -0
  20. package/dist/nest-vercel-builder.d.ts +2 -0
  21. package/dist/nest-vercel-builder.d.ts.map +1 -0
  22. package/dist/nest-vercel-builder.js +2 -0
  23. package/dist/observability.d.ts +1 -1
  24. package/dist/observability.js +2 -2
  25. package/dist/runtime.d.ts +2 -1
  26. package/dist/runtime.d.ts.map +1 -1
  27. package/dist/runtime.js +4 -1
  28. package/docs/ai/chat-session-modeling.mdx +29 -26
  29. package/docs/ai/defining-tools.mdx +6 -7
  30. package/docs/ai/human-in-the-loop.mdx +11 -11
  31. package/docs/ai/index.mdx +50 -45
  32. package/docs/ai/message-queueing.mdx +16 -16
  33. package/docs/ai/meta.json +1 -0
  34. package/docs/ai/resumable-streams.mdx +40 -28
  35. package/docs/ai/sleep-and-delays.mdx +10 -10
  36. package/docs/ai/streaming-updates-from-tools.mdx +6 -6
  37. package/docs/api-reference/index.mdx +24 -0
  38. package/docs/api-reference/meta.json +8 -0
  39. package/docs/api-reference/vitest/index.mdx +9 -15
  40. package/docs/api-reference/workflow/create-hook.mdx +89 -10
  41. package/docs/api-reference/workflow/create-webhook.mdx +16 -15
  42. package/docs/api-reference/workflow/define-hook.mdx +35 -33
  43. package/docs/api-reference/workflow/fatal-error.mdx +30 -8
  44. package/docs/api-reference/workflow/fetch.mdx +14 -10
  45. package/docs/api-reference/workflow/get-step-metadata.mdx +2 -2
  46. package/docs/api-reference/workflow/get-workflow-metadata.mdx +3 -3
  47. package/docs/api-reference/workflow/get-writable.mdx +7 -7
  48. package/docs/api-reference/workflow/index.mdx +4 -1
  49. package/docs/api-reference/workflow/retryable-error.mdx +1 -1
  50. package/docs/api-reference/workflow/set-attributes.mdx +61 -0
  51. package/docs/api-reference/workflow/sleep.mdx +4 -4
  52. package/docs/api-reference/workflow-ai/durable-agent.mdx +48 -86
  53. package/docs/api-reference/workflow-ai/index.mdx +3 -3
  54. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +67 -24
  55. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +26 -12
  56. package/docs/api-reference/workflow-api/get-run.mdx +43 -8
  57. package/docs/api-reference/workflow-api/index.mdx +6 -10
  58. package/docs/api-reference/workflow-api/resume-hook.mdx +73 -12
  59. package/docs/api-reference/workflow-api/resume-webhook.mdx +11 -9
  60. package/docs/api-reference/workflow-api/start.mdx +60 -13
  61. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  62. package/docs/api-reference/workflow-astro/meta.json +4 -0
  63. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  64. package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +4 -4
  65. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  66. package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
  67. package/docs/api-reference/workflow-errors/index.mdx +88 -0
  68. package/docs/api-reference/workflow-errors/meta.json +6 -0
  69. package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -0
  70. package/docs/api-reference/workflow-errors/run-expired-error.mdx +2 -2
  71. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  72. package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +5 -5
  73. package/docs/api-reference/workflow-errors/throttle-error.mdx +2 -2
  74. package/docs/api-reference/workflow-errors/too-early-error.mdx +2 -2
  75. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  76. package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +5 -6
  77. package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +6 -6
  78. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +5 -5
  79. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  80. package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +4 -4
  81. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  82. package/docs/api-reference/workflow-errors/workflow-world-error.mdx +8 -8
  83. package/docs/api-reference/workflow-globals.mdx +14 -10
  84. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
  85. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  86. package/docs/api-reference/workflow-nest/meta.json +9 -0
  87. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  88. package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
  89. package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
  90. package/docs/api-reference/workflow-next/with-workflow.mdx +39 -17
  91. package/docs/api-reference/workflow-nitro/index.mdx +60 -0
  92. package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
  93. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  94. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  95. package/docs/api-reference/workflow-observability/index.mdx +62 -0
  96. package/docs/api-reference/workflow-observability/meta.json +11 -0
  97. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  98. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  99. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  100. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  101. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  102. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  103. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +11 -14
  104. package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
  105. package/docs/api-reference/workflow-runtime/index.mdx +41 -0
  106. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  107. package/docs/api-reference/workflow-runtime/set-world.mdx +51 -0
  108. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +43 -0
  109. package/docs/api-reference/workflow-runtime/world/analytics.mdx +315 -0
  110. package/docs/api-reference/workflow-runtime/world/index.mdx +60 -0
  111. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  112. package/docs/api-reference/workflow-runtime/world/queue.mdx +88 -0
  113. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +98 -34
  114. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +8 -8
  115. package/docs/api-reference/workflow-serde/index.mdx +1 -2
  116. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +3 -4
  117. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +8 -8
  118. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  119. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  120. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  121. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  122. package/docs/api-reference/workflow-vite/meta.json +4 -0
  123. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  124. package/docs/changelog/attributes-mvp.mdx +380 -0
  125. package/docs/changelog/batched-event-writes.mdx +79 -0
  126. package/docs/changelog/eager-processing.mdx +110 -436
  127. package/docs/changelog/index.mdx +4 -2
  128. package/docs/changelog/lazy-event-creation.md +127 -0
  129. package/docs/changelog/lazy-hook-resume.mdx +78 -0
  130. package/docs/changelog/meta.json +11 -1
  131. package/docs/changelog/resilient-resume.mdx +32 -0
  132. package/docs/changelog/resilient-start.mdx +33 -285
  133. package/docs/changelog/step-message-ownership.mdx +360 -0
  134. package/docs/changelog/turbo-mode.md +87 -0
  135. package/docs/comparisons/index.mdx +66 -0
  136. package/docs/comparisons/meta.json +11 -0
  137. package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
  138. package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
  139. package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
  140. package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
  141. package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
  142. package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +104 -0
  143. package/docs/configuration/build-and-diagnostics.mdx +70 -0
  144. package/docs/configuration/cli-and-web-ui.mdx +241 -0
  145. package/docs/configuration/framework-options.mdx +165 -0
  146. package/docs/configuration/index.mdx +32 -0
  147. package/docs/configuration/meta.json +12 -0
  148. package/docs/configuration/runtime-tuning.mdx +376 -0
  149. package/docs/configuration/worlds.mdx +313 -0
  150. package/docs/cookbook/advanced/child-workflows.mdx +211 -264
  151. package/docs/cookbook/advanced/meta.json +6 -1
  152. package/docs/cookbook/advanced/publishing-libraries.mdx +65 -56
  153. package/docs/cookbook/advanced/serializable-steps.mdx +28 -20
  154. package/docs/cookbook/advanced/upgrading-workflows.mdx +199 -0
  155. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +27 -19
  156. package/docs/cookbook/agent-patterns/durable-agent.mdx +14 -142
  157. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +30 -22
  158. package/docs/cookbook/common-patterns/batching.mdx +18 -14
  159. package/docs/cookbook/common-patterns/idempotency.mdx +41 -53
  160. package/docs/cookbook/common-patterns/rate-limiting.mdx +8 -4
  161. package/docs/cookbook/common-patterns/saga.mdx +23 -19
  162. package/docs/cookbook/common-patterns/scheduling.mdx +34 -22
  163. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +29 -25
  164. package/docs/cookbook/common-patterns/timeouts.mdx +26 -21
  165. package/docs/cookbook/common-patterns/webhooks.mdx +10 -6
  166. package/docs/cookbook/common-patterns/workflow-composition.mdx +30 -27
  167. package/docs/cookbook/index.mdx +22 -21
  168. package/docs/cookbook/integrations/ai-sdk.mdx +85 -47
  169. package/docs/cookbook/integrations/chat-sdk.mdx +50 -33
  170. package/docs/cookbook/integrations/sandbox.mdx +62 -45
  171. package/docs/deploying.mdx +95 -0
  172. package/docs/errors/abort-signal-timeout-in-workflow.mdx +16 -12
  173. package/docs/errors/corrupted-event-log.mdx +29 -18
  174. package/docs/errors/deployment-mismatch.mdx +71 -0
  175. package/docs/errors/fetch-in-workflow.mdx +11 -7
  176. package/docs/errors/hook-conflict.mdx +69 -13
  177. package/docs/errors/index.mdx +2 -36
  178. package/docs/errors/node-js-module-in-workflow.mdx +9 -5
  179. package/docs/errors/replay-divergence.mdx +27 -0
  180. package/docs/errors/run-expired.mdx +85 -0
  181. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  182. package/docs/errors/serialization-failed.mdx +44 -12
  183. package/docs/errors/start-invalid-workflow-function.mdx +9 -5
  184. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  185. package/docs/errors/step-not-registered.mdx +6 -6
  186. package/docs/errors/timeout-in-workflow.mdx +12 -8
  187. package/docs/errors/webhook-invalid-respond-with-value.mdx +18 -18
  188. package/docs/errors/webhook-response-not-sent.mdx +20 -16
  189. package/docs/errors/workflow-not-registered.mdx +5 -5
  190. package/docs/foundations/cancellation.mdx +31 -32
  191. package/docs/foundations/errors-and-retries.mdx +42 -11
  192. package/docs/foundations/hooks.mdx +64 -35
  193. package/docs/foundations/idempotency.mdx +244 -12
  194. package/docs/foundations/index.mdx +1 -23
  195. package/docs/foundations/meta.json +2 -1
  196. package/docs/foundations/serialization.mdx +21 -22
  197. package/docs/foundations/starting-workflows.mdx +106 -30
  198. package/docs/foundations/streaming.mdx +107 -59
  199. package/docs/foundations/versioning.mdx +263 -0
  200. package/docs/foundations/workflows-and-steps.mdx +9 -9
  201. package/docs/getting-started/astro.mdx +22 -18
  202. package/docs/getting-started/express.mdx +15 -11
  203. package/docs/getting-started/fastify.mdx +15 -11
  204. package/docs/getting-started/hono.mdx +15 -11
  205. package/docs/getting-started/index.mdx +10 -3
  206. package/docs/getting-started/meta.json +3 -1
  207. package/docs/getting-started/nestjs.mdx +87 -20
  208. package/docs/getting-started/next.mdx +22 -16
  209. package/docs/getting-started/nitro.mdx +22 -18
  210. package/docs/getting-started/nuxt.mdx +15 -11
  211. package/docs/getting-started/python.mdx +135 -40
  212. package/docs/getting-started/react-router/index.mdx +33 -0
  213. package/docs/getting-started/react-router/meta.json +5 -0
  214. package/docs/getting-started/react-router/v7.mdx +237 -0
  215. package/docs/getting-started/react-router/v8.mdx +232 -0
  216. package/docs/getting-started/sveltekit.mdx +20 -16
  217. package/docs/getting-started/tanstack-start.mdx +17 -13
  218. package/docs/getting-started/vite.mdx +15 -11
  219. package/docs/how-it-works/cancellation.mdx +63 -63
  220. package/docs/how-it-works/code-transform.mdx +83 -67
  221. package/docs/how-it-works/encryption.mdx +30 -26
  222. package/docs/how-it-works/event-sourcing.mdx +98 -34
  223. package/docs/how-it-works/framework-integrations.mdx +96 -337
  224. package/docs/how-it-works/understanding-directives.mdx +22 -22
  225. package/docs/internal/index.mdx +6 -4
  226. package/docs/internal/meta.json +6 -1
  227. package/docs/internal/nitro-native-build.mdx +38 -0
  228. package/docs/internal/nitro-web-ui.mdx +24 -0
  229. package/docs/internal/serializable-abort-controller.mdx +7 -7
  230. package/docs/meta.json +3 -2
  231. package/docs/observability/attributes.mdx +134 -0
  232. package/docs/observability/index.mdx +32 -10
  233. package/docs/observability/meta.json +1 -1
  234. package/docs/observability/retention.mdx +93 -0
  235. package/docs/observability/tracing.mdx +124 -0
  236. package/docs/testing/index.mdx +36 -36
  237. package/docs/testing/server-based.mdx +10 -10
  238. package/docs/whats-new.mdx +186 -0
  239. package/package.json +17 -14
  240. package/docs/api-reference/workflow-api/world/index.mdx +0 -58
  241. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  242. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  243. package/docs/api-reference/workflow-api/world/queue.mdx +0 -86
  244. package/docs/deploying/building-a-world.mdx +0 -251
  245. package/docs/deploying/index.mdx +0 -95
  246. package/docs/deploying/meta.json +0 -4
  247. package/docs/deploying/world/local-world.mdx +0 -84
  248. package/docs/deploying/world/meta.json +0 -4
  249. package/docs/deploying/world/postgres-world.mdx +0 -224
  250. package/docs/deploying/world/vercel-world.mdx +0 -179
  251. package/docs/migration-guides/index.mdx +0 -34
  252. package/docs/migration-guides/meta.json +0 -9
  253. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -363
  254. package/docs/migration-guides/migrating-from-inngest.mdx +0 -314
  255. package/docs/migration-guides/migrating-from-temporal.mdx +0 -318
  256. package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -337
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  title: Sandbox
3
- description: Model one Vercel Sandbox per workflow run — durable, idle-efficient, and not bound by the 5-hour sandbox hard cap.
3
+ description: Model one Vercel Sandbox per workflow run, durable, idle-efficient, and not bound by the 5-hour sandbox hard cap.
4
4
  type: guide
5
- summary: Own a sandbox for the lifetime of a workflow run. Hibernate on idle via snapshot(), proactively refresh before the sandbox hard cap, and reconnect by runId — so one logical session can run effectively forever.
5
+ summary: Own a sandbox for the lifetime of a workflow run. Hibernate on idle via snapshot(), proactively refresh before the sandbox hard cap, and reconnect by `runId`, so one logical session can run effectively forever.
6
6
  related:
7
7
  - /docs/ai/defining-tools
8
8
  - /docs/foundations/errors-and-retries
@@ -10,30 +10,38 @@ related:
10
10
  - /docs/cookbook/agent-patterns/durable-agent
11
11
  ---
12
12
 
13
- [Vercel Sandbox](https://vercel.com/docs/sandbox) provides isolated code execution environments. The `@vercel/sandbox` package has first-class support for the Workflow SDK — the `Sandbox` class is serializable, and its methods (`create`, `runCommand`, `stop`, `snapshot`) implicitly run as steps. You can use `Sandbox` directly inside a workflow function without wrapping each call in a separate `"use step"` function.
13
+ <CopyPrompt
14
+ text="Implement a durable Vercel Sandbox-backed coding-agent workflow. Install the Sandbox package used by this project and `workflow`. Create an exported workflow function with &quot;use workflow&quot; that owns the agent session. Put sandbox creation, command execution, snapshot, refresh, and cleanup into helper functions with &quot;use step&quot;. Persist the sandbox ID, snapshot ID, and workflow run ID in the project data store so clients can reconnect. Use `getWritable()` from `workflow` to stream agent progress and command output. Use `sleep()` to hibernate, refresh, or enforce idle timeouts. Add API routes to start a session, reconnect by run ID, and stop/cleanup. Verify first run, reconnect after reload, snapshot restore, timeout, and cleanup behavior."
15
+ />
16
+
17
+ [Vercel Sandbox](https://vercel.com/docs/sandbox) provides isolated code execution environments. The `@vercel/sandbox` package has first-class support for the Workflow SDK: the `Sandbox` class is serializable, and its methods (`create`, `runCommand`, `stop`, `snapshot`) implicitly run as steps. You can use `Sandbox` directly inside a workflow function without wrapping each call in a separate `"use step"` function.
14
18
 
15
19
  ## Why Workflow + Sandbox
16
20
 
17
- A sandbox alone gets you an isolated VM. A workflow around it gets you a **durable controller** for that VM's entire lifetime:
21
+ A sandbox alone provides an isolated virtual machine (VM). A workflow provides a **durable controller** for that VM's entire lifetime:
18
22
 
19
23
  - **One workflow run = one sandbox session.** The `runId` is the only state you need to persist on the client. Close the tab, come back a week later, POST the same `runId` and you're back in the same session.
20
- - **Efficient resource use.** Active sandboxes cost money; hibernated workflows cost nothing. The workflow races a command hook against a `sleep()` timer — when idle, it calls `sandbox.snapshot()` (which also stops the VM) and waits indefinitely. Next command → spin a new sandbox from the snapshot with filesystem, installed packages, and git history intact.
21
- - **Beyond the 5-hour hard cap.** Every Vercel Sandbox has a maximum lifetime. The workflow tracks that deadline and proactively snapshots + recreates *before* the cap, so the logical session outlives any one VM. Effectively unbounded session duration on top of time-bounded infrastructure.
24
+ - **Efficient resource use.** Active sandboxes cost money; hibernated workflows cost nothing. The workflow races a command hook against a `sleep()` timer. When idle, it calls `sandbox.snapshot()` (which also stops the VM) and waits indefinitely. When the next command arrives, the workflow starts a new sandbox from the snapshot with the filesystem, installed packages, and git history intact.
25
+ - **Beyond the 5-hour hard cap.** Every Vercel Sandbox has a maximum lifetime. The workflow tracks that deadline and proactively creates a snapshot and replacement before the cap, so the logical session outlives any one VM. This provides an effectively unbounded session on time-bounded infrastructure.
22
26
  - **Automatic cleanup.** `try/finally` in the workflow guarantees the VM is stopped on failure or destroy.
23
27
 
24
- ## Use Case: Coding Agents
28
+ <Callout type="info">
29
+ An effectively unbounded sandbox session is still one workflow run, so it stays on the deployment that started it. If the controller or agent code should upgrade over time, use an explicit version boundary and pass the serialized state or stream handles forward. See [Versioning](/docs/foundations/versioning).
30
+ </Callout>
31
+
32
+ ## Use case: coding agents
25
33
 
26
- This is the pattern [Open Agents](https://open-agents.dev/) uses to spawn coding agents that run "infinitely in the cloud." Each agent session gets its own sandbox — full filesystem, network, and runtime access — and the durable workflow keeps the agent loop resumable across restarts, auto-hibernates when the user walks away, and reconnects instantly when they return.
34
+ This is the pattern [Open Agents](https://open-agents.dev/) uses to spawn coding agents that run "infinitely in the cloud." Each agent session gets its own sandbox (full filesystem, network, and runtime access), and the durable workflow keeps the agent loop resumable across restarts, auto-hibernates when the user walks away, and reconnects instantly when they return.
27
35
 
28
36
  Most coding-agent workloads look like this:
29
37
 
30
- - User sends a task → agent plans, reads files, runs shell commands, commits.
31
- - User walks away mid-run → agent keeps going, eventually goes idle waiting for input.
32
- - User comes back days later → same branch, same filesystem, same conversation history.
38
+ - The user sends a task, and the agent plans, reads files, runs shell commands, and commits.
39
+ - If the user leaves mid-run, the agent continues and eventually waits for input.
40
+ - When the user returns, the same branch, filesystem, and conversation history remain available.
33
41
 
34
- Without durable workflows you'd need a separate state store for the agent loop, a separate job queue for retries, a separate scheduler for idle cleanup, and bespoke reconnection logic. With the pattern below, all of it is one file.
42
+ Without durable workflows, you'd need a separate state store for the agent loop, a job queue for retries, a scheduler for idle cleanup, and custom reconnection logic. The pattern below keeps the workflow controller in one file.
35
43
 
36
- ## Quickstart: One-shot Pipeline
44
+ ## Quickstart: one-shot pipeline
37
45
 
38
46
  Before the full session pattern, the simplest shape. Each sandbox method is an implicit step, so the event log records every command and the workflow replays from the last completed call on restart.
39
47
 
@@ -66,17 +74,17 @@ export async function sandboxPipeline(input: { commands: string[] }) {
66
74
  }
67
75
  ```
68
76
 
69
- ## Session Pattern: Persistent Sandbox Beyond the Hard Cap
77
+ ## Session pattern: persistent sandbox beyond the hard cap
70
78
 
71
79
  One workflow run owns a sandbox for its whole lifetime. The workflow's loop does two jobs simultaneously:
72
80
 
73
- 1. **Command pipeline** — await a hook, run the next user command, stream output, loop.
74
- 2. **Sandbox lifecycle** — race the hook against a `sleep()` timer armed for whichever comes first: the idle deadline or the sandbox's refresh deadline (a safety margin before its hard cap).
81
+ 1. **Command pipeline**: await a hook, run the next user command, stream output, loop.
82
+ 2. **Sandbox lifecycle**: race the hook against a `sleep()` timer armed for whichever comes first: the idle deadline or the sandbox's refresh deadline (a safety margin before its hard cap).
75
83
 
76
84
  When the timer wins:
77
85
 
78
- - **Idle** → `sandbox.snapshot()` and wait indefinitely for the next command. No compute while asleep.
79
- - **Near sandbox hard cap** → `sandbox.snapshot()` and immediately create a new sandbox from the snapshot. The session appears continuous; the underlying VM just rotated.
86
+ - **Idle**: Call `sandbox.snapshot()` and wait indefinitely for the next command. The workflow uses no compute while suspended.
87
+ - **Near the sandbox hard cap**: Call `sandbox.snapshot()` and immediately create a new sandbox from the snapshot. The session remains continuous while the underlying VM rotates.
80
88
 
81
89
  The only way out is an explicit `/destroy` command.
82
90
 
@@ -162,7 +170,7 @@ export async function sandboxSessionWorkflow() {
162
170
  "use workflow";
163
171
 
164
172
  const { workflowRunId } = getWorkflowMetadata();
165
- // Create the hook once, outside the loop — reusing the same token from inside // [!code highlight]
173
+ // Create the hook once, outside the loop: reusing the same token from inside // [!code highlight]
166
174
  // the loop would throw HookConflictError. // [!code highlight]
167
175
  const hook = commandHook.create({ token: workflowRunId });
168
176
 
@@ -197,8 +205,8 @@ export async function sandboxSessionWorkflow() {
197
205
  try {
198
206
  while (!destroyed) {
199
207
  if (hibernated && snapshot) {
200
- // While hibernated, the VM is already stopped. Just wait for the next
201
- // command — no idle timer, no compute cost.
208
+ // While hibernated, the VM is already stopped. Wait for the next
209
+ // command: no idle timer, no compute cost.
202
210
  const payload = await hook;
203
211
  if (payload.command === "/destroy") { destroyed = true; break; }
204
212
 
@@ -223,7 +231,7 @@ export async function sandboxSessionWorkflow() {
223
231
  continue;
224
232
  }
225
233
 
226
- // Active — wake at whichever comes first: idle-deadline or refresh-deadline.
234
+ // Active. Wake at whichever comes first: idle-deadline or refresh-deadline.
227
235
  const idleDeadline = lastActivityAt + HIBERNATE_AFTER_MS;
228
236
  const refreshDeadline = sandboxExpiresAt - REFRESH_SAFETY_MS;
229
237
  const wakeAt = Math.min(idleDeadline, refreshDeadline);
@@ -238,7 +246,7 @@ export async function sandboxSessionWorkflow() {
238
246
  const nearExpiry = Date.now() >= refreshDeadline;
239
247
 
240
248
  if (nearExpiry) {
241
- // Proactive refresh — snapshot and immediately recreate so the
249
+ // Proactive refresh: snapshot and immediately recreate so the
242
250
  // session outlives the sandbox hard cap.
243
251
  await emit({ type: "status", state: "refreshing", at: Date.now() });
244
252
  const snap = await sandbox.snapshot(); // [!code highlight]
@@ -255,7 +263,7 @@ export async function sandboxSessionWorkflow() {
255
263
  });
256
264
  lastActivityAt = Date.now();
257
265
  } else {
258
- // Idle — snapshot and hibernate indefinitely.
266
+ // Idle: snapshot and hibernate indefinitely.
259
267
  await emit({ type: "status", state: "hibernating", at: Date.now() });
260
268
  snapshot = await sandbox.snapshot(); // [!code highlight]
261
269
  hibernated = true;
@@ -294,7 +302,11 @@ export async function sandboxSessionWorkflow() {
294
302
 
295
303
  <Tab value="API Routes">
296
304
 
297
- Two endpoints. `/start` accepts an optional `{ runId }` — if the run still exists, it replays the event log from index 0 so a returning client fully rehydrates. `/command` resumes the hook and returns immediately; command output lands on the `/start` stream.
305
+ Two endpoints manage the session. `/start` accepts an optional `{ runId }`: if the run still exists, it replays the event log from index 0 so a returning client fully rehydrates. `/command` resumes the hook and returns immediately; command output lands on the `/start` stream.
306
+
307
+ <Callout type="info">
308
+ This example starts a fresh sandbox session when no `runId` is provided. If your product needs one sandbox session per user, project, or task, use a deterministic hook token derived from that session key and route retries through the active hook. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
309
+ </Callout>
298
310
 
299
311
  ```typescript title="app/api/sandbox/start/route.ts" lineNumbers
300
312
  import { start, getRun } from "workflow/api";
@@ -322,7 +334,7 @@ export async function POST(req: Request) {
322
334
  },
323
335
  });
324
336
  }
325
- // Stale runId — fall through to start fresh.
337
+ // Stale runId: fall through to start fresh.
326
338
  }
327
339
 
328
340
  const run = await start(sandboxSessionWorkflow, []);
@@ -371,7 +383,7 @@ export async function POST(req: Request) {
371
383
 
372
384
  <Tab value="Client">
373
385
 
374
- On mount, if a `runId` is stashed in `localStorage`, reconnect to the existing run. Otherwise start fresh. Commands are POSTed to `/command` — output lands on the `/start` stream.
386
+ On mount, reconnect to the existing run if `localStorage` contains a `runId`. Otherwise, start a new run. Send commands to `/command` with POST requests. Output arrives on the `/start` stream.
375
387
 
376
388
  ```tsx title="components/sandbox-runner.tsx" lineNumbers
377
389
  "use client";
@@ -464,22 +476,22 @@ export function SandboxRunner() {
464
476
 
465
477
  </Tabs>
466
478
 
467
- ## How It Works
479
+ ## How it works
468
480
 
469
- 1. **One workflow = one session.** The workflow owns a sandbox for its entire lifetime. The `runId` is the only state the client has to remember.
470
- 2. **Hook created once.** `commandHook.create({ token: workflowRunId })` outside the loop. Creating it twice with the same token throws `HookConflictError`.
471
- 3. **Two timer branches.** The active-state race wakes on the earlier of `idleDeadline` and `refreshDeadline`. The hibernated state awaits the hook alone — no timer, no compute.
472
- 4. **Proactive refresh.** `refreshDeadline = sandboxExpiresAt - REFRESH_SAFETY_MS`. Hitting this triggers a snapshot + immediate new sandbox from that snapshot, rolling over the hard cap without user intervention.
473
- 5. **`sandbox.snapshot()` stops the VM.** It's documented as part of the snapshot process — don't call `stop()` separately.
474
- 6. **Resume = new sandbox.** `Sandbox.create({ source: { type: "snapshot", snapshotId } })` creates a fresh VM from the snapshot. The new sandbox has a different `sandboxId`; filesystem, installed packages, and git history are preserved.
475
- 7. **Reconnect by runId.** `getRun(runId).getReadable({ startIndex: 0 })` replays the durable event log to a returning client, who rebuilds UI state from the replay.
476
- 8. **Exit only on `/destroy`.** The workflow loop has no hard deadline of its own. Individual sandboxes time out; the session doesn't.
481
+ 1. **One workflow represents one session**: The workflow owns a sandbox for its entire lifetime. The `runId` is the only state the client has to remember.
482
+ 2. **Create the hook once**: Call `commandHook.create({ token: workflowRunId })` outside the loop. Creating it twice with the same token throws `HookConflictError`.
483
+ 3. **Two timer branches control wake-up**: The active-state race wakes on the earlier of `idleDeadline` and `refreshDeadline`. The hibernated state awaits the hook alone, with no timer or compute.
484
+ 4. **Proactive refresh replaces the sandbox**: When `refreshDeadline = sandboxExpiresAt - REFRESH_SAFETY_MS` arrives, the workflow takes a snapshot and immediately creates a new sandbox from it. This rolls over the hard cap without user intervention.
485
+ 5. **`sandbox.snapshot()` stops the VM**: The snapshot process stops the VM, so don't call `stop()` separately.
486
+ 6. **Resume creates a new sandbox**: `Sandbox.create({ source: { type: "snapshot", snapshotId } })` creates a new VM from the snapshot. The new sandbox has a different `sandboxId`; the filesystem, installed packages, and git history are preserved.
487
+ 7. **Reconnect by `runId`**: `getRun(runId).getReadable({ startIndex: 0 })` replays the durable event log to a returning client, which rebuilds UI state from the replay.
488
+ 8. **Exit only on `/destroy`**: The workflow loop has no hard deadline of its own. Individual sandboxes time out, but the session doesn't.
477
489
 
478
490
  ## Pitfalls
479
491
 
480
492
  ### `sandbox.stop()` is terminal
481
493
 
482
- A stopped sandbox cannot be restarted — you have to create a new one. Hibernation is only possible via `snapshot()` + new-sandbox-from-snapshot. Don't try to "pause" an active sandbox with `stop()` and resume later.
494
+ A stopped sandbox cannot be restarted: you have to create a new one. Hibernation is only possible via `snapshot()` + new-sandbox-from-snapshot. Don't try to "pause" an active sandbox with `stop()` and resume later.
483
495
 
484
496
  ### `snapshot()` already stops the VM
485
497
 
@@ -491,7 +503,7 @@ Both `resuming` (idle → command) and `refreshing` (near-hard-cap rotation) cre
491
503
 
492
504
  ### Keep the refresh margin generous
493
505
 
494
- `snapshot()` + `Sandbox.create({ source })` takes real time (typically tens of seconds). If `REFRESH_SAFETY_MS` is too small, the old sandbox hits its hard cap mid-snapshot. Leave at least 60–90 seconds; 5 minutes is comfortable.
506
+ `snapshot()` followed by `Sandbox.create({ source })` takes time, typically tens of seconds. If `REFRESH_SAFETY_MS` is too small, the old sandbox hits its hard cap mid-snapshot. Leave at least 60–90 seconds; the example uses 5 minutes.
495
507
 
496
508
  ### Don't call `writable.close()` inside a workflow function
497
509
 
@@ -501,16 +513,21 @@ Stream closure must happen inside a `"use step"` function. Calling `writable.clo
501
513
 
502
514
  Clients can hold `runId`s from long-gone workflow runs (localStorage, back button, server restart). Gate the reconnect path on `run.exists` and fall through to starting fresh. On `hook.resume`, catch `not found` / `expired` and return 410 so the client clears its state.
503
515
 
516
+ ### Decide whether `/start` should be idempotent
517
+
518
+ The sample treats a missing or stale `runId` as a request for a new session. For one-session-per-resource behavior, use a durable resource key, such as `projectId` or `taskId`, to claim or retrieve the run before starting a new one.
519
+
504
520
  ### Keep the hook outside the loop
505
521
 
506
522
  Each iteration's `hook.then(...)` attaches a listener to the same hook instance. Creating a new hook per iteration with the same token throws `HookConflictError`. One hook, one token (`workflowRunId`), reused every iteration.
507
523
 
508
524
  ## Key APIs
509
525
 
510
- - [`Sandbox.create`](https://vercel.com/docs/sandbox) — provision a VM (runtime, source, timeout)
511
- - [`sandbox.runCommand`](https://vercel.com/docs/sandbox) — execute a command; implicit step
512
- - [`sandbox.snapshot`](https://vercel.com/docs/sandbox) — save state and stop the VM; returns `Snapshot`
513
- - [`defineHook()`](/docs/api-reference/workflow/define-hook) — suspension point for user commands
514
- - [`sleep()`](/docs/api-reference/workflow/sleep) — durable timer that powers both idle hibernation and proactive refresh
515
- - [`getRun()`](/docs/api-reference/workflow-api/get-run) — look up a run and replay its event log for reconnection
516
- - [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable NDJSON event stream
526
+ - [`Sandbox.create`](https://vercel.com/docs/sandbox): provision a VM (runtime, source, timeout)
527
+ - [`sandbox.runCommand`](https://vercel.com/docs/sandbox): execute a command; implicit step
528
+ - [`sandbox.snapshot`](https://vercel.com/docs/sandbox): save state and stop the VM; returns `Snapshot`
529
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook): suspension point for user commands
530
+ - [`sleep()`](/docs/api-reference/workflow/sleep): durable timer that powers both idle hibernation and proactive refresh
531
+ - [`getRun()`](/docs/api-reference/workflow-api/get-run): look up a run and replay its event log for reconnection
532
+ - [`getWritable()`](/docs/api-reference/workflow/get-writable): resumable newline-delimited JSON (NDJSON) event stream
533
+ - [Idempotency](/docs/foundations/idempotency): choose when `/start` should reuse an existing run
@@ -0,0 +1,95 @@
1
+ ---
2
+ title: Deploying
3
+ description: Deploy workflows locally, on Vercel, or anywhere using pluggable World adapters.
4
+ type: overview
5
+ summary: Learn how to deploy workflows to different environments using World adapters.
6
+ related:
7
+ - /worlds/local
8
+ - /worlds/postgres
9
+ - /worlds/vercel
10
+ - /worlds/building-a-world
11
+ ---
12
+
13
+ The same workflow code can run locally during development, on Vercel with zero configuration, or on any infrastructure using **Worlds**, which are pluggable adapters that handle storage, queuing, and communication.
14
+
15
+ ## Local development
16
+
17
+ During local development, workflows use the **Local World** without configuration. The Local World stores workflow data in a `.workflow-data/` directory and processes steps synchronously for development and testing.
18
+
19
+ ```bash
20
+ # Run your dev server to use workflows locally
21
+ npm run dev
22
+ ```
23
+
24
+ You can inspect local workflow data using the CLI:
25
+
26
+ ```bash
27
+ npx workflow inspect runs
28
+ ```
29
+
30
+ <Callout>
31
+ Learn more about the [Local World](/worlds/local) configuration and internals.
32
+ </Callout>
33
+
34
+ ## Deploying to Vercel
35
+
36
+ Deploy workflows to production on Vercel without configuration. Workflows deployed to Vercel use the **Vercel World**.
37
+
38
+ The Vercel World provides:
39
+
40
+ - **Durable storage**: Workflow state persists across function invocations
41
+ - **Managed queuing**: The queue processes steps reliably with automatic retries
42
+ - **Automatic scaling**: Workflows scale with your application
43
+ - **Built-in observability**: View workflow runs in the Vercel dashboard
44
+ - **Multi-region**: The region that creates each run also hosts its workflow data, queue, and streams, keeping them close to your users
45
+
46
+ Deploy your application:
47
+
48
+ ```bash
49
+ vercel deploy
50
+ ```
51
+
52
+ <FluidComputeCallout />
53
+
54
+ <Callout>
55
+ Learn more about the [Vercel World](/worlds/vercel) and its capabilities, including [multi-region](/worlds/vercel#multi-region).
56
+ </Callout>
57
+
58
+ ## Self-hosting & other providers
59
+
60
+ For self-hosting or deploying to other cloud providers, you can use community-maintained Worlds or build your own.
61
+
62
+ <Cards>
63
+ <Card title="Explore Worlds" href="/worlds">
64
+ Browse official and community World implementations with compatibility status and performance benchmarks.
65
+ </Card>
66
+ <Card title="Build Your Own" href="/worlds/building-a-world">
67
+ Learn how to implement a custom World for your infrastructure.
68
+ </Card>
69
+ </Cards>
70
+
71
+ ### Using a third-party World
72
+
73
+ To use a different World implementation, set the `WORKFLOW_TARGET_WORLD` environment variable:
74
+
75
+ ```bash
76
+ export WORKFLOW_TARGET_WORLD=@workflow/world-postgres
77
+ # Plus any world-specific configuration
78
+ export DATABASE_URL=postgres://...
79
+ ```
80
+
81
+ Each World may have its own configuration requirements. Refer to that World's documentation for details.
82
+
83
+ ## Observability
84
+
85
+ The [Observability tools](/docs/observability) work with any World backend. By default, they connect to your local environment, but you can configure them to inspect remote deployments:
86
+
87
+ ```bash
88
+ # Inspect local workflows
89
+ npx workflow inspect runs
90
+
91
+ # Inspect remote workflows
92
+ npx workflow inspect runs --backend @workflow/world-postgres
93
+ ```
94
+
95
+ Learn more about [Observability](/docs/observability) tools.
@@ -11,23 +11,27 @@ related:
11
11
  - /docs/errors/timeout-in-workflow
12
12
  ---
13
13
 
14
+ <CopyPrompt
15
+ text="Replace `AbortSignal.timeout()` inside workflow functions. Find the failing `AbortSignal.timeout(ms)` call in a `&quot;use workflow&quot;` function. Import `sleep` from `workflow`. Create `const controller = new AbortController()`, pass `controller.signal` into the async work that supports cancellation, and start a deterministic timeout with `void sleep(&quot;10s&quot;).then(() => controller.abort())` or a `Promise.race` between the work and `sleep(...)`. Keep actual fetch/SDK side effects inside `&quot;use step&quot;` helpers when they need Node.js APIs. Treat intentional aborts as non-retryable if appropriate by throwing `FatalError`. Verify the operation succeeds before the timeout, aborts after the timeout, and replays without abort-signal-timeout-in-workflow."
16
+ />
17
+
14
18
  ## Error
15
19
 
16
- ```
20
+ ```text
17
21
  AbortSignal.timeout() is not supported in workflow functions.
18
22
  Use sleep() with an AbortController instead.
19
23
  ```
20
24
 
21
- ## Why This Happens
25
+ ## Why this happens
22
26
 
23
- `AbortSignal.timeout()` creates a signal that aborts after a real-time delay using an internal timer. Workflow functions must be [deterministic](/docs/foundations/workflows-and-steps) to support replay — they run the same code multiple times during the workflow's lifecycle, using the [event log](/docs/how-it-works/event-sourcing) to resume execution to the correct point.
27
+ `AbortSignal.timeout()` creates a signal that aborts after a real-time delay using an internal timer. Workflow functions must be [deterministic](/docs/foundations/workflows-and-steps) to support replay: they run the same code multiple times during the workflow's lifecycle, using the [event log](/docs/how-it-works/event-sourcing) to resume execution to the correct point.
24
28
 
25
29
  Real-time timers break this determinism because:
26
- - On the first execution, the timer might fire after 10 seconds
27
- - On replay, the timer would fire again, but the event log may have already advanced past that point
28
- - The timer's behavior depends on wall-clock time, which varies between executions
30
+ - On the first execution, the timer might fire after 10 seconds.
31
+ - On replay, the timer would fire again, but the event log may have already advanced past that point.
32
+ - The timer's behavior depends on wall-clock time, which varies between executions.
29
33
 
30
- ## How to Fix
34
+ ## How to fix
31
35
 
32
36
  Use [`sleep()`](/docs/api-reference/workflow/sleep) with an `AbortController` to create a deterministic timeout that cancels in-flight work:
33
37
 
@@ -66,7 +70,7 @@ async function fetchData(signal: AbortSignal) {
66
70
  }
67
71
  ```
68
72
 
69
- The `sleep()` + `AbortController` pattern is the durable equivalent of `AbortSignal.timeout()`. The sleep is recorded in the event log, so it replays deterministically. If `fetchData` finishes within 10 seconds you get the response; if not, the timer fires `controller.abort()`, `fetch` rejects with an `AbortError`, and the step's failure propagates to the workflow as a `FatalError` (no retries — abort is intentional cancellation).
73
+ The `sleep()` and `AbortController` pattern is the durable equivalent of `AbortSignal.timeout()`. The sleep is recorded in the event log, so it replays deterministically. If `fetchData` finishes within 10 seconds, you get the response. Otherwise, the timer calls `controller.abort()`, `fetch` rejects with an `AbortError`, and the step's failure propagates to the workflow as a `FatalError` (no retries because the abort is intentional cancellation).
70
74
 
71
75
  <Callout type="info">
72
76
  `AbortSignal.timeout()` works normally inside step functions, since steps have full Node.js runtime access and are not replayed.
@@ -74,7 +78,7 @@ The `sleep()` + `AbortController` pattern is the durable equivalent of `AbortSig
74
78
 
75
79
  ## Related
76
80
 
77
- - [Cancellation](/docs/foundations/cancellation) — Patterns for cancelling in-flight work
78
- - [`sleep()` API Reference](/docs/api-reference/workflow/sleep) — Durable sleep primitive
79
- - [Workflows and Steps](/docs/foundations/workflows-and-steps) — Why workflow functions must be deterministic
80
- - [`setTimeout` in Workflow](/docs/errors/timeout-in-workflow) — Similar restriction on `setTimeout`
81
+ - [Cancellation](/docs/foundations/cancellation): Patterns for canceling in-flight work
82
+ - [`sleep()` API Reference](/docs/api-reference/workflow/sleep): Durable sleep primitive
83
+ - [Workflows and Steps](/docs/foundations/workflows-and-steps): Why workflow functions must be deterministic
84
+ - [`setTimeout` in Workflow](/docs/errors/timeout-in-workflow): Similar restriction on `setTimeout`
@@ -1,39 +1,50 @@
1
1
  ---
2
2
  title: corrupted-event-log
3
- description: The workflow's event log contains an event that no consumer can process, indicating corruption or invalid state.
3
+ description: The workflow's event log contains an event that cannot be processed or a stored payload that cannot be read.
4
4
  type: troubleshooting
5
- summary: Resolve corrupted event log errors caused by duplicate or orphaned events.
5
+ summary: Resolve corrupted event log errors caused by invalid events or unreadable stored payloads.
6
6
  prerequisites:
7
7
  - /docs/foundations/workflows-and-steps
8
8
  related:
9
9
  - /docs/foundations/errors-and-retries
10
10
  ---
11
11
 
12
- This error occurs when the Workflow runtime encounters an event in the event log that no registered consumer can process. This means the event log is in an invalid state — typically due to duplicate or orphaned events.
12
+ This error occurs when the Workflow runtime cannot safely replay the event log. The log may be in an invalid state, such as an orphaned event or one no consumer can attribute to anything the workflow did, or it may reference a stored payload that the World can no longer read.
13
13
 
14
- This is a **workflow-level fatal error**. It cannot be caught or handled inside your workflow code. A corrupted event log immediately fails the entire run without executing any more user code. The run must be retried from outside the workflow.
14
+ This is a **workflow-level fatal error**. It cannot be caught or handled inside your workflow code. The runtime retries transient replay divergence automatically, but an unreadable stored payload is terminal immediately because replaying cannot restore it.
15
15
 
16
- ## Error Message
16
+ ## Error message
17
17
 
18
+ For replay divergence:
19
+
20
+ ```text
21
+ Workflow replay diverged <divergenceCount> times after <maxRecoveryReplays> recovery replays; latest divergent event was <eventId>. Last divergence: <details>
18
22
  ```
19
- Unconsumed event in event log: eventType=<type>, correlationId=<id>, eventId=<id>. This indicates a corrupted or invalid event log.
23
+
24
+ For an unreadable stored payload:
25
+
26
+ ```text
27
+ the event log references a payload that no longer exists in storage: <details>
20
28
  ```
21
29
 
22
- ## Why This Happens
30
+ ## Why this happens
31
+
32
+ Workflows persist their progress as an ordered event log. During replay, the runtime processes each event in sequence. Every event must be consumed by a matching callback, such as a step or sleep waiting for its result. An event no callback ever claims is one the runtime would have to drop to finish the run, so it fails the run instead of returning a result that silently ignored it.
23
33
 
24
- Workflows persist their progress as an ordered event log. During replay, the runtime processes each event in sequence — every event must be consumed by a matching callback (e.g., a step or sleep waiting for its result). When an event has no matching consumer, the runtime cannot advance past it, which would block all subsequent events and hang the workflow indefinitely.
34
+ A delivery written from outside the replay, such as a hook firing or a step completing on another invocation, can land ahead of the events the replay is writing itself. That is ordinary concurrency rather than corruption, so the runtime holds such an event and offers it to each consumer the replay registers afterwards. The failure comes only when the workflow function returns while an event is still held, at which point no consumer can ever appear. A replay that suspends still holding one reports it on the span (`workflow.events.parked.count`, `.event_id`, `.event_type`) and leaves the decision to the replay that follows.
25
35
 
26
- Instead of silently hanging, the runtime raises a `WorkflowRuntimeError` to fail the workflow fast and surface the problem.
36
+ Before failing on divergence, the runtime retries the replay and surfaces this terminal error only if replay still cannot recover. It does not retry a payload that the World reports as permanently missing.
27
37
 
28
38
  Common scenarios that produce this error:
29
39
 
30
- 1. **Duplicate completion events** — Two `wait_completed` events for a single `wait_created`, or two `step_completed` events for the same step. The first is consumed normally, but the second has no consumer.
31
- 2. **Orphaned events** — A `step_completed` or `wait_completed` event whose `correlationId` doesn't match any step or sleep in the workflow code.
32
- 3. **Events after terminal state** — An event that arrives after its corresponding step or wait has already reached a terminal state (e.g., `step_retrying` after `step_completed`).
40
+ - **An unclaimed event that repeats nothing**: A duplicate of a kind the log already records for that entity is read past rather than failing the run, so a second `step_completed` or `wait_completed` is not this error (see [Duplicate Events](/docs/how-it-works/event-sourcing#duplicate-events)). What fails is an unclaimed event with no earlier counterpart to defer to: a `step_started` behind a `step_completed` on a log that never recorded a `step_started`, for instance. No consumer remains for the step, and there is no earlier event of that kind the replay could be reading instead.
41
+ - **Orphaned events**: A `step_completed` or `wait_completed` event whose `correlationId` doesn't match any step or sleep in the workflow code, so the replay reaches its end still holding it.
42
+ - **A hole in the log**: Events are numbered by their position in the run's log, and those positions are dense, so a position below the log's highest that holds no event means the log the replay loaded is incomplete. The runtime cannot tell a position no write ever occupied from one whose event it failed to read, so it refuses to replay rather than produce a result that may be silently wrong. See [`WORKFLOW_SLOT_GAP_CHECK`](/docs/configuration/runtime-tuning#workflow_slot_gap_check).
43
+ - **An unreadable stored payload**: An event row still references a payload object, but the World reports that the object no longer exists in its storage. The same log would fail on every replay, so the run fails immediately instead of retrying forever.
33
44
 
34
- ## What To Do
45
+ ## What to do
35
46
 
36
- This error indicates a bug in the Workflow SDK or Workflow server — not in your workflow code. Your workflow code does not need to change. Follow these steps to resolve the issue:
47
+ This error indicates a bug in the Workflow SDK or Workflow server, not in your workflow code. Your workflow code does not need to change. Follow these steps to resolve the issue:
37
48
 
38
49
  ### 1. Upgrade to the latest `workflow` package
39
50
 
@@ -45,18 +56,18 @@ npm install workflow@latest
45
56
 
46
57
  ### 2. Retry the failed run
47
58
 
48
- Since this is a fatal error, the run is automatically marked as `failed`. You can re-run it using the **Re-run** button in the Workflow Dashboard.
59
+ If this error reports replay divergence, automatic replay recovery has already been exhausted. If it reports an unreadable payload, recovery cannot recreate that payload. In either case, the run has been marked as `failed`. You can re-run the workflow using the **Re-run** button in the Workflow Dashboard; a re-run starts a new run with a new event log.
49
60
 
50
61
  ### 3. Report the issue
51
62
 
52
- If the error persists after upgrading, please [open an issue on GitHub](https://github.com/vercel/workflow/issues/new) so we can investigate and fix the underlying bug. Include the following details to help us diagnose the problem:
63
+ If the error persists after upgrading, [open an issue on GitHub](https://github.com/vercel/workflow/issues/new) so we can investigate and fix the underlying bug. Include the following details to help us diagnose the problem:
53
64
 
54
65
  - The version of the `workflow` package you are using
55
66
  - The run ID(s) of the affected workflow run(s)
56
- - The error message (including `eventType`, `correlationId`, and `eventId`)
67
+ - The complete error message, including any `eventType`, `correlationId`, `eventId`, or payload details
57
68
  - Any details about the event log or the workflow that triggered the error
58
69
 
59
- ## This Error Cannot Be Caught
70
+ ## This error cannot be caught
60
71
 
61
72
  Unlike other workflow errors, a corrupted event log error is **not catchable** inside your workflow function. Because the event log itself is invalid, the runtime cannot safely continue executing any user code. The entire run fails immediately and is marked as `failed`.
62
73
 
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: deployment-mismatch
3
+ description: A workflow run was delivered to a deployment other than the one it is pinned to.
4
+ type: troubleshooting
5
+ summary: Understand how Workflow recovers from a misrouted delivery, and why a run eventually fails with DEPLOYMENT_MISMATCH.
6
+ prerequisites:
7
+ - /docs/foundations/workflows-and-steps
8
+ related:
9
+ - /docs/foundations/versioning
10
+ - /docs/errors/runtime-decryption-failed
11
+ - /docs/foundations/errors-and-retries
12
+ ---
13
+
14
+ Every run is pinned to a single deployment when it starts. When a queued workflow or step callback is delivered to a **different** deployment, Workflow does not execute it there. Instead it re-routes the message to the deployment the run is pinned to, and only if the run keeps arriving elsewhere does it fail with the `DEPLOYMENT_MISMATCH` classification.
15
+
16
+ This is an SDK/runtime signal, not an error thrown by your workflow code, and it is not catchable inside a workflow function.
17
+
18
+ ## Error message
19
+
20
+ ```text
21
+ Workflow run "wrun_..." is pinned to deployment "dpl_A", but was received by deployment "dpl_B". The runtime re-routed the message to "dpl_A" 3 times and it kept arriving elsewhere, so the run was stopped to protect against code-skew errors. Verify that the run's deployment is still available and that queue callbacks are routed to it.
22
+ ```
23
+
24
+ When the queue definitively reports that the run's deployment cannot be reached (it was deleted, or aged out of its retention window), no re-route is possible and the message omits the re-routing clause. Transient or unknown publishing failures leave the current delivery unacknowledged so the queue can redeliver it; they do not fail the run or consume this recovery budget.
25
+
26
+ ## Why a run is pinned
27
+
28
+ A run's deployment is chosen once, at [`start()`](/docs/api-reference/workflow-api/start):
29
+
30
+ - By default it is the deployment that called `start()`. See [Versioning](/docs/foundations/versioning) for why runs are pinned this way.
31
+ - With `start(workflow, args, { deploymentId })` it is the id you pass, so a run can deliberately target a deployment other than the one that created it.
32
+ - With `deploymentId: "latest"` it is the most recent deployment for the current environment, resolved at start time.
33
+
34
+ Whichever it is, that `deploymentId` is recorded on the run, and every subsequent workflow replay and step execution must happen on that deployment. Continuing on a different one is unsafe:
35
+
36
+ 1. **Code skew.** The workflow and step bundles on the receiving deployment may not match the code that produced the run's recorded history, so replay could diverge or produce incorrect results.
37
+ 2. **Encryption.** Step inputs and other event-log payloads are encrypted with a per-run key derived from the pinned deployment's key material. A different deployment derives the wrong key and cannot decrypt them, previously the source of a confusing [runtime-decryption-failed](/docs/errors/runtime-decryption-failed) that exhausted retries with no clear cause.
38
+
39
+ So the runtime checks the pinned deployment before it executes anything, and `DEPLOYMENT_MISMATCH` names the result, instead of the mismatch surfacing later as an unrelated decryption failure.
40
+
41
+ ## Automatic recovery
42
+
43
+ A deployment that receives a run it does not own first tries to fix the delivery rather than fail the run:
44
+
45
+ 1. It re-enqueues the message **explicitly addressed** to the run's own deployment. This is strictly better-addressed than the send that misrouted, which inherited the producing deployment's ambient id.
46
+ 2. Delivery is delayed with a short exponential backoff (1s, 2s, 4s).
47
+ 3. If the run keeps arriving at the wrong deployment, the run is failed with `DEPLOYMENT_MISMATCH` after `WORKFLOW_DEPLOYMENT_MISMATCH_MAX_RETRIES` attempts (default `3`). Set it to `0` to fail on the first misrouted delivery instead.
48
+
49
+ Nothing is executed on the wrong deployment during recovery: no workflow code, no step body, no `step_started`, and no hook resume. Whatever the delivery was carrying travels with it, so a pending step keeps its identity and a hook resume keeps its payload: they run on the deployment that can actually decrypt them.
50
+
51
+ Recovery attempts do not create events on the run, so a run that self-heals looks completely normal. They are reported on the invocation's trace span (`workflow.deployment.pinned_id`, `workflow.deployment_mismatch.retry_count`, `workflow.deployment_mismatch.recovered`) and as a runtime warning in your function logs.
52
+
53
+ ## What to do
54
+
55
+ - **Re-run from the current deployment.** Trigger the workflow again from your latest deployment (or use the **Re-run** button in the Workflow Dashboard). The new run is pinned to the current deployment.
56
+ - **Keep a run's deployment available** for the lifetime of that run. A run whose deployment has been deleted or has aged out cannot be resumed and must be re-run: recovery cannot help, so these fail on the first misrouted delivery. This applies to runs started with an explicit `deploymentId` too: pinning a run to an older deployment keeps it dependent on that deployment for its whole lifetime.
57
+ - **Report it** if the pinned deployment was still available. Include both deployment IDs and the run ID from the error message, plus the trace span attributes above. A run that failed this way despite a reachable target is a routing fault worth investigating rather than something to work around.
58
+
59
+ ## This error cannot be caught
60
+
61
+ Like other runtime signals, `DEPLOYMENT_MISMATCH` is **not catchable** inside your workflow function: the run is failed before any workflow or step code executes on the receiving deployment. Check the run status from outside instead:
62
+
63
+ ```typescript lineNumbers
64
+ import { getRun } from "workflow/api";
65
+
66
+ const run = getRun("wrun_abc123");
67
+ const status = await run.status;
68
+ if (status === "failed") {
69
+ console.error("Run failed");
70
+ }
71
+ ```
@@ -9,21 +9,25 @@ related:
9
9
  - /docs/api-reference/workflow/fetch
10
10
  ---
11
11
 
12
+ <CopyPrompt
13
+ text="Fix `fetch` usage inside workflow functions. Search workflow files for direct global `fetch(...)` calls and libraries such as AI SDK calls that use fetch. For basic HTTP calls inside a `&quot;use workflow&quot;` function, import `{ fetch }` from `workflow` and replace the global call. For SDK/client calls that need normal Node.js or provider behavior, move the call into a helper function with `&quot;use step&quot;` and call that step from the workflow. Keep all step inputs and outputs serializable. Verify the workflow starts and replays without the fetch-in-workflow error."
14
+ />
15
+
12
16
  This error occurs when you try to use `fetch()` directly in a workflow function, or when a library (like the AI SDK) tries to call `fetch()` under the hood.
13
17
 
14
- ## Error Message
18
+ ## Error message
15
19
 
16
- ```
20
+ ```text
17
21
  Global "fetch" is unavailable in workflow functions. Use the "fetch" step function from "workflow" to make HTTP requests.
18
22
  ```
19
23
 
20
- ## Why This Happens
24
+ ## Why this happens
21
25
 
22
26
  Workflow functions run in a sandboxed environment without direct access to `fetch()`.
23
27
 
24
28
  Many libraries make HTTP requests under the hood. For example, the AI SDK's `generateText()` function calls `fetch()` to make HTTP requests to AI providers. When these libraries run inside a workflow function, they fail because the global `fetch` is not available.
25
29
 
26
- ## Quick Fix
30
+ ## Quick fix
27
31
 
28
32
  Import the `fetch` step function from the `workflow` package and assign it to `globalThis.fetch` inside your workflow function. This version of `fetch` is a step function that wraps the standard `fetch` API, automatically handling serialization and providing retry capabilities. This will also make `fetch()` available to all functions and libraries in the current workflow function.
29
33
 
@@ -68,9 +72,9 @@ export async function chatWorkflow(prompt: string) {
68
72
  }
69
73
  ```
70
74
 
71
- ## Common Scenarios
75
+ ## Common scenarios
72
76
 
73
- ### AI SDK Integration
77
+ ### AI SDK integration
74
78
 
75
79
  This is the most common scenario - using AI SDK functions that make HTTP requests:
76
80
 
@@ -94,7 +98,7 @@ export async function aiWorkflow(userMessage: string) {
94
98
  }
95
99
  ```
96
100
 
97
- ### Direct API Calls
101
+ ### Direct API calls
98
102
 
99
103
  You can also use the fetch step function directly for your own HTTP requests:
100
104