workflow 5.0.0-beta.1 → 5.0.0-beta.2

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 (68) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.js +1 -1
  3. package/dist/api.js +1 -1
  4. package/dist/astro.js +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/internal/builtins.js +1 -1
  7. package/dist/internal/class-serialization.js +1 -1
  8. package/dist/internal/errors.js +1 -1
  9. package/dist/nest.js +1 -1
  10. package/dist/next.cjs +1 -1
  11. package/dist/nitro.js +1 -1
  12. package/dist/nuxt.js +1 -1
  13. package/dist/observability.js +1 -1
  14. package/dist/runtime.js +1 -1
  15. package/dist/stdlib.js +1 -1
  16. package/dist/sveltekit.js +1 -1
  17. package/dist/typescript-plugin.cjs +1 -1
  18. package/dist/vite.js +1 -1
  19. package/dist/workflow.js +1 -1
  20. package/docs/ai/resumable-streams.mdx +1 -1
  21. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  22. package/docs/api-reference/workflow/get-workflow-metadata.mdx +34 -0
  23. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  24. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  25. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  26. package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
  27. package/docs/cookbook/advanced/durable-objects.mdx +148 -0
  28. package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
  29. package/docs/cookbook/advanced/meta.json +10 -0
  30. package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
  31. package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
  32. package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
  33. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
  34. package/docs/cookbook/agent-patterns/meta.json +10 -0
  35. package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
  36. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
  37. package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
  38. package/docs/cookbook/common-patterns/batching.mdx +179 -0
  39. package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
  40. package/docs/cookbook/common-patterns/content-router.mdx +207 -0
  41. package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
  42. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  43. package/docs/cookbook/common-patterns/meta.json +15 -0
  44. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  45. package/docs/cookbook/common-patterns/saga.mdx +152 -0
  46. package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
  47. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  48. package/docs/cookbook/index.mdx +41 -0
  49. package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
  50. package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
  51. package/docs/cookbook/integrations/meta.json +4 -0
  52. package/docs/cookbook/integrations/sandbox.mdx +128 -0
  53. package/docs/cookbook/meta.json +5 -0
  54. package/docs/deploying/world/local-world.mdx +1 -1
  55. package/docs/deploying/world/postgres-world.mdx +1 -1
  56. package/docs/deploying/world/vercel-world.mdx +1 -1
  57. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  58. package/docs/getting-started/index.mdx +8 -1
  59. package/docs/getting-started/meta.json +2 -1
  60. package/docs/getting-started/python.mdx +165 -0
  61. package/docs/meta.json +1 -0
  62. package/docs/migration-guides/index.mdx +34 -0
  63. package/docs/migration-guides/meta.json +9 -0
  64. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
  65. package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
  66. package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
  67. package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
  68. package/package.json +13 -13
@@ -0,0 +1,135 @@
1
+ ---
2
+ title: Serializable Steps
3
+ description: Wrap non-serializable objects (like AI model providers) inside step functions so they can cross the workflow boundary.
4
+ type: guide
5
+ summary: Return a callback from a step to defer provider initialization, making non-serializable AI SDK models work inside durable workflows.
6
+ ---
7
+
8
+ <Callout>
9
+ This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.
10
+ </Callout>
11
+
12
+ ## The Problem
13
+
14
+ Workflow functions run inside a sandboxed VM where every value that crosses a function boundary must be serializable (JSON-safe). AI SDK model providers — `openai("gpt-4o")`, `anthropic("claude-sonnet-4-20250514")`, etc. — return complex objects with methods, closures, and internal state. Passing one directly into a step causes a serialization error.
15
+
16
+ ```typescript lineNumbers
17
+ import { openai } from "@ai-sdk/openai";
18
+ import { DurableAgent } from "@workflow/ai/agent";
19
+ import { getWritable } from "workflow";
20
+ import type { UIMessageChunk } from "ai";
21
+
22
+ export async function brokenAgent(prompt: string) {
23
+ "use workflow";
24
+
25
+ const writable = getWritable<UIMessageChunk>();
26
+ const agent = new DurableAgent({
27
+ // This fails — the model object is not serializable
28
+ model: openai("gpt-4o"),
29
+ });
30
+
31
+ await agent.stream({ messages: [{ role: "user", content: prompt }], writable });
32
+ }
33
+ ```
34
+
35
+ ## The Solution: Step-as-Factory
36
+
37
+ Instead of passing the model object, pass a **callback function** that returns the model. Marking that callback with `"use step"` tells the compiler to serialize the *function reference* (which is just a string identifier) rather than its return value. The provider is only instantiated at execution time, inside the step's full Node.js runtime.
38
+
39
+ ```typescript lineNumbers
40
+ import { openai as openaiProvider } from "@ai-sdk/openai";
41
+
42
+ // Returns a step function, not a model object
43
+ export function openai(...args: Parameters<typeof openaiProvider>) {
44
+ return async () => {
45
+ "use step";
46
+ return openaiProvider(...args); // [!code highlight]
47
+ };
48
+ }
49
+ ```
50
+
51
+ The `DurableAgent` receives a function (`() => Promise<LanguageModel>`) instead of a model object. When the agent needs to call the LLM, it invokes the factory inside a step where the real provider can be constructed with full Node.js access.
52
+
53
+ ## How `@workflow/ai` Uses This
54
+
55
+ The `@workflow/ai` package ships pre-wrapped providers for all major AI SDK backends. Each one follows the same pattern:
56
+
57
+ ```typescript lineNumbers
58
+ // packages/ai/src/providers/anthropic.ts
59
+ import { anthropic as anthropicProvider } from "@ai-sdk/anthropic";
60
+
61
+ export function anthropic(...args: Parameters<typeof anthropicProvider>) {
62
+ return async () => {
63
+ "use step";
64
+ return anthropicProvider(...args); // [!code highlight]
65
+ };
66
+ }
67
+ ```
68
+
69
+ This means you import from `@workflow/ai` instead of `@ai-sdk/*` directly:
70
+
71
+ ```typescript lineNumbers
72
+ import { anthropic } from "@workflow/ai/anthropic";
73
+ import { DurableAgent } from "@workflow/ai/agent";
74
+ import { getWritable } from "workflow";
75
+ import type { UIMessageChunk } from "ai";
76
+
77
+ export async function chatAgent(prompt: string) {
78
+ "use workflow";
79
+
80
+ const writable = getWritable<UIMessageChunk>();
81
+ const agent = new DurableAgent({
82
+ model: anthropic("claude-sonnet-4-20250514"), // [!code highlight]
83
+ });
84
+
85
+ await agent.stream({ messages: [{ role: "user", content: prompt }], writable });
86
+ }
87
+ ```
88
+
89
+ ## Writing Your Own Serializable Wrapper
90
+
91
+ Apply the same pattern to any non-serializable dependency. The key rule: **the outer function captures serializable arguments, and the inner `"use step"` function constructs the real object at runtime**.
92
+
93
+ ```typescript lineNumbers
94
+ import type { S3Client as S3ClientType } from "@aws-sdk/client-s3";
95
+
96
+ // The arguments (region, bucket) are plain strings — serializable
97
+ export function createS3Client(region: string) {
98
+ return async (): Promise<S3ClientType> => {
99
+ "use step";
100
+ const { S3Client } = await import("@aws-sdk/client-s3");
101
+ return new S3Client({ region });
102
+ };
103
+ }
104
+
105
+ // Usage in a workflow
106
+ export async function processUpload(region: string, key: string) {
107
+ "use workflow";
108
+
109
+ const getClient = createS3Client(region); // [!code highlight]
110
+ // getClient is a serializable step reference, not an S3Client
111
+ await uploadFile(getClient, key);
112
+ }
113
+
114
+ async function uploadFile(
115
+ getClient: () => Promise<S3ClientType>,
116
+ key: string
117
+ ) {
118
+ "use step";
119
+ const client = await getClient(); // [!code highlight]
120
+ // Now you have a real S3Client with full Node.js access
121
+ await client.send(/* ... */);
122
+ }
123
+ ```
124
+
125
+ ## Why This Works
126
+
127
+ 1. **Compiler transformation**: `"use step"` tells the SWC plugin to extract the function into a separate bundle. The workflow VM only sees a serializable reference (function ID + captured arguments).
128
+ 2. **Closure tracking**: The compiler tracks which variables the step function closes over. Only serializable values (strings, numbers, plain objects) can be captured.
129
+ 3. **Deferred construction**: The actual provider/client is only constructed when the step executes in the Node.js runtime — never in the sandboxed workflow VM.
130
+
131
+ ## Key APIs
132
+
133
+ - [`"use step"`](/docs/api-reference/workflow/use-step) — marks a function for extraction and serialization
134
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
135
+ - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — accepts a model factory for durable AI agent streaming
@@ -0,0 +1,191 @@
1
+ ---
2
+ title: Durable Agent
3
+ description: Replace a stateless AI agent with a durable one that survives crashes, retries tool calls, and streams output.
4
+ type: guide
5
+ summary: Convert an AI SDK Agent into a DurableAgent backed by a workflow, with tools as retryable steps.
6
+ ---
7
+
8
+ Use this pattern to make any AI SDK agent durable. The agent becomes a workflow, tools become steps, and the framework handles retries, streaming, and state persistence automatically.
9
+
10
+ ## Pattern
11
+
12
+ Replace `Agent` with `DurableAgent`, wrap the function in `"use workflow"`, mark each tool with `"use step"`, and stream output through `getWritable()`.
13
+
14
+ ### Simplified
15
+
16
+ ```typescript lineNumbers
17
+ import { DurableAgent } from "@workflow/ai/agent";
18
+ import { getWritable } from "workflow";
19
+ import { z } from "zod";
20
+ import type { ModelMessage, UIMessageChunk } from "ai";
21
+
22
+ declare function searchFlights(args: { from: string; to: string; date: string }): Promise<{ flights: { id: string; price: number }[] }>; // @setup
23
+ declare function bookFlight(args: { flightId: string; passenger: string }): Promise<{ confirmationId: string }>; // @setup
24
+
25
+ export async function flightAgent(messages: ModelMessage[]) {
26
+ "use workflow";
27
+
28
+ const agent = new DurableAgent({ // [!code highlight]
29
+ model: "anthropic/claude-haiku-4.5",
30
+ instructions: "You are a helpful flight booking assistant.",
31
+ tools: {
32
+ searchFlights: {
33
+ description: "Search for available flights",
34
+ inputSchema: z.object({
35
+ from: z.string(),
36
+ to: z.string(),
37
+ date: z.string(),
38
+ }),
39
+ execute: searchFlights,
40
+ },
41
+ bookFlight: {
42
+ description: "Book a specific flight",
43
+ inputSchema: z.object({
44
+ flightId: z.string(),
45
+ passenger: z.string(),
46
+ }),
47
+ execute: bookFlight,
48
+ },
49
+ },
50
+ });
51
+
52
+ await agent.stream({ // [!code highlight]
53
+ messages,
54
+ writable: getWritable<UIMessageChunk>(),
55
+ });
56
+ }
57
+ ```
58
+
59
+ ### Full Implementation
60
+
61
+ ```typescript lineNumbers
62
+ import { DurableAgent } from "@workflow/ai/agent";
63
+ import { getWritable } from "workflow";
64
+ import { z } from "zod";
65
+ import type { ModelMessage, UIMessageChunk } from "ai";
66
+
67
+ // Step: Search flights with full Node.js access and automatic retries
68
+ async function searchFlights({
69
+ from,
70
+ to,
71
+ date,
72
+ }: {
73
+ from: string;
74
+ to: string;
75
+ date: string;
76
+ }) {
77
+ "use step";
78
+
79
+ const response = await fetch(
80
+ `https://api.example.com/flights?from=${from}&to=${to}&date=${date}`
81
+ );
82
+ if (!response.ok) throw new Error(`Search failed: ${response.status}`);
83
+ return response.json();
84
+ }
85
+
86
+ // Step: Book a flight — retries on transient failures
87
+ async function bookFlight({
88
+ flightId,
89
+ passenger,
90
+ }: {
91
+ flightId: string;
92
+ passenger: string;
93
+ }) {
94
+ "use step";
95
+
96
+ const response = await fetch("https://api.example.com/bookings", {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify({ flightId, passenger }),
100
+ });
101
+ if (!response.ok) throw new Error(`Booking failed: ${response.status}`);
102
+ return response.json();
103
+ }
104
+
105
+ // Step: Check flight status
106
+ async function checkStatus({ flightId }: { flightId: string }) {
107
+ "use step";
108
+
109
+ const response = await fetch(
110
+ `https://api.example.com/flights/${flightId}/status`
111
+ );
112
+ return response.json();
113
+ }
114
+
115
+ export async function flightAgent(messages: ModelMessage[]) {
116
+ "use workflow";
117
+
118
+ const writable = getWritable<UIMessageChunk>(); // [!code highlight]
119
+
120
+ const agent = new DurableAgent({ // [!code highlight]
121
+ model: "anthropic/claude-haiku-4.5",
122
+ instructions: "You are a helpful flight booking assistant.",
123
+ tools: {
124
+ searchFlights: {
125
+ description: "Search for available flights between two airports",
126
+ inputSchema: z.object({
127
+ from: z.string().describe("Departure airport code"),
128
+ to: z.string().describe("Arrival airport code"),
129
+ date: z.string().describe("Travel date (YYYY-MM-DD)"),
130
+ }),
131
+ execute: searchFlights,
132
+ },
133
+ bookFlight: {
134
+ description: "Book a specific flight for a passenger",
135
+ inputSchema: z.object({
136
+ flightId: z.string().describe("Flight ID from search results"),
137
+ passenger: z.string().describe("Passenger full name"),
138
+ }),
139
+ execute: bookFlight,
140
+ },
141
+ checkStatus: {
142
+ description: "Check the current status of a flight",
143
+ inputSchema: z.object({
144
+ flightId: z.string().describe("Flight ID to check"),
145
+ }),
146
+ execute: checkStatus,
147
+ },
148
+ },
149
+ });
150
+
151
+ const result = await agent.stream({ // [!code highlight]
152
+ messages,
153
+ writable,
154
+ maxSteps: 10,
155
+ });
156
+
157
+ return { messages: result.messages };
158
+ }
159
+ ```
160
+
161
+ ### API Route
162
+
163
+ ```typescript lineNumbers
164
+ import { createUIMessageStreamResponse } from "ai";
165
+ import { start } from "workflow/api";
166
+ import { flightAgent } from "@/workflows/flight-agent";
167
+ import type { UIMessage } from "ai";
168
+ import { convertToModelMessages } from "ai";
169
+
170
+ export async function POST(req: Request) {
171
+ const { messages }: { messages: UIMessage[] } = await req.json();
172
+ const modelMessages = await convertToModelMessages(messages);
173
+
174
+ const run = await start(flightAgent, [modelMessages]); // [!code highlight]
175
+
176
+ return createUIMessageStreamResponse({
177
+ stream: run.readable,
178
+ headers: {
179
+ "x-workflow-run-id": run.runId,
180
+ },
181
+ });
182
+ }
183
+ ```
184
+
185
+ ## Key APIs
186
+
187
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
188
+ - [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries and full Node.js access
189
+ - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — durable wrapper around AI SDK's Agent
190
+ - [`getWritable()`](/docs/api-reference/workflow/get-writable) — streams agent output to the client
191
+ - [`start()`](/docs/api-reference/workflow-api/start) — starts a workflow run from an API route
@@ -0,0 +1,278 @@
1
+ ---
2
+ title: Human-in-the-Loop
3
+ description: Pause an AI agent to wait for human approval, then resume based on the decision.
4
+ type: guide
5
+ summary: Use defineHook with the tool call ID to suspend an agent for human approval, with an optional timeout.
6
+ ---
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.
9
+
10
+ ## Pattern
11
+
12
+ Create a typed hook using `defineHook()`. When the agent calls the approval tool, the tool creates a hook instance using the tool call ID as the token, then awaits it. The UI renders approval controls, and an API route resumes the hook with the decision.
13
+
14
+ ### Simplified
15
+
16
+ ```typescript lineNumbers
17
+ import { DurableAgent } from "@workflow/ai/agent";
18
+ import { defineHook, sleep, getWritable } from "workflow";
19
+ import { z } from "zod";
20
+ import type { ModelMessage, UIMessageChunk } from "ai";
21
+
22
+ export const bookingApprovalHook = defineHook({
23
+ schema: z.object({
24
+ approved: z.boolean(),
25
+ comment: z.string().optional(),
26
+ }),
27
+ });
28
+
29
+ declare function confirmBooking(args: { flightId: string; passenger: string }): Promise<{ confirmationId: string }>; // @setup
30
+
31
+ // This tool runs at the workflow level (no "use step") because hooks are workflow primitives
32
+ async function requestBookingApproval(
33
+ { flightId, passenger, price }: { flightId: string; passenger: string; price: number },
34
+ { toolCallId }: { toolCallId: string }
35
+ ) {
36
+ const hook = bookingApprovalHook.create({ token: toolCallId }); // [!code highlight]
37
+
38
+ const result = await Promise.race([ // [!code highlight]
39
+ hook.then((payload) => ({ type: "decision" as const, ...payload })),
40
+ sleep("24h").then(() => ({ type: "timeout" as const, approved: false })),
41
+ ]);
42
+
43
+ if (result.type === "timeout") return "Booking request expired after 24 hours.";
44
+ if (!result.approved) return `Booking rejected: ${result.comment || "No reason given"}`;
45
+
46
+ const booking = await confirmBooking({ flightId, passenger });
47
+ return `Booked! Confirmation: ${booking.confirmationId}`;
48
+ }
49
+
50
+ export async function bookingAgent(messages: ModelMessage[]) {
51
+ "use workflow";
52
+
53
+ const agent = new DurableAgent({
54
+ model: "anthropic/claude-haiku-4.5",
55
+ instructions: "You help book flights. Always request approval before booking.",
56
+ tools: {
57
+ requestBookingApproval: {
58
+ description: "Request human approval before booking a flight",
59
+ inputSchema: z.object({
60
+ flightId: z.string(),
61
+ passenger: z.string(),
62
+ price: z.number(),
63
+ }),
64
+ execute: requestBookingApproval,
65
+ },
66
+ },
67
+ });
68
+
69
+ await agent.stream({ // [!code highlight]
70
+ messages,
71
+ writable: getWritable<UIMessageChunk>(),
72
+ });
73
+ }
74
+ ```
75
+
76
+ ### Full Implementation
77
+
78
+ ```typescript lineNumbers
79
+ import { DurableAgent } from "@workflow/ai/agent";
80
+ import { defineHook, sleep, getWritable } from "workflow";
81
+ import { z } from "zod";
82
+ import type { ModelMessage, UIMessageChunk } from "ai";
83
+
84
+ // Define the approval hook with schema validation
85
+ export const bookingApprovalHook = defineHook({
86
+ schema: z.object({
87
+ approved: z.boolean(),
88
+ comment: z.string().optional(),
89
+ }),
90
+ });
91
+
92
+ // Step: Search for flights (full Node.js access, automatic retries)
93
+ async function searchFlights({
94
+ from,
95
+ to,
96
+ date,
97
+ }: {
98
+ from: string;
99
+ to: string;
100
+ date: string;
101
+ }) {
102
+ "use step";
103
+
104
+ // Your real flight search API call here
105
+ await new Promise((resolve) => setTimeout(resolve, 500));
106
+ return {
107
+ flights: [
108
+ { id: "FL-100", airline: "Example Air", price: 299, from, to, date },
109
+ { id: "FL-200", airline: "Demo Airlines", price: 349, from, to, date },
110
+ ],
111
+ };
112
+ }
113
+
114
+ // Step: Confirm the booking after approval
115
+ async function confirmBooking({
116
+ flightId,
117
+ passenger,
118
+ }: {
119
+ flightId: string;
120
+ passenger: string;
121
+ }) {
122
+ "use step";
123
+
124
+ await new Promise((resolve) => setTimeout(resolve, 500));
125
+ return { confirmationId: `CONF-${flightId}-${Date.now().toString(36)}` };
126
+ }
127
+
128
+ // Workflow-level tool: hooks must be created in workflow context, not inside steps
129
+ async function requestBookingApproval(
130
+ {
131
+ flightId,
132
+ passenger,
133
+ price,
134
+ }: { flightId: string; passenger: string; price: number },
135
+ { toolCallId }: { toolCallId: string }
136
+ ) {
137
+ // No "use step" — hooks are workflow-level primitives
138
+
139
+ const hook = bookingApprovalHook.create({ token: toolCallId }); // [!code highlight]
140
+
141
+ // Race: human approval vs. 24-hour timeout
142
+ const result = await Promise.race([ // [!code highlight]
143
+ hook.then((payload) => ({ type: "decision" as const, ...payload })),
144
+ sleep("24h").then(() => ({ type: "timeout" as const, approved: false })),
145
+ ]);
146
+
147
+ if (result.type === "timeout") {
148
+ return "Booking request expired after 24 hours.";
149
+ }
150
+
151
+ if (!result.approved) {
152
+ return `Booking rejected: ${result.comment || "No reason given"}`;
153
+ }
154
+
155
+ // Approved — proceed with booking
156
+ const booking = await confirmBooking({ flightId, passenger });
157
+ return `Flight ${flightId} booked for ${passenger}. Confirmation: ${booking.confirmationId}`;
158
+ }
159
+
160
+ export async function bookingAgent(messages: ModelMessage[]) {
161
+ "use workflow";
162
+
163
+ const writable = getWritable<UIMessageChunk>();
164
+
165
+ const agent = new DurableAgent({
166
+ model: "anthropic/claude-haiku-4.5",
167
+ instructions:
168
+ "You are a flight booking assistant. Search for flights, then request approval before booking.",
169
+ tools: {
170
+ searchFlights: {
171
+ description: "Search for available flights",
172
+ inputSchema: z.object({
173
+ from: z.string().describe("Departure airport code"),
174
+ to: z.string().describe("Arrival airport code"),
175
+ date: z.string().describe("Travel date (YYYY-MM-DD)"),
176
+ }),
177
+ execute: searchFlights,
178
+ },
179
+ requestBookingApproval: {
180
+ description: "Request human approval before booking a flight",
181
+ inputSchema: z.object({
182
+ flightId: z.string().describe("Flight ID to book"),
183
+ passenger: z.string().describe("Passenger name"),
184
+ price: z.number().describe("Total price"),
185
+ }),
186
+ execute: requestBookingApproval,
187
+ },
188
+ },
189
+ });
190
+
191
+ await agent.stream({ messages, writable }); // [!code highlight]
192
+ }
193
+ ```
194
+
195
+ ### API Route for Approvals
196
+
197
+ ```typescript lineNumbers
198
+ import { bookingApprovalHook } from "@/workflows/booking-agent";
199
+
200
+ export async function POST(request: Request) {
201
+ const { toolCallId, approved, comment } = await request.json();
202
+
203
+ // Schema validation happens automatically via defineHook
204
+ await bookingApprovalHook.resume(toolCallId, { approved, comment }); // [!code highlight]
205
+
206
+ return Response.json({ success: true });
207
+ }
208
+ ```
209
+
210
+ ### Approval Component
211
+
212
+ ```tsx lineNumbers
213
+ "use client";
214
+
215
+ import { useState } from "react";
216
+
217
+ export function BookingApproval({
218
+ toolCallId,
219
+ input,
220
+ output,
221
+ }: {
222
+ toolCallId: string;
223
+ input?: { flightId: string; passenger: string; price: number };
224
+ output?: string;
225
+ }) {
226
+ const [comment, setComment] = useState("");
227
+ const [isSubmitting, setIsSubmitting] = useState(false);
228
+
229
+ if (output) {
230
+ return <p className="text-sm text-muted-foreground">{output}</p>;
231
+ }
232
+
233
+ const handleSubmit = async (approved: boolean) => {
234
+ setIsSubmitting(true);
235
+ await fetch("/api/hooks/approval", {
236
+ method: "POST",
237
+ headers: { "Content-Type": "application/json" },
238
+ body: JSON.stringify({ toolCallId, approved, comment }),
239
+ });
240
+ setIsSubmitting(false);
241
+ };
242
+
243
+ return (
244
+ <div className="border rounded-lg p-4 space-y-3">
245
+ {input && (
246
+ <div className="text-sm space-y-1">
247
+ <div>Flight: {input.flightId}</div>
248
+ <div>Passenger: {input.passenger}</div>
249
+ <div>Price: ${input.price}</div>
250
+ </div>
251
+ )}
252
+ <textarea
253
+ value={comment}
254
+ onChange={(e) => setComment(e.target.value)}
255
+ placeholder="Add a comment (optional)..."
256
+ className="w-full border rounded p-2 text-sm"
257
+ rows={2}
258
+ />
259
+ <div className="flex gap-2">
260
+ <button type="button" onClick={() => handleSubmit(true)} disabled={isSubmitting}>
261
+ Approve
262
+ </button>
263
+ <button type="button" onClick={() => handleSubmit(false)} disabled={isSubmitting}>
264
+ Reject
265
+ </button>
266
+ </div>
267
+ </div>
268
+ );
269
+ }
270
+ ```
271
+
272
+ ## Key APIs
273
+
274
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
275
+ - [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries
276
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook with schema validation
277
+ - [`sleep()`](/docs/api-reference/workflow/sleep) — durable timeout for approval expiry
278
+ - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — durable agent with tool definitions
@@ -0,0 +1,10 @@
1
+ {
2
+ "title": "Agent Patterns",
3
+ "pages": [
4
+ "durable-agent",
5
+ "tool-streaming",
6
+ "human-in-the-loop",
7
+ "tool-orchestration",
8
+ "stop-workflow"
9
+ ]
10
+ }