# How to migrate from DurableAgent to WorkflowAgent

**Author:** Ben Sabic

---

[AI SDK](https://ai-sdk.dev/v7/docs/agents/workflow-agent) 7 introduces `WorkflowAgent` in the `@ai-sdk/workflow` package, which replaces `DurableAgent` from the [Workflow SDK](https://workflow-sdk.dev/docs/api-reference/workflow-ai/durable-agent).

Both run a durable agent loop within a workflow, so tool calls, agent state, and human approvals persist through process restarts and Vercel Function timeouts. `WorkflowAgent` moves the class into the AI SDK, tightens its types, and makes tool approval a first-class property instead of a manual hook. Most of your agent code carries over unchanged, with the differences concentrated in imports, stream output, and a few renamed options.

## Overview

In this guide, you'll learn how to:

- Install `@ai-sdk/workflow` and update your imports
  
- Stream `ModelCallStreamPart` and convert it for the client
  
- Convert messages at the request and persistence boundaries
  
- Replace `maxSteps` with `stopWhen`, and `experimental_output` with `output`
  
- Move tool approval from a hook to the `needsApproval` property
  
- Replace `experimental_context` with `runtimeContext` and `toolsContext`
  
- Update the `WorkflowChatTransport` import on the client
  

## Quick start with an AI coding agent

If you're using an AI coding agent like Claude Code or Cursor, give it this prompt, and it'll help you migrate your code from `DurableAgent` to `WorkflowAgent`:

## Prerequisites

Before you begin, make sure you have:

- An existing agent built with `DurableAgent` from Workflow SDK
  
- The `ai` package (v7) and `workflow` runtime installed
  

## What changes at a glance

Most options keep their names and behavior.

The table below maps the APIs that change between `DurableAgent` and `WorkflowAgent`.

| Area                    | DurableAgent (`@workflow/ai`)                                 | WorkflowAgent (`@ai-sdk/workflow`)                                                 |
| ----------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Package and class       | `import { DurableAgent } from '@workflow/ai/agent'`           | `import { WorkflowAgent } from '@ai-sdk/workflow'`                                 |
| Stream output           | Writes `UIMessageChunk` to `getWritable()`                    | Writes `ModelCallStreamPart`, converted with `createModelCallToUIChunkTransform()` |
| Step limit              | `maxSteps: 10`                                                | `stopWhen: isStepCount(10)`                                                        |
| Structured output       | `experimental_output`, read from `result.experimental_output` | `output`, read from `result.output`                                                |
| Tool approval           | Hook awaited inside `execute`                                 | `needsApproval` on the tool definition                                             |
| Server-side context     | `experimental_context`                                        | `runtimeContext` and `toolsContext`                                                |
| Accumulated UI messages | `collectUIMessages`, read from `result.uiMessages`            | Removed. Use `result.messages` and convert                                         |
| Non-streaming output    | `generate()`                                                  | Removed. Use `stream()`                                                            |
| Client transport        | `WorkflowChatTransport` from `@workflow/ai`                   | `WorkflowChatTransport` from `@ai-sdk/workflow`                                    |
| Step callback           | `onStepFinish`                                                | `onStepEnd` (`onStepFinish` is deprecated)                                         |

## Migration steps

### Update the package and imports

Install `@ai-sdk/workflow`, then change the import and class name.

`@ai-sdk/workflow` requires the `ai` package and `zod` as peer dependencies, plus the `workflow` package for the runtime.

`- import { DurableAgent } from '@workflow/ai/agent'; + 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: { /* ... */ }, });`

If your project imported `DurableAgent` from a different path, update that import to point at `@ai-sdk/workflow`.

### 2\. Stream `ModelCallStreamPart` and convert it at the response

`WorkflowAgent` writes raw `ModelCallStreamPart` chunks to the writable stream, where `DurableAgent` wrote `UIMessageChunk`. Change the type on `getWritable()` inside your workflow, then convert the stream to UI chunks at the response boundary with `createModelCallToUIChunkTransform()`. This keeps the durable stream provider-shaped and avoids baking a UI protocol into the workflow payload.

`// Inside the workflow await agent.stream({ messages, - writable: getWritable<UIMessageChunk>(), + writable: getWritable<ModelCallStreamPart>(), });`

`// Inside the route handler + import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow'; return createUIMessageStreamResponse({ - stream: run.readable, + stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()), });`

### 3\. Convert messages at the boundaries

`WorkflowAgent.stream()` expects `ModelMessage[]`. When your workflow receives `UIMessage[]` from the client (for example, through `useChat`), convert them with `convertToModelMessages()` before calling `stream()`.

``import { convertToModelMessages, type UIMessage } from 'ai'; import { getWritable } from 'workflow'; import { type ModelCallStreamPart } from '@ai-sdk/workflow'; export async function chat(messages: UIMessage[]) { 'use workflow'; const modelMessages = await convertToModelMessages(messages); // `agent` is your existing WorkflowAgent instance const result = await agent.stream({ messages: modelMessages, writable: getWritable<ModelCallStreamPart>(), }); return { messages: result.messages }; }``

`result.messages` is now an array of `ModelMessage`. Persist those, and convert them back with `convertToUIMessages()` when you need to render a saved conversation.

### 4\. Replace `maxSteps` with `stopWhen`

Replace the `maxSteps` option with a `stopWhen` condition. For the same step-count limit, use `isStepCount()` from the `ai` package.

`+ import { isStepCount } from 'ai'; await agent.stream({ messages, - maxSteps: 10, + stopWhen: isStepCount(10), });`

To let the agent run until it finishes calling tools, use `isLoopFinished()`. Pass an array of conditions to cap the total number of steps, stopping the loop when either condition is met.

`import { isLoopFinished, isStepCount } from 'ai'; await agent.stream({ messages, stopWhen: [isLoopFinished(), isStepCount(20)], });`

### 5\. Rename `experimental_output` to `output`

Rename `experimental_output` to `output`, import `Output` from `@ai-sdk/workflow`, and read the parsed result from `result.output`.

`- import { DurableAgent, Output } from '@workflow/ai/agent'; + import { Output } from '@ai-sdk/workflow'; const result = await agent.stream({ messages, - experimental_output: Output.object({ schema }), + output: Output.object({ schema }), }); - console.log(result.experimental_output); + console.log(result.output);`

### 6\. Move tool approval to `needsApproval`

Replace hook-based approval with the `needsApproval` property on the tool definition. `WorkflowAgent` emits the approval request, suspends the workflow, and resumes automatically when the user responds, so you no longer await a hook inside `execute`. Because the workflow is durable, the request survives process restarts, and the user can approve hours later.

`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 require approval based on the tool input.

`cancelBooking: tool({ description: 'Cancel a booking', inputSchema: z.object({ bookingId: z.string() }), needsApproval: async (input) => input.bookingId.startsWith('VIP-'), execute: cancelBookingStep, }),`

`needsApproval` is specific to `WorkflowAgent`. For `generateText`, `streamText`, and `ToolLoopAgent`, use `toolApproval` instead.

### 7\. Split `experimental_context` into `runtimeContext` and `toolsContext`

Replace `experimental_context` with `runtimeContext` for shared agent state and `toolsContext` for per-tool state. Each tool's `execute` receives only its own validated entry as `context`.

`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-...' } }, });`

A tool reads its context from the second argument of `execute`. Add a `contextSchema` to validate the entry before the tool runs.

`const weatherTool = tool({ description: 'Get the weather for a city.', inputSchema: z.object({ city: z.string() }), contextSchema: z.object({ apiKey: z.string() }), execute: async ({ city }, { context }) => { return getWeather(city, context.apiKey); }, });`

`runtimeContext` flows through `prepareStep`, the lifecycle callbacks, and `onEnd`. Treat it as immutable, and return a new value from `prepareStep` to update it for the current and later steps.

`WorkflowAgent` runs inside the Workflow runtime, so context values can be persisted and replayed across step boundaries. Keep `runtimeContext`, `toolsContext`, and anything returned from `prepareStep` serializable. Use plain data such as strings, numbers, booleans, arrays, plain objects, dates, URLs, maps, and sets. Do not put functions, class instances, symbols, database clients, or SDK clients in context. Pass identifiers or configuration instead, and recreate those resources inside step functions.

### 8\. Update the client transport import

On the client, `WorkflowChatTransport` now comes from `@ai-sdk/workflow` instead of `@workflow/ai`. The transport options and your `useChat` setup stay the same.

`- import { WorkflowChatTransport } from '@workflow/ai'; + import { WorkflowChatTransport } from '@ai-sdk/workflow';`

Your reconnection route should pipe the resumed stream through `createModelCallToUIChunkTransform()` and return the `x-workflow-run-id` response header, the same transform you added in step 2. See `WorkflowChatTransport` for the full client-server setup.

## Handle removed APIs

`WorkflowAgent` exposes only `stream()`. If your `DurableAgent` code called `generate()`, switch to `stream()` and read `result.messages` and `result.output` once the promise resolves.

`collectUIMessages` and `result.uiMessages` are also removed. Read `result.messages`, an array of `ModelMessage`, and convert it with `convertToUIMessages()` when you need UI messages, as shown in step 3.

`const result = await agent.stream({ messages: modelMessages, writable: getWritable<ModelCallStreamPart>(), - collectUIMessages: true, }); - return { uiMessages: result.uiMessages }; + return { messages: result.messages };`

## What carries over unchanged

The following options keep the same names and behavior, so you don't need to change them:

- `prepareStep`
  
- `onEnd`
  
- `onError`
  
- `toolChoice`
  
- `activeTools`
  
- `timeout`
  
- `experimental_repairToolCall`
  

Along with the generation settings (`temperature`, `maxOutputTokens`, `topP`, and the rest).

`WorkflowAgent` also adds `prepareCall`, which runs once before the agent loop starts, along with the following lifecycle callbacks:

- `onStart`
  
- `onStepStart`
  
- `onToolExecutionStart`
  
- `onToolExecutionEnd`
  

If you used `onStepFinish`, switch to `onStepEnd`. `onStepFinish` still works as a deprecated fallback, but `onStepEnd` is the supported name.

## Verify the migration

After you finish, confirm the agent works end-to-end:

- The project installs and builds with `@ai-sdk/workflow` in place.
  
- The client renders streamed output, which confirms that the response pipes through `createModelCallToUIChunkTransform()`.
  
- Tools that run as steps still execute and retry, and approval-gated tools pause and resume on the user's response.
  
- Structured output parses into `result.output`.
  
- Saved conversations load correctly, which confirms messages convert in both directions.

---

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