workflow 5.0.0-beta.8 → 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 (262) 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 +4 -1
  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 +61 -46
  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 +136 -0
  231. package/docs/observability/index.mdx +32 -10
  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-api/world/index.mdx +0 -58
  247. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  248. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  249. package/docs/api-reference/workflow-api/world/queue.mdx +0 -86
  250. package/docs/deploying/building-a-world.mdx +0 -251
  251. package/docs/deploying/index.mdx +0 -95
  252. package/docs/deploying/meta.json +0 -4
  253. package/docs/deploying/world/local-world.mdx +0 -84
  254. package/docs/deploying/world/meta.json +0 -4
  255. package/docs/deploying/world/postgres-world.mdx +0 -224
  256. package/docs/deploying/world/vercel-world.mdx +0 -181
  257. package/docs/migration-guides/index.mdx +0 -34
  258. package/docs/migration-guides/meta.json +0 -9
  259. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -358
  260. package/docs/migration-guides/migrating-from-inngest.mdx +0 -304
  261. package/docs/migration-guides/migrating-from-temporal.mdx +0 -313
  262. package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -328
@@ -1,38 +1,37 @@
1
1
  ---
2
2
  title: Human-in-the-Loop
3
- description: Pause an AI agent to wait for human approval, then resume based on the decision.
3
+ description: Pause a WorkflowAgent for human approval before a consequential tool executes.
4
4
  type: guide
5
- summary: Use defineHook with the tool call ID to suspend an agent for human approval, with an optional timeout.
5
+ summary: Use WorkflowAgent's needsApproval option and AI SDK approval responses to build durable human approval flows.
6
6
  ---
7
7
 
8
- Use this pattern when an AI agent needs human confirmation before performing a consequential action like booking, purchasing, or publishing. The workflow suspends without consuming resources until the human responds.
8
+ <CopyPrompt
9
+ text="Add a human approval gate to this AI SDK WorkflowAgent. Define the consequential action with AI SDK's `tool()` helper, set `needsApproval: true` (or an input-dependent function), and keep the tool's `execute` function as a durable `&quot;use step&quot;` function. Configure `experimental_toolApprovalSecret` with the name of a high-entropy secret environment variable so client-supplied approvals are signed and verified. Stream `ModelCallStreamPart` values through `getWritable()` and convert them with `createModelCallToUIChunkTransform()` in the API route. In the client, render tool parts whose state is `approval-requested`, call `addToolApprovalResponse()` with the approval ID and decision, and use `lastAssistantMessageIsCompleteWithApprovalResponses` to continue automatically. Verify approve and reject paths, invalid or missing signatures, duplicate responses, and that the side effect never runs before approval."
10
+ />
11
+
12
+ Use this pattern when an AI agent needs confirmation before performing an action such as booking, purchasing, publishing, or deleting data. `WorkflowAgent` makes approval a first-class part of the durable agent loop: it emits an approval request, pauses before the tool executes, and resumes after the user responds.
9
13
 
10
14
  ## When to use this
11
15
 
12
16
  - Booking confirmations where users must approve before charges are made
13
17
  - Content publishing gates where an editor must sign off
14
- - Any agent action where the cost of getting it wrong justifies a human check
15
- - Actions with side effects that can't be easily undone
16
-
17
- ## Pattern
18
-
19
- Create a typed hook using `defineHook()`. When the agent calls the approval tool, the tool emits a custom data part to the stream so the client can render approval controls, then creates a hook and suspends. An API route resumes the hook with the decision.
20
-
21
- ### Workflow
22
-
23
- ```typescript
24
- import { DurableAgent } from "@workflow/ai/agent";
25
- import { defineHook, sleep, getWritable } from "workflow";
18
+ - Agent actions where the cost of an error justifies human review
19
+ - Side effects that are difficult to reverse
20
+
21
+ ## Define an approval-gated tool
22
+
23
+ Set `needsApproval` on the tool. Keep the action itself in a step so it receives Workflow retries and observability only after approval succeeds.
24
+
25
+ ```typescript title="workflows/booking-agent.ts" lineNumbers
26
+ import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
27
+ import {
28
+ tool,
29
+ type InferUITools,
30
+ type ModelMessage,
31
+ type UIMessage,
32
+ } from "ai";
33
+ import { getWritable } from "workflow";
26
34
  import { z } from "zod";
27
- import type { ModelMessage, UIMessageChunk } from "ai";
28
-
29
- // Exported so the approval API route can call .resume()
30
- export const bookingApprovalHook = defineHook({ // [!code highlight]
31
- schema: z.object({
32
- approved: z.boolean(),
33
- comment: z.string().optional(),
34
- }),
35
- });
36
35
 
37
36
  async function searchFlights({ from, to, date }: {
38
37
  from: string;
@@ -40,216 +39,202 @@ async function searchFlights({ from, to, date }: {
40
39
  date: string;
41
40
  }) {
42
41
  "use step";
43
- const res = await fetch(
42
+
43
+ const response = await fetch(
44
44
  `https://api.example.com/flights?from=${from}&to=${to}&date=${date}`
45
45
  );
46
- return res.json();
46
+ return response.json();
47
47
  }
48
48
 
49
- async function confirmBooking({ flightId, passenger }: {
49
+ async function confirmBooking({ flightId, passenger, price }: {
50
50
  flightId: string;
51
51
  passenger: string;
52
+ price: number;
52
53
  }) {
53
54
  "use step";
54
- const res = await fetch("https://api.example.com/bookings", {
55
+
56
+ const response = await fetch("https://api.example.com/bookings", {
55
57
  method: "POST",
56
- body: JSON.stringify({ flightId, passenger }),
58
+ body: JSON.stringify({ flightId, passenger, price }),
57
59
  });
58
- return res.json();
59
- }
60
-
61
- // Stream a custom data part so the client can render the approval UI.
62
- // This MUST run before the hook suspends the workflow — otherwise
63
- // the tool-invocation won't appear in the stream until the tool returns,
64
- // and the client would have no way to show approval buttons.
65
- async function emitApprovalRequest(details: {
66
- flightId: string;
67
- passenger: string;
68
- price: number;
69
- toolCallId: string;
70
- }) {
71
- "use step";
72
- const writer = getWritable<UIMessageChunk>().getWriter();
73
- try {
74
- await writer.write({
75
- type: "data-approval-needed", // [!code highlight]
76
- id: details.toolCallId,
77
- data: details,
78
- } as UIMessageChunk);
79
- } finally {
80
- writer.releaseLock();
81
- }
60
+ return response.json();
82
61
  }
83
62
 
84
- // Stream the resolution so the client can update the approval card.
85
- async function emitApprovalResolved(details: {
86
- toolCallId: string;
87
- result: string;
88
- }) {
89
- "use step";
90
- const writer = getWritable<UIMessageChunk>().getWriter();
91
- try {
92
- await writer.write({
93
- type: "data-approval-resolved", // [!code highlight]
94
- id: details.toolCallId,
95
- data: details,
96
- } as UIMessageChunk);
97
- } finally {
98
- writer.releaseLock();
99
- }
100
- }
63
+ export const bookingTools = {
64
+ searchFlights: tool({
65
+ description: "Search for available flights",
66
+ inputSchema: z.object({
67
+ from: z.string().describe("Departure airport code"),
68
+ to: z.string().describe("Arrival airport code"),
69
+ date: z.string().describe("Travel date (YYYY-MM-DD)"),
70
+ }),
71
+ execute: searchFlights,
72
+ }),
73
+ confirmBooking: tool({
74
+ description: "Book a selected flight for a passenger",
75
+ inputSchema: z.object({
76
+ flightId: z.string(),
77
+ passenger: z.string(),
78
+ price: z.number(),
79
+ }),
80
+ needsApproval: true, // [!code highlight]
81
+ execute: confirmBooking,
82
+ }),
83
+ };
101
84
 
102
- // No "use step" — hooks are workflow-level primitives
103
- async function requestBookingApproval(
104
- { flightId, passenger, price }: {
105
- flightId: string;
106
- passenger: string;
107
- price: number;
108
- },
109
- { toolCallId }: { toolCallId: string }
110
- ) {
111
- // Emit to the stream before suspending so the UI can show buttons
112
- await emitApprovalRequest({ flightId, passenger, price, toolCallId }); // [!code highlight]
113
-
114
- const hook = bookingApprovalHook.create({ token: toolCallId });
115
-
116
- // Race: human decision vs. timeout
117
- const result = await Promise.race([
118
- hook.then((payload) => ({ type: "decision" as const, ...payload })),
119
- sleep("24h").then(() => ({ type: "timeout" as const, approved: false as const })),
120
- ]);
121
-
122
- if (result.type === "timeout") {
123
- const msg = "Booking request expired.";
124
- await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
125
- return msg;
126
- }
127
- if (!result.approved) {
128
- const msg = `Rejected: ${result.comment || "No reason given"}`;
129
- await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
130
- return msg;
131
- }
132
-
133
- const booking = await confirmBooking({ flightId, passenger });
134
- const msg = `Booked! Confirmation: ${booking.confirmationId}`;
135
- await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
136
- return msg;
137
- }
85
+ export type BookingAgentUIMessage = UIMessage<
86
+ unknown,
87
+ never,
88
+ InferUITools<typeof bookingTools>
89
+ >;
138
90
 
139
91
  export async function bookingAgent(messages: ModelMessage[]) {
140
92
  "use workflow";
141
93
 
142
- const agent = new DurableAgent({
143
- model: "anthropic/claude-haiku-4.5",
144
- instructions: "You help book flights. Always request approval before booking.",
145
- tools: {
146
- searchFlights: {
147
- description: "Search for available flights",
148
- inputSchema: z.object({
149
- from: z.string().describe("Departure airport code"),
150
- to: z.string().describe("Arrival airport code"),
151
- date: z.string().describe("Travel date (YYYY-MM-DD)"),
152
- }),
153
- execute: searchFlights,
154
- },
155
- requestBookingApproval: {
156
- description: "Request human approval before booking a flight",
157
- inputSchema: z.object({
158
- flightId: z.string().describe("Flight ID to book"),
159
- passenger: z.string().describe("Passenger name"),
160
- price: z.number().describe("Total price"),
161
- }),
162
- execute: requestBookingApproval,
163
- },
164
- },
94
+ const agent = new WorkflowAgent({
95
+ model: "spacexai/grok-4.6",
96
+ instructions: "Help the user find and book flights.",
97
+ tools: bookingTools,
98
+ experimental_toolApprovalSecret: { // [!code highlight]
99
+ environmentVariable: "WORKFLOW_TOOL_APPROVAL_SECRET", // [!code highlight]
100
+ }, // [!code highlight]
165
101
  });
166
102
 
167
- await agent.stream({
103
+ return agent.stream({
168
104
  messages,
169
- writable: getWritable<UIMessageChunk>(),
105
+ writable: getWritable<ModelCallStreamPart>(),
170
106
  });
171
107
  }
172
108
  ```
173
109
 
174
- ### Approval API route
110
+ ## Sign approval requests
175
111
 
176
- The approval route imports the hook definition and calls `.resume()` with the tool call ID as the token:
112
+ When approval responses come from client-supplied message history, configure `experimental_toolApprovalSecret` as shown above. `WorkflowAgent` signs the approval ID, tool-call ID, tool name, and validated input when it emits the approval request, then verifies that signature before an approved tool can execute. Missing or invalid signatures prevent the action from running.
177
113
 
178
- ```typescript
179
- import { bookingApprovalHook } from "@/app/workflows/booking-agent";
114
+ Set `WORKFLOW_TOOL_APPROVAL_SECRET` to a high-entropy secret in every environment that can execute the workflow. For example, generate one with `openssl rand -base64 32`, then store it in your deployment's secret environment variables. Only the environment variable name crosses the workflow boundary; the secret is read inside signing and verification steps and is never serialized into workflow history.
180
115
 
181
- export async function POST(req: Request) {
182
- const { toolCallId, approved, comment } = await req.json();
116
+ Signed approvals require `@ai-sdk/workflow` 2.0.16 or later.
183
117
 
184
- await bookingApprovalHook.resume(toolCallId, { approved, comment }); // [!code highlight]
118
+ `needsApproval` can also decide from the parsed tool input. For example, require approval only when a booking costs more than a threshold:
185
119
 
186
- return Response.json({ success: true });
120
+ {/* @skip-typecheck: property excerpt */}
121
+ ```typescript
122
+ needsApproval: async ({ price }) => price > 500,
123
+ ```
124
+
125
+ ## Start the workflow and transform its stream
126
+
127
+ `WorkflowAgent` stores `ModelCallStreamPart` values. Convert those durable parts into AI SDK UI chunks at the HTTP boundary:
128
+
129
+ ```typescript title="app/api/chat/route.ts" lineNumbers
130
+ import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
131
+ import {
132
+ convertToModelMessages,
133
+ createUIMessageStreamResponse,
134
+ } from "ai";
135
+ import { start } from "workflow/api";
136
+ import {
137
+ bookingAgent,
138
+ type BookingAgentUIMessage,
139
+ } from "@/workflows/booking-agent";
140
+
141
+ export async function POST(request: Request) {
142
+ const { messages }: { messages: BookingAgentUIMessage[] } =
143
+ await request.json();
144
+ const modelMessages = await convertToModelMessages(messages);
145
+ const run = await start(bookingAgent, [modelMessages]);
146
+
147
+ return createUIMessageStreamResponse({
148
+ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
149
+ headers: { "x-workflow-run-id": run.runId },
150
+ });
187
151
  }
188
152
  ```
189
153
 
190
- ### Client rendering
191
-
192
- Listen for `data-approval-needed` and `data-approval-resolved` custom data parts in the message stream. The approval tool invocation itself won't appear until the tool returns, so the custom data parts are the mechanism for showing and updating the approval UI.
193
-
194
- ```tsx
195
- // Scan all messages for the resolution
196
- const approvalResult = messages
197
- .flatMap((m) => m.parts)
198
- .find((p) => p.type === "data-approval-resolved")
199
- ?.data?.result;
200
-
201
- // In your message parts loop:
202
- {message.parts.map((part, i) => {
203
- if (part.type === "data-approval-needed") { // [!code highlight]
204
- const { flightId, passenger, price, toolCallId } = part.data;
205
- if (approvalResult) {
206
- return <div key={i}>Result: {approvalResult}</div>;
207
- }
208
- return (
209
- <div key={i} className="rounded-lg border p-4 space-y-3">
210
- <div className="text-sm">
211
- <div>Flight: {flightId}</div>
212
- <div>Passenger: {passenger}</div>
213
- <div>Price: ${price}</div>
214
- </div>
215
- <div className="flex gap-2">
216
- <button onClick={() => approve(toolCallId)}>Approve</button> {/* [!code highlight] */}
217
- <button onClick={() => reject(toolCallId)}>Reject</button> {/* [!code highlight] */}
154
+ ## Render and answer approval requests
155
+
156
+ Approval requests arrive as typed tool parts with `state: "approval-requested"`. Call `addToolApprovalResponse()` with the approval ID. The AI SDK then sends the updated message history back to the route and `WorkflowAgent` continues the durable tool flow.
157
+
158
+ ```tsx title="app/chat.tsx" lineNumbers
159
+ "use client";
160
+
161
+ import { useChat } from "@ai-sdk/react";
162
+ import { WorkflowChatTransport } from "@ai-sdk/workflow";
163
+ import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
164
+ import { useMemo } from "react";
165
+ import type { BookingAgentUIMessage } from "@/workflows/booking-agent";
166
+
167
+ export function Chat() {
168
+ const transport = useMemo(
169
+ () => new WorkflowChatTransport({ api: "/api/chat" }),
170
+ []
171
+ );
172
+ const { messages, addToolApprovalResponse } = useChat<BookingAgentUIMessage>({
173
+ transport,
174
+ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
175
+ });
176
+
177
+ return messages.map((message) =>
178
+ message.parts.map((part) => {
179
+ if (
180
+ part.type !== "tool-confirmBooking" ||
181
+ part.state !== "approval-requested" ||
182
+ part.approval.isAutomatic
183
+ ) {
184
+ return null;
185
+ }
186
+
187
+ return (
188
+ <div key={part.toolCallId}>
189
+ <p>
190
+ Book flight {part.input.flightId} for {part.input.passenger} at
191
+ ${part.input.price}?
192
+ </p>
193
+ <button
194
+ onClick={() =>
195
+ addToolApprovalResponse({
196
+ id: part.approval.id,
197
+ approved: true,
198
+ })
199
+ }
200
+ >
201
+ Approve
202
+ </button>
203
+ <button
204
+ onClick={() =>
205
+ addToolApprovalResponse({
206
+ id: part.approval.id,
207
+ approved: false,
208
+ })
209
+ }
210
+ >
211
+ Reject
212
+ </button>
218
213
  </div>
219
- </div>
220
- );
221
- }
222
- // Hide the requestBookingApproval tool-invocation part
223
- if (part.type === "tool-invocation" &&
224
- part.toolInvocation.toolName === "requestBookingApproval") {
225
- return null;
226
- }
227
- // ... other part types
228
- })}
214
+ );
215
+ })
216
+ );
217
+ }
229
218
  ```
230
219
 
231
220
  ## How it works
232
221
 
233
- 1. **`defineHook()` with schema** — creates a typed hook with Zod validation. The approval payload is validated before the workflow receives it.
234
- 2. **`toolCallId` as token** — the approval tool uses the tool call ID as the hook token, naturally linking the hook to the specific tool invocation.
235
- 3. **`emitApprovalRequest` step** — writes a `data-approval-needed` custom data part to the stream *before* the hook suspends. Without this, the client would never see the approval controls because tool invocations don't stream until the tool returns.
236
- 4. **No `"use step"` on the approval tool** — the tool runs at the workflow level because `defineHook().create()` is a workflow primitive. It calls step functions (`emitApprovalRequest`, `emitApprovalResolved`, `confirmBooking`) for I/O.
237
- 5. **`Promise.race` with sleep** — the approval races against a durable timeout. If nobody responds, the workflow continues with an expiration message.
238
- 6. **`emitApprovalResolved` step** — writes the outcome to the stream so the client can update the card immediately, without waiting for the tool-invocation result.
222
+ 1. The model calls `confirmBooking` with validated input.
223
+ 2. `needsApproval` prevents the tool's `execute` function from running and emits an approval request.
224
+ 3. The durable stream preserves the request across disconnects and process restarts.
225
+ 4. The client adds an approval response to the conversation.
226
+ 5. If approved, `confirmBooking` runs as a durable step. If rejected, the model receives the denial and can respond without performing the side effect.
239
227
 
240
- ## Adapting to your use case
228
+ ## Adapting the pattern
241
229
 
242
- - **Change the approval schema** — add fields like `reason`, `amount`, `reviewerEmail` to match your domain.
243
- - **Multiple approval gates** — the pattern works for any number of tools. Each tool creates its own hook with its own `toolCallId`.
244
- - **Escalation** — if the first approver doesn't respond, use `sleep()` + another hook to escalate to a backup reviewer.
245
- - **Adjust timeout** — use `"24h"` for production, shorter durations for demos.
246
- - **Workflow-level vs step tools** — tools that use `sleep()`, `defineHook()`, or other workflow primitives must NOT use `"use step"`. Tools with only I/O (API calls, DB queries) should use `"use step"` for retries.
230
+ - **Conditional approval**: Return a boolean from `needsApproval` based on amount, tenant policy, or risk.
231
+ - **Timeouts and escalation**: Combine the surrounding workflow with `sleep()` and hooks when an approval must expire or escalate.
232
+ - **Audit context**: Include durable user and tenant identifiers in the workflow input, then record the approver in your application database.
233
+ - **Multiple gates**: Set `needsApproval` on every consequential tool independently.
247
234
 
248
235
  ## Key APIs
249
236
 
250
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
251
- - [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries
252
- - [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook with schema validation
253
- - [`sleep()`](/docs/api-reference/workflow/sleep) — durable timeout for approval expiry
254
- - [`getWritable()`](/docs/api-reference/workflow/get-writable) — stream custom data parts from steps
255
- - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — durable agent with tool definitions
237
+ - [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): Durable AI SDK agent with first-class tool approvals
238
+ - [`tool()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool): Defines a typed tool and its approval policy
239
+ - [`getWritable()`](/docs/api-reference/workflow/get-writable): Stores durable model-call stream parts
240
+ - [`WorkflowChatTransport`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#resumable-streaming-with-workflowchattransport): Reconnects interrupted chat streams
@@ -5,19 +5,24 @@ type: guide
5
5
  summary: Split items into fixed-size batches, process each batch concurrently with Promise.allSettled, and pace batches with sleep to avoid overloading downstream services.
6
6
  ---
7
7
 
8
+ <CopyPrompt
9
+ text="Implement durable batch processing. Import `sleep` from `workflow`. In an exported &quot;use workflow&quot; function, split the input records into chunks of a fixed `batchSize`. For each batch, call a &quot;use step&quot; helper such as `processRecord(record)` for every record using `Promise.allSettled` so one record failure does not hide the rest. Record successes and failures in a serializable result object. Between batches, `await sleep(&quot;1s&quot;)` or another configured delay to respect downstream rate limits. Make the step idempotent using record IDs or external idempotency keys. Verify all-success, partial-failure, and rate-paced execution paths."
10
+ />
11
+
8
12
  Use batching when you need to process a large list of items in parallel while controlling concurrency. Items are split into fixed-size batches, each batch runs concurrently, and failures in one batch don't affect others.
9
13
 
10
14
  ## When to use this
11
15
 
12
- - Bulk data imports (contacts, orders, products from a CSV)
16
+ - Bulk data imports (contacts, orders, or products from a comma-separated values (CSV) file)
13
17
  - Processing hundreds or thousands of items against external APIs
14
18
  - Calling rate-limited APIs where you need to control concurrency
15
19
  - Any fan-out where you want failure isolation between groups
20
+ - High-concurrency fan-out, where one flat `Promise.all` over the whole list would put more work in flight than your downstream services or the run's event log should carry at once
16
21
 
17
22
  ## How it works
18
23
 
19
24
  1. Records are split into fixed-size batches.
20
- 2. Each batch runs in parallel via `Promise.allSettled` — failures in one record don't affect others.
25
+ 2. Each batch runs in parallel through `Promise.allSettled`, so failures in one record don't affect others.
21
26
  3. A `sleep()` between batches paces requests to avoid overloading downstream services.
22
27
  4. After all batches, a summary is returned with succeeded/failed counts.
23
28
 
@@ -41,7 +46,7 @@ export async function batchImport(records: Record[], batchSize: number) {
41
46
  for (let i = 0; i < records.length; i += batchSize) {
42
47
  const batch = records.slice(i, i + batchSize);
43
48
 
44
- // Run batch in parallel — failures are isolated per record
49
+ // Run batch in parallel: failures are isolated per record
45
50
  const outcomes = await Promise.allSettled( // [!code highlight]
46
51
  batch.map((record) => processRecord(record))
47
52
  );
@@ -85,21 +90,22 @@ async function processRecord(record: Record): Promise<string> {
85
90
 
86
91
  ## Adapting to your use case
87
92
 
88
- - Replace the `Record` type with your actual data shape (orders, images, products, etc.).
89
- - Replace `processRecord()` with your real import logic — DB upserts, API calls, file processing.
93
+ - Replace the `Record` type with your actual data shape, such as orders, images, or products.
94
+ - Replace `processRecord()` with your import logic, such as database upserts, API calls, or file processing.
90
95
  - Tune `batchSize` and the `sleep()` duration to match your downstream rate limits.
91
- - Add or remove tracking as needed — the pattern works with any item type.
96
+ - Add or remove tracking as needed; the pattern works with any item type.
92
97
 
93
98
  ## Tips
94
99
 
95
- - **Use `Promise.allSettled` over `Promise.all`** when you want to continue even if some items fail. `Promise.all` rejects on the first failure; `allSettled` waits for everything and tells you what failed.
96
- - **Tune batch size to your downstream API limits.** If the API allows 10 concurrent requests, use `batchSize: 10`.
97
- - **Add pacing with `sleep()`** between batches to respect rate limits. The sleep is durable — it survives cold starts.
98
- - **Each `processRecord` call is an independent step.** If one fails, it retries up to 3 times without affecting other items in the batch.
100
+ - **Use `Promise.allSettled` instead of `Promise.all`**: Use this pattern when you want to continue even if some items fail. `Promise.all` rejects on the first failure, while `allSettled` waits for everything and identifies failures.
101
+ - **Tune batch size to your downstream API limits**: If the API allows 10 concurrent requests, use `batchSize: 10`.
102
+ - **Batching bounds concurrency, not the run's total size**: Every batch still appends to the same [event log](/docs/how-it-works/event-sourcing#how-fast-a-log-grows), so a long enough list walks one run toward its [run limits](/worlds/vercel#per-run-limits) no matter how small the batches are. To shrink the run itself, bundle more items per step so one step covers many items, or spawn a [child workflow](/cookbook/advanced/child-workflows) per batch. Reach for one of those once a single run would grow past a few thousand events.
103
+ - **Add pacing with `sleep()`**: Add a delay between batches to respect rate limits. The sleep is durable and survives cold starts.
104
+ - **Treat each `processRecord` call as an independent step**: If one call fails, it retries up to three times without affecting other items in the batch.
99
105
 
100
106
  ## Key APIs
101
107
 
102
- - [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
103
- - [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions that run with full Node.js access
104
- - [`sleep()`](/docs/api-reference/workflow/sleep) -- pacing delay between batches
105
- - [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) -- runs items in parallel, isolating failures
108
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps): Marks the orchestrator function.
109
+ - [`"use step"`](/docs/foundations/workflows-and-steps): Marks functions that run with full Node.js access.
110
+ - [`sleep()`](/docs/api-reference/workflow/sleep): Adds a pacing delay between batches.
111
+ - [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled): Runs items in parallel and isolates failures.