workflow 5.0.0-beta.9 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/README.md +68 -23
  2. package/dist/api-workflow.d.ts +3 -1
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +2 -1
  5. package/dist/api.d.ts +5 -4
  6. package/dist/api.d.ts.map +1 -1
  7. package/dist/api.js +6 -7
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -1
  11. package/dist/internal/builtins.d.ts +4 -4
  12. package/dist/internal/builtins.js +6 -6
  13. package/dist/internal/errors.d.ts +1 -1
  14. package/dist/internal/errors.d.ts.map +1 -1
  15. package/dist/internal/errors.js +2 -2
  16. package/dist/nest-builder.d.ts +2 -0
  17. package/dist/nest-builder.d.ts.map +1 -0
  18. package/dist/nest-builder.js +2 -0
  19. package/dist/nest-vercel-builder.d.ts +2 -0
  20. package/dist/nest-vercel-builder.d.ts.map +1 -0
  21. package/dist/nest-vercel-builder.js +2 -0
  22. package/dist/runtime.d.ts +2 -1
  23. package/dist/runtime.d.ts.map +1 -1
  24. package/dist/runtime.js +4 -1
  25. package/docs/advanced/dynamic-workflows.mdx +227 -0
  26. package/docs/advanced/index.mdx +13 -0
  27. package/docs/advanced/meta.json +5 -0
  28. package/docs/ai/chat-session-modeling.mdx +176 -422
  29. package/docs/ai/defining-tools.mdx +6 -7
  30. package/docs/ai/human-in-the-loop.mdx +16 -12
  31. package/docs/ai/index.mdx +67 -72
  32. package/docs/ai/message-queueing.mdx +71 -110
  33. package/docs/ai/meta.json +1 -0
  34. package/docs/ai/resumable-streams.mdx +40 -28
  35. package/docs/ai/sleep-and-delays.mdx +10 -10
  36. package/docs/ai/streaming-updates-from-tools.mdx +6 -6
  37. package/docs/api-reference/index.mdx +25 -1
  38. package/docs/api-reference/meta.json +8 -0
  39. package/docs/api-reference/vitest/index.mdx +68 -15
  40. package/docs/api-reference/workflow/create-hook.mdx +170 -10
  41. package/docs/api-reference/workflow/create-webhook.mdx +16 -15
  42. package/docs/api-reference/workflow/define-hook.mdx +41 -33
  43. package/docs/api-reference/workflow/fatal-error.mdx +30 -8
  44. package/docs/api-reference/workflow/fetch.mdx +14 -10
  45. package/docs/api-reference/workflow/get-step-metadata.mdx +2 -2
  46. package/docs/api-reference/workflow/get-workflow-metadata.mdx +3 -3
  47. package/docs/api-reference/workflow/get-writable.mdx +7 -7
  48. package/docs/api-reference/workflow/index.mdx +3 -3
  49. package/docs/api-reference/workflow/retryable-error.mdx +1 -1
  50. package/docs/api-reference/workflow/set-attributes.mdx +63 -0
  51. package/docs/api-reference/workflow/sleep.mdx +4 -4
  52. package/docs/api-reference/workflow-ai/durable-agent.mdx +63 -101
  53. package/docs/api-reference/workflow-ai/index.mdx +5 -5
  54. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +67 -24
  55. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +37 -15
  56. package/docs/api-reference/workflow-api/get-run.mdx +43 -8
  57. package/docs/api-reference/workflow-api/index.mdx +8 -9
  58. package/docs/api-reference/workflow-api/register-lifecycle-hooks.mdx +87 -0
  59. package/docs/api-reference/workflow-api/resume-hook.mdx +77 -12
  60. package/docs/api-reference/workflow-api/resume-webhook.mdx +15 -9
  61. package/docs/api-reference/workflow-api/start.mdx +107 -12
  62. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  63. package/docs/api-reference/workflow-astro/meta.json +4 -0
  64. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  65. package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +4 -4
  66. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  67. package/docs/api-reference/workflow-errors/hook-force-claimed-error.mdx +70 -0
  68. package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
  69. package/docs/api-reference/workflow-errors/index.mdx +91 -0
  70. package/docs/api-reference/workflow-errors/meta.json +7 -0
  71. package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -0
  72. package/docs/api-reference/workflow-errors/run-expired-error.mdx +2 -2
  73. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  74. package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +5 -5
  75. package/docs/api-reference/workflow-errors/throttle-error.mdx +2 -2
  76. package/docs/api-reference/workflow-errors/too-early-error.mdx +2 -2
  77. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  78. package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +5 -6
  79. package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +13 -6
  80. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +12 -5
  81. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  82. package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +4 -4
  83. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  84. package/docs/api-reference/workflow-errors/workflow-world-error.mdx +8 -8
  85. package/docs/api-reference/workflow-globals.mdx +19 -11
  86. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +37 -0
  87. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  88. package/docs/api-reference/workflow-nest/meta.json +9 -0
  89. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  90. package/docs/api-reference/workflow-nest/workflow-controller.mdx +46 -0
  91. package/docs/api-reference/workflow-nest/workflow-module.mdx +134 -0
  92. package/docs/api-reference/workflow-next/with-workflow.mdx +39 -17
  93. package/docs/api-reference/workflow-nitro/index.mdx +60 -0
  94. package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
  95. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  96. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  97. package/docs/api-reference/workflow-observability/index.mdx +62 -0
  98. package/docs/api-reference/workflow-observability/meta.json +11 -0
  99. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  100. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  101. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  102. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  103. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  104. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  105. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +11 -14
  106. package/docs/api-reference/workflow-runtime/health-check.mdx +51 -0
  107. package/docs/api-reference/workflow-runtime/index.mdx +41 -0
  108. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  109. package/docs/api-reference/workflow-runtime/set-world.mdx +51 -0
  110. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +43 -0
  111. package/docs/api-reference/workflow-runtime/world/analytics.mdx +315 -0
  112. package/docs/api-reference/workflow-runtime/world/index.mdx +60 -0
  113. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  114. package/docs/api-reference/workflow-runtime/world/queue.mdx +88 -0
  115. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +133 -35
  116. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +8 -8
  117. package/docs/api-reference/workflow-serde/index.mdx +1 -2
  118. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +3 -4
  119. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +8 -8
  120. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  121. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  122. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  123. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  124. package/docs/api-reference/workflow-vite/meta.json +4 -0
  125. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  126. package/docs/changelog/attributes-mvp.mdx +53 -41
  127. package/docs/changelog/batched-event-writes.mdx +79 -0
  128. package/docs/changelog/eager-processing.mdx +110 -436
  129. package/docs/changelog/index.mdx +4 -2
  130. package/docs/changelog/lazy-event-creation.md +127 -0
  131. package/docs/changelog/lazy-hook-resume.mdx +78 -0
  132. package/docs/changelog/meta.json +11 -1
  133. package/docs/changelog/resilient-resume.mdx +32 -0
  134. package/docs/changelog/resilient-start.mdx +33 -285
  135. package/docs/changelog/step-message-ownership.mdx +360 -0
  136. package/docs/changelog/turbo-mode.md +87 -0
  137. package/docs/comparisons/index.mdx +66 -0
  138. package/docs/comparisons/meta.json +11 -0
  139. package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
  140. package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
  141. package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
  142. package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
  143. package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
  144. package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +104 -0
  145. package/docs/configuration/build-and-diagnostics.mdx +89 -0
  146. package/docs/configuration/cli-and-web-ui.mdx +241 -0
  147. package/docs/configuration/framework-options.mdx +165 -0
  148. package/docs/configuration/index.mdx +32 -0
  149. package/docs/configuration/meta.json +12 -0
  150. package/docs/configuration/runtime-tuning.mdx +424 -0
  151. package/docs/configuration/worlds.mdx +341 -0
  152. package/docs/cookbook/advanced/child-workflows.mdx +33 -25
  153. package/docs/cookbook/advanced/publishing-libraries.mdx +65 -56
  154. package/docs/cookbook/advanced/serializable-steps.mdx +48 -68
  155. package/docs/cookbook/advanced/upgrading-workflows.mdx +35 -31
  156. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +78 -60
  157. package/docs/cookbook/agent-patterns/durable-agent.mdx +23 -135
  158. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +180 -195
  159. package/docs/cookbook/common-patterns/batching.mdx +20 -14
  160. package/docs/cookbook/common-patterns/idempotency.mdx +41 -53
  161. package/docs/cookbook/common-patterns/rate-limiting.mdx +8 -4
  162. package/docs/cookbook/common-patterns/saga.mdx +23 -19
  163. package/docs/cookbook/common-patterns/scheduling.mdx +30 -22
  164. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +29 -25
  165. package/docs/cookbook/common-patterns/timeouts.mdx +26 -21
  166. package/docs/cookbook/common-patterns/webhooks.mdx +12 -7
  167. package/docs/cookbook/common-patterns/workflow-composition.mdx +27 -17
  168. package/docs/cookbook/index.mdx +22 -22
  169. package/docs/cookbook/integrations/ai-sdk.mdx +63 -48
  170. package/docs/cookbook/integrations/chat-sdk.mdx +46 -33
  171. package/docs/cookbook/integrations/sandbox.mdx +58 -45
  172. package/docs/deploying.mdx +106 -0
  173. package/docs/errors/abort-signal-timeout-in-workflow.mdx +16 -12
  174. package/docs/errors/corrupted-event-log.mdx +39 -18
  175. package/docs/errors/deployment-mismatch.mdx +71 -0
  176. package/docs/errors/fetch-in-workflow.mdx +15 -14
  177. package/docs/errors/hook-conflict.mdx +38 -11
  178. package/docs/errors/hook-force-claimed.mdx +96 -0
  179. package/docs/errors/index.mdx +24 -37
  180. package/docs/errors/node-js-module-in-workflow.mdx +9 -5
  181. package/docs/errors/replay-divergence.mdx +27 -0
  182. package/docs/errors/run-expired.mdx +85 -0
  183. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  184. package/docs/errors/serialization-failed.mdx +44 -12
  185. package/docs/errors/start-invalid-workflow-function.mdx +9 -5
  186. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  187. package/docs/errors/step-not-registered.mdx +6 -6
  188. package/docs/errors/timeout-in-workflow.mdx +12 -8
  189. package/docs/errors/webhook-invalid-respond-with-value.mdx +18 -18
  190. package/docs/errors/webhook-response-not-sent.mdx +20 -16
  191. package/docs/errors/workflow-not-registered.mdx +5 -5
  192. package/docs/foundations/cancellation.mdx +31 -32
  193. package/docs/foundations/errors-and-retries.mdx +54 -11
  194. package/docs/foundations/hooks.mdx +187 -36
  195. package/docs/foundations/idempotency.mdx +267 -12
  196. package/docs/foundations/index.mdx +1 -26
  197. package/docs/foundations/serialization.mdx +22 -22
  198. package/docs/foundations/starting-workflows.mdx +104 -30
  199. package/docs/foundations/streaming.mdx +108 -60
  200. package/docs/foundations/versioning.mdx +4 -4
  201. package/docs/foundations/workflows-and-steps.mdx +10 -10
  202. package/docs/getting-started/astro.mdx +22 -18
  203. package/docs/getting-started/express.mdx +15 -11
  204. package/docs/getting-started/fastify.mdx +15 -11
  205. package/docs/getting-started/hono.mdx +15 -11
  206. package/docs/getting-started/index.mdx +10 -3
  207. package/docs/getting-started/meta.json +3 -1
  208. package/docs/getting-started/nestjs.mdx +264 -21
  209. package/docs/getting-started/next.mdx +18 -14
  210. package/docs/getting-started/nitro.mdx +22 -18
  211. package/docs/getting-started/nuxt.mdx +15 -11
  212. package/docs/getting-started/python.mdx +190 -41
  213. package/docs/getting-started/react-router/index.mdx +33 -0
  214. package/docs/getting-started/react-router/meta.json +5 -0
  215. package/docs/getting-started/react-router/v7.mdx +237 -0
  216. package/docs/getting-started/react-router/v8.mdx +232 -0
  217. package/docs/getting-started/sveltekit.mdx +20 -16
  218. package/docs/getting-started/tanstack-start.mdx +17 -13
  219. package/docs/getting-started/vite.mdx +15 -11
  220. package/docs/how-it-works/cancellation.mdx +63 -63
  221. package/docs/how-it-works/code-transform.mdx +82 -66
  222. package/docs/how-it-works/encryption.mdx +30 -26
  223. package/docs/how-it-works/event-sourcing.mdx +132 -35
  224. package/docs/how-it-works/framework-integrations.mdx +96 -337
  225. package/docs/how-it-works/understanding-directives.mdx +22 -22
  226. package/docs/internal/index.mdx +6 -4
  227. package/docs/internal/meta.json +6 -1
  228. package/docs/internal/nitro-native-build.mdx +38 -0
  229. package/docs/internal/nitro-web-ui.mdx +24 -0
  230. package/docs/internal/serializable-abort-controller.mdx +7 -7
  231. package/docs/meta.json +4 -2
  232. package/docs/observability/attributes.mdx +91 -21
  233. package/docs/observability/index.mdx +29 -15
  234. package/docs/observability/lifecycle-hooks.mdx +95 -0
  235. package/docs/observability/meta.json +1 -1
  236. package/docs/observability/retention.mdx +95 -0
  237. package/docs/observability/tracing.mdx +124 -0
  238. package/docs/testing/index.mdx +118 -38
  239. package/docs/testing/server-based.mdx +10 -10
  240. package/docs/whats-new.mdx +196 -0
  241. package/docs/worlds/building-a-world.mdx +600 -0
  242. package/docs/worlds/local.mdx +129 -0
  243. package/docs/worlds/meta.json +10 -0
  244. package/docs/worlds/postgres.mdx +428 -0
  245. package/docs/worlds/upgrading-to-v5.mdx +183 -0
  246. package/docs/worlds/vercel.mdx +389 -0
  247. package/package.json +17 -14
  248. package/docs/api-reference/workflow/experimental-set-attributes.mdx +0 -63
  249. package/docs/api-reference/workflow-api/world/index.mdx +0 -58
  250. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  251. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  252. package/docs/api-reference/workflow-api/world/queue.mdx +0 -86
  253. package/docs/deploying/building-a-world.mdx +0 -251
  254. package/docs/deploying/index.mdx +0 -95
  255. package/docs/deploying/meta.json +0 -4
  256. package/docs/deploying/world/local-world.mdx +0 -84
  257. package/docs/deploying/world/meta.json +0 -4
  258. package/docs/deploying/world/postgres-world.mdx +0 -224
  259. package/docs/deploying/world/vercel-world.mdx +0 -181
  260. package/docs/migration-guides/index.mdx +0 -34
  261. package/docs/migration-guides/meta.json +0 -9
  262. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -358
  263. package/docs/migration-guides/migrating-from-inngest.mdx +0 -304
  264. package/docs/migration-guides/migrating-from-temporal.mdx +0 -313
  265. package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -328
@@ -11,9 +11,9 @@ related:
11
11
  - /docs/ai/human-in-the-loop
12
12
  ---
13
13
 
14
- Hooks provide a powerful mechanism for pausing workflow execution and resuming it later with external data. They enable workflows to wait for external events, user interactions (also known as "human in the loop"), or HTTP requests. This guide will teach you the core concepts, starting with the low-level Hook primitive and building up to the higher-level Webhook abstraction.
14
+ Hooks pause workflow execution and resume it later with external data. Workflows can wait for external events, user interactions (also known as "human in the loop"), or HTTP requests.
15
15
 
16
- ## Understanding Hooks
16
+ ## Understanding hooks
17
17
 
18
18
  At their core, **Hooks** are a low-level primitive that allows you to pause a workflow and resume it later with arbitrary [serializable data](/docs/foundations/serialization). Think of them as suspension points in your workflow where you're waiting for external input.
19
19
 
@@ -23,9 +23,9 @@ When you create a hook, it generates a unique token that external systems can us
23
23
  - Receiving data from an external system or service
24
24
  - Implementing event-driven workflows that react to multiple events over time
25
25
 
26
- ### Creating Your First Hook
26
+ ### Creating your first hook
27
27
 
28
- Let's start with a simple example. Here's a workflow that creates a hook and waits for external data:
28
+ This workflow creates a hook and waits for external data:
29
29
 
30
30
  ```typescript lineNumbers
31
31
  import { createHook } from "workflow";
@@ -59,7 +59,7 @@ We recommend using the `using` keyword which implements the [TC39 Explicit Resou
59
59
  See the full API reference for [`createHook()`](/docs/api-reference/workflow/create-hook) for all available options.
60
60
  </Callout>
61
61
 
62
- ### Resuming a Hook
62
+ ### Resuming a hook
63
63
 
64
64
  To send data to a waiting workflow, use [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) from an API route, server action, or any other external context:
65
65
 
@@ -82,12 +82,41 @@ export async function POST(request: Request) {
82
82
 
83
83
  The key points:
84
84
  - Hooks allow you to pass **any [serializable data](/docs/foundations/serialization)** as the payload
85
- - You need the hook's `token` to resume it
85
+ - You need the hook's `token` to resume it, but knowing the token does not authorize the caller. Check who is calling before `resumeHook()`; see [Security](#security)
86
86
  - The workflow will resume execution right where it left off
87
87
 
88
- ### Custom Tokens for Deterministic Hooks
88
+ ### Checking for token conflicts
89
89
 
90
- By default, hooks generate a random token. However, you often want to use a **custom token** that external systems can reconstruct. This is especially useful for long-running workflows where the same workflow instance should handle multiple events.
90
+ Sometimes you need to know that a hook token has been claimed, but you do not want to wait for external data yet. Await `hook.getConflict()` for that:
91
+
92
+ ```typescript lineNumbers
93
+ import { createHook } from "workflow";
94
+
95
+ declare function processOrder(orderId: string): Promise<void>; // @setup
96
+
97
+ export async function orderWorkflow(orderId: string) {
98
+ "use workflow";
99
+
100
+ using hook = createHook({
101
+ token: `order:${orderId}`
102
+ });
103
+
104
+ const conflict = await hook.getConflict(); // [!code highlight]
105
+ if (conflict) { // [!code highlight]
106
+ // Another active run already owns this token.
107
+ return { dedupedTo: conflict.runId };
108
+ }
109
+
110
+ // The hook token is registered and reserved here.
111
+ await processOrder(orderId);
112
+ }
113
+ ```
114
+
115
+ Calling `createHook()` on its own does not register the hook; registration is only committed when the workflow suspends. Awaiting `hook.getConflict()` suspends the workflow to commit the hook registration, then resolves with `null` once the hook is registered and ready to receive payloads, or with a `Run` handle for the run that owns the token (see [`HookConflictError`](/docs/errors/hook-conflict)). For `hook_conflict` events persisted by older worlds that did not record the owning run's ID, `getConflict()` rejects with `HookConflictError` instead of resolving with an incomplete handle. The conflicting run's accessors are durable steps, so the workflow can inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()`. See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies.
116
+
117
+ ### Custom tokens for deterministic hooks
118
+
119
+ By default, hooks generate their own token. However, you often want to use a **custom token** that external systems can reconstruct. This is especially useful for long-running workflows where the same workflow instance should handle multiple events.
91
120
 
92
121
  For example, imagine a Slack bot where each channel should have its own workflow instance:
93
122
 
@@ -139,9 +168,9 @@ export async function POST(request: Request) {
139
168
  }
140
169
  ```
141
170
 
142
- ### Receiving Multiple Events
171
+ ### Receiving multiple events
143
172
 
144
- Hooks are _reusable_ - they implement `AsyncIterable`, which means you can use `for await...of` to receive multiple events over time:
173
+ Hooks are _reusable_. They implement `AsyncIterable`, which means you can use `for await...of` to receive multiple events over time:
145
174
 
146
175
  ```typescript lineNumbers
147
176
  import { createHook } from "workflow";
@@ -169,7 +198,7 @@ export async function dataCollectionWorkflow() {
169
198
 
170
199
  Each time you call `resumeHook()` with the same token, the loop receives another value.
171
200
 
172
- ### Disposing Hooks Early
201
+ ### Disposing hooks early
173
202
 
174
203
  When a workflow ends, hooks are automatically disposed. However, you may want to release a hook token early so another workflow can use it while your workflow continues running. Use a block scope with `using` to control when disposal happens:
175
204
 
@@ -211,9 +240,43 @@ hook.dispose(); // Manually release the token
211
240
  After disposal, the hook will no longer receive events and the async iterator will stop yielding values.
212
241
  </Callout>
213
242
 
214
- ## Understanding Webhooks
243
+ ### Taking over a token from another run
244
+
245
+ Disposing early only works when the run holding the token cooperates. When the newest run should own a token regardless, such as a redeployed bot that must replace the run still waiting on a channel, pass `experimental_force` and let the runtime perform the handoff:
246
+
247
+ ```typescript lineNumbers
248
+ import { createHook } from "workflow";
249
+ import { HookForceClaimedError } from "workflow/errors";
250
+
251
+ declare function processMessage(message: { text: string }): Promise<void>; // @setup
215
252
 
216
- While hooks are powerful, they require you to manually handle HTTP requests and route them to workflows. **Webhooks** solve this by providing a higher-level abstraction built on top of hooks that:
253
+ export async function channelWorkflow(channelId: string) {
254
+ "use workflow";
255
+
256
+ const hook = createHook<{ text: string }>({
257
+ token: `channel:${channelId}`,
258
+ experimental_force: true, // [!code highlight]
259
+ });
260
+
261
+ try {
262
+ for await (const message of hook) {
263
+ await processMessage(message);
264
+ }
265
+ } catch (error) {
266
+ if (HookForceClaimedError.is(error)) { // [!code highlight]
267
+ // A newer run for this channel took the token. Wrap up and exit.
268
+ return { replacedBy: error.claimedByRunId };
269
+ }
270
+ throw error;
271
+ }
272
+ }
273
+ ```
274
+
275
+ The run that held the token is woken and its `await hook` (or `for await...of`) rejects with [`HookForceClaimedError`](/docs/api-reference/workflow-errors/hook-force-claimed-error) once it has drained the payloads it received before the takeover. Every `resumeHook()` for the token from then on reaches the new run, including one that was already in flight, so senders never notice the handoff. Several runs forcing the same token at once chain in the same way and always end with exactly one owner. A run started at a spec version below 8 cannot be taken from; the forced hook then gets an ordinary `HookConflictError`. See [`createHook()`](/docs/api-reference/workflow/create-hook#take-over-a-token-another-run-holds) for the full semantics.
276
+
277
+ ## Understanding webhooks
278
+
279
+ Hooks require you to manually handle HTTP requests and route them to workflows. **Webhooks** provide a higher-level abstraction built on top of hooks that:
217
280
 
218
281
  1. Automatically serializes the entire HTTP [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
219
282
  2. Provides an automatically addressable `url` property pointing to the generated webhook endpoint
@@ -222,16 +285,16 @@ While hooks are powerful, they require you to manually handle HTTP requests and
222
285
  When using Workflow SDK, webhooks are automatically wired up at `/.well-known/workflow/v1/webhook/:token` without any additional setup.
223
286
 
224
287
  <Callout type="warn">
225
- `createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes and a simple developer experience because you can share the webhook URL (endpoint) without creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
288
+ `createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. Make sure to read up on [security](#security) before using a webhook for calls that need to be authenticated.
226
289
  </Callout>
227
290
 
228
291
  <Callout type="info">
229
292
  See the full API reference for [`createWebhook()`](/docs/api-reference/workflow/create-webhook) for all available options.
230
293
  </Callout>
231
294
 
232
- ### Creating Your First Webhook
295
+ ### Creating your first webhook
233
296
 
234
- Here's a simple webhook that receives HTTP requests. Like hooks, webhooks support the `using` keyword for automatic cleanup:
297
+ Here's a webhook that receives HTTP requests. Like hooks, webhooks support the `using` keyword for automatic cleanup:
235
298
 
236
299
  ```typescript lineNumbers
237
300
  import { createWebhook } from "workflow";
@@ -255,13 +318,13 @@ export async function webhookWorkflow() {
255
318
  }
256
319
  ```
257
320
 
258
- The webhook will automatically respond with a `202 Accepted` status by default. External systems can simply make an HTTP request to the `webhook.url` to resume your workflow.
321
+ The webhook will automatically respond with a `202 Accepted` status by default. External systems can make an HTTP request to the `webhook.url` to resume your workflow.
259
322
 
260
- ### Sending Custom Responses
323
+ ### Sending custom responses
261
324
 
262
325
  Webhooks provide two ways to send custom HTTP responses: **static responses** and **dynamic responses**.
263
326
 
264
- #### Static Responses
327
+ #### Static responses
265
328
 
266
329
  Use the `respondWith` option to provide a static response that will be sent automatically for every request:
267
330
 
@@ -290,7 +353,7 @@ async function processData(data: any) {
290
353
  }
291
354
  ```
292
355
 
293
- #### Dynamic Responses (Manual Mode)
356
+ #### Dynamic responses (manual mode)
294
357
 
295
358
  For dynamic responses based on the request content, set `respondWith: "manual"` and call the `respondWith()` method on the request:
296
359
 
@@ -336,7 +399,7 @@ export async function webhookWithDynamicResponse() {
336
399
  When using `respondWith: "manual"`, the `respondWith()` method **must** be called from within a step function due to serialization requirements. This requirement may be removed in the future.
337
400
  </Callout>
338
401
 
339
- ### Handling Multiple Webhook Requests
402
+ ### Handling multiple webhook requests
340
403
 
341
404
  Like hooks, webhooks support iteration:
342
405
 
@@ -376,7 +439,7 @@ export async function eventCollectorWorkflow() {
376
439
  }
377
440
  ```
378
441
 
379
- ## Hooks vs. Webhooks: When to Use Each
442
+ ## Hooks vs. webhooks: when to use each
380
443
 
381
444
  | Feature | Hooks | Webhooks |
382
445
  |---------|-------|----------|
@@ -386,19 +449,107 @@ export async function eventCollectorWorkflow() {
386
449
  | **Use Case** | Custom integrations, type-safe payloads | HTTP webhooks, standard REST APIs |
387
450
  | **Resuming** | [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) | Automatic via HTTP, or [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook) |
388
451
 
389
- **Use Hooks when:**
452
+ **Use hooks when:**
390
453
  - You need full control over the payload structure
391
454
  - You're integrating with custom event sources
392
455
  - You want strong TypeScript typing with [`defineHook()`](/docs/api-reference/workflow/define-hook)
393
456
 
394
- **Use Webhooks when:**
457
+ **Use webhooks when:**
395
458
  - You're receiving HTTP requests from external services
396
459
  - You need to send HTTP responses back to the caller
397
460
  - You want automatic URL routing without writing API handlers
398
461
 
399
- ## Advanced Patterns
462
+ ## Security
463
+
464
+ A hook token tells the runtime which hook a payload belongs to. It is not an authentication mechanism, and neither `resumeHook()` nor the webhook endpoint checks who is sending the payload.
465
+
466
+ ### Generated tokens are hard to guess, not secret
467
+
468
+ When you don't pass a `token`, the SDK generates one inside the workflow function. Workflow code must produce the same values on every replay, so the generated token comes from the run's deterministic random number generator, the same one that backs [`Math.random()` and `crypto.randomUUID()`](/docs/api-reference/workflow-globals) in workflow functions. That generator is seeded from identifiers of the run, including the run ID, not from a secret key.
469
+
470
+ A generated token is hard to guess without knowing the run, but the values it is derived from are not designed to be kept secret. Treat a generated token like an unlisted link, not like a credential.
471
+
472
+ Custom tokens passed to `createHook({ token })` are usually built from domain data such as an order ID, so they are even easier to reconstruct. That is what makes them useful for routing, and it is why the route that resumes them must do its own authorization. If you can not perform your own authorization on the route that calls `resume` for any reason, and need to generate an unguessable token instead, generate it in a step where `crypto` is not seeded and pass it as `token`:
473
+
474
+ ```typescript lineNumbers
475
+ import { createHook } from "workflow";
476
+
477
+ async function generateToken() {
478
+ "use step";
479
+ // Steps run outside the workflow sandbox, so this uses the platform's
480
+ // cryptographic random source instead of the run's seed.
481
+ return crypto.randomUUID();
482
+ }
483
+
484
+ export async function approvalWorkflow() {
485
+ "use workflow";
486
+
487
+ const token = await generateToken(); // [!code highlight]
488
+ using hook = createHook<{ approved: boolean }>({ token }); // [!code highlight]
489
+
490
+ return (await hook).approved;
491
+ }
492
+ ```
493
+
494
+ The step result is recorded in the run's event log like any other step result. See [Encryption](/docs/how-it-works/encryption) to keep it encrypted at rest.
495
+
496
+ ### Webhook URLs
497
+
498
+ [`createWebhook()`](/docs/api-reference/workflow/create-webhook) serves a public route at `/.well-known/workflow/v1/webhook/:token`, and matching the token is the only check it performs. Anyone who has the URL, or can compute its token, can resume the workflow with a request of their choosing. That is fine for low-stakes callbacks and prototypes. When a webhook request triggers something consequential, either:
499
+
500
+ - Verify each request before acting on it, for example by checking the provider's HMAC signature in a step with [`respondWith: "manual"`](#dynamic-responses-manual-mode), and keep waiting for the next request when verification fails.
501
+ - Use [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the caller before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook), as shown below.
502
+
503
+ ### Authorize before calling `resumeHook()`
504
+
505
+ `resumeHook()` delivers the payload to whichever hook owns the token. The route that calls it has to authenticate the caller and check that they are allowed to resume that specific hook. Knowing the token is not proof of either. One way is to record who may resume the hook in its `metadata`, then compare it to the signed-in user:
506
+
507
+ ```typescript lineNumbers
508
+ import { createHook } from "workflow";
509
+
510
+ export async function expenseWorkflow(approverId: string) {
511
+ "use workflow";
512
+
513
+ using hook = createHook<{ approved: boolean }>({
514
+ metadata: { approverId }, // [!code highlight]
515
+ });
516
+
517
+ return (await hook).approved;
518
+ }
519
+ ```
520
+
521
+ ```typescript lineNumbers
522
+ import { getHookByToken, resumeHook } from "workflow/api";
523
+
524
+ declare function getSession(request: Request): Promise<{ userId: string } | null>; // @setup
525
+
526
+ export async function POST(request: Request) {
527
+ const session = await getSession(request); // [!code highlight]
528
+ if (!session) {
529
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
530
+ }
531
+
532
+ const { token, approved } = await request.json();
533
+ const hook = await getHookByToken(token);
534
+ const metadata = (await hook.metadata) as { approverId?: string } | undefined;
535
+ if (metadata?.approverId !== session.userId) { // [!code highlight]
536
+ return Response.json({ error: "Forbidden" }, { status: 403 });
537
+ }
538
+
539
+ await resumeHook(token, { approved });
540
+ return Response.json({ success: true });
541
+ }
542
+ ```
543
+
544
+ Take the user identity from your authentication layer, never from the request body. Other examples in these docs omit authorization to stay short.
545
+
546
+ ### Randomness in workflow functions
547
+
548
+ The same determinism applies to your own code. `Math.random()`, `crypto.randomUUID()`, and `crypto.getRandomValues()` in a workflow function return values derived from the run's seed, so they are predictable to anyone who knows it. Don't use them for secrets, passwords, one-time codes, or any value that must be unguessable. Generate those in a step, as in the token example above.
549
+
550
+ ## Advanced patterns
400
551
 
401
- ### Type-Safe Hooks with `defineHook()`
552
+ ### Type-safe hooks with `defineHook()`
402
553
 
403
554
  The [`defineHook()`](/docs/api-reference/workflow/define-hook) helper provides type safety and runtime validation between creating and resuming hooks using [Standard Schema v1](https://standardschema.dev). Use any compliant validator like Zod or Valibot:
404
555
 
@@ -447,25 +598,25 @@ export async function POST(request: Request) {
447
598
 
448
599
  This pattern is especially valuable in larger applications where the workflow and API code are in separate files, providing both compile-time type safety and runtime validation.
449
600
 
450
- ## Best Practices
601
+ ## Best practices
451
602
 
452
- ### Token Design
603
+ ### Token design
453
604
 
454
- Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always use randomly generated tokens to prevent unauthorized access to public webhook endpoints.
605
+ Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always generate their own unique tokens. Neither kind of token authorizes the sender; see [Security](#security).
455
606
 
456
607
  When using custom tokens with `createHook()`:
457
608
 
458
- - **Make them deterministic**: Base them on data the external system can reconstruct (like channel IDs, user IDs, etc.)
459
- - **Use namespacing**: Prefix tokens to avoid conflicts (e.g., `slack:${channelId}`, `github:${repoId}`)
460
- - **Include routing information**: Ensure the token contains enough information to identify the correct workflow instance
609
+ - **Make them deterministic**: Base them on data the external system can reconstruct, such as channel IDs or user IDs.
610
+ - **Use namespacing**: Prefix tokens to avoid conflicts, such as `slack:${channelId}` or `github:${repoId}`.
611
+ - **Include routing information**: Ensure the token contains enough information to identify the correct workflow instance.
461
612
 
462
- ### Response Handling in Webhooks
613
+ ### Response handling in webhooks
463
614
 
464
- - Use **static responses** (`respondWith: Response`) for simple acknowledgments
615
+ - Use **static responses** (`respondWith: Response`) for acknowledgments
465
616
  - Use **manual mode** (`respondWith: "manual"`) when responses depend on request processing
466
617
  - Remember that `respondWith()` must be called from within a step function
467
618
 
468
- ### Iterating Over Events
619
+ ### Iterating over events
469
620
 
470
621
  Both hooks and webhooks support iteration, making them perfect for long-running event loops:
471
622
 
@@ -484,7 +635,7 @@ for await (const event of hook) {
484
635
 
485
636
  This pattern allows a single workflow instance to handle multiple events over time, maintaining state between events.
486
637
 
487
- ## Related Documentation
638
+ ## Related documentation
488
639
 
489
640
  - [Serialization](/docs/foundations/serialization) - Understanding what data can be passed through hooks
490
641
  - [`createHook()` API Reference](/docs/api-reference/workflow/create-hook)