# What is WorkflowAgent?

**Author:** Ben Sabic

---

You can build AI agents that survive process restarts, function timeouts, and long pauses for human approval by combining the AI SDK with Workflow SDK. `WorkflowAgent` from `@ai-sdk/workflow` runs the same agent loop as `ToolLoopAgent`, but each tool call is marked with `'use step'`, thus becoming a durable step inside a workflow. This persists progress, so failed steps retry from the last checkpoint. Tools marked `needsApproval: true` can even suspend the agent for hours or days until a user responds. No custom state store or polling required.

This guide walks you through what `WorkflowAgent` adds on top of `ToolLoopAgent`, how to define tools that suspend for approval with `needsApproval`, and how `WorkflowChatTransport` keeps chat streams resumable across function timeouts and page refreshes. You'll also learn how to migrate an existing `DurableAgent` to `WorkflowAgent`, and which constructor options carry over unchanged.

## Why durable agents?

A standard `ToolLoopAgent` runs entirely in memory. If the process crashes, the function times out, or the user refreshes the page, the agent's progress is lost. For short, single-tool interactions, that's usually fine. For production agents that chain multiple tool calls (e.g., booking a flight, processing a refund, running a research task across several APIs), losing state mid-loop is costly.

`WorkflowAgent` addresses four specific gaps:

- **Statefulness:** Agent state persists across process boundaries, so a multi-step loop doesn't have to fit inside a single function invocation.
  
- **Resumability:** Each tool call is a discrete workflow step. If a step fails, it retries automatically (default: 3 attempts) instead of restarting the whole agent.
  
- **Human-in-the-loop:** Tools marked with `needsApproval` pause the agent until the user responds. Because the workflow is durable, the user can approve hours or days later, and the agent picks up where it left off.
  
- **Observability:** Every tool call appears as a discrete step in the workflow dashboard, with inputs, outputs, retries, and timing.
  

These come from running inside the Workflow SDK runtime, not from the agent itself.

## `WorkflowAgent` vs `ToolLoopAgent`

Both classes implement the same agent loop and accept the same generation settings (`temperature`, `maxOutputTokens`, `topP`, and so on). The differences are about where and how the loop runs.

|                        | `ToolLoopAgent`              | `WorkflowAgent`                                  |
| ---------------------- | ---------------------------- | ------------------------------------------------ |
| **Package**            | `ai`                         | `@ai-sdk/workflow`                               |
| **Runtime**            | In-memory                    | Workflow runtime                                 |
| **Durability**         | Lost on crash                | Survives restarts                                |
| **Tool retries**       | Manual                       | Automatic, per step                              |
| **Human approval**     | `toolApproval` option        | `needsApproval` on the tool, survives suspension |
| `**generate()**`       | Available                    | Not available                                    |
| `**stream()**`         | Returns a stream you consume | Writes to a `writable` provided by the workflow  |
| **Stream output type** | `streamText` return value    | `ModelCallStreamPart` chunks                     |

As a default, start with `ToolLoopAgent` first. Use `WorkflowAgent` when tool calls outlive their request, approvals exceed function timeouts, or each call should be independently retryable and traced.

## What a `WorkflowAgent` looks like

The constructor takes the same shape as `ToolLoopAgent` (model, instructions, tools) plus workflow-specific options.

To get durability, the agent has to run inside a function marked with `'use workflow'`, and tool `execute` functions are marked with `'use step'`:

``import { WorkflowAgent, type ModelCallStreamPart } from '@ai-sdk/workflow'; import { convertToModelMessages, tool, type UIMessage } from 'ai'; import { getWritable } from 'workflow'; import { z } from 'zod'; async function searchFlightsStep(input: { origin: string; destination: string; date: string; }) { 'use step'; const response = await fetch(`https://api.flights.example/search?...`); return response.json(); } 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, }), }, }); const result = await agent.stream({ messages: modelMessages, writable: getWritable<ModelCallStreamPart>(), }); return { messages: result.messages }; }`` **Two things to note:** 1. `WorkflowAgent.stream()` expects `ModelMessage[]`, so messages from `useChat` need to be converted with `convertToModelMessages`.     2. The agent doesn't return a stream you read from. It writes `ModelCallStreamPart` chunks to the workflow's writable, and the route handler converts those to UI chunks at the edge with `createModelCallToUIChunkTransform()`. This keeps the durable stream in a provider-shaped format and leaves the UI protocol at the boundary.     Tool `execute` functions don't have to use `'use step'`, but without it, they run as regular in-memory functions with no durability guarantees. The `'use step'` directive gives a tool call automatic retries, persistence, and its own line in the workflow dashboard. ## Tool approval with `needsApproval` Approval is a first-class property on the tool definition in `WorkflowAgent`. When a tool with `needsApproval` is called, the agent emits an approval request to the writable stream and the workflow suspends. The user can respond seconds or hours later, and the durable workflow holds the state until they do. `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, execute: bookFlightStep, }), cancelBooking: tool({ description: 'Cancel a booking', inputSchema: z.object({ bookingId: z.string() }), needsApproval: async (input) => input.bookingId.startsWith('VIP-'), execute: cancelBookingStep, }), }, });` `needsApproval` accepts a boolean for blanket approval or an async function for per-input decisions. This is specific to `WorkflowAgent`. For `generateText`, `streamText`, and `ToolLoopAgent`, the equivalent feature is the `toolApproval` option. ## Resumable streaming with `WorkflowChatTransport` Workflow functions can hit timeouts or be interrupted by network failures. `WorkflowChatTransport` is a `ChatTransport` implementation for `useChat` that handles those interruptions automatically. It posts messages to your chat endpoint, reads the `x-workflow-run-id` response header, and if the stream closes without a `finish` event, reconnects to `{api}/{runId}/stream` to resume from the last received chunk. `'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, initialStartIndex: -50, // On page refresh, fetch last 50 chunks }), [], ); const { messages, sendMessage } = useChat({ transport }); // ...render chat UI }` The transport requires two server endpoints: - POST handler at `api` that returns an `x-workflow-run-id` header    - GET handler at `{api}/{runId}/stream` that accepts a `startIndex` query parameter    Using a negative `initialStartIndex` (like `-50`) can be useful for page refreshes, since the client reconnects without replaying the whole conversation. The server is expected to return `x-workflow-stream-tail-index` so subsequent retries can compute their position. If that header is missing, the transport falls back to replaying from the start. ## Migrating from `DurableAgent` `WorkflowAgent` replaces the [Workflow SDK](https://workflow-sdk.dev/docs/api-reference/workflow-ai/durable-agent)'s `DurableAgent`. The core idea is the same, but the class lives in the AI SDK now, types are tighter, and tool approval is first-class. The main changes when migrating:

- **Import path:** DurableAgent came from the Workflow SDK. WorkflowAgent and its helpers come from `@ai-sdk/workflow`, the package you install alongside `workflow`.
  
- **Stream payload:** DurableAgent wrote UI message chunks directly to the workflow's writable. WorkflowAgent writes lower-level model stream parts and converts them to UI chunks at the response boundary with `createModelCallToUIChunkTransform()`.
  
- **Stop conditions:** The `maxSteps` option is replaced by the AI SDK's shared stop conditions, such as `isStepCount(10)`.
  
- **Structured output:** The `experimental_output` option is now simply `output`, and the result reads from the matching property.
  
- **Approval:** Tool approval is configured with the `needsApproval` property on the tool itself, not a Hook call inside the execute function.
  
- **UI messages:** The `collectUIMessages` flag is gone, and the stream result now returns model messages instead. Convert them at the edge with `convertToUIMessages` if your client needs UI messages.
  
- **No generate() method:** WorkflowAgent only exposes `stream()`. Read the messages and output once the promise resolves.
  
- **Context split:** The old `experimental_context` is replaced by two separate options: `runtimeContext` for shared agent state, and `toolsContext` for per-tool context that each tool receives as its `context` argument.
  

Other options carry over with the same names: `prepareStep`, `onStepFinish`, `onFinish`, `onError`, `toolChoice`, `activeTools`, `timeout`, and the standard generation settings.

When working with `runtimeContext` and `toolsContext`, both can cross workflow and step boundaries, so they need to be serializable. Strings, numbers, arrays, plain objects, dates, and other Workflow-supported structured data are safe. Functions, class instances, database clients, and SDK clients are not. Pass identifiers or configuration data instead, and recreate non-serializable resources inside step functions.

## Learn more

---

[View full KB sitemap](/kb/sitemap.md)
