---
title: What is WorkflowAgent?
description: "Build durable AI agents with WorkflowAgent: tool calls that retry as workflow steps, suspend for human approval, and resume after timeouts or page refreshes."
url: "https://vercel.com/kb/guide/what-is-workflowagent"
published: 2026-05-21
last_updated: 2026-05-21
authors: Ben Sabic
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

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'`:

```typescript
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.

```typescript
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.

```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,
        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

Combine Chat SDK and Workflow SDK to suspend workflows on approval cards in a chat platform, then resume on click via createWebhook.

[Human-in-the-Loop with Chat SDK and Workflow SDK](/kb/guide/human-in-the-loop-with-chat-sdk-and-workflow-sdk)

## FAQ

### What is a durable AI agent?

A durable agent is one whose state persists across process boundaries. A standard in-memory agent loses everything if the process crashes, the function times out, or the connection drops. A durable agent stores its progress so the same loop can pause, suspend, retry, or resume later without restarting from the beginning. In the AI SDK, you build durable agents with `WorkflowAgent`, which runs the agent loop inside a workflow so each tool call becomes a persisted step with automatic retries, observability, and the ability to suspend for human approval that survives restarts.

### What's the difference between WorkflowAgent and ToolLoopAgent?

Both run the same agent loop and accept the same generation settings, but they differ in where the loop runs. `ToolLoopAgent` from the `ai` package runs entirely in memory, so progress is lost if the process crashes or the function times out. By contrast, `WorkflowAgent` from `@ai-sdk/workflow` runs inside a workflow, where each tool call marked with `'use step'` becomes a durable step that persists across process boundaries and retries automatically on failure. Reach for `ToolLoopAgent` first, and switch to `WorkflowAgent` when a tool call needs to outlive its request, an approval might take longer than a function timeout, or you want each tool call traced as its own retryable unit.

### Should I migrate from DurableAgent to WorkflowAgent?

Yes. WorkflowAgent replaces DurableAgent and is where new development happens. The core idea is the same (a durable agent loop that runs inside a workflow), but the class now lives in the AI SDK rather than the Workflow DevKit, types are tighter, and tool approval is a first-class property (`needsApproval`) instead of a Hook call. See the migration section above for the full list of changes, including the new package, the switch to `ModelCallStreamPart` on the writable, and the replacement of `experimental_context` with `runtimeContext` and `toolsContext`.

### How do I pause an AI agent for human approval?

Add `needsApproval: true` to any tool that needs approval before it runs. When the agent tries to call that tool, it emits an approval request to the writable stream and the workflow suspends. The user can respond minutes, hours, or days later, and because the workflow is durable, the agent picks up exactly where it left off when the response arrives. You can also pass an async function to `needsApproval` to decide per-input whether approval is required, which is useful for things like requiring approval only for high-value or sensitive operations.

### How do I resume a chat stream after a function timeout or page refresh?

Use `WorkflowChatTransport` from `@ai-sdk/workflow` as the transport for `useChat`. It posts your messages to your chat endpoint, reads the `x-workflow-run-id` response header, and if the stream closes without a finish event (because of a timeout, network error, or refresh), it reconnects to `{api}/{runId}/stream` and resumes from the last received chunk. Set `initialStartIndex` to a negative number like `-50` to fetch only the most recent chunks on reconnection, which avoids replaying the full conversation after a refresh.

## More Vercel Workflows guides

- [Run recurring security reviews with deepsec on Vercel](/kb/guide/deepsec-reviews-on-vercel): Learn how to run periodic security scans on your code with deepsec. Set up deepsec to review GitHub repositories and deliver findings to GitHub and Slack using Vercel Connect, Workflows, Sandbox, and AI Gateway.
- [How to handle order-ahead traffic spikes on Vercel](/kb/guide/handle-order-ahead-traffic-spikes-on-vercel): Order-ahead traffic spikes hit the same routes every lunch hour. Learn how to scale, cache, queue, and test a restaurant ordering system on Vercel.
- [How to build a durable AI code agent on Vercel](/kb/guide/how-to-build-a-durable-ai-code-agent-on-vercel): Build an AI agent that generates code, writes its own tests, and executes them in an isolated microVM with automatic retries.