workflow 5.0.0-beta.50 → 5.0.0-beta.52

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.
@@ -1,46 +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
8
  <CopyPrompt
9
- text="Add a human approval gate to this AI workflow. Define a typed hook with `defineHook()` from `workflow` for approval payloads. At the approval point, create the hook once with a stable token, await it inside the `&quot;use workflow&quot;` function, and branch on approved/rejected input. Add a server route that receives the human decision and calls `resumeHook(token, payload)` from `workflow/api`. If the approval should expire, race the hook against `sleep()` from `workflow`. Update the UI to show the pending approval and call the resume route. Verify approve, reject, timeout, and duplicate resume behavior."
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
10
  />
11
11
 
12
- <Callout type="warn">
13
- This recipe uses the deprecated `DurableAgent` API. For new agents, use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) and follow the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent). The human-in-the-loop pattern here (hooks, `Promise.race`, approval gating) applies to either API.
14
- </Callout>
15
-
16
- 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 through a user interface (UI) or API.
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.
17
13
 
18
14
  ## When to use this
19
15
 
20
16
  - Booking confirmations where users must approve before charges are made
21
17
  - Content publishing gates where an editor must sign off
22
- - Agent actions where the cost of an error justifies a human check
23
- - Actions with side effects that are difficult to reverse
24
-
25
- ## Pattern
26
-
27
- 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.
28
-
29
- ### Workflow
30
-
31
- ```typescript
32
- import { DurableAgent } from "@workflow/ai/agent";
33
- 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";
34
34
  import { z } from "zod";
35
- import type { ModelMessage, UIMessageChunk } from "ai";
36
-
37
- // Exported so the approval API route can call .resume()
38
- export const bookingApprovalHook = defineHook({ // [!code highlight]
39
- schema: z.object({
40
- approved: z.boolean(),
41
- comment: z.string().optional(),
42
- }),
43
- });
44
35
 
45
36
  async function searchFlights({ from, to, date }: {
46
37
  from: string;
@@ -48,216 +39,202 @@ async function searchFlights({ from, to, date }: {
48
39
  date: string;
49
40
  }) {
50
41
  "use step";
51
- const res = await fetch(
42
+
43
+ const response = await fetch(
52
44
  `https://api.example.com/flights?from=${from}&to=${to}&date=${date}`
53
45
  );
54
- return res.json();
46
+ return response.json();
55
47
  }
56
48
 
57
- async function confirmBooking({ flightId, passenger }: {
49
+ async function confirmBooking({ flightId, passenger, price }: {
58
50
  flightId: string;
59
51
  passenger: string;
52
+ price: number;
60
53
  }) {
61
54
  "use step";
62
- const res = await fetch("https://api.example.com/bookings", {
55
+
56
+ const response = await fetch("https://api.example.com/bookings", {
63
57
  method: "POST",
64
- body: JSON.stringify({ flightId, passenger }),
58
+ body: JSON.stringify({ flightId, passenger, price }),
65
59
  });
66
- return res.json();
67
- }
68
-
69
- // Stream a custom data part so the client can render the approval UI.
70
- // This MUST run before the hook suspends the workflow, otherwise
71
- // the tool-invocation won't appear in the stream until the tool returns,
72
- // and the client would have no way to show approval buttons.
73
- async function emitApprovalRequest(details: {
74
- flightId: string;
75
- passenger: string;
76
- price: number;
77
- toolCallId: string;
78
- }) {
79
- "use step";
80
- const writer = getWritable<UIMessageChunk>().getWriter();
81
- try {
82
- await writer.write({
83
- type: "data-approval-needed", // [!code highlight]
84
- id: details.toolCallId,
85
- data: details,
86
- } as UIMessageChunk);
87
- } finally {
88
- writer.releaseLock();
89
- }
60
+ return response.json();
90
61
  }
91
62
 
92
- // Stream the resolution so the client can update the approval card.
93
- async function emitApprovalResolved(details: {
94
- toolCallId: string;
95
- result: string;
96
- }) {
97
- "use step";
98
- const writer = getWritable<UIMessageChunk>().getWriter();
99
- try {
100
- await writer.write({
101
- type: "data-approval-resolved", // [!code highlight]
102
- id: details.toolCallId,
103
- data: details,
104
- } as UIMessageChunk);
105
- } finally {
106
- writer.releaseLock();
107
- }
108
- }
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
+ };
109
84
 
110
- // No "use step": hooks are workflow-level primitives
111
- async function requestBookingApproval(
112
- { flightId, passenger, price }: {
113
- flightId: string;
114
- passenger: string;
115
- price: number;
116
- },
117
- { toolCallId }: { toolCallId: string }
118
- ) {
119
- // Emit to the stream before suspending so the UI can show buttons
120
- await emitApprovalRequest({ flightId, passenger, price, toolCallId }); // [!code highlight]
121
-
122
- const hook = bookingApprovalHook.create({ token: toolCallId });
123
-
124
- // Race: human decision vs. timeout
125
- const result = await Promise.race([
126
- hook.then((payload) => ({ type: "decision" as const, ...payload })),
127
- sleep("24h").then(() => ({ type: "timeout" as const, approved: false as const })),
128
- ]);
129
-
130
- if (result.type === "timeout") {
131
- const msg = "Booking request expired.";
132
- await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
133
- return msg;
134
- }
135
- if (!result.approved) {
136
- const msg = `Rejected: ${result.comment || "No reason given"}`;
137
- await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
138
- return msg;
139
- }
140
-
141
- const booking = await confirmBooking({ flightId, passenger });
142
- const msg = `Booked! Confirmation: ${booking.confirmationId}`;
143
- await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
144
- return msg;
145
- }
85
+ export type BookingAgentUIMessage = UIMessage<
86
+ unknown,
87
+ never,
88
+ InferUITools<typeof bookingTools>
89
+ >;
146
90
 
147
91
  export async function bookingAgent(messages: ModelMessage[]) {
148
92
  "use workflow";
149
93
 
150
- const agent = new DurableAgent({
151
- model: "anthropic/claude-haiku-4.5",
152
- instructions: "You help book flights. Always request approval before booking.",
153
- tools: {
154
- searchFlights: {
155
- description: "Search for available flights",
156
- inputSchema: z.object({
157
- from: z.string().describe("Departure airport code"),
158
- to: z.string().describe("Arrival airport code"),
159
- date: z.string().describe("Travel date (YYYY-MM-DD)"),
160
- }),
161
- execute: searchFlights,
162
- },
163
- requestBookingApproval: {
164
- description: "Request human approval before booking a flight",
165
- inputSchema: z.object({
166
- flightId: z.string().describe("Flight ID to book"),
167
- passenger: z.string().describe("Passenger name"),
168
- price: z.number().describe("Total price"),
169
- }),
170
- execute: requestBookingApproval,
171
- },
172
- },
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]
173
101
  });
174
102
 
175
- await agent.stream({
103
+ return agent.stream({
176
104
  messages,
177
- writable: getWritable<UIMessageChunk>(),
105
+ writable: getWritable<ModelCallStreamPart>(),
178
106
  });
179
107
  }
180
108
  ```
181
109
 
182
- ### Approval API route
110
+ ## Sign approval requests
183
111
 
184
- 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.
185
113
 
186
- ```typescript
187
- 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.
188
115
 
189
- export async function POST(req: Request) {
190
- const { toolCallId, approved, comment } = await req.json();
116
+ Signed approvals require `@ai-sdk/workflow` 2.0.16 or later.
191
117
 
192
- 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:
193
119
 
194
- 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
+ });
195
151
  }
196
152
  ```
197
153
 
198
- ### Client rendering
199
-
200
- 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.
201
-
202
- ```tsx
203
- // Scan all messages for the resolution
204
- const approvalResult = messages
205
- .flatMap((m) => m.parts)
206
- .find((p) => p.type === "data-approval-resolved")
207
- ?.data?.result;
208
-
209
- // In your message parts loop:
210
- {message.parts.map((part, i) => {
211
- if (part.type === "data-approval-needed") { // [!code highlight]
212
- const { flightId, passenger, price, toolCallId } = part.data;
213
- if (approvalResult) {
214
- return <div key={i}>Result: {approvalResult}</div>;
215
- }
216
- return (
217
- <div key={i} className="rounded-lg border p-4 space-y-3">
218
- <div className="text-sm">
219
- <div>Flight: {flightId}</div>
220
- <div>Passenger: {passenger}</div>
221
- <div>Price: ${price}</div>
222
- </div>
223
- <div className="flex gap-2">
224
- <button onClick={() => approve(toolCallId)}>Approve</button> {/* [!code highlight] */}
225
- <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>
226
213
  </div>
227
- </div>
228
- );
229
- }
230
- // Hide the requestBookingApproval tool-invocation part
231
- if (part.type === "tool-invocation" &&
232
- part.toolInvocation.toolName === "requestBookingApproval") {
233
- return null;
234
- }
235
- // ... other part types
236
- })}
214
+ );
215
+ })
216
+ );
217
+ }
237
218
  ```
238
219
 
239
220
  ## How it works
240
221
 
241
- 1. **`defineHook()` with schema**: Creates a typed hook with Zod validation. The approval payload is validated before the workflow receives it.
242
- 2. **`toolCallId` as token**: Uses the tool call ID as the hook token, linking the hook to the specific tool invocation.
243
- 3. **`emitApprovalRequest` step**: Writes a `data-approval-needed` custom data part to the stream *before* the hook suspends. Without this step, the client wouldn't see the approval controls because tool invocations don't stream until the tool returns.
244
- 4. **No `"use step"` on the approval tool**: Runs the tool at the workflow level because `defineHook().create()` is a workflow primitive. The tool calls step functions (`emitApprovalRequest`, `emitApprovalResolved`, and `confirmBooking`) for I/O.
245
- 5. **`Promise.race` with sleep**: Races the approval against a durable timeout. If nobody responds, the workflow continues with an expiration message.
246
- 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.
247
227
 
248
- ## Adapting to your use case
228
+ ## Adapting the pattern
249
229
 
250
- - **Change the approval schema**: Add fields such as `reason`, `amount`, and `reviewerEmail` to match your domain.
251
- - **Multiple approval gates**: Apply the pattern to any number of tools. Each tool creates its own hook with its own `toolCallId`.
252
- - **Escalation**: If the first approver doesn't respond, use `sleep()` and another hook to escalate to a backup reviewer.
253
- - **Adjust the timeout**: Use `"24h"` for production and shorter durations for demos.
254
- - **Workflow-level versus step tools**: Tools that use `sleep()`, `defineHook()`, or other workflow primitives must not use `"use step"`. Tools with only I/O, such as API calls and database 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.
255
234
 
256
235
  ## Key APIs
257
236
 
258
- - [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions): Declares the orchestrator function.
259
- - [`"use step"`](/docs/foundations/workflows-and-steps#step-functions): Declares step functions with retries.
260
- - [`defineHook()`](/docs/api-reference/workflow/define-hook): Defines a type-safe hook with schema validation.
261
- - [`sleep()`](/docs/api-reference/workflow/sleep): Provides a durable timeout for approval expiration.
262
- - [`getWritable()`](/docs/api-reference/workflow/get-writable): Streams custom data parts from steps.
263
- - [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): Provides AI SDK's durable agent and replaces `DurableAgent`.
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
@@ -91,7 +91,7 @@ async function runTurn(messages: ModelMessage[]) {
91
91
  "use step";
92
92
 
93
93
  const result = streamText({
94
- model: "anthropic/claude-haiku-4.5",
94
+ model: "spacexai/grok-4.6",
95
95
  system: "You are a customer support agent.",
96
96
  messages,
97
97
  tools: TOOLS,
@@ -373,7 +373,7 @@ This example stores the `runId` after the first response. For strict one-session
373
373
  | **Tool call durability** | Not individually durable: re-executes with the parent turn | Per tool: mark `"use step"` for a durable, retryable step, or keep at workflow level for `sleep()` / hooks |
374
374
  | **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` |
375
375
  | **Structured output** | `Output.object()`, `Output.array()` | `output` (`Output.object()`, `Output.text()`) |
376
- | **Step callbacks** | `onStepFinish`, `onChunk`, and others | `onStepFinish`, `onFinish`, `onError`, `onAbort` (`onChunk` not available) |
376
+ | **Step callbacks** | `onStepFinish`, `onChunk`, and others | `onStepEnd`, `onEnd`, `onError`, `onAbort` (`onChunk` not available) |
377
377
  | **Setup** | Manual stream piping and turn slicing | Automatic |
378
378
 
379
379
  Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the raw AI SDK surface or a per-turn durability boundary.
@@ -18,9 +18,19 @@ This is a **workflow-level fatal error**. It cannot be caught or handled inside
18
18
  For replay divergence:
19
19
 
20
20
  ```text
21
- Workflow replay diverged <divergenceCount> times after <maxRecoveryReplays> recovery replays; latest divergent event was <eventId>. Last divergence: <details>
21
+ Workflow replay diverged <divergenceCount> times after <maxRecoveryReplays> recovery replays; latest divergent event was <eventId>; divergent event ids: <eventId>, <eventId>, ... Last divergence: <details>
22
22
  ```
23
23
 
24
+ The `divergent event ids` list has one entry per divergence in the recovery chain, oldest first. Every recovery replay diverging at the same event points at a fixed disagreement between the log and the code; ids that wander point at a race with another writer.
25
+
26
+ `<details>` is the last divergence's own message. When the replay could not place an event, it names the invocation that was pending under that event's position at the time, and where the replay's walk over the log stood:
27
+
28
+ ```text
29
+ Replay could not consume event: eventType=wait_created, correlationId=wait_<id>, eventId=<eventId>. pending at this id: step <stepName> (step_<id>). consumer: index=<n>, length=<n>, parked=<n>, lastConsumed=<eventId>
30
+ ```
31
+
32
+ Steps, sleeps and hooks draw their ids from one sequence, so `pending at this id` reports the entity the replay put at that position, whatever its kind. In the example, the log recorded a sleep where this replay reached a step named `<stepName>`.
33
+
24
34
  For an unreadable stored payload:
25
35
 
26
36
  ```text
@@ -35,14 +35,13 @@ Import the `fetch` step function from the `workflow` package and assign it to `g
35
35
 
36
36
  ```typescript lineNumbers title="workflows/ai.ts"
37
37
  import { generateText } from "ai";
38
- import { openai } from "@ai-sdk/openai";
39
38
 
40
39
  export async function chatWorkflow(prompt: string) {
41
40
  "use workflow";
42
41
 
43
42
  // Error - generateText() calls fetch() under the hood
44
43
  const result = await generateText({ // [!code highlight]
45
- model: openai("gpt-4"), // [!code highlight]
44
+ model: "spacexai/grok-4.6", // [!code highlight]
46
45
  prompt, // [!code highlight]
47
46
  }); // [!code highlight]
48
47
 
@@ -54,7 +53,6 @@ export async function chatWorkflow(prompt: string) {
54
53
 
55
54
  ```typescript lineNumbers title="workflows/ai.ts"
56
55
  import { generateText } from "ai";
57
- import { openai } from "@ai-sdk/openai";
58
56
  import { fetch } from "workflow"; // [!code highlight]
59
57
 
60
58
  export async function chatWorkflow(prompt: string) {
@@ -64,7 +62,7 @@ export async function chatWorkflow(prompt: string) {
64
62
 
65
63
  // Now generateText() can make HTTP requests via the fetch step
66
64
  const result = await generateText({
67
- model: openai("gpt-4"),
65
+ model: "spacexai/grok-4.6",
68
66
  prompt,
69
67
  });
70
68
 
@@ -80,7 +78,6 @@ This is the most common scenario - using AI SDK functions that make HTTP request
80
78
 
81
79
  ```typescript lineNumbers
82
80
  import { generateText, streamText } from "ai";
83
- import { openai } from "@ai-sdk/openai";
84
81
  import { fetch } from "workflow"; // [!code highlight]
85
82
 
86
83
  export async function aiWorkflow(userMessage: string) {
@@ -88,9 +85,9 @@ export async function aiWorkflow(userMessage: string) {
88
85
 
89
86
  globalThis.fetch = fetch; // [!code highlight]
90
87
 
91
- // generateText makes HTTP requests to OpenAI
88
+ // generateText makes an HTTP request under the hood
92
89
  const response = await generateText({
93
- model: openai("gpt-4"),
90
+ model: "spacexai/grok-4.6",
94
91
  prompt: userMessage,
95
92
  });
96
93
 
@@ -42,8 +42,8 @@ Two ways to reach it:
42
42
 
43
43
  ## How to respond
44
44
 
45
- `RunExpiredError` is terminal. Retrying will not bring the data back, so catch
46
- Catch the error and use the run's metadata to decide what you want to do.
45
+ `RunExpiredError` is terminal. Retrying will not bring the data back. Catch it
46
+ and use the run's metadata to decide what you want to do.
47
47
 
48
48
  ```typescript lineNumbers
49
49
  import { getRun } from "workflow/api"
@@ -73,10 +73,10 @@ them.
73
73
  ### If you need the result of a zero-retention run
74
74
 
75
75
  Do not read it back off the run. Send it somewhere you control while the run
76
- is still executing — a step that writes it to your own store. That is the intended pattern
77
- for `experimental_retention: 0`: the point of the option is that the platform
78
- does not keep your data, so the platform cannot also be where you fetch it
79
- from afterwards.
76
+ is still executing — a step that writes it to your own store. That is the
77
+ intended pattern for `experimental_retention: 0`: the point of the option is
78
+ that the platform does not keep your data, so the platform cannot also be where
79
+ you fetch it from afterwards.
80
80
 
81
81
  ## Related
82
82
 
@@ -419,7 +419,7 @@ export async function aiAssistantWorkflow(userMessage: string) {
419
419
  "use workflow";
420
420
 
421
421
  const agent = new WorkflowAgent({
422
- model: "anthropic/claude-haiku-4.5",
422
+ model: "spacexai/grok-4.6",
423
423
  instructions: "You are a helpful flight assistant.",
424
424
  tools: {
425
425
  searchFlights: tool({