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.
- package/README.md +68 -23
- package/dist/api-workflow.d.ts +3 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +2 -1
- package/dist/api.d.ts +5 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +6 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/internal/builtins.d.ts +4 -4
- package/dist/internal/builtins.js +6 -6
- package/dist/internal/errors.d.ts +1 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +2 -2
- package/dist/nest-builder.d.ts +2 -0
- package/dist/nest-builder.d.ts.map +1 -0
- package/dist/nest-builder.js +2 -0
- package/dist/nest-vercel-builder.d.ts +2 -0
- package/dist/nest-vercel-builder.d.ts.map +1 -0
- package/dist/nest-vercel-builder.js +2 -0
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +4 -1
- package/docs/advanced/dynamic-workflows.mdx +224 -0
- package/docs/ai/chat-session-modeling.mdx +176 -422
- package/docs/ai/defining-tools.mdx +6 -7
- package/docs/ai/human-in-the-loop.mdx +11 -11
- package/docs/ai/index.mdx +67 -72
- package/docs/ai/message-queueing.mdx +71 -110
- package/docs/ai/meta.json +1 -0
- package/docs/ai/resumable-streams.mdx +40 -28
- package/docs/ai/sleep-and-delays.mdx +10 -10
- package/docs/ai/streaming-updates-from-tools.mdx +6 -6
- package/docs/api-reference/index.mdx +25 -1
- package/docs/api-reference/meta.json +8 -0
- package/docs/api-reference/vitest/index.mdx +68 -15
- package/docs/api-reference/workflow/create-hook.mdx +166 -10
- package/docs/api-reference/workflow/create-webhook.mdx +16 -15
- package/docs/api-reference/workflow/define-hook.mdx +37 -33
- package/docs/api-reference/workflow/fatal-error.mdx +30 -8
- package/docs/api-reference/workflow/fetch.mdx +14 -10
- package/docs/api-reference/workflow/get-step-metadata.mdx +2 -2
- package/docs/api-reference/workflow/get-workflow-metadata.mdx +3 -3
- package/docs/api-reference/workflow/get-writable.mdx +7 -7
- package/docs/api-reference/workflow/index.mdx +4 -1
- package/docs/api-reference/workflow/retryable-error.mdx +1 -1
- package/docs/api-reference/workflow/set-attributes.mdx +63 -0
- package/docs/api-reference/workflow/sleep.mdx +4 -4
- package/docs/api-reference/workflow-ai/durable-agent.mdx +63 -101
- package/docs/api-reference/workflow-ai/index.mdx +5 -5
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +67 -24
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +28 -12
- package/docs/api-reference/workflow-api/get-run.mdx +43 -8
- package/docs/api-reference/workflow-api/index.mdx +8 -9
- package/docs/api-reference/workflow-api/register-lifecycle-hooks.mdx +87 -0
- package/docs/api-reference/workflow-api/resume-hook.mdx +73 -12
- package/docs/api-reference/workflow-api/resume-webhook.mdx +11 -9
- package/docs/api-reference/workflow-api/start.mdx +107 -12
- package/docs/api-reference/workflow-astro/index.mdx +18 -0
- package/docs/api-reference/workflow-astro/meta.json +4 -0
- package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
- package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
- package/docs/api-reference/workflow-errors/hook-force-claimed-error.mdx +70 -0
- package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
- package/docs/api-reference/workflow-errors/index.mdx +91 -0
- package/docs/api-reference/workflow-errors/meta.json +7 -0
- package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -0
- package/docs/api-reference/workflow-errors/run-expired-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +5 -5
- package/docs/api-reference/workflow-errors/throttle-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/too-early-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
- package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +5 -6
- package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +13 -6
- package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +12 -5
- package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-world-error.mdx +8 -8
- package/docs/api-reference/workflow-globals.mdx +15 -11
- package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +37 -0
- package/docs/api-reference/workflow-nest/index.mdx +31 -0
- package/docs/api-reference/workflow-nest/meta.json +9 -0
- package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
- package/docs/api-reference/workflow-nest/workflow-controller.mdx +46 -0
- package/docs/api-reference/workflow-nest/workflow-module.mdx +134 -0
- package/docs/api-reference/workflow-next/with-workflow.mdx +39 -17
- package/docs/api-reference/workflow-nitro/index.mdx +60 -0
- package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
- package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
- package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
- package/docs/api-reference/workflow-observability/index.mdx +62 -0
- package/docs/api-reference/workflow-observability/meta.json +11 -0
- package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
- package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
- package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
- package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
- package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
- package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +11 -14
- package/docs/api-reference/workflow-runtime/health-check.mdx +51 -0
- package/docs/api-reference/workflow-runtime/index.mdx +41 -0
- package/docs/api-reference/workflow-runtime/meta.json +12 -0
- package/docs/api-reference/workflow-runtime/set-world.mdx +51 -0
- package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +43 -0
- package/docs/api-reference/workflow-runtime/world/analytics.mdx +315 -0
- package/docs/api-reference/workflow-runtime/world/index.mdx +60 -0
- package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
- package/docs/api-reference/workflow-runtime/world/queue.mdx +88 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +104 -35
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +8 -8
- package/docs/api-reference/workflow-serde/index.mdx +1 -2
- package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +3 -4
- package/docs/api-reference/workflow-serde/workflow-serialize.mdx +8 -8
- package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
- package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
- package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
- package/docs/api-reference/workflow-vite/index.mdx +18 -0
- package/docs/api-reference/workflow-vite/meta.json +4 -0
- package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
- package/docs/changelog/attributes-mvp.mdx +61 -46
- package/docs/changelog/batched-event-writes.mdx +79 -0
- package/docs/changelog/eager-processing.mdx +110 -436
- package/docs/changelog/index.mdx +4 -2
- package/docs/changelog/lazy-event-creation.md +127 -0
- package/docs/changelog/lazy-hook-resume.mdx +78 -0
- package/docs/changelog/meta.json +11 -1
- package/docs/changelog/resilient-resume.mdx +32 -0
- package/docs/changelog/resilient-start.mdx +33 -285
- package/docs/changelog/step-message-ownership.mdx +360 -0
- package/docs/changelog/turbo-mode.md +87 -0
- package/docs/comparisons/index.mdx +66 -0
- package/docs/comparisons/meta.json +11 -0
- package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
- package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
- package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
- package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
- package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
- package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +104 -0
- package/docs/configuration/build-and-diagnostics.mdx +79 -0
- package/docs/configuration/cli-and-web-ui.mdx +241 -0
- package/docs/configuration/framework-options.mdx +165 -0
- package/docs/configuration/index.mdx +32 -0
- package/docs/configuration/meta.json +12 -0
- package/docs/configuration/runtime-tuning.mdx +399 -0
- package/docs/configuration/worlds.mdx +315 -0
- package/docs/cookbook/advanced/child-workflows.mdx +33 -25
- package/docs/cookbook/advanced/publishing-libraries.mdx +65 -56
- package/docs/cookbook/advanced/serializable-steps.mdx +48 -68
- package/docs/cookbook/advanced/upgrading-workflows.mdx +35 -31
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +78 -60
- package/docs/cookbook/agent-patterns/durable-agent.mdx +23 -135
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +180 -195
- package/docs/cookbook/common-patterns/batching.mdx +20 -14
- package/docs/cookbook/common-patterns/idempotency.mdx +41 -53
- package/docs/cookbook/common-patterns/rate-limiting.mdx +8 -4
- package/docs/cookbook/common-patterns/saga.mdx +23 -19
- package/docs/cookbook/common-patterns/scheduling.mdx +30 -22
- package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +29 -25
- package/docs/cookbook/common-patterns/timeouts.mdx +26 -21
- package/docs/cookbook/common-patterns/webhooks.mdx +10 -6
- package/docs/cookbook/common-patterns/workflow-composition.mdx +27 -17
- package/docs/cookbook/index.mdx +22 -22
- package/docs/cookbook/integrations/ai-sdk.mdx +63 -48
- package/docs/cookbook/integrations/chat-sdk.mdx +46 -33
- package/docs/cookbook/integrations/sandbox.mdx +58 -45
- package/docs/deploying.mdx +106 -0
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +16 -12
- package/docs/errors/corrupted-event-log.mdx +39 -18
- package/docs/errors/deployment-mismatch.mdx +71 -0
- package/docs/errors/fetch-in-workflow.mdx +15 -14
- package/docs/errors/hook-conflict.mdx +38 -11
- package/docs/errors/hook-force-claimed.mdx +96 -0
- package/docs/errors/index.mdx +24 -37
- package/docs/errors/node-js-module-in-workflow.mdx +9 -5
- package/docs/errors/replay-divergence.mdx +27 -0
- package/docs/errors/run-expired.mdx +85 -0
- package/docs/errors/runtime-decryption-failed.mdx +77 -0
- package/docs/errors/serialization-failed.mdx +44 -12
- package/docs/errors/start-invalid-workflow-function.mdx +9 -5
- package/docs/errors/step-executed-multiple-times.mdx +23 -0
- package/docs/errors/step-not-registered.mdx +6 -6
- package/docs/errors/timeout-in-workflow.mdx +12 -8
- package/docs/errors/webhook-invalid-respond-with-value.mdx +18 -18
- package/docs/errors/webhook-response-not-sent.mdx +20 -16
- package/docs/errors/workflow-not-registered.mdx +5 -5
- package/docs/foundations/cancellation.mdx +31 -32
- package/docs/foundations/errors-and-retries.mdx +54 -11
- package/docs/foundations/hooks.mdx +98 -35
- package/docs/foundations/idempotency.mdx +267 -12
- package/docs/foundations/index.mdx +1 -26
- package/docs/foundations/serialization.mdx +22 -22
- package/docs/foundations/starting-workflows.mdx +104 -30
- package/docs/foundations/streaming.mdx +108 -60
- package/docs/foundations/versioning.mdx +4 -4
- package/docs/foundations/workflows-and-steps.mdx +9 -9
- package/docs/getting-started/astro.mdx +22 -18
- package/docs/getting-started/express.mdx +15 -11
- package/docs/getting-started/fastify.mdx +15 -11
- package/docs/getting-started/hono.mdx +15 -11
- package/docs/getting-started/index.mdx +10 -3
- package/docs/getting-started/meta.json +3 -1
- package/docs/getting-started/nestjs.mdx +264 -21
- package/docs/getting-started/next.mdx +18 -14
- package/docs/getting-started/nitro.mdx +22 -18
- package/docs/getting-started/nuxt.mdx +15 -11
- package/docs/getting-started/python.mdx +190 -41
- package/docs/getting-started/react-router/index.mdx +33 -0
- package/docs/getting-started/react-router/meta.json +5 -0
- package/docs/getting-started/react-router/v7.mdx +237 -0
- package/docs/getting-started/react-router/v8.mdx +232 -0
- package/docs/getting-started/sveltekit.mdx +20 -16
- package/docs/getting-started/tanstack-start.mdx +17 -13
- package/docs/getting-started/vite.mdx +15 -11
- package/docs/how-it-works/cancellation.mdx +63 -63
- package/docs/how-it-works/code-transform.mdx +82 -66
- package/docs/how-it-works/encryption.mdx +30 -26
- package/docs/how-it-works/event-sourcing.mdx +132 -35
- package/docs/how-it-works/framework-integrations.mdx +96 -337
- package/docs/how-it-works/understanding-directives.mdx +22 -22
- package/docs/internal/index.mdx +6 -4
- package/docs/internal/meta.json +6 -1
- package/docs/internal/nitro-native-build.mdx +38 -0
- package/docs/internal/nitro-web-ui.mdx +24 -0
- package/docs/internal/serializable-abort-controller.mdx +7 -7
- package/docs/meta.json +4 -2
- package/docs/observability/attributes.mdx +136 -0
- package/docs/observability/index.mdx +32 -10
- package/docs/observability/lifecycle-hooks.mdx +95 -0
- package/docs/observability/meta.json +1 -1
- package/docs/observability/retention.mdx +95 -0
- package/docs/observability/tracing.mdx +124 -0
- package/docs/testing/index.mdx +120 -38
- package/docs/testing/server-based.mdx +10 -10
- package/docs/whats-new.mdx +190 -0
- package/docs/worlds/building-a-world.mdx +538 -0
- package/docs/worlds/local.mdx +129 -0
- package/docs/worlds/meta.json +10 -0
- package/docs/worlds/postgres.mdx +424 -0
- package/docs/worlds/upgrading-to-v5.mdx +162 -0
- package/docs/worlds/vercel.mdx +345 -0
- package/package.json +17 -14
- package/docs/api-reference/workflow-api/world/index.mdx +0 -58
- package/docs/api-reference/workflow-api/world/meta.json +0 -4
- package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
- package/docs/api-reference/workflow-api/world/queue.mdx +0 -86
- package/docs/deploying/building-a-world.mdx +0 -251
- package/docs/deploying/index.mdx +0 -95
- package/docs/deploying/meta.json +0 -4
- package/docs/deploying/world/local-world.mdx +0 -84
- package/docs/deploying/world/meta.json +0 -4
- package/docs/deploying/world/postgres-world.mdx +0 -224
- package/docs/deploying/world/vercel-world.mdx +0 -181
- package/docs/migration-guides/index.mdx +0 -34
- package/docs/migration-guides/meta.json +0 -9
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -358
- package/docs/migration-guides/migrating-from-inngest.mdx +0 -304
- package/docs/migration-guides/migrating-from-temporal.mdx +0 -313
- 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
|
|
3
|
+
description: Pause a WorkflowAgent for human approval before a consequential tool executes.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Use
|
|
5
|
+
summary: Use WorkflowAgent's needsApproval option and AI SDK approval responses to build durable human approval flows.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
|
|
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 `"use step"` 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
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
42
|
+
|
|
43
|
+
const response = await fetch(
|
|
44
44
|
`https://api.example.com/flights?from=${from}&to=${to}&date=${date}`
|
|
45
45
|
);
|
|
46
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
143
|
-
model: "
|
|
144
|
-
instructions: "
|
|
145
|
-
tools:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
103
|
+
return agent.stream({
|
|
168
104
|
messages,
|
|
169
|
-
writable: getWritable<
|
|
105
|
+
writable: getWritable<ModelCallStreamPart>(),
|
|
170
106
|
});
|
|
171
107
|
}
|
|
172
108
|
```
|
|
173
109
|
|
|
174
|
-
|
|
110
|
+
## Sign approval requests
|
|
175
111
|
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
const { toolCallId, approved, comment } = await req.json();
|
|
116
|
+
Signed approvals require `@ai-sdk/workflow` 2.0.16 or later.
|
|
183
117
|
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
```tsx
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
234
|
-
2.
|
|
235
|
-
3.
|
|
236
|
-
4.
|
|
237
|
-
5.
|
|
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
|
|
228
|
+
## Adapting the pattern
|
|
241
229
|
|
|
242
|
-
- **
|
|
243
|
-
- **
|
|
244
|
-
- **
|
|
245
|
-
- **
|
|
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
|
-
- [`
|
|
251
|
-
- [`
|
|
252
|
-
- [`
|
|
253
|
-
- [`
|
|
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 "use workflow" function, split the input records into chunks of a fixed `batchSize`. For each batch, call a "use step" 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("1s")` 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
|
|
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
|
|
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
|
|
89
|
-
- Replace `processRecord()` with your
|
|
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
|
|
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`
|
|
96
|
-
- **Tune batch size to your downstream API limits
|
|
97
|
-
- **
|
|
98
|
-
- **
|
|
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)
|
|
103
|
-
- [`"use step"`](/docs/foundations/workflows-and-steps)
|
|
104
|
-
- [`sleep()`](/docs/api-reference/workflow/sleep)
|
|
105
|
-
- [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled)
|
|
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.
|