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
@@ -0,0 +1,124 @@
1
+ ---
2
+ title: Tracing
3
+ description: Distributed tracing with OpenTelemetry for workflow runs, steps, and queue deliveries.
4
+ type: guide
5
+ summary: Trace workflow execution end to end with OpenTelemetry.
6
+ prerequisites:
7
+ - /docs/foundations/workflows-and-steps
8
+ related:
9
+ - /docs/observability
10
+ - /docs/observability/attributes
11
+ - /docs/how-it-works/event-sourcing
12
+ ---
13
+
14
+ The Workflow SDK includes [OpenTelemetry](https://opentelemetry.io) instrumentation. It emits spans for workflow starts, every workflow and step invocation, and the HTTP calls it makes to the workflow backend, and it propagates trace context across queue deliveries so a run remains traceable end to end.
15
+
16
+ The SDK only depends on the OpenTelemetry **API**, never on an SDK or exporter. If your application does not register an OpenTelemetry SDK, all tracing code is a silent no-op with no overhead and no behavior change.
17
+
18
+ ## Enabling tracing
19
+
20
+ Register any OpenTelemetry Node SDK in your application. On Vercel with Next.js, the simplest setup is [`@vercel/otel`](https://vercel.com/docs/tracing/instrumentation) in `instrumentation.ts`:
21
+
22
+ ```typescript title="instrumentation.ts" lineNumbers
23
+ import { registerOTel } from "@vercel/otel"
24
+
25
+ export function register() {
26
+ registerOTel({ serviceName: "my-app" })
27
+ }
28
+ ```
29
+
30
+ No workflow-specific configuration is required. As soon as a tracer provider and propagator are registered, the SDK's spans, context propagation, and span links activate automatically.
31
+
32
+ <Callout>
33
+ `@opentelemetry/api` is an **optional peer dependency**. An OpenTelemetry SDK such as `@vercel/otel` normally pulls it in transitively, but installing it directly (`npm i @opentelemetry/api`) guarantees it is present in your build, particularly for bundled or serverless targets where the SDK's tracing is inlined at build time. If it can't be resolved, tracing is a silent no-op.
34
+ </Callout>
35
+
36
+ ## Spans
37
+
38
+ | Span name | Kind | Emitted when |
39
+ | --- | --- | --- |
40
+ | `workflow.start <name>` | internal | `start()` is called in your application code |
41
+ | `workflow.execute <name>` | consumer (root) | a queue delivery invokes the workflow; replay, orchestration, and inline steps run under it |
42
+ | `step.execute <name>` | internal (inline) / consumer + root (queue-delivered) | a step function executes |
43
+ | `http <method>` | client | the SDK calls the workflow backend (event reads/writes) |
44
+ | `workflow.stream.write` | client | a stream chunk (or the stream close) is flushed to the backend |
45
+ | `workflow.stream.flush` | client | a buffered batch of stream writes settles; back-dated to the batch's first `write()`, so its duration is the app-perceived batch latency (buffer dwell + RPC) |
46
+ | `workflow.stream.close` | client | the stream-close RPC; its duration is the close round trip |
47
+ | `workflow.stream.read.complete` | client | a stream read drains; back-dated to the read dispatch, so its duration is the total read (`workflow.stream.read.chunks` / `.bytes` carry throughput counts) |
48
+ | `workflow.stream.read.connect` | client | a live stream read opens; the span covers dispatch → response headers (network connect) |
49
+ | `workflow.stream.read` | client | a live stream read receives its first chunk; the span's duration is the end-to-end time-to-first-chunk (see `workflow.stream.read.ttfc_ms`) |
50
+
51
+ `<name>` is the short function name (for example `processOrder`); the full machine name, including the source module, is available in the `workflow.name` / `step.name` attributes.
52
+
53
+ Stream spans are emitted by the SDK's world backend on the client that writes or reads the stream, and (like all SDK spans) are no-ops when no OpenTelemetry SDK is registered. The `workflow.stream.read` span only appears once the first non-empty chunk arrives.
54
+
55
+ ## Key attributes
56
+
57
+ | Attribute | Description |
58
+ | --- | --- |
59
+ | `workflow.run.id` | The run ID (`wrun_...`). Present on every workflow and step span; the primary key for finding all spans of a run. |
60
+ | `workflow.name` | The workflow function name. |
61
+ | `workflow.trace.mode` | The active trace mode (`linked` or `continuous`). |
62
+ | `workflow.trace.propagated` | Whether the invocation received trace context from the queue message. |
63
+ | `workflow.queue.overhead_ms` | Time between the message being enqueued and the handler starting: queue dwell plus any cold start. |
64
+ | `workflow.stream.name` | The stream name, on stream write/read spans. |
65
+ | `workflow.stream.operation` | The stream operation: `write`, `write_multi`, `close`, `read`, or `flush`. |
66
+ | `workflow.stream.write.chunk_rtt` | Time between emission of a chunk to the wire, and receiving the `ack` message for that chunk. Also stamped on `workflow.stream.flush` (the batch's write RPC duration, network included). |
67
+ | `workflow.stream.flush.buffer_dwell_ms` | On `workflow.stream.flush`: time the batch's first chunk waited in the client-side write buffer (flush timer, run-ready barrier) before the request was dispatched. `workflow.stream.flush.chunks` / `.bytes` carry the batch shape. |
68
+ | `workflow.stream.read.ttfc_ms` | Time between opening a read connection and observing and receiving the first chunk back. |
69
+ | `workflow.stream.read.connect_ms` | On `workflow.stream.read`: the connect portion (read dispatch → stream handle/response headers), network included. |
70
+
71
+ ## Trace shape: one trace per invocation
72
+
73
+ A single workflow run can span hours or days across many separate function invocations: every `sleep()` wake-up, hook resume, retry, and queued step continuation is a new queue delivery. Stitching all of that into one trace produces giant, slow-loading traces that most tracing backends truncate.
74
+
75
+ Instead, the SDK creates **one bounded trace per invocation**. Each `workflow.execute` (or background `step.execute`) span starts a new trace root and attaches two **span links**:
76
+
77
+ - a link to the **enqueue site**, the span that queued the message which triggered this invocation, and
78
+ - a link to the **run origin**, the trace in which `start()` was originally called.
79
+
80
+ A span link is OpenTelemetry's relationship for "causally related, but in a different trace." It is the standard pattern for asynchronous messaging, where producing and consuming a message can be separated by arbitrary time.
81
+
82
+ ```mermaid
83
+ flowchart LR
84
+ O["start() request trace"]
85
+ A["invocation 1"]
86
+ B["invocation 2"]
87
+ C["invocation 3 ..."]
88
+ A -. "link" .-> O
89
+ B -. "link" .-> O
90
+ C -. "link" .-> O
91
+ B -. "link" .-> A
92
+ C -. "link" .-> B
93
+
94
+ style O fill:#a78bfa,stroke:#8b5cf6,color:#000
95
+ ```
96
+
97
+ Each invocation links back to the trace that enqueued it and to the run origin.
98
+
99
+ To see a whole run, query by attribute rather than by trace ID (for example `workflow.run.id = wrun_...` in your tracing backend), or follow the span links between invocation traces.
100
+
101
+ ## Trace modes
102
+
103
+ The `WORKFLOW_TRACE_MODE` environment variable controls the shape:
104
+
105
+ | Mode | Behavior |
106
+ | --- | --- |
107
+ | `linked` (default) | Each invocation is its own trace root with span links to the enqueue site and the run origin. Traces stay small; sampling is decided per invocation. |
108
+ | `continuous` | The run-origin context becomes the **parent** of every invocation, so the entire run shares one trace ID. |
109
+
110
+ <Callout type="warn">
111
+ This is a behavior change from v4, which always used `continuous`-style tracing. If you have dashboards or queries that assume one trace ID per run, either update them to use `workflow.run.id` and span links, or set `WORKFLOW_TRACE_MODE=continuous` to restore the previous shape. In `linked` mode, each invocation root makes its own sampling decision, and the number of root spans increases to one per invocation.
112
+ </Callout>
113
+
114
+ ## Context propagation
115
+
116
+ When tracing is enabled, the SDK propagates [W3C Trace Context](https://www.w3.org/TR/trace-context/) on its outbound calls:
117
+
118
+ - **Backend requests** carry `traceparent`, `tracestate`, and `baggage` headers, so backend spans can join your trace.
119
+ - **Queue messages** carry the run-origin trace context in the message payload, and the queue re-delivers the producer's context to the workflow handler, where it becomes the enqueue-site span link.
120
+ - **Baggage** carries `workflow.run_id` and `workflow.name` entries during workflow execution, allowing downstream services you call from steps to tag their own telemetry with the run ID.
121
+
122
+ <Callout>
123
+ Baggage entries set by your application are propagated as a `baggage` HTTP header on the SDK's backend requests, like any other OpenTelemetry-instrumented HTTP call. Avoid placing sensitive values in baggage.
124
+ </Callout>
@@ -3,18 +3,18 @@ title: Testing
3
3
  description: Unit test individual steps and integration test entire workflows using Vitest.
4
4
  ---
5
5
 
6
- Testing is a critical part of building reliable workflows. Because steps are just functions annotated with directives, they can be unit tested like any other JavaScript function. Workflow SDK also provides a Vitest plugin that runs full workflows in-process no running server required.
6
+ Test steps like any other JavaScript function, or use the Workflow SDK Vitest plugin to run complete workflows in-process without a server.
7
7
 
8
8
  This guide covers two approaches:
9
9
 
10
- 1. **Unit testing** - Test individual steps as plain functions, without the workflow runtime.
11
- 2. **Integration testing** - Test entire workflows in-process using the `workflow()` Vitest plugin. Required when you want to test workflow specific code paths, like those using [hooks](/docs/foundations/hooks), webhooks, [`sleep()`](/docs/api-reference/workflow/sleep), retries, etc.
10
+ 1. **Unit testing**: Test individual steps as plain functions without the workflow runtime.
11
+ 2. **Integration testing**: Test entire workflows in-process using the `workflow()` Vitest plugin. Use integration tests for workflow-specific code paths that use [hooks](/docs/foundations/hooks), webhooks, [`sleep()`](/docs/api-reference/workflow/sleep), or retries.
12
12
 
13
- ## Unit Testing Steps
13
+ ## Unit testing steps
14
14
 
15
- Without the workflow compiler, the `"use step"` directive is a no-op. Your step functions run as regular JavaScript functions, making them straightforward to unit test with no special configuration.
15
+ Without the workflow compiler, the `"use step"` directive is a no-op. Your step functions run as regular JavaScript functions, so you can unit test them without special configuration.
16
16
 
17
- ### Example Steps
17
+ ### Example steps
18
18
 
19
19
  Given a workflow file with step functions like this:
20
20
 
@@ -49,7 +49,7 @@ export async function sendOnboardingEmail(user: { id: string; email: string }) {
49
49
  }
50
50
  ```
51
51
 
52
- ### Writing Unit Tests for Steps
52
+ ### Writing unit tests for steps
53
53
 
54
54
  You can import and test step functions directly with Vitest. No special configuration or workflow plugin is needed:
55
55
 
@@ -77,18 +77,18 @@ describe("sendWelcomeEmail step", () => {
77
77
  This approach is ideal for verifying the business logic inside individual steps in isolation.
78
78
 
79
79
  <Callout type="info">
80
- Unit testing works well for individual steps. A simple workflow that only calls steps can also be unit tested this way, since `"use workflow"` is similarly a no-op without the compiler. However, any workflow that uses runtime features like [`sleep()`](/docs/api-reference/workflow/sleep), [hooks](/docs/foundations/hooks), or [webhooks](/docs/foundations/hooks#understanding-webhooks) cannot be unit tested directly because those APIs require the workflow runtime. Use [integration testing](#integration-testing-with-the-vitest-plugin) for testing entire workflows, especially those that depend on workflow-only features.
80
+ Unit testing works well for individual steps. A workflow that only calls steps can also be unit tested this way, since `"use workflow"` is similarly a no-op without the compiler. However, any workflow that uses runtime features like [`sleep()`](/docs/api-reference/workflow/sleep), [hooks](/docs/foundations/hooks), or [webhooks](/docs/foundations/hooks#understanding-webhooks) cannot be unit tested directly because those APIs require the workflow runtime. Use [integration testing](#integration-testing-with-the-vitest-plugin) for testing entire workflows, especially those that depend on workflow-only features.
81
81
  </Callout>
82
82
 
83
- ## Integration Testing with the Vitest Plugin
83
+ ## Integration testing with the Vitest plugin
84
84
 
85
- For workflows that rely on runtime features like [hooks](/docs/foundations/hooks), [webhooks](/docs/foundations/hooks#understanding-webhooks), [`sleep()`](/docs/api-reference/workflow/sleep), or error retries, you need to test against a real workflow setup. The `@workflow/vitest` plugin handles everything automatically it compiles your workflow directives, builds the runtime bundles, and executes workflows entirely in-process. No server required.
85
+ For workflows that rely on runtime features like [hooks](/docs/foundations/hooks), [webhooks](/docs/foundations/hooks#understanding-webhooks), [`sleep()`](/docs/api-reference/workflow/sleep), or error retries, you need to test against a real workflow setup. The `@workflow/vitest` plugin handles everything automatically: it compiles your workflow directives, builds the runtime bundles, and executes workflows entirely in-process. No server required.
86
86
 
87
87
  <Callout type="warn">
88
88
  `vi.mock()` and related calls do _not_ work inside workflow functions, only step functions. Your workflow functions cannot import third party code that needs to be mocked. Mocking works for npm packages imported in step functions. If something needs to be mocked, it likely belongs inside a step function either way.
89
89
  </Callout>
90
90
 
91
- ### Vitest Configuration
91
+ ### Vitest configuration
92
92
 
93
93
  Create a separate Vitest config for integration tests that includes the `workflow()` plugin:
94
94
 
@@ -109,15 +109,15 @@ That's it. The plugin automatically:
109
109
 
110
110
  1. Transforms `"use workflow"` and `"use step"` directives via SWC
111
111
  2. Builds workflow and step bundles before tests run
112
- 3. Sets up an in-process workflow runtime using a fresh [Local World](/docs/worlds/local) instance in each test worker all workflow data is cleared automatically between test files for full isolation
112
+ 3. Sets up an in-process workflow runtime using a fresh [Local World](/worlds/local) instance in each test worker; all workflow data is cleared automatically between test files for full isolation
113
113
 
114
114
  <Callout type="info">
115
115
  Use a separate Vitest configuration and a distinct file naming convention (e.g. `*.integration.test.ts`) to keep unit tests and integration tests separate. Unit tests run with a standard Vitest config without the workflow plugin, while integration tests use the config above.
116
116
  </Callout>
117
117
 
118
- ### Writing Integration Tests
118
+ ### Writing integration tests
119
119
 
120
- Use [`start()`](/docs/api-reference/workflow-api/start) to trigger a workflow and [`run.returnValue`](/docs/api-reference/workflow-api/start#returnvalue) to get the result. `returnValue` is a promise that blocks until the workflow completes (or throws if it fails):
120
+ Use [`start()`](/docs/api-reference/workflow-api/start) to trigger a workflow and [`run.returnValue`](/docs/api-reference/workflow-api/start#returns) to get the result. `returnValue` is a promise that blocks until the workflow completes (or throws if it fails):
121
121
 
122
122
  ```typescript title="workflows/calculate.integration.test.ts" lineNumbers
123
123
  import { describe, it, expect } from "vitest";
@@ -144,9 +144,9 @@ describe("calculateWorkflow", () => {
144
144
  });
145
145
  ```
146
146
 
147
- ### Testing Hooks and Waits
147
+ ### Testing hooks and waits
148
148
 
149
- The real power of integration testing comes when testing workflow-only features. Hooks and waits can be resumed programmatically using the [`workflow/api`](/docs/api-reference/workflow-api) functions, making it straightforward to simulate external events in your tests.
149
+ Integration testing is most useful for workflow-only features. You can resume hooks and waits programmatically using the [`workflow/api`](/docs/api-reference/workflow-api) functions to simulate external events in your tests.
150
150
 
151
151
  Given a workflow that waits for approval via a hook, then sleeps before publishing:
152
152
 
@@ -228,7 +228,7 @@ describe("approvalWorkflow", () => {
228
228
  reviewer: "bob",
229
229
  });
230
230
 
231
- // No wakeUp() needed here the rejected path has no sleep
231
+ // No wakeUp() needed here; the rejected path has no sleep
232
232
  const result = await run.returnValue;
233
233
  expect(result).toEqual({
234
234
  status: "rejected",
@@ -243,12 +243,12 @@ describe("approvalWorkflow", () => {
243
243
  </Callout>
244
244
 
245
245
  <Callout type="info">
246
- `waitForSleep()` returns the first **pending** sleep one that has a `wait_created` event but no corresponding `wait_completed` event. If your workflow has multiple parallel sleeps, `waitForSleep()` returns whichever is found first. After waking one, call `waitForSleep()` again to get the next pending one. For sequential sleeps, `waitForSleep()` naturally returns each one as the workflow reaches it.
246
+ `waitForSleep()` returns the first **pending** sleep, one that has a `wait_created` event but no corresponding `wait_completed` event. If your workflow has multiple parallel sleeps, `waitForSleep()` returns whichever is found first. After waking one, call `waitForSleep()` again to get the next pending one. For sequential sleeps, `waitForSleep()` naturally returns each one as the workflow reaches it.
247
247
  </Callout>
248
248
 
249
- ### Testing Webhooks
249
+ ### Testing webhooks
250
250
 
251
- Webhooks are hooks that receive HTTP `Request` objects. In tests, resume them using [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook) with a `Request` payload no HTTP server needed:
251
+ Webhooks are hooks that receive HTTP `Request` objects. In tests, resume them using [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook) with a `Request` payload, with no HTTP server needed:
252
252
 
253
253
  ```typescript title="workflows/ingest.ts" lineNumbers
254
254
  import { createWebhook } from "workflow";
@@ -256,7 +256,7 @@ import { createWebhook } from "workflow";
256
256
  export async function ingestWorkflow(endpointId: string) {
257
257
  "use workflow";
258
258
 
259
- // Webhook tokens are always randomly generated
259
+ // Webhook tokens are always generated for you
260
260
  using webhook = createWebhook(); // [!code highlight]
261
261
 
262
262
  const request = await webhook; // [!code highlight]
@@ -282,7 +282,7 @@ describe("ingestWorkflow", () => {
282
282
  it("should process webhook data", async () => {
283
283
  const run = await start(ingestWorkflow, ["ep-1"]);
284
284
 
285
- // Discover the randomly generated webhook token
285
+ // Discover the generated webhook token
286
286
  const hook = await waitForHook(run); // [!code highlight]
287
287
 
288
288
  // Resume the webhook with a Request object
@@ -303,7 +303,7 @@ describe("ingestWorkflow", () => {
303
303
  });
304
304
  ```
305
305
 
306
- ### Manual Setup
306
+ ### Manual setup
307
307
 
308
308
  If you need more control over the test lifecycle, the plugin also exports the individual setup functions:
309
309
 
@@ -354,11 +354,11 @@ afterAll(async () => {
354
354
  For advanced setups that require a running server (e.g. testing against your actual framework's HTTP layer), see [Server-based integration testing](/docs/testing/server-based).
355
355
  </Callout>
356
356
 
357
- ## Debugging Test Runs
357
+ ## Debugging test runs
358
358
 
359
- When integration tests fail, the [Workflow SDK CLI and Web UI](/docs/observability) can help you inspect what happened. Because integration tests persist workflow state locally, you can use the same observability tools you would use in development.
359
+ When integration tests fail, the [Workflow SDK CLI and web UI](/docs/observability) can help you inspect what happened. Because integration tests persist workflow state locally, you can use the same observability tools you use in development.
360
360
 
361
- Launch the Web UI to visually explore your test workflow runs:
361
+ Launch the web UI to explore your test workflow runs:
362
362
 
363
363
  ```bash
364
364
  npx workflow web
@@ -374,7 +374,7 @@ npx workflow inspect runs
374
374
  npx workflow inspect run <run-id>
375
375
  ```
376
376
 
377
- The Web UI shows each step, its inputs and outputs, retry attempts, hook state, and timing. This is especially useful for diagnosing issues with hooks that were not resumed, steps that failed unexpectedly, or workflows that timed out.
377
+ The web UI shows each step, its inputs and outputs, retry attempts, hook state, and timing. Use it to diagnose hooks that were not resumed, steps that failed unexpectedly, or workflows that timed out.
378
378
 
379
379
  ![Workflow SDK Web UI](/o11y-ui.png)
380
380
 
@@ -382,28 +382,28 @@ The Web UI shows each step, its inputs and outputs, retry attempts, hook state,
382
382
  See the [Observability](/docs/observability) docs for the full set of CLI commands and Web UI features.
383
383
  </Callout>
384
384
 
385
- ## Best Practices
385
+ ## Best practices
386
386
 
387
- ### Separate Unit and Integration Tests
387
+ ### Separate unit and integration tests
388
388
 
389
389
  Keep two test configurations:
390
390
 
391
- - **Unit tests** - Standard Vitest config, no workflow plugin. Fast, no infrastructure required.
392
- - **Integration tests** - Vitest config with `workflow()` plugin. Tests the full workflow lifecycle including hooks, sleeps, and retries.
391
+ - **Unit tests**: Standard Vitest config with no workflow plugin. These tests require no infrastructure.
392
+ - **Integration tests**: Vitest config with the `workflow()` plugin. These tests cover the full workflow lifecycle, including hooks, sleeps, and retries.
393
393
 
394
- ### Use Custom Hook Tokens for Deterministic Testing
394
+ ### Use custom hook tokens for deterministic testing
395
395
 
396
- When testing workflows with hooks, use [custom tokens](/docs/foundations/hooks#custom-tokens-for-deterministic-hooks) based on predictable values (like document IDs or test identifiers). This makes it easy to resume the correct hook in your test code.
396
+ When testing workflows with hooks, use [custom tokens](/docs/foundations/hooks#custom-tokens-for-deterministic-hooks) based on predictable values (like document IDs or test identifiers). This lets you resume the correct hook in your test code.
397
397
 
398
- ### Set Appropriate Timeouts
398
+ ### Set appropriate timeouts
399
399
 
400
400
  Workflows may take longer to execute than typical unit tests, especially when they involve multiple steps or retries. Set a generous `testTimeout` in your integration test config.
401
401
 
402
- ### Test Error and Retry Scenarios
402
+ ### Test error and retry scenarios
403
403
 
404
404
  Integration tests are the right place to verify that your workflows handle errors correctly, including retryable errors, fatal errors, and timeout scenarios.
405
405
 
406
- ## Further Reading
406
+ ## Further reading
407
407
 
408
408
  - [Hooks & Webhooks](/docs/foundations/hooks) - Pausing and resuming workflows with external data
409
409
  - [`start()` API Reference](/docs/api-reference/workflow-api/start) - Start workflows programmatically
@@ -9,9 +9,9 @@ The [Vitest plugin](/docs/testing#integration-testing-with-the-vitest-plugin) ru
9
9
  - Reproducing behavior that only occurs in a specific framework's runtime (e.g. Next.js, Nitro)
10
10
  - Testing webhook endpoints that receive real HTTP requests
11
11
 
12
- This guide shows how to set up integration tests that spawn a dev server as a sidecar process. The example below uses [Nitro](https://v3.nitro.build), but the same pattern works with any supported server framework. It is meant as a starting point customize the server setup to match your own deployment environment.
12
+ This guide shows how to set up integration tests that spawn a dev server as a sidecar process. The example below uses [Nitro](https://v3.nitro.build), but the same pattern works with any supported server framework. It is meant as a starting point; customize the server setup to match your own deployment environment.
13
13
 
14
- ## Vitest Configuration
14
+ ## Vitest configuration
15
15
 
16
16
  Create a Vitest config with the `workflow()` Vite plugin for code transforms and a `globalSetup` script that manages the server lifecycle:
17
17
 
@@ -36,7 +36,7 @@ export default defineConfig({
36
36
  Note the import path: `workflow/vite` (not `@workflow/vitest`). The Vite plugin handles code transforms but does not set up in-process execution. The server handles workflow execution instead.
37
37
  </Callout>
38
38
 
39
- ## Global Setup Script
39
+ ## Global setup script
40
40
 
41
41
  The `globalSetup` script starts a dev server before tests run and tears it down afterwards. This example uses [Nitro](https://v3.nitro.build), but you can use any server framework that supports the workflow runtime.
42
42
 
@@ -148,15 +148,15 @@ export async function teardown() { // [!code highlight]
148
148
 
149
149
  These JSON log lines are intentional. They give CI jobs, local tooling, and agents stable events to watch for (`server_starting`, `server_stdout`, `server_stderr`, `server_ready`, `server_exit`), and the thrown timeout error includes the command, expected `WORKFLOW_LOCAL_BASE_URL`, and buffered stdout/stderr so a failed setup is actionable without interactive debugging.
150
150
 
151
- The setup script sets `WORKFLOW_LOCAL_BASE_URL` so the workflow runtime sends step execution requests to the running server.
151
+ The setup script sets `WORKFLOW_LOCAL_BASE_URL` so the workflow runtime sends flow-route requests (workflow orchestration and step execution) to the running server.
152
152
 
153
153
  <Callout type="info">
154
154
  You can use any server framework that supports the workflow runtime. The example above uses [Nitro](https://v3.nitro.build), but you could also use [Next.js](https://nextjs.org), [Hono](https://hono.dev), or any other supported server.
155
155
  </Callout>
156
156
 
157
- ## Writing Tests
157
+ ## Writing tests
158
158
 
159
- Tests are written the same way as [in-process integration tests](/docs/testing#writing-integration-tests). You can use the same programmatic APIs [`start()`](/docs/api-reference/workflow-api/start), [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook), [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook), and [`getRun().wakeUp()`](/docs/api-reference/workflow-api/get-run) to control workflow execution:
159
+ Tests are written the same way as [in-process integration tests](/docs/testing#writing-integration-tests). You can use the same programmatic APIs ([`start()`](/docs/api-reference/workflow-api/start), [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook), [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook), and [`getRun().wakeUp()`](/docs/api-reference/workflow-api/get-run)) to control workflow execution:
160
160
 
161
161
  ```typescript title="workflows/calculate.server.test.ts" lineNumbers
162
162
  import { describe, it, expect } from "vitest";
@@ -199,10 +199,10 @@ describe("approvalWorkflow", () => {
199
199
  ```
200
200
 
201
201
  <Callout type="info">
202
- In server-based tests, the `waitForSleep()` and `waitForHook()` helpers from `@workflow/vitest` are not available since there is no in-process world. Instead, use the programmatic APIs directly you may need to add short delays or polling to ensure the workflow has reached the desired state before resuming.
202
+ In server-based tests, the `waitForSleep()` and `waitForHook()` helpers from `@workflow/vitest` are not available since there is no in-process world. Instead, use the programmatic APIs directly. You may need to add short delays or polling to ensure the workflow has reached the desired state before resuming.
203
203
  </Callout>
204
204
 
205
- ## Running Tests
205
+ ## Running tests
206
206
 
207
207
  Add a script to your `package.json`:
208
208
 
@@ -215,12 +215,12 @@ Add a script to your `package.json`:
215
215
  }
216
216
  ```
217
217
 
218
- ## When to Use This Approach
218
+ ## When to use this approach
219
219
 
220
220
  | Scenario | Recommended approach |
221
221
  | --- | --- |
222
222
  | Testing workflow logic, steps, hooks, retries | [In-process plugin](/docs/testing) |
223
223
  | Testing HTTP middleware or authentication | Server-based |
224
224
  | Testing webhook endpoints with real HTTP | Server-based |
225
- | CI/CD pipeline testing | [In-process plugin](/docs/testing) |
225
+ | Continuous integration and continuous delivery (CI/CD) pipeline testing | [In-process plugin](/docs/testing) |
226
226
  | Reproducing framework-specific behavior | Server-based |
@@ -0,0 +1,186 @@
1
+ ---
2
+ title: What's new in v5?
3
+ description: Workflow SDK 5.0 highlights, breaking changes, and how to upgrade from 4.x.
4
+ type: guide
5
+ summary: See what changed in Workflow SDK 5.0 and how to move an app from 4.x.
6
+ related:
7
+ - /docs/getting-started
8
+ - /docs/configuration
9
+ - /docs/foundations/cancellation
10
+ ---
11
+
12
+ We recommend upgrading to v5 to get all of the performance, cost, and feature improvements listed below. Install the migration skill, then tell your agent to migrate your app from Workflow SDK v4 to v5.
13
+
14
+ ```bash
15
+ npm install workflow@latest
16
+ npx skills add https://github.com/vercel/workflow --skill migrating-workflow-v4-to-v5
17
+ ```
18
+
19
+ <CopyPrompt text="Upgrade this app from Workflow SDK v4 to v5 using the migrating-workflow-v4-to-v5 skill. Bump `workflow` and every `@workflow/*` dependency to ^5.0.0, apply the skill's mechanical rewrites, and report the behavior changes it flags instead of silently changing them. Then verify: reinstall, rebuild, typecheck, run the test suite, and execute one workflow end to end." />
20
+
21
+ <Callout type="info">
22
+ Workflow SDK v4 remains installable as `workflow@4` and receives stability
23
+ fixes. Switch to its documentation with the version picker in the sidebar.
24
+ </Callout>
25
+
26
+ ## Highlights
27
+
28
+ ### Faster and cheaper runs
29
+
30
+ The largest change in v5 has no API surface: the runtime does far less work per unit of progress. The time between calling `start()` and your first step body executing is now less than half of what it was in v4. This is made possible by many smaller optimizations:
31
+
32
+ **A workflow invocation now does as much as it can in a single pass.** In 4.x, progress was largely deferred to the queue: an invocation would execute a step, hand back to the queue, and let a fresh invocation pick up the next one. v5 creates and executes several steps inline per suspension, in parallel, and only uses the queue for a wait, a hook, or when the function approaches its timeout.
33
+
34
+ **The runtime avoids waiting on the persistence layer where it can determine that it is safe for your workload.** The runtime skips many API calls when they aren't needed, such as requesting the event log on a run's first invocation. Step creation is folded into step execution rather than being its own round trip. The inline loop consumes the event-log delta from the previous step's write instead of re-listing events. Each optimization is gated on specific runtime conditions and can be turned off individually. See [Runtime tuning](/docs/configuration/runtime-tuning).
35
+
36
+ **The workflow VM is kept alive across inline steps.** Within one invocation, a step- or attribute-driven suspension keeps the live VM and hydrated state, including when hooks or waits are open or created at the same boundary, so the next iteration appends only the newly written events instead of rebuilding the sandbox and replaying the whole log. Step inputs made of plain data or standard built-ins keep this fast path; see [`WORKFLOW_RETAINED_VM`](/docs/configuration/runtime-tuning#workflow_retained_vm).
37
+
38
+ **Resuming a hook takes one round trip instead of two.** `resumeHook()` writes the `hook_received` event and dispatches the queue message concurrently, with a `(runId, resumeId)` dedup constraint keeping the two writers converging on exactly one event. See [Resilient hook resumption](/docs/changelog/resilient-resume).
39
+
40
+ **Payloads are compressed** before they are encrypted and sent to the API. Repetitive payloads compress heavily; AI token streams average around 80% smaller. That is less stored data and less to move over the network.
41
+
42
+ On [Vercel Workflows](/worlds/vercel) these benefits compound to reduce compute costs by up to 80% for workflows made of many small steps, and storage cost by up to 70%, depending on the workload. Depending on your setup, you may see similar gains using self-hosted or third-party Worlds.
43
+
44
+ How much work fits in one invocation now scales with the function's own limit: the inline budget is derived from the runtime deadline the World reports, so raising `maxDuration` widens it without configuration. See [`WORKFLOW_V2_TIMEOUT_MS`](/docs/configuration/runtime-tuning#workflow_v2_timeout_ms).
45
+
46
+ ### Multi-region support
47
+
48
+ Workflow run IDs are now tagged with the compute region they were created in, or the region specified when calling `start()`. Worlds can use this to pin execution to that region.
49
+ This is now the default when deploying to Vercel, and run data will automatically be co-located with the compute region that the run was started in.
50
+
51
+ See [Multi-region on the Vercel World](/worlds/vercel#multi-region) for how automatic pinning works and how to [select a region explicitly](/worlds/vercel#explicit-region-selection), and [Building a World](/worlds/building-a-world) for adding regional placement to a custom World.
52
+
53
+ ### Inflight cancellation
54
+
55
+ `AbortController` and `AbortSignal` are now serializable, and signals can be passed into steps, so a workflow can abandon in-flight work without waiting for a step to finish.
56
+
57
+ ```typescript lineNumbers
58
+ import { sleep } from "workflow";
59
+
60
+ export async function raceWithTimeout() {
61
+ "use workflow";
62
+
63
+ const controller = new AbortController(); // [!code highlight]
64
+
65
+ const result = await Promise.race([
66
+ fetchData(controller.signal), // [!code highlight]
67
+ sleep("10s").then(() => null),
68
+ ]);
69
+
70
+ if (result === null) {
71
+ controller.abort(); // [!code highlight]
72
+ }
73
+
74
+ return result;
75
+ }
76
+
77
+ async function fetchData(signal: AbortSignal) {
78
+ "use step";
79
+ const response = await fetch("https://api.example.com/data", { signal });
80
+ return response.json();
81
+ }
82
+ ```
83
+
84
+ The abort reaches a step that is already executing, not just the next one to start, so a `fetch()` in flight when the controller aborts is torn down. Cancellation stays cooperative: a step that ignores its signal still runs to completion.
85
+
86
+ Cancelling from the outside gained detail too: [`run.cancel()`](/docs/api-reference/workflow-api/get-run) accepts a `cancelReason`, recorded on the cancellation event and shown in the run detail view. And cancellation now works in bulk: `workflow cancel` takes `--status pending|running` and `--workflowName` to cancel a batch in one operation, and the runs table in the web UI cancels every selected run in a single request. See [CLI and web UI](/docs/configuration/cli-and-web-ui).
87
+
88
+ See [Cancellation](/docs/foundations/cancellation), [How cancellation works](/docs/how-it-works/cancellation), and the [in-flight cancellation changelog](https://vercel.com/changelog/workflow-sdk-now-supports-inflight-cancellation).
89
+
90
+ ### Run attributes
91
+
92
+ Attach string metadata to a run with [`setAttributes()`](/docs/api-reference/workflow/set-attributes), or seed it at creation with the `attributes` option of [`start()`](/docs/api-reference/workflow-api/start), then search and filter runs by `key=value`. Attributes were available in beta and are generally available in v5 under their final names: the `experimental_setAttributes` alias is gone.
93
+
94
+ Some attributes are set for you: a run started from inside another workflow or step is automatically tagged with the reserved `$parentRunId` and `$rootRunId` attributes, so a whole chain or fan-out of related runs can be found with a single attribute filter.
95
+
96
+ Vercel Observability can search runs by attribute. `workflow inspect` and the local web UI read them from `world.analytics` on any World that implements it.
97
+
98
+ See [Attributes](/docs/observability/attributes).
99
+
100
+ ### Richer serialization
101
+
102
+ Everything that crosses a workflow/step boundary is serialized, and v5 widens what survives the trip with its identity intact. Errors, including your own classes and built-ins like `TypeError`, keep their class and `cause` chain through `WorkflowRunFailedError.cause`:
103
+
104
+ ```typescript lineNumbers
105
+ import { WorkflowRunFailedError } from "workflow/errors";
106
+ import { getRun } from "workflow/api";
107
+ declare class PaymentDeclinedError extends Error {} // @setup
108
+
109
+ try {
110
+ await getRun(runId).returnValue;
111
+ } catch (error) {
112
+ if (
113
+ error instanceof WorkflowRunFailedError &&
114
+ error.cause instanceof PaymentDeclinedError // [!code highlight]
115
+ ) {
116
+ // your class, not a flattened generic Error
117
+ }
118
+ }
119
+ ```
120
+
121
+ Workflow function references and [`Run`](/docs/api-reference/workflow-api/get-run) handles are serializable too, so a step can receive a workflow function to `start()` or a run handle to await. And when a value cannot cross a boundary, the failure is precise instead of generic: dedicated [`SerializationError`](/docs/api-reference/workflow-errors) and structured context-violation errors name the offending value and where it was used.
122
+
123
+ See [Serialization](/docs/foundations/serialization).
124
+
125
+ ### A redesigned trace viewer
126
+
127
+ The trace viewer has been rebuilt with a visible timeline, a minimap, pan, zoom, debug functionality, a new JSON viewer, keyboard navigation, and more.
128
+
129
+ Vercel Observability uses this trace viewer for all runs, v4 included, but with v5, you get the same new design for self-hosted UI and local debugging. See [Observability](/docs/observability).
130
+
131
+ The local tooling around it grew as well: on [Nitro](/docs/getting-started/nitro) the dev server has the web UI built in at `/_workflow`, `workflow inspect runs` accepts `--since`/`--until` listing windows, run lookups by name search past the backend's default 24-hour window, and Worlds can surface their own run fields in `inspect` output. For example, the Vercel World shows each run's region. On any other framework, `createWorkflowWebHandler()` from `@workflow/web/handler` serves the same UI as one `Request` to `Response` handler under a base path of your choosing.
132
+
133
+ Each run also carries more of the infrastructure it ran on. A step attempt records the compute instance that executed it, surfaced as **Compute Instance ID** in the run sidebar and as a `faas.instance` span attribute on flow and step spans, so a run that behaves oddly can be correlated with one warm instance. The sidebar also shows a copyable **Request ID** for looking the invocation up in your platform's logs.
134
+
135
+ ### Custom hook token retention
136
+
137
+ A [Hook](/docs/foundations/hooks) token is normally reserved only while its workflow is running. Pass `experimental_minRetention` to [`createHook()`](/docs/api-reference/workflow/create-hook#keep-a-token-unavailable-after-the-run-ends) to keep the token unavailable for at least a given duration after the Hook is created, so a duplicate request arriving after the original run finished still collides instead of starting fresh work. This allows for durable [run idempotency](/docs/foundations/idempotency#run-idempotency) with custom durations.
138
+
139
+ All three first-party Worlds now implement it: Vercel accepts up to 30 days, and the Local and Postgres Worlds default to the same ceiling (see [`WORKFLOW_LOCAL_HOOK_RETENTION_LIMIT_DAYS`](/docs/configuration/worlds) and its Postgres equivalent). A retained Hook remains readable with [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) after its run ends, but cannot be resumed.
140
+
141
+ ### Also new in 5.0
142
+
143
+ - **`start()` from inside a workflow.** Spawn a child run or hand off to a new run directly in a workflow function, without wrapping it in a step. See [Starting workflows](/docs/foundations/starting-workflows).
144
+ - **Stronger hook coordination.** `hook.getConflict()` resolves with the conflicting [`Run`](/docs/api-reference/workflow-api/get-run) rather than a bare `{ runId }`, so a duplicate can `await conflict.status`, `await conflict.returnValue`, or `await conflict.cancel()` directly. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
145
+ - **Support for more frameworks.** [React Router](/docs/getting-started/react-router) (v7 and v8, via Nitro) and [NestJS](/docs/getting-started/nestjs) are now supported.
146
+ - **A misrouted delivery no longer fails a run.** Runs are pinned to the deployment that created them. A delivery that arrives at a different deployment is now re-routed to the pinned one with backoff instead of failing, and only gives up with the new [`DEPLOYMENT_MISMATCH`](/docs/errors/deployment-mismatch) error once the recovery budget is spent. Nothing executes on the wrong deployment while this happens. In 4.x the same situation surfaced as an unexplained decryption failure.
147
+ - **A run cannot be forked across environments.** `start()` stamps the environment it was called from onto the queue message, and a deployment refuses a delivery whose run was created in a different environment. Previously a preview client and a production deployment could each hold half of one run ID.
148
+ - **An experimental QuickJS VM engine.** Set [`WORKFLOW_VM=quickjs`](/docs/configuration/runtime-tuning#workflow_vm) to run workflow functions in a QuickJS VM compiled to WebAssembly instead of `node:vm`, for platforms that do not provide `node:vm`. Replay semantics are identical, but the available globals are not: check the differences before switching an existing deployment.
149
+ - **An opt-in WebSocket transport for event writes** on the Vercel World, via [`WORKFLOW_EVENTS_TRANSPORT=ws`](/docs/configuration/worlds). HTTP remains the default.
150
+ - **An event arriving mid-replay no longer fails the run.** A hook resume or step completion landing while a replay is in flight used to be able to fail it with [`CORRUPTED_EVENT_LOG`](/docs/errors/corrupted-event-log). Writes now come back with the events the replay had not seen, and the event is held for whichever part of the workflow awaits it. A run only fails when the log is genuinely missing a position.
151
+
152
+ ## Breaking changes
153
+
154
+ ### Application code
155
+
156
+ | Change | What to do |
157
+ | --- | --- |
158
+ | `runStep` removed from `workflow/api` | Call your step function directly; the compiler routes it through the step runtime. |
159
+ | `hook.getConflict()` resolves with a `Run` | Replace `conflict.runId` round trips through `getRun()` inside a step with the accessors on `conflict` directly. `conflict.runId` still works. |
160
+ | `hook.metadata` is a Promise on hooks returned by `getHookByToken()` and `resumeHook()` | Write `await hook.metadata` where you read it. Hydrating metadata can add network round trips, and those are now paid only by code that reads it, so the lookup itself is a single read. The `Hook` type is exported from `workflow/api`; the World-level record from `world.hooks.getByToken()` is unchanged. See [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token). |
161
+ | `experimental_setAttributes` removed | Import `setAttributes` instead, and `SetAttributesOptions` in place of `ExperimentalSetAttributesOptions`. The deprecated aliases are gone. |
162
+ | [`getWorld()`](/docs/api-reference/workflow-runtime/get-world), [`getWorldHandlers()`](/docs/api-reference/workflow-runtime/get-world-handlers), and [`createWorld()`](/docs/api-reference/workflow-runtime/create-world) are async | They resolve a `Promise` now, so await the call before reaching for anything on it: `const world = await getWorld();` then `await world.start?.();`. This mainly affects the `instrumentation.ts` bootstrap that starts a World with background workers, such as the [Postgres World](/worlds/postgres#starting-the-world). Under TypeScript the old shape fails the build; in plain JavaScript it does not, and `.start` reads as `undefined` on a promise, so the worker never starts and nothing is logged. Writing `await getWorld()` is also valid on 4.x, so the change can be made before upgrading. |
163
+ | Duplicate step or workflow IDs fail the build | 4.x resolved collisions across non-exported workspace files last-write-wins. If you start encountering build failures after upgrading, rename the colliding functions. |
164
+ | The generated bundles are renamed, and the step route is gone | Only affects apps that wire the output of `workflow build` into their own server instead of using a framework integration. `flow.js` and `webhook.js` are now `flow.mjs` and `webhook.mjs` with named exports only, so a default import resolves to `undefined`. `step.js` became `__step_registrations.mjs`, an internal module that `flow.mjs` imports: delete the `POST /.well-known/workflow/v1/step` route rather than repointing it, because the flow handler now serves step deliveries too. See [Framework integrations](/docs/how-it-works/framework-integrations). |
165
+ | Default trace mode is `linked` | Update dashboards that assume one trace per run, or set `WORKFLOW_TRACE_MODE=continuous`. |
166
+ | The event-creation precondition guard is gone | `WORKFLOW_PRECONDITION_GUARD` no longer exists, and no World in the SDK rejects a write for a stale snapshot. Remove the variable if you set it. A replay that is behind now learns what it missed from the write it makes next instead of from a rejection, and [`PreconditionFailedError`](/docs/api-reference/workflow-errors/precondition-failed-error) remains only for a custom World that would still rather refuse. |
167
+ | Event IDs are slot numbers, not ULIDs | An event ID is now its 1-based position in the run's log (`evnt_00000000000000000000000042`). It is unique only within a run, so pair it with the `runId` as a key, and it carries no timestamp: decoding one yields the Unix epoch rather than a creation time, so read `createdAt` off the event instead. Other entity IDs are unchanged. See [Event IDs](/docs/how-it-works/event-sourcing#event-ids). |
168
+ | A per-run event limit is enforced | The World supplies the ceiling, which is 25,000 events on the Local and Vercel Worlds. A run that reaches it fails with `MAX_EVENTS_EXCEEDED`. Split unbounded loops into [child workflows](/cookbook/advanced/child-workflows). As a fallback, you can tune the ceiling. See [Limits](/docs/configuration/runtime-tuning#limits). |
169
+ | Stream writes flush the first chunk immediately | The leading-edge flush window defaults to `0` instead of 10ms. Restore a window with `streamFlushIntervalMs` or `WORKFLOW_STREAM_FLUSH_INTERVAL_MS`. |
170
+ | The workflow sandbox is stricter about nondeterminism | `WeakRef`, `FinalizationRegistry`, `Atomics.waitAsync`, and async `WebAssembly` compilation are no longer available inside workflow functions, and `crypto.subtle.digest` computes synchronously (same results, deterministic timing). Move code that needs them into a step. |
171
+ | `Date()` without `new` returns a string inside workflow functions | This matches the language spec, and 4.x returned a `Date` object. Use `new Date()` where you need the object. Subclassing `Date` now works, so libraries like `TZDate` keep their identity across the sandbox boundary. |
172
+ | `NestLocalBuilder` moved out of `@workflow/nest` root | Import it from `workflow/nest/builder`, so `WorkflowModule` no longer pulls the build toolchain into the runtime bundle. `NestVercelBuilder` lives at `workflow/nest/vercel-builder`. |
173
+ | `workflow/internal/private` and `@workflow/core/private` removed | These were never public API. The compiler no longer emits imports from them, so regenerate build output rather than importing them yourself. |
174
+ | The legacy trace viewer is gone from `@workflow/web-shared` | Only affects apps embedding the observability UI. `RunTraceView` and `WorkflowTraceViewer` are removed, and `NewTraceViewer` is now `TraceViewer` (module path `trace-viewer`). `Span`, `SpanEvent`, and `Trace` are still exported from the package root. |
175
+
176
+ Runs created on 4.x keep executing on the deployment that created them, so upgrading a deployment does not migrate in-flight runs. One storage caveat is worth knowing about: failed runs stored by `@workflow/world-postgres` before the upgrade read back with `error: undefined`, because the payload lives in the legacy `error` text column rather than `errorJson`.
177
+
178
+ ## If you maintain a World
179
+
180
+ The World interface, which defines the storage, queue, streaming, and analytics contract that a Workflow SDK deployment runs against, also changed in v5. Those changes are not visible from application code. If you implement `World` yourself or maintain a build integration that compiles workflow files, upgrade it alongside the SDK. See [Upgrading a World to v5](/worlds/upgrading-to-v5) for the full interface delta and the contract changes that affect existing implementations. There is a separate skill for that job because none of it applies to application code:
181
+
182
+ ```bash
183
+ npx skills add https://github.com/vercel/workflow --skill migrating-world-v4-to-v5
184
+ ```
185
+
186
+ Applications on the [Vercel](/worlds/vercel), [Local](/worlds/local), and [Postgres](/worlds/postgres) Worlds need nothing from that page: those implementations ship with the SDK and are already on the v5 spec.