workflow 5.0.0-beta.9 → 5.0.0

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 (263) 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 +224 -0
  26. package/docs/ai/chat-session-modeling.mdx +176 -422
  27. package/docs/ai/defining-tools.mdx +6 -7
  28. package/docs/ai/human-in-the-loop.mdx +11 -11
  29. package/docs/ai/index.mdx +67 -72
  30. package/docs/ai/message-queueing.mdx +71 -110
  31. package/docs/ai/meta.json +1 -0
  32. package/docs/ai/resumable-streams.mdx +40 -28
  33. package/docs/ai/sleep-and-delays.mdx +10 -10
  34. package/docs/ai/streaming-updates-from-tools.mdx +6 -6
  35. package/docs/api-reference/index.mdx +25 -1
  36. package/docs/api-reference/meta.json +8 -0
  37. package/docs/api-reference/vitest/index.mdx +68 -15
  38. package/docs/api-reference/workflow/create-hook.mdx +166 -10
  39. package/docs/api-reference/workflow/create-webhook.mdx +16 -15
  40. package/docs/api-reference/workflow/define-hook.mdx +37 -33
  41. package/docs/api-reference/workflow/fatal-error.mdx +30 -8
  42. package/docs/api-reference/workflow/fetch.mdx +14 -10
  43. package/docs/api-reference/workflow/get-step-metadata.mdx +2 -2
  44. package/docs/api-reference/workflow/get-workflow-metadata.mdx +3 -3
  45. package/docs/api-reference/workflow/get-writable.mdx +7 -7
  46. package/docs/api-reference/workflow/index.mdx +3 -3
  47. package/docs/api-reference/workflow/retryable-error.mdx +1 -1
  48. package/docs/api-reference/workflow/set-attributes.mdx +63 -0
  49. package/docs/api-reference/workflow/sleep.mdx +4 -4
  50. package/docs/api-reference/workflow-ai/durable-agent.mdx +63 -101
  51. package/docs/api-reference/workflow-ai/index.mdx +5 -5
  52. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +67 -24
  53. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +28 -12
  54. package/docs/api-reference/workflow-api/get-run.mdx +43 -8
  55. package/docs/api-reference/workflow-api/index.mdx +8 -9
  56. package/docs/api-reference/workflow-api/register-lifecycle-hooks.mdx +87 -0
  57. package/docs/api-reference/workflow-api/resume-hook.mdx +73 -12
  58. package/docs/api-reference/workflow-api/resume-webhook.mdx +11 -9
  59. package/docs/api-reference/workflow-api/start.mdx +107 -12
  60. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  61. package/docs/api-reference/workflow-astro/meta.json +4 -0
  62. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  63. package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +4 -4
  64. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  65. package/docs/api-reference/workflow-errors/hook-force-claimed-error.mdx +70 -0
  66. package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
  67. package/docs/api-reference/workflow-errors/index.mdx +91 -0
  68. package/docs/api-reference/workflow-errors/meta.json +7 -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 +13 -6
  78. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +12 -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 +15 -11
  84. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +37 -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 +46 -0
  89. package/docs/api-reference/workflow-nest/workflow-module.mdx +134 -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 +51 -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 +104 -35
  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 +53 -41
  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 +79 -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 +399 -0
  149. package/docs/configuration/worlds.mdx +315 -0
  150. package/docs/cookbook/advanced/child-workflows.mdx +33 -25
  151. package/docs/cookbook/advanced/publishing-libraries.mdx +65 -56
  152. package/docs/cookbook/advanced/serializable-steps.mdx +48 -68
  153. package/docs/cookbook/advanced/upgrading-workflows.mdx +35 -31
  154. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +78 -60
  155. package/docs/cookbook/agent-patterns/durable-agent.mdx +23 -135
  156. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +180 -195
  157. package/docs/cookbook/common-patterns/batching.mdx +20 -14
  158. package/docs/cookbook/common-patterns/idempotency.mdx +41 -53
  159. package/docs/cookbook/common-patterns/rate-limiting.mdx +8 -4
  160. package/docs/cookbook/common-patterns/saga.mdx +23 -19
  161. package/docs/cookbook/common-patterns/scheduling.mdx +30 -22
  162. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +29 -25
  163. package/docs/cookbook/common-patterns/timeouts.mdx +26 -21
  164. package/docs/cookbook/common-patterns/webhooks.mdx +10 -6
  165. package/docs/cookbook/common-patterns/workflow-composition.mdx +27 -17
  166. package/docs/cookbook/index.mdx +22 -22
  167. package/docs/cookbook/integrations/ai-sdk.mdx +63 -48
  168. package/docs/cookbook/integrations/chat-sdk.mdx +46 -33
  169. package/docs/cookbook/integrations/sandbox.mdx +58 -45
  170. package/docs/deploying.mdx +106 -0
  171. package/docs/errors/abort-signal-timeout-in-workflow.mdx +16 -12
  172. package/docs/errors/corrupted-event-log.mdx +39 -18
  173. package/docs/errors/deployment-mismatch.mdx +71 -0
  174. package/docs/errors/fetch-in-workflow.mdx +15 -14
  175. package/docs/errors/hook-conflict.mdx +38 -11
  176. package/docs/errors/hook-force-claimed.mdx +96 -0
  177. package/docs/errors/index.mdx +24 -37
  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 +54 -11
  192. package/docs/foundations/hooks.mdx +98 -35
  193. package/docs/foundations/idempotency.mdx +267 -12
  194. package/docs/foundations/index.mdx +1 -26
  195. package/docs/foundations/serialization.mdx +22 -22
  196. package/docs/foundations/starting-workflows.mdx +104 -30
  197. package/docs/foundations/streaming.mdx +108 -60
  198. package/docs/foundations/versioning.mdx +4 -4
  199. package/docs/foundations/workflows-and-steps.mdx +9 -9
  200. package/docs/getting-started/astro.mdx +22 -18
  201. package/docs/getting-started/express.mdx +15 -11
  202. package/docs/getting-started/fastify.mdx +15 -11
  203. package/docs/getting-started/hono.mdx +15 -11
  204. package/docs/getting-started/index.mdx +10 -3
  205. package/docs/getting-started/meta.json +3 -1
  206. package/docs/getting-started/nestjs.mdx +264 -21
  207. package/docs/getting-started/next.mdx +18 -14
  208. package/docs/getting-started/nitro.mdx +22 -18
  209. package/docs/getting-started/nuxt.mdx +15 -11
  210. package/docs/getting-started/python.mdx +190 -41
  211. package/docs/getting-started/react-router/index.mdx +33 -0
  212. package/docs/getting-started/react-router/meta.json +5 -0
  213. package/docs/getting-started/react-router/v7.mdx +237 -0
  214. package/docs/getting-started/react-router/v8.mdx +232 -0
  215. package/docs/getting-started/sveltekit.mdx +20 -16
  216. package/docs/getting-started/tanstack-start.mdx +17 -13
  217. package/docs/getting-started/vite.mdx +15 -11
  218. package/docs/how-it-works/cancellation.mdx +63 -63
  219. package/docs/how-it-works/code-transform.mdx +82 -66
  220. package/docs/how-it-works/encryption.mdx +30 -26
  221. package/docs/how-it-works/event-sourcing.mdx +132 -35
  222. package/docs/how-it-works/framework-integrations.mdx +96 -337
  223. package/docs/how-it-works/understanding-directives.mdx +22 -22
  224. package/docs/internal/index.mdx +6 -4
  225. package/docs/internal/meta.json +6 -1
  226. package/docs/internal/nitro-native-build.mdx +38 -0
  227. package/docs/internal/nitro-web-ui.mdx +24 -0
  228. package/docs/internal/serializable-abort-controller.mdx +7 -7
  229. package/docs/meta.json +4 -2
  230. package/docs/observability/attributes.mdx +91 -21
  231. package/docs/observability/index.mdx +29 -15
  232. package/docs/observability/lifecycle-hooks.mdx +95 -0
  233. package/docs/observability/meta.json +1 -1
  234. package/docs/observability/retention.mdx +95 -0
  235. package/docs/observability/tracing.mdx +124 -0
  236. package/docs/testing/index.mdx +120 -38
  237. package/docs/testing/server-based.mdx +10 -10
  238. package/docs/whats-new.mdx +190 -0
  239. package/docs/worlds/building-a-world.mdx +538 -0
  240. package/docs/worlds/local.mdx +129 -0
  241. package/docs/worlds/meta.json +10 -0
  242. package/docs/worlds/postgres.mdx +424 -0
  243. package/docs/worlds/upgrading-to-v5.mdx +162 -0
  244. package/docs/worlds/vercel.mdx +345 -0
  245. package/package.json +17 -14
  246. package/docs/api-reference/workflow/experimental-set-attributes.mdx +0 -63
  247. package/docs/api-reference/workflow-api/world/index.mdx +0 -58
  248. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  249. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  250. package/docs/api-reference/workflow-api/world/queue.mdx +0 -86
  251. package/docs/deploying/building-a-world.mdx +0 -251
  252. package/docs/deploying/index.mdx +0 -95
  253. package/docs/deploying/meta.json +0 -4
  254. package/docs/deploying/world/local-world.mdx +0 -84
  255. package/docs/deploying/world/meta.json +0 -4
  256. package/docs/deploying/world/postgres-world.mdx +0 -224
  257. package/docs/deploying/world/vercel-world.mdx +0 -181
  258. package/docs/migration-guides/index.mdx +0 -34
  259. package/docs/migration-guides/meta.json +0 -9
  260. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -358
  261. package/docs/migration-guides/migrating-from-inngest.mdx +0 -304
  262. package/docs/migration-guides/migrating-from-temporal.mdx +0 -313
  263. package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -328
@@ -3,7 +3,19 @@ title: "@workflow/vitest"
3
3
  description: Vitest plugin and test helpers for integration testing workflows in-process.
4
4
  ---
5
5
 
6
- The `@workflow/vitest` package provides a Vitest plugin and test helpers for running full workflow integration tests in-process — no server required.
6
+ The `@workflow/vitest` package provides a Vitest plugin and test helpers for running full workflow integration tests in-process, no server required.
7
+
8
+ ## Installation
9
+
10
+ ```package-install
11
+ npm i -D @workflow/vitest@beta
12
+ ```
13
+
14
+ <Callout type="warn">
15
+ `@workflow/vitest@latest` is still the 4.x line, so a Workflow 5 app has to install the `beta` tag (or pin the matching beta, for example `@workflow/vitest@5.0.0-beta.53`). The package carries its own copy of `@workflow/core` and runs your workflows against it, so it has to move with `workflow`.
16
+
17
+ `globalSetup` compares the two copies once per run: a different major fails the run with the install command that fixes it, and any other difference logs a warning. Set [`WORKFLOW_VITEST_VERSION_CHECK=off`](/docs/configuration/build-and-diagnostics#workflow_vitest_version_check) to skip the check.
18
+ </Callout>
7
19
 
8
20
  ## Plugin
9
21
 
@@ -11,7 +23,6 @@ The `@workflow/vitest` package provides a Vitest plugin and test helpers for run
11
23
 
12
24
  Returns a Vite plugin array that handles SWC transforms, bundle building, and in-process handler registration automatically.
13
25
 
14
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
15
26
 
16
27
  ```typescript
17
28
  import { defineConfig } from "vitest/config";
@@ -22,9 +33,8 @@ export default defineConfig({
22
33
  });
23
34
  ```
24
35
 
25
- Pass a [`WorkflowTestOptions`](#workflowtestoptions) object when your project uses a non-standard layout — for example, a monorepo where `workflows/` does not live at the Vitest config's directory, or when the default `.workflow-data` / `.workflow-vitest` output locations need to move. The plugin forwards these paths to `buildWorkflowTests()` and `setupWorkflowTests()` through Vitest's per-project provided context, so each Vitest workspace project stays isolated.
36
+ Pass a [`WorkflowTestOptions`](#workflowtestoptions) object when your project uses a non-standard layout, for example, a monorepo where `workflows/` does not live at the Vitest config's directory, or when the default `.workflow-data` / `.workflow-vitest` output locations need to move. The plugin forwards these paths to `buildWorkflowTests()` and `setupWorkflowTests()` through Vitest's per-project provided context, so each Vitest workspace project stays isolated.
26
37
 
27
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
28
38
 
29
39
  ```typescript
30
40
  import { defineConfig } from "vitest/config";
@@ -48,13 +58,12 @@ export default defineConfig({
48
58
 
49
59
  **Returns:** `Plugin[]`
50
60
 
51
- ## Setup Functions
61
+ ## Setup functions
52
62
 
53
63
  ### `buildWorkflowTests()`
54
64
 
55
65
  Builds workflow and step bundles to disk. Called automatically by the `workflow()` plugin in `globalSetup`. Use directly only for [manual setup](/docs/testing#manual-setup).
56
66
 
57
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
58
67
 
59
68
  ```typescript
60
69
  import { buildWorkflowTests } from "@workflow/vitest";
@@ -72,11 +81,10 @@ export async function setup() {
72
81
 
73
82
  ### `setupWorkflowTests()`
74
83
 
75
- Sets up an in-process workflow runtime in each test worker. Imports pre-built bundles, creates a [Local World](/docs/worlds/local) instance with direct handlers, and sets it as the global world. Clears all workflow data on each invocation for full test isolation.
84
+ Sets up an in-process workflow runtime in each test worker. Imports pre-built bundles, creates a [Local World](/worlds/local) instance with direct handlers, and sets it as the global world. Clears all workflow data on each invocation for full test isolation.
76
85
 
77
86
  Called automatically by the `workflow()` plugin in `setupFiles`. Use directly only for [manual setup](/docs/testing#manual-setup).
78
87
 
79
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
80
88
 
81
89
  ```typescript
82
90
  import { beforeAll, afterAll } from "vitest";
@@ -112,13 +120,59 @@ Tears down the workflow test world. Clears the global world and closes the Local
112
120
  | `dataDir` | `string` | `<rootDir>/.workflow-data` | Directory for workflow runtime data written by the test world. Relative paths resolve against `cwd`. |
113
121
  | `outDir` | `string` | `<rootDir>/.workflow-vitest` | Directory for generated workflow and step bundles. Relative paths resolve against `cwd`. |
114
122
 
115
- ## Test Helpers
123
+ ## Workflow references
124
+
125
+ The test build writes the same `manifest.json` the other Workflow builders write into `outDir`. These helpers read it, so a test can name a workflow instead of hand-writing the generated `workflow//...` id. Importing the workflow function is still preferable when a test can do it, because it keeps the argument and return types.
126
+
127
+ ### `getWorkflowRef()`
128
+
129
+ Looks up one workflow in the test build and returns it in the shape [`start()`](/docs/api-reference/workflow-api/start) accepts.
130
+
131
+ ```typescript
132
+ import { getWorkflowRef } from "@workflow/vitest"; // [!code highlight]
133
+ import { start } from "workflow/api";
134
+
135
+ const run = await start(getWorkflowRef("approvalWorkflow"), ["doc-1"]); // [!code highlight]
136
+ ```
137
+
138
+ `query` is an exported workflow name, or a file-qualified name when the same name appears in more than one file: `getWorkflowRef("workflows/approval.ts#approvalWorkflow")`. The file part also matches by path suffix, so `"approval.ts#approvalWorkflow"` resolves the same entry.
139
+
140
+ **Parameters:**
141
+
142
+ | Parameter | Type | Description |
143
+ | --- | --- | --- |
144
+ | `query` | `string` | Workflow name, or `<file>#<name>` |
145
+
146
+ **Returns:** [`WorkflowRef`](#workflowref).
147
+
148
+ **Throws** when the manifest is missing (the plugin or `buildWorkflowTests()` has not run), when nothing matches, or when the name is ambiguous. The message lists the workflows the build contains.
149
+
150
+ ### `listWorkflowRefs()`
151
+
152
+ Returns every workflow in the test build, sorted by file and then name.
153
+
154
+ ```typescript
155
+ import { listWorkflowRefs } from "@workflow/vitest"; // [!code highlight]
156
+
157
+ const names = listWorkflowRefs().map((ref) => ref.name); // [!code highlight]
158
+ ```
159
+
160
+ **Returns:** [`WorkflowRef`](#workflowref)`[]`.
161
+
162
+ ### `WorkflowRef`
163
+
164
+ | Property | Type | Description |
165
+ | --- | --- | --- |
166
+ | `name` | `string` | Exported name of the workflow function |
167
+ | `file` | `string` | Project-relative path of the file it was compiled from |
168
+ | `workflowId` | `string` | Generated workflow id, the field `start()` reads |
169
+
170
+ ## Test helpers
116
171
 
117
172
  ### `waitForSleep()`
118
173
 
119
- Polls the event log until the workflow has a pending `sleep()` call — one with a `wait_created` event but no corresponding `wait_completed` event. Returns the correlation ID of the pending sleep, which can be passed to [`wakeUp()`](/docs/api-reference/workflow-api/get-run) to target a specific sleep.
174
+ Polls the event log until the workflow has a pending `sleep()` call, one with a `wait_created` event but no corresponding `wait_completed` event. Returns the correlation ID of the pending sleep, which can be passed to [`wakeUp()`](/docs/api-reference/workflow-api/get-run) to target a specific sleep.
120
175
 
121
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
122
176
 
123
177
  ```typescript
124
178
  import { waitForSleep } from "@workflow/vitest"; // [!code highlight]
@@ -136,9 +190,9 @@ await getRun(run.runId).wakeUp({ correlationIds: [sleepId] }); // [!code highlig
136
190
  | `run` | `Run<any>` | The workflow run to monitor |
137
191
  | `options?` | `WaitOptions` | Polling and timeout configuration |
138
192
 
139
- **Returns:** `Promise<string>` — The correlation ID of the first pending sleep. Pass this to `wakeUp({ correlationIds: [id] })` to target a specific sleep.
193
+ **Returns:** `Promise<string>`, the correlation ID of the first pending sleep. Pass this to `wakeUp({ correlationIds: [id] })` to target a specific sleep.
140
194
 
141
- #### Behavior with Multiple Sleeps
195
+ #### Behavior with multiple sleeps
142
196
 
143
197
  - **Sequential sleeps**: `waitForSleep()` returns each sleep as the workflow reaches it. After waking one, call `waitForSleep()` again for the next.
144
198
  - **Parallel sleeps**: `waitForSleep()` returns whichever pending sleep is found first. After waking it, call `waitForSleep()` again to get the next one.
@@ -147,7 +201,6 @@ await getRun(run.runId).wakeUp({ correlationIds: [sleepId] }); // [!code highlig
147
201
 
148
202
  Polls the hook list and event log until a hook matching the optional `token` filter exists that hasn't been received yet. Returns the matching hook object.
149
203
 
150
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
151
204
 
152
205
  ```typescript
153
206
  import { waitForHook } from "@workflow/vitest"; // [!code highlight]
@@ -165,7 +218,7 @@ await resumeHook(hook.token, { approved: true }); // [!code highlight]
165
218
  | `run` | `Run<any>` | The workflow run to monitor |
166
219
  | `options?` | `WaitOptions & { token?: string }` | Polling, timeout, and optional token filter |
167
220
 
168
- **Returns:** `Promise<Hook>` — The first pending hook matching the filter. The hook object includes `token`, `hookId`, and `runId`.
221
+ **Returns:** `Promise<Hook>`, the first pending hook matching the filter. The hook object includes `token`, `hookId`, and `runId`.
169
222
 
170
223
  ### `WaitOptions`
171
224
 
@@ -8,6 +8,7 @@ prerequisites:
8
8
  related:
9
9
  - /docs/api-reference/workflow/define-hook
10
10
  - /docs/api-reference/workflow/create-webhook
11
+ - /docs/foundations/idempotency
11
12
  ---
12
13
 
13
14
  Creates a low-level hook primitive that can be used to resume a workflow run with arbitrary payloads.
@@ -25,7 +26,7 @@ export async function hookWorkflow() {
25
26
  }
26
27
  ```
27
28
 
28
- ## API Signature
29
+ ## API signature
29
30
 
30
31
  ### Parameters
31
32
 
@@ -65,9 +66,11 @@ export default Hook;`}
65
66
 
66
67
  The returned `Hook` object also implements `AsyncIterable<T>`, which allows you to iterate over incoming payloads using `for await...of` syntax.
67
68
 
69
+ Use `hook.getConflict()` to check whether the hook token is already claimed by another hook, including one kept reserved after its run ends, without waiting for hook payload data. 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 registration, then resolves with `null` once `hook_created` is recorded, or with the conflicting [`Run`](/docs/api-reference/workflow-api/get-run).
70
+
68
71
  ## Examples
69
72
 
70
- ### Basic Usage
73
+ ### Basic usage
71
74
 
72
75
  When creating a hook, you can specify a payload type for automatic type safety:
73
76
 
@@ -88,7 +91,7 @@ export async function approvalWorkflow() {
88
91
  }
89
92
  ```
90
93
 
91
- ### Customizing Tokens
94
+ ### Customizing tokens
92
95
 
93
96
  Tokens are used to identify a specific hook. You can customize the token to be more specific to a use case.
94
97
 
@@ -112,7 +115,159 @@ export async function slackBotWorkflow(channelId: string) {
112
115
  }
113
116
  ```
114
117
 
115
- ### Waiting for Multiple Payloads
118
+ ### Detecting token conflicts
119
+
120
+ Use `hook.getConflict()` when the workflow needs to claim a hook token before doing other work, but does not need a payload yet:
121
+
122
+ ```typescript lineNumbers
123
+ import { createHook } from "workflow";
124
+
125
+ declare function chargeOrder(orderId: string): Promise<void>; // @setup
126
+
127
+ async function processOrder(orderId: string) {
128
+ "use workflow";
129
+
130
+ using hook = createHook({ // [!code highlight]
131
+ token: `order:${orderId}` // [!code highlight]
132
+ }); // [!code highlight]
133
+
134
+ const conflict = await hook.getConflict(); // [!code highlight]
135
+ if (conflict) { // [!code highlight]
136
+ // Another active workflow run already owns this token.
137
+ return { dedupedTo: conflict.runId };
138
+ }
139
+
140
+ await chargeOrder(orderId);
141
+ }
142
+ ```
143
+
144
+ Because `createHook()` alone does not suspend the workflow, awaiting `hook.getConflict()` is what actually suspends the run and commits the hook registration. It only waits for registration. To receive payload data from a future `resumeHook()` call, await the hook itself or iterate it with `for await...of`.
145
+
146
+ ### Registering a hook before a step uses it
147
+
148
+ A hook's registration is committed alongside everything else the workflow started before it suspended, not ahead of it. When a workflow creates a hook and calls a step without awaiting anything in between, the step can start running before the hook is registered, and it can run even if the registration turns out to conflict. That matters in two cases:
149
+
150
+ - The step hands the token to something that may call `resumeHook()` right away, which throws `HookNotFoundError` until the hook exists.
151
+ - The hook guards against duplicate runs. A run that only learns of the conflict after calling the step, for example by awaiting the hook and letting `HookConflictError` end the run, may already have started that step.
152
+
153
+ In either case, await `hook.getConflict()` before calling the step:
154
+
155
+ ```typescript lineNumbers
156
+ import { createHook } from "workflow";
157
+
158
+ declare function requestApproval(token: string): Promise<void>; // @setup
159
+
160
+ async function approvalWorkflow() {
161
+ "use workflow";
162
+
163
+ using hook = createHook<{ approved: boolean }>();
164
+ await hook.getConflict(); // [!code highlight]
165
+
166
+ // The hook is registered, so an approver that resumes it immediately
167
+ // finds it.
168
+ await requestApproval(hook.token);
169
+
170
+ const { approved } = await hook;
171
+ return approved;
172
+ }
173
+ ```
174
+
175
+ On a conflict, the resolved value is a `Run` handle for the run that owns the token, with durable step-backed accessors. The duplicate run can decide in code how to handle it: return or log `conflict.runId`, inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()` and continue in the current run. See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies in context.
176
+
177
+ <Callout type="info">
178
+ Custom hook tokens are the recommended way to coordinate active workflow runs. Use a deterministic token from your domain, such as an order ID or conversation ID, create the hook near the beginning of the workflow, and check `await hook.getConflict()` before work that depends on owning the token. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
179
+ </Callout>
180
+
181
+ ### Keep a token unavailable after the run ends
182
+
183
+ By default, another Hook can use the token after its workflow ends. Set `experimental_minRetention` to keep the token unavailable for at least a specific time after `createHook()` runs:
184
+
185
+ ```typescript lineNumbers
186
+ import { createHook } from "workflow";
187
+
188
+ declare function processOwnedOrder(orderId: string): Promise<void>; // @setup
189
+
190
+ export async function processOrder(orderId: string) {
191
+ "use workflow";
192
+
193
+ const hook = createHook({ // [!code highlight]
194
+ token: `order:${orderId}`, // [!code highlight]
195
+ experimental_minRetention: "30d", // [!code highlight]
196
+ }); // [!code highlight]
197
+
198
+ const conflict = await hook.getConflict();
199
+ if (conflict) {
200
+ return { status: "duplicate" as const, runId: conflict.runId };
201
+ }
202
+
203
+ await processOwnedOrder(orderId);
204
+ return { status: "processed" as const };
205
+ }
206
+ ```
207
+
208
+ `experimental_minRetention` accepts the same values as [`sleep()`](/docs/api-reference/workflow/sleep): a duration string such as `"30d"`, a number of milliseconds, or an absolute `Date`. Durations start when `createHook()` runs.
209
+
210
+ The Hook remains active until the workflow ends, even if the configured time passes first. Another Hook can use the token only after both the workflow has ended and the configured time has passed. For example, `"30d"` keeps the token unavailable for 29 more days if the workflow ends after 1 day. A workflow that runs for more than 30 days releases the token when it ends.
211
+
212
+ After the workflow ends, [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) can still find the Hook until retention ends, but the Hook cannot be resumed.
213
+
214
+ <Callout type="warn">
215
+ `using` auto-disposes the Hook at scope exit, which releases the token immediately and defeats `experimental_minRetention`. Declare retained Hooks with `const` and let the runtime clean them up when the run ends.
216
+ </Callout>
217
+
218
+ <Callout type="warn">
219
+ This option is experimental. Worlds can limit how long tokens are retained; see [World configuration](/docs/configuration/worlds) for each World's limit. If the configured World does not support minimum retention, the workflow fails when registering the Hook. `createWebhook()` does not accept this option.
220
+ </Callout>
221
+
222
+ ### Take over a token another run holds
223
+
224
+ By default, a token that another active run already registered makes the new Hook reject with [`HookConflictError`](/docs/api-reference/workflow-errors/hook-conflict-error). Set `experimental_force` when the newest run should own the token instead, for example when a fresh deployment or a restarted conversation must replace a run that is still waiting:
225
+
226
+ ```typescript lineNumbers
227
+ import { createHook } from "workflow";
228
+
229
+ declare function processMessage(message: SlackMessage): Promise<void>; // @setup
230
+ type SlackMessage = { text: string }; // @setup
231
+
232
+ export async function slackChannelWorkflow(channelId: string) {
233
+ "use workflow";
234
+
235
+ // Whichever run for this channel started most recently owns the token.
236
+ const hook = createHook<SlackMessage>({ // [!code highlight]
237
+ token: `slack_messages:${channelId}`, // [!code highlight]
238
+ experimental_force: true, // [!code highlight]
239
+ }); // [!code highlight]
240
+
241
+ for await (const message of hook) {
242
+ await processMessage(message);
243
+ }
244
+ }
245
+ ```
246
+
247
+ With `experimental_force`, this run always ends up owning the token:
248
+
249
+ - The previous owner's Hook is disposed, recorded in that run's event log, and the previous owner is woken. If it was awaiting the Hook, that `await` rejects with [`HookForceClaimedError`](/docs/api-reference/workflow-errors/hook-force-claimed-error), which names the run that took the token. Payloads it received before the takeover stay with it; a `for await...of` loop drains them before it throws.
250
+ - Every [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) for the token from then on reaches this run, including a call that was already in flight when the takeover happened. Callers never see the token move; a delivery aimed at the previous owner is redirected to this run inside `resumeHook()`.
251
+ - Any number of runs forcing the same token at the same time converge on a single owner. The takeovers form a chain: each run that loses the token gets `HookForceClaimedError`, exactly one run ends up owning it, and none of them can get stuck. Which run wins among simultaneous claimers is not defined; if the order matters, start them in order.
252
+ - A finished run that still holds the token under [`experimental_minRetention`](#keep-a-token-unavailable-after-the-run-ends) is taken over silently, since there is nothing left to wake. A run can also take over a token held by its own earlier Hook.
253
+
254
+ The takeover is durable. If either run's compute fails partway through, the next request for the token completes it, so the token never ends up held by nobody or by both runs. The previous owner's wake is durable too: if the new owner's compute fails between registering the Hook and waking the previous owner, the new owner's next invocation republishes the wake, whatever else the new owner has recorded since (a step it started alongside the Hook, for example). Every invocation of the new owner within 24 hours of the takeover republishes it under the same idempotency key, which collapses the repeats into one wake; a repeat that does get through only replays the previous owner, which finds nothing new.
255
+
256
+ <Callout type="info">
257
+ A token can only be taken from a run whose runtime understands being taken from. Runs started at a Workflow spec version below 8, which includes every run started by an older SDK release, a Python SDK run, or a deployment with `WORKFLOW_SEALED_LOG=0`, would never learn that their Hook was disposed. The World declines to take their token and the forced Hook rejects with the ordinary [`HookConflictError`](/docs/api-reference/workflow-errors/hook-conflict-error) instead, exactly as if `experimental_force` had not been set. Finished runs holding a retained token are taken over at any version.
258
+ </Callout>
259
+
260
+ `hook.getConflict()` on a forced Hook resolves with `null` once the takeover succeeds: the token is this run's by construction. If the World declines the takeover because the current owner predates spec version 8, `getConflict()` behaves as it does for an ordinary conflict and resolves with that owner's `Run`. Read `hook.claimedFrom` on the value returned by [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) to find out which run, if any, the token was taken from.
261
+
262
+ <Callout type="warn">
263
+ This option is experimental. It requires an explicit `token` (a generated token can never conflict) and is not accepted by `createWebhook()`. If the configured World does not support force-claiming at all, the workflow fails when registering the Hook; a World that declines a specific takeover because the current owner cannot be woken answers with `HookConflictError` instead.
264
+
265
+ Senders on an older SDK release are not redirected. A `resumeHook()` from a deployment that predates this option and that looked the token up inside the short handoff window gets an error (`EntityConflictError`) instead of following the token; the payload is refused, never delivered to the wrong run, and a retry resolves the new owner. Upgrade the sending deployment for the transparent redirect.
266
+
267
+ Webhook requests are not redirected either. [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook) streams the request body once, and buffering a copy of every webhook body on the chance that its token is being taken over at that moment would be a cost paid by everyone who never uses this option. A webhook delivery that lands inside the handoff window fails with a retryable error naming the takeover; nothing is delivered anywhere, and the sender's retry reaches the new owner. A forced `createHook()` can take over a token that a webhook holds, but the token then belongs to a Hook that is not a webhook, so `resumeWebhook()` answers every later request for it as not found, exactly as it does for any `createHook()` token, and never redirects one into it.
268
+ </Callout>
269
+
270
+ ### Waiting for multiple payloads
116
271
 
117
272
  You can also wait for multiple payloads by using the `for await...of` syntax.
118
273
 
@@ -135,7 +290,7 @@ export async function collectHookWorkflow() {
135
290
  }
136
291
  ```
137
292
 
138
- ### Disposing Hooks Early
293
+ ### Disposing hooks early
139
294
 
140
295
  You can dispose a hook early to release its token for reuse by another workflow. This is useful for handoff patterns where one workflow needs to transfer a hook token to another workflow while still running.
141
296
 
@@ -164,7 +319,7 @@ export async function handoffWorkflow(channelId: string) {
164
319
 
165
320
  After calling `dispose()`, the hook will no longer receive events and its token becomes available for other workflows to use.
166
321
 
167
- ### Automatic Disposal with `using`
322
+ ### Automatic disposal with `using`
168
323
 
169
324
  Hooks implement the [TC39 Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) proposal, allowing automatic disposal with the `using` keyword:
170
325
 
@@ -190,8 +345,9 @@ export async function scopedHookWorkflow(channelId: string) {
190
345
 
191
346
  This is equivalent to manually calling `dispose()` but ensures the hook is always cleaned up, even if an error occurs.
192
347
 
193
- ## Related Functions
348
+ ## Related functions
194
349
 
195
- - [`defineHook()`](/docs/api-reference/workflow/define-hook) - Type-safe hook helper
196
- - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) - Resume a hook with a payload
197
- - [`createWebhook()`](/docs/api-reference/workflow/create-webhook) - Higher-level HTTP webhook abstraction
350
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook): Type-safe hook helper
351
+ - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Resume a hook with a payload
352
+ - [`createWebhook()`](/docs/api-reference/workflow/create-webhook): Higher-level HTTP webhook abstraction
353
+ - [Idempotency](/docs/foundations/idempotency): Deduplicate step side effects and workflow starts
@@ -14,7 +14,7 @@ Creates a webhook that can be used to suspend and resume a workflow run upon rec
14
14
  Webhooks provide a way for external systems to send HTTP requests directly to your workflow. Unlike hooks which accept arbitrary payloads, webhooks work with standard HTTP `Request` objects and can return HTTP `Response` objects.
15
15
 
16
16
  <Callout type="warn">
17
- `createWebhook()` creates a public endpoint at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests resuming that webhook. This is convenient for prototypes and simple resume links because it avoids 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.
17
+ `createWebhook()` creates a public endpoint at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests resuming that webhook. This is convenient for prototypes and basic resume links because it avoids 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.
18
18
  </Callout>
19
19
 
20
20
  ```ts lineNumbers
@@ -31,7 +31,7 @@ export async function webhookWorkflow() {
31
31
  }
32
32
  ```
33
33
 
34
- ## API Signature
34
+ ## API signature
35
35
 
36
36
  ### Parameters
37
37
 
@@ -55,6 +55,7 @@ The returned `Webhook` object has:
55
55
 
56
56
  - `url`: The HTTP endpoint URL that external systems can call
57
57
  - `token`: The unique token identifying this webhook
58
+ - `getConflict()`: A promise that resolves with the conflicting run if another active hook already owns this token, or `null` once the webhook endpoint has been registered. The endpoint is registered alongside the steps the workflow starts at the same time, not ahead of them, so await `getConflict()` before a step that hands `url` to a caller who may request it right away. See [Registering a hook before a step uses it](/docs/api-reference/workflow/create-hook#registering-a-hook-before-a-step-uses-it).
58
59
  - Implements `AsyncIterable<T>` for handling multiple requests, where `T` is `Request` (default) or `RequestWithResponse` (manual mode)
59
60
 
60
61
  When using `createWebhook({ respondWith: 'manual' })`, the resolved request type is `RequestWithResponse`, which extends the standard `Request` interface with a `respondWith(response: Response): Promise<void>` method for sending custom responses back to the caller.
@@ -62,22 +63,22 @@ When using `createWebhook({ respondWith: 'manual' })`, the resolved request type
62
63
  <Callout type="info">
63
64
  Use the simplest option that satisfies the prompt:
64
65
 
65
- - `createWebhook()` — generated callback URL, and the default `202 Accepted` response is fine
66
- - `createWebhook({ respondWith: 'manual' })` — generated callback URL, but you must send a custom body, status, or headers
67
- - `createHook()` + `resumeHook()` — the app resumes from server-side code with a deterministic business token instead of a generated callback URL
66
+ - `createWebhook()`: generated callback URL, and the default `202 Accepted` response is fine
67
+ - `createWebhook({ respondWith: 'manual' })`: generated callback URL, but you must send a custom body, status, or headers
68
+ - `createHook()` + `resumeHook()`: the app resumes from server-side code with a deterministic business token instead of a generated callback URL
68
69
  </Callout>
69
70
 
70
71
  <details>
71
72
  <summary>Common wrong turns</summary>
72
73
 
73
- - Do not use `respondWith: 'manual'` just because the flow has a callback URL.
74
+ - A callback URL alone does not require `respondWith: 'manual'`.
74
75
  - Do not use `RequestWithResponse` unless you chose manual mode.
75
76
  - Do not invent a custom callback route when `webhook.url` is the intended callback surface.
76
77
  </details>
77
78
 
78
79
  ## Examples
79
80
 
80
- ### Basic Usage
81
+ ### Basic usage
81
82
 
82
83
  Create a webhook that receives HTTP requests and logs the request details:
83
84
 
@@ -100,11 +101,11 @@ export async function basicWebhookWorkflow() {
100
101
  }
101
102
  ```
102
103
 
103
- ### Responding to Webhook Requests (Manual Mode)
104
+ ### Responding to webhook requests (manual mode)
104
105
 
105
106
  Use this section only when the caller requires a non-default HTTP response. If `202 Accepted` is acceptable, use `createWebhook()` without `respondWith: "manual"`.
106
107
 
107
- Pass `{ respondWith: "manual" }` to get a `RequestWithResponse` object with a `respondWith()` method. Note that `respondWith()` must be called from within a step function:
108
+ Pass `{ respondWith: "manual" }` to get a `RequestWithResponse` object with a `respondWith()` method. Call `respondWith()` from within a step function:
108
109
 
109
110
  ```typescript lineNumbers
110
111
  import { createWebhook, type RequestWithResponse } from "workflow"
@@ -142,7 +143,7 @@ async function processData(data: any): Promise<void> {
142
143
  }
143
144
  ```
144
145
 
145
- ### Waiting for Multiple Requests
146
+ ### Waiting for multiple requests
146
147
 
147
148
  You can also wait for multiple requests by using the `for await...of` syntax.
148
149
 
@@ -181,9 +182,9 @@ export async function eventCollectorWorkflow() {
181
182
  }
182
183
  ```
183
184
 
184
- ## Related Functions
185
+ ## Related functions
185
186
 
186
- - [`createHook()`](/docs/api-reference/workflow/create-hook) — Use when the app resumes from server-side code with a deterministic business token.
187
- - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) — Pairs with `createHook()` for deterministic server-side resume.
188
- - [`defineHook()`](/docs/api-reference/workflow/define-hook) — Type-safe hook helper.
189
- - [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook) — Low-level runtime API. Most integrations should call `webhook.url` directly instead of adding a custom callback route.
187
+ - [`createHook()`](/docs/api-reference/workflow/create-hook): Use when the app resumes from server-side code with a deterministic business token.
188
+ - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Pairs with `createHook()` for deterministic server-side resume.
189
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook): Type-safe hook helper.
190
+ - [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook): Low-level runtime API. Most integrations should call `webhook.url` directly instead of adding a custom callback route.
@@ -33,7 +33,7 @@ export async function nameWorkflow() {
33
33
  }
34
34
  ```
35
35
 
36
- ## API Signature
36
+ ## API signature
37
37
 
38
38
  ### Parameters
39
39
 
@@ -46,27 +46,24 @@ showSections={['parameters']}
46
46
 
47
47
  ### Returns
48
48
 
49
+ `defineHook()` returns a `TypedHook<TInput, TOutput>`:
50
+
49
51
  <TSDoc
50
52
  definition={`
51
- interface DefineHook<T> {
52
- /**
53
-
54
- * Creates a new hook with the defined payload type.
55
- */
56
- create: (options?: HookOptions) => Hook<T>;
57
-
58
- /**
59
-
60
- * Resumes a hook by sending a payload with the defined type.
61
- */
62
- resume: (token: string, payload: T) => Promise<HookEntity | null>;
53
+ interface TypedHook<TInput, TOutput> {
54
+ /** Creates the hook. Call inside a "use workflow" function. */
55
+ create(options?: HookOptions): Hook<TOutput>;
56
+ /** Resumes the hook from runtime code. Resolves to the resumed hook; throws HookNotFoundError if the token does not match an active hook. */
57
+ resume(token: string, payload: TInput): Promise<HookEntity>;
63
58
  }
64
- export default DefineHook;`}
59
+ export default TypedHook;`}
65
60
  />
66
61
 
62
+ `create()` is called inside a `"use workflow"` function to create the hook; `resume()` is called from runtime code (an API route or server action). When a `schema` is provided, `resume()` accepts the raw input type (`TInput`) and the workflow receives the validated and possibly transformed output type (`TOutput`); without a schema, `TOutput` defaults to `TInput`. `resume()` throws [`HookNotFoundError`](/docs/api-reference/workflow-errors/hook-not-found-error) if the token does not match an active hook. It does not return `null`.
63
+
67
64
  ## Examples
68
65
 
69
- ### Basic Type-Safe Hook Definition
66
+ ### Basic type-safe hook definition
70
67
 
71
68
  By defining the hook once with a specific payload type, you can reuse it in multiple workflows and API routes with automatic type safety.
72
69
 
@@ -91,30 +88,35 @@ export async function workflowWithApproval() {
91
88
  }
92
89
  ```
93
90
 
94
- ### Resuming with Type Safety
91
+ ### Resuming with type safety
95
92
 
96
- Hooks can be resumed using the same defined hook and a token. By using the same hook, you can ensure that the payload matches the defined type when resuming a hook.
93
+ Hooks can be resumed using the same defined hook and a token. By using the same hook, you can ensure that the payload matches the defined type when resuming a hook. `resume()` resolves to the resumed hook and throws [`HookNotFoundError`](/docs/api-reference/workflow-errors/hook-not-found-error) if the token does not match an active hook.
97
94
 
98
95
  ```typescript lineNumbers
96
+ import { HookNotFoundError } from "workflow/errors";
97
+
99
98
  // Use the same defined hook to resume
100
99
  export async function POST(request: Request) {
101
100
  const { token, approved, comment } = await request.json();
102
101
 
103
- // Type-safe resumption - TypeScript ensures the payload matches
104
- const result = await approvalHook.resume(token, { // [!code highlight]
105
- approved, // [!code highlight]
106
- comment, // [!code highlight]
107
- }); // [!code highlight]
108
-
109
- if (!result) {
110
- return Response.json({ error: "Hook not found" }, { status: 404 });
102
+ try {
103
+ // Type-safe resumption - TypeScript ensures the payload matches
104
+ const hook = await approvalHook.resume(token, { // [!code highlight]
105
+ approved, // [!code highlight]
106
+ comment, // [!code highlight]
107
+ }); // [!code highlight]
108
+
109
+ return Response.json({ success: true, runId: hook.runId });
110
+ } catch (error) {
111
+ if (HookNotFoundError.is(error)) { // [!code highlight]
112
+ return Response.json({ error: "Hook not found" }, { status: 404 });
113
+ }
114
+ throw error;
111
115
  }
112
-
113
- return Response.json({ success: true, runId: result.runId });
114
116
  }
115
117
  ```
116
118
 
117
- ### Validate and Transform with Schema
119
+ ### Validate and transform with schema
118
120
 
119
121
  You can provide runtime validation and transformation of hook payloads using the `schema` option. This option accepts any validator that conforms to the [Standard Schema v1](https://standardschema.dev) specification.
120
122
 
@@ -172,7 +174,7 @@ export async function POST(request: Request) {
172
174
  }
173
175
  ```
174
176
 
175
- #### Using Other Standard Schema Libraries
177
+ #### Using other Standard Schema libraries
176
178
 
177
179
  The same pattern works with any Standard Schema v1 compliant library. Here's an example with [Valibot](https://valibot.dev):
178
180
 
@@ -188,7 +190,7 @@ export const approvalHook = defineHook({
188
190
  });
189
191
  ```
190
192
 
191
- ### Customizing Tokens
193
+ ### Customizing tokens
192
194
 
193
195
  Tokens are used to identify a specific hook and for resuming a hook. You can customize the token to be more specific to a use case.
194
196
 
@@ -209,7 +211,9 @@ export async function slackBotWorkflow(channelId: string) {
209
211
  }
210
212
  ```
211
213
 
212
- ## Related Functions
214
+ `create()` accepts the same options as `createHook()`. If a newer run should replace one that still holds the token, pass [`experimental_force: true`](/docs/api-reference/workflow/create-hook#take-over-a-token-another-run-holds) to take the token over instead of getting [`HookConflictError`](/docs/api-reference/workflow-errors/hook-conflict-error).
215
+
216
+ ## Related functions
213
217
 
214
- * [`createHook()`](/docs/api-reference/workflow/create-hook) - Create a hook in a workflow.
215
- * [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) - Resume a hook with a payload.
218
+ - [`createHook()`](/docs/api-reference/workflow/create-hook): Create a hook in a workflow.
219
+ - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Resume a hook with a payload.