UNPKG

ai

Version:

AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.

795 lines (618 loc) 29.2 kB
--- title: WorkflowAgent description: Build durable, resumable agents with the WorkflowAgent from @ai-sdk/workflow. --- # WorkflowAgent The `WorkflowAgent` from `@ai-sdk/workflow` is designed for building **durable, resumable agents** that run inside a [workflow](https://vercel.com/docs/workflow). It provides the same agent loop as the [`ToolLoopAgent`](/docs/agents/building-agents), but adds automatic state persistence, tool schema serialization, and built-in tool approval flows that survive workflow step boundaries. ## Why Durable Agents? A standard `ToolLoopAgent` runs entirely in memory — if the process crashes, all progress is lost. For production agents that make multiple tool calls, this creates problems: - **Statefulness** — Long-running agent loops need to persist state across process boundaries - **Resumability** — If a step fails, you want to retry from the last checkpoint, not restart from scratch - **Human-in-the-loop** — Tools that require user approval need to pause the agent and resume later - **Observability** — Each tool call runs as a discrete workflow step, visible in dashboards `WorkflowAgent` solves these by running inside a workflow, where each tool execution is a durable step with automatic retries. ## When to Use WorkflowAgent vs ToolLoopAgent | | ToolLoopAgent | WorkflowAgent | | ----------------------- | ------------------------- | ----------------------------------------------- | | **Package** | `ai` | `@ai-sdk/workflow` | | **Runtime** | In-memory | Workflow | | **Durability** | Lost on crash | Survives restarts | | **Tool retries** | Manual | Automatic (via workflow steps) | | **Human approval** | Built-in | Built-in + survives suspension | | **`generate()` method** | Available | Not available | | **`stream()` method** | Available | Primary API | | **Stream output** | `streamText` return value | `writable` parameter with `ModelCallStreamPart` | For simpler use cases that don't need durability, use [`ToolLoopAgent`](/docs/agents/building-agents) from the `ai` package. ## Installation ```bash npm install @ai-sdk/workflow workflow@beta ``` `@ai-sdk/workflow` requires Workflow 5, which is currently available under the `beta` tag, as well as the `ai` package and `zod` peer dependencies. The `workflow` package provides the Workflow DevKit runtime (`getWritable`, `'use workflow'`, `'use step'`). ## Creating a WorkflowAgent Define an agent by instantiating the `WorkflowAgent` class with a model, instructions, and tools: ```ts import { WorkflowAgent } from '@ai-sdk/workflow'; import { tool } from 'ai'; import { z } from 'zod'; const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', instructions: 'You are a helpful assistant.', tools: { weather: tool({ description: 'Get weather for a location', inputSchema: z.object({ location: z.string(), }), execute: async ({ location }) => ({ location, temperature: 72, }), }), }, }); ``` ### Model Resolution The `model` parameter accepts two forms: ```ts // String — AI Gateway model ID new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6' }); // Provider instance import { openai } from '@ai-sdk/openai'; new WorkflowAgent({ model: openai('gpt-4o') }); ``` ## Using the Agent in a Workflow `WorkflowAgent` is designed to run inside a workflow function. The key integration points are: 1. Mark your function with `'use workflow'` 2. Pass `getWritable()` to the agent's `stream()` method 3. Start the workflow from your API route ### End-to-End Example ```ts filename="workflow/agent-chat.ts" import { WorkflowAgent, type ModelCallStreamPart } from '@ai-sdk/workflow'; import { convertToModelMessages, tool, type UIMessage } from 'ai'; import { getWritable } from 'workflow'; import { z } from 'zod'; export async function chat(messages: UIMessage[]) { 'use workflow'; const modelMessages = await convertToModelMessages(messages); const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', instructions: 'You are a flight booking assistant.', tools: { searchFlights: tool({ description: 'Search for available flights', inputSchema: z.object({ origin: z.string(), destination: z.string(), date: z.string(), }), execute: searchFlightsStep, }), bookFlight: tool({ description: 'Book a specific flight', inputSchema: z.object({ flightId: z.string(), passengerName: z.string(), }), execute: bookFlightStep, }), }, }); const result = await agent.stream({ messages: modelMessages, writable: getWritable<ModelCallStreamPart>(), }); return { messages: result.messages }; } ``` ```ts filename="app/api/chat/route.ts" import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow'; import { createUIMessageStreamResponse, type UIMessage } from 'ai'; import { start } from 'workflow/api'; import { chat } from '@/workflow/agent-chat'; export async function POST(request: Request) { const { messages }: { messages: UIMessage[] } = await request.json(); const run = await start(chat, [messages]); return createUIMessageStreamResponse({ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()), }); } ``` ### Message Conversion `WorkflowAgent.stream()` expects `ModelMessage[]`, not `UIMessage[]`. When receiving messages from the client (via `useChat`), convert them first: ```ts import { convertToModelMessages, type UIMessage } from 'ai'; export async function chat(messages: UIMessage[]) { 'use workflow'; const modelMessages = await convertToModelMessages(messages); const result = await agent.stream({ messages: modelMessages, // ... }); } ``` ### Writable Streams Unlike `ToolLoopAgent` where you consume the returned stream, `WorkflowAgent` writes raw `ModelCallStreamPart` chunks to a `writable` stream provided by the workflow runtime via `getWritable()`. At the response boundary, use `createModelCallToUIChunkTransform()` to convert these into `UIMessageChunk` objects for the client: ```ts import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow'; import { createUIMessageStreamResponse } from 'ai'; // Convert raw model stream parts → UI message chunks return createUIMessageStreamResponse({ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()), }); ``` The transform also forwards `reset-step` events emitted by `WorkflowAgent` on retries. Clients remove partial parts from the failed model-call step before processing the retried output. ## Resumable Streaming with WorkflowChatTransport Workflow functions can time out or be interrupted by network failures. `WorkflowChatTransport` is a [`ChatTransport`](/docs/ai-sdk-ui/transport) implementation that handles these interruptions automatically — it detects when a stream ends without a `finish` event and reconnects to resume from where it left off. ```tsx filename="app/page.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { WorkflowChatTransport } from '@ai-sdk/workflow'; import { useMemo } from 'react'; export default function Chat() { const transport = useMemo( () => new WorkflowChatTransport({ api: '/api/chat', maxConsecutiveErrors: 5, }), [], ); const { messages, sendMessage } = useChat({ transport }); // ... render chat UI } ``` The transport requires your POST endpoint to return an `x-workflow-run-id` response header, and a GET endpoint at `{api}/{runId}/stream` for reconnection: ```ts filename="app/api/chat/route.ts" import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow'; import { createUIMessageStreamResponse, type UIMessage } from 'ai'; import { start } from 'workflow/api'; import { chat } from '@/workflow/agent-chat'; export async function POST(request: Request) { const { messages }: { messages: UIMessage[] } = await request.json(); const run = await start(chat, [messages]); return createUIMessageStreamResponse({ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()), headers: { 'x-workflow-run-id': run.runId, }, }); } ``` ```ts filename="app/api/chat/[runId]/stream/route.ts" import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow'; import { createUIMessageStreamResponse } from 'ai'; import type { NextRequest } from 'next/server'; import { getRun } from 'workflow/api'; export async function GET( request: NextRequest, { params }: { params: Promise<{ runId: string }> }, ) { const { runId } = await params; const startIndex = Number( new URL(request.url).searchParams.get('startIndex') ?? '0', ); if (!Number.isSafeInteger(startIndex) || startIndex < 0) { return Response.json( { error: 'startIndex must be a non-negative safe integer' }, { status: 400 }, ); } const run = await getRun(runId); const readable = run .getReadable({ startIndex: 0 }) .pipeThrough( createModelCallToUIChunkTransform({ uiStartIndex: startIndex }), ); return createUIMessageStreamResponse({ stream: readable, headers: { 'x-workflow-run-id': runId, }, }); } ``` `WorkflowChatTransport` counts `UIMessageChunk` objects, while the durable `WorkflowAgent` stream stores raw `ModelCallStreamPart` objects. Replay the raw stream from index `0` and apply the non-negative UI cursor in `createModelCallToUIChunkTransform()` as shown above. Negative start indexes require a durable stream that already stores `UIMessageChunk` objects and cannot be used with this raw-to-UI conversion. For the full API reference, see [`WorkflowChatTransport`](/docs/reference/ai-sdk-workflow/workflow-chat-transport). ## Tools as Workflow Steps Mark tool execute functions with `'use step'` to make them durable workflow steps. This gives each tool call: - **Automatic retries** — Failed tool calls are retried automatically (default: 3 attempts) - **Persistence** — Results survive process restarts - **Observability** — Each tool call appears as a discrete step in the workflow dashboard ```ts async function searchFlightsStep(input: { origin: string; destination: string; date: string; }) { 'use step'; const response = await fetch(`https://api.flights.example/search?...`); return response.json(); } async function bookFlightStep(input: { flightId: string; passengerName: string; }) { 'use step'; const response = await fetch('https://api.flights.example/book', { method: 'POST', body: JSON.stringify(input), }); return response.json(); } ``` Tools without `'use step'` still work but run as regular in-memory functions without durability guarantees. ## Tool Approval For `WorkflowAgent`, human approval is configured on the tool definition with `needsApproval`. This is specific to `WorkflowAgent`; for `generateText`, `streamText`, and `ToolLoopAgent`, use `toolApproval` instead. When a workflow tool has `needsApproval` set, the agent pauses and emits an approval request to the writable stream. The workflow suspends until the user approves or denies: ```ts const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', tools: { bookFlight: tool({ description: 'Book a flight', inputSchema: z.object({ flightId: z.string(), passengerName: z.string(), }), needsApproval: true, // Always require approval execute: bookFlightStep, }), cancelBooking: tool({ description: 'Cancel a booking', inputSchema: z.object({ bookingId: z.string() }), // Conditional approval based on input needsApproval: async input => { return input.bookingId.startsWith('VIP-'); }, execute: cancelBookingStep, }), }, }); ``` Because the workflow is durable, the approval request survives process restarts — the user can approve hours later and the agent will resume. ### Signed Tool Approvals Client-supplied message history can be modified before it is replayed to the workflow. For tools that perform sensitive operations, configure `experimental_toolApprovalSecret` to authenticate approval requests: ```ts highlight="4" const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', experimental_toolApprovalSecret: { environmentVariable: 'TOOL_APPROVAL_SECRET', }, tools: { bookFlight: tool({ description: 'Book a flight', inputSchema: z.object({ flightId: z.string() }), needsApproval: true, execute: bookFlightStep, }), }, }); ``` The agent HMAC-signs the approval ID, tool call ID, tool name, and validated input. When approved message history is replayed, a missing or invalid signature prevents the tool from executing. The signature is preserved through the durable model-call stream, `createModelCallToUIChunkTransform()`, `addToolApprovalResponse()`, and `convertToModelMessages()`. Use a high-entropy secret of at least 32 bytes and make the same secret available to every worker that can issue or resume an approval. Keep old keys available while approvals signed with them are pending; changing the secret invalidates those pending approvals. `WorkflowAgent` passes only the environment variable name into signing and verification steps. Each step reads the secret from its local environment, and the raw value is never included in step arguments, durable stream parts, callbacks, or telemetry events. Configure the environment variable on every worker, and do not put the secret value in `runtimeContext` or `toolsContext`. You can also provide `experimental_toolApprovalSecret` to `agent.stream()`. The stream-level value overrides the constructor default. ## Loop Control Control how many steps the agent can take: ```ts import { isStepCount } from 'ai'; const result = await agent.stream({ messages, stopWhen: isStepCount(10), // Stop after 10 LLM calls }); ``` If you want the agent to keep running until it has finished calling tools, you can also use `isLoopFinished()`: ```ts import { isLoopFinished } from 'ai'; const result = await agent.stream({ messages, stopWhen: isLoopFinished(), }); ``` `isLoopFinished()` lets the agent run until all tool calls have completed, but you should still pair it with `maxSteps` to avoid runaway loops. See https://ai-sdk.dev/v7/docs/reference/ai-sdk-core/loop-finished#isloopfinished. By default, the agent loops until the model stops calling tools (no maximum). ## Structured Output Parse agent responses into typed objects using `Output`: ```ts import { Output } from '@ai-sdk/workflow'; import { z } from 'zod'; const result = await agent.stream({ messages, output: Output.object({ schema: z.object({ sentiment: z.enum(['positive', 'neutral', 'negative']), summary: z.string(), }), }), }); console.log(result.output); // { sentiment: 'positive', summary: '...' } ``` ## Configuration Options `WorkflowAgent` accepts the same generation settings as `ToolLoopAgent` (`temperature`, `maxOutputTokens`, `topP`, etc.) plus workflow-specific options. ### runtimeContext and toolsContext Pass server-side state through the agent loop without putting it into the prompt. Use these instead of the previous `experimental_context` option. - `runtimeContext` is shared agent state that flows through `prepareStep`, lifecycle callbacks, and `onEnd`. Treat it as immutable; return a new value from `prepareStep` to update it for the current and subsequent steps. - `toolsContext` is a per-tool map keyed by tool name. Each tool's `execute` only sees its own validated entry as `context`. Tools that declare a `contextSchema` validate their entry against the schema before execution. ```ts filename="workflow/agent-chat.ts" import { WorkflowAgent } from '@ai-sdk/workflow'; import { tool } from 'ai'; import { z } from 'zod'; const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', tools: { weather: tool({ description: 'Get the weather for a city.', inputSchema: z.object({ city: z.string() }), contextSchema: z.object({ defaultUnit: z.enum(['celsius', 'fahrenheit']), }), execute: async ({ city }, { context }) => ({ city, unit: context.defaultUnit, }), }), }, // Shared agent state — available in `prepareStep`, lifecycle callbacks, and `onEnd`. runtimeContext: { tenantId: 'tenant_123', requestId: 'req_abc', plan: 'enterprise', }, // Per-tool context — each tool sees only its own validated entry. toolsContext: { weather: { defaultUnit: 'celsius' }, }, prepareStep: ({ runtimeContext }) => { if (runtimeContext.plan === 'enterprise') { return { temperature: 0.2 }; } return {}; }, }); ``` `runtimeContext` and `toolsContext` can also be passed per-call to `stream()`, where they override the constructor-level defaults. Because `WorkflowAgent` runs inside the Workflow runtime, context values may be persisted and replayed across workflow and step boundaries. Keep `runtimeContext`, `toolsContext`, and any context values returned from `prepareStep` serializable. Use plain data such as strings, numbers, booleans, arrays, plain objects, dates, URLs, maps, sets, and other Workflow-supported structured data. Do not put functions, class instances, symbols, `WeakMap`, `WeakSet`, database clients, or SDK clients in context. Pass identifiers or configuration data instead, and recreate non-serializable resources inside step functions. This differs from `ToolLoopAgent`, which runs in memory and can carry richer JavaScript values for the lifetime of a single process. With `WorkflowAgent`, treating context as durable data keeps workflow replay and step execution reliable. ### experimental_sandbox Pass a sandbox session when tools need an execution environment. The sandbox is available to tool descriptions and `execute` functions as `experimental_sandbox`, and to `prepareStep`, where you can override it for the current step: ```ts const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', tools: { shell: tool({ description: 'Run a shell command in the sandbox.', inputSchema: z.object({ command: z.string() }), execute: async ({ command }, { experimental_sandbox }) => { if (!experimental_sandbox) { throw new Error('Sandbox is not available'); } return experimental_sandbox.run({ command }); }, }), }, experimental_sandbox: sandbox, }); await agent.stream({ messages, writable: getWritable(), experimental_sandbox: requestSandbox, // Overrides the constructor default. }); ``` `experimental_sandbox` is a live runtime handle, not durable context. Do not store it in `runtimeContext` or `toolsContext`. If a tool runs as a separate workflow step, pass serializable sandbox identifiers or configuration and reattach inside that step. ### prepareCall Called once before the agent loop starts. Use it to transform model, instructions, or other settings based on runtime context: ```ts const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', prepareCall: async ({ model, tools, messages }) => { return { instructions: `Current time: ${new Date().toISOString()}`, }; }, }); ``` ### prepareStep Called before each step (LLM call). Use it to modify settings, manage context, or inject messages dynamically: ```ts const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', prepareStep: async ({ stepNumber, experimental_sandbox }) => { if (stepNumber > 5) { return { toolChoice: 'none' }; // Force text response after 5 steps } if (experimental_sandbox) { return { temperature: 0.2 }; } return {}; }, }); ``` Both `prepareCall` and `prepareStep` can also be passed per-call in `stream()`. ## Lifecycle Callbacks Agents provide lifecycle callbacks for logging, observability, and custom telemetry. All callbacks can be defined in the constructor (agent-wide) or in `stream()` (per-call). When both are provided, both fire (constructor first): ```ts const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', onStart({ messages }) { console.log(`Agent started with ${messages.length} messages`); }, onStepStart({ stepNumber }) { console.log(`Step ${stepNumber} starting`); }, onToolExecutionStart({ toolCall }) { console.log(`Calling tool: ${toolCall.toolName}`); }, onToolExecutionEnd({ toolCall, toolOutput }) { console.log(`Tool finished: ${toolCall.toolName}`); }, onStepEnd({ usage, finishReason }) { console.log('Step done:', { finishReason }); }, onEnd({ steps, totalUsage }) { console.log(`Completed in ${steps.length} steps`); }, }); ``` Tool input callbacks (`onInputStart`, `onInputDelta`, and `onInputAvailable`) are also preserved by `WorkflowAgent`. The model call runs inside a durable step, while callback functions remain in the workflow context because arbitrary functions cannot cross the step boundary. As a result, `WorkflowAgent` records the callback events during the model step and replays them in order immediately after that step completes, before tool execution and step lifecycle callbacks. They do not run concurrently with model generation and cannot provide in-flight cancellation or backpressure. Each callback receives its tool's `toolsContext` entry after `contextSchema` validation. For highly fragmented tool inputs, `onInputDelta` replay data is part of the durable model-step result. Only configure `onInputDelta` when each generated delta is needed; omit it to avoid retaining delta replay data. The deprecated `experimental_onStart` and `experimental_onStepStart` names remain available for backwards compatibility. When both the stable and experimental name are provided in the same constructor or `stream()` call, the stable callback is used. ## Type Inference Infer the UI message type for type-safe client components: ```ts import { WorkflowAgent, InferWorkflowAgentUIMessage } from '@ai-sdk/workflow'; const myAgent = new WorkflowAgent({ // ... configuration }); export type MyAgentUIMessage = InferWorkflowAgentUIMessage<typeof myAgent>; ``` ## Migrating from `DurableAgent` `WorkflowAgent` replaces the Workflow DevKit's [`DurableAgent`](https://workflow-sdk.dev/docs/api-reference/workflow-ai/durable-agent). The two share the same core idea — a durable agent loop that runs inside a workflow — but `WorkflowAgent` moves the class into the AI SDK, tightens typing, and introduces first-class tool approval. If you are using `DurableAgent` today, follow the steps below to switch. ### Change the import and class name `DurableAgent` was exported from `workflow/ai`. `WorkflowAgent` is exported from `@ai-sdk/workflow`, alongside its helpers. ```diff - import { DurableAgent } from 'workflow/ai'; + import { WorkflowAgent, type ModelCallStreamPart } from '@ai-sdk/workflow'; - const agent = new DurableAgent({ + const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', instructions: 'You are a helpful assistant.', tools: { /* ... */ }, }); ``` Install the new package alongside `workflow`: ```bash npm install @ai-sdk/workflow workflow@beta ``` ### Write `ModelCallStreamPart`, not `UIMessageChunk` `DurableAgent` wrote `UIMessageChunk` objects directly to the writable returned by `getWritable()`. `WorkflowAgent` writes the lower-level `ModelCallStreamPart` shape and leaves the conversion to a transform at the response boundary. This keeps the durable stream provider-shaped and avoids baking a UI protocol into the workflow payload. ```diff // Inside the workflow await agent.stream({ messages, - writable: getWritable<UIMessageChunk>(), + writable: getWritable<ModelCallStreamPart>(), }); ``` ```diff // Inside the route handler + import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow'; return createUIMessageStreamResponse({ - stream: run.readable, + stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()), }); ``` ### Replace `maxSteps` with `stopWhen` `DurableAgent` accepted `maxSteps` directly. `WorkflowAgent` uses the AI SDK's shared `stopWhen` conditions so the same stop logic works across `ToolLoopAgent`, `generateText`, and `streamText`. ```diff + import { isStepCount } from 'ai'; await agent.stream({ messages, - maxSteps: 10, + stopWhen: isStepCount(10), }); ``` See [Loop Control](/docs/agents/loop-control) for the full list of stop conditions. ### Replace `experimental_output` with `output` ```diff + import { Output } from '@ai-sdk/workflow'; await agent.stream({ messages, - experimental_output: Output.object({ schema }), + output: Output.object({ schema }), }); ``` The returned value is now on `result.output` (previously `result.experimental_output`). ### WorkflowAgent: Use `needsApproval` for human-in-the-loop tools With `DurableAgent`, tool approval was implemented by calling a Hook from inside the tool's `execute` function. `WorkflowAgent` makes approval a first-class tool property — the agent emits the approval request, suspends the workflow, and resumes automatically when the user responds. ```diff bookFlight: tool({ description: 'Book a flight', inputSchema: z.object({ flightId: z.string() }), + needsApproval: true, - execute: async (input) => { - const approved = await waitForApprovalHook(input); - if (!approved) throw new Error('Denied'); - return bookFlightStep(input); - }, + execute: bookFlightStep, }), ``` `needsApproval` also accepts an async function so you can decide per-input whether approval is required (see [Tool Approval](#tool-approval) above). ### `uiMessages` / `collectUIMessages` is gone `DurableAgent.stream()` returned accumulated `uiMessages` when `collectUIMessages: true` was set. `WorkflowAgent.stream()` returns `ModelMessage[]` on `result.messages` instead. For persistence, store `UIMessage[]` as your source of truth and call [`convertToModelMessages`](/docs/reference/ai-sdk-ui/convert-to-model-messages) before passing them to the agent — this is the pattern described in [Chatbot Message Persistence](/docs/ai-sdk-ui/chatbot-message-persistence). There is no built-in `ModelMessage` → `UIMessage` conversion, so avoid persisting `result.messages` as your only copy if you need to render the conversation in the UI later. ```diff const result = await agent.stream({ messages, writable: getWritable<ModelCallStreamPart>(), - collectUIMessages: true, }); - return { uiMessages: result.uiMessages }; + return { messages: result.messages }; ``` ### No `generate()` method `WorkflowAgent` only exposes `stream()`. If you were calling `agent.generate()`, switch to `stream()` and read `result.messages` / `result.output` once the promise resolves. ### Replace `experimental_context` with `runtimeContext` and `toolsContext` `WorkflowAgent` no longer accepts `experimental_context`. Split the value into shared agent state (`runtimeContext`) and per-tool state (`toolsContext`); each tool's `execute` then receives only its own validated entry as `context`. See [runtimeContext and toolsContext](#runtimecontext-and-toolscontext) for the full shape. ```diff const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', tools: { weather: weatherTool }, - experimental_context: { tenantId: 'tenant_123', apiKey: 'sk-...' }, + runtimeContext: { tenantId: 'tenant_123' }, + toolsContext: { weather: { apiKey: 'sk-...' } }, }); ``` ### Everything else Other options carry over with the same names: `prepareStep`, `onStart`, `onStepStart`, `onStepEnd`, `onEnd`, `onError`, `toolChoice`, `activeTools`, `timeout`, `repairToolCall`, `experimental_sandbox`, and the usual generation settings (`temperature`, `maxOutputTokens`, `topP`, …). `WorkflowAgent` additionally adds `prepareCall` (runs once before the loop) and the `onToolExecutionStart` / `onToolExecutionEnd` lifecycle callbacks documented above. The deprecated `experimental_onStart` and `experimental_onStepStart` aliases remain available for backwards compatibility. ## Next Steps - [WorkflowAgent API Reference](/docs/reference/ai-sdk-workflow/workflow-agent) for detailed parameter documentation - [WorkflowChatTransport API Reference](/docs/reference/ai-sdk-workflow/workflow-chat-transport) for stream reconnection options - [Building Agents](/docs/agents/building-agents) for the in-memory `ToolLoopAgent` alternative - [Loop Control](/docs/agents/loop-control) for advanced stop conditions