DurableAgent

The @workflow/ai package is currently in active development and should be considered experimental.

The DurableAgent class enables you to create AI-powered agents that can maintain state across workflow steps, call tools, and gracefully handle interruptions and resumptions.

Tool calls can be implemented as workflow steps for automatic retries, or as regular workflow-level logic utilizing core library features such as sleep() and Hooks.

import { DurableAgent } from '@workflow/ai/agent';
import { getWritable } from 'workflow';
import { z } from 'zod';
import type { UIMessageChunk } from 'ai';

async function getWeather({ city }: { city: string }) {
  "use step";

  return `Weather in ${city} is sunny`;
}

async function myAgent() {
  "use workflow";

  const agent = new DurableAgent({
    model: 'anthropic/claude-haiku-4.5',
    system: 'You are a helpful weather assistant.',
    tools: {
      getWeather: {
        description: 'Get weather for a city',
        inputSchema: z.object({ city: z.string() }),
        execute: getWeather,
      },
    },
  });

  // The agent will stream its output to the workflow
  // run's default output stream
  const writable = getWritable<UIMessageChunk>();

  await agent.stream({
    messages: [{ role: 'user', content: 'How is the weather in San Francisco?' }],
    writable,
  });
}

API Signature

Class

NameTypeDescription
modelany
toolsany
systemany
generate() => void
stream<TTools extends ToolSet = ToolSet>(options: DurableAgentStreamOptions<TTools>) => Promise<{ messages: ModelMessage[]; }>

DurableAgentOptions

NameTypeDescription
modelstring | (() => Promise<LanguageModelV2>)The model provider to use for the agent. This should be a string compatible with the Vercel AI Gateway (e.g., 'anthropic/claude-opus'), or a step function that returns a LanguageModelV2 instance.
toolsToolSetA set of tools available to the agent. Tools can be implemented as workflow steps for automatic retries and persistence, or as regular workflow-level logic using core library features like sleep() and Hooks.
systemstringOptional system prompt to guide the agent's behavior.

DurableAgentStreamOptions

NameTypeDescription
messagesModelMessage[]The conversation messages to process. Should follow the AI SDK's ModelMessage format.
systemstringOptional system prompt override. If provided, overrides the system prompt from the constructor.
writableWritableStream<UIMessageChunk>The stream to which the agent writes message chunks. For example, use getWritable<UIMessageChunk>() to write to the workflow's default output stream.
preventClosebooleanIf true, prevents the writable stream from being closed after streaming completes. Defaults to false (stream will be closed).
sendStartbooleanIf true, sends a 'start' chunk at the beginning of the stream. Defaults to true.
sendFinishbooleanIf true, sends a 'finish' chunk at the end of the stream. Defaults to true.
stopWhenStopCondition<NoInfer<ToolSet>> | StopCondition<NoInfer<ToolSet>>[]Condition for stopping the generation when there are tool results in the last step. When the condition is an array, any of the conditions can be met to stop the generation.
onStepFinishStreamTextOnStepFinishCallback<any>Callback function to be called after each step completes.
prepareStepPrepareStepCallback<TTools>Callback function called before each step in the agent loop. Use this to modify settings, manage context, or inject messages dynamically.

PrepareStepInfo

Information passed to the prepareStep callback:

NameTypeDescription
modelstring | (() => Promise<LanguageModelV2>)The current model configuration (string or function).
stepNumbernumberThe current step number (0-indexed).
stepsStepResult<TTools>[]All previous steps with their results.
messagesLanguageModelV2PromptThe messages that will be sent to the model. This is the LanguageModelV2Prompt format used internally.

PrepareStepResult

Return type from the prepareStep callback:

NameTypeDescription
modelstring | (() => Promise<LanguageModelV2>)Override the model for this step.
messagesLanguageModelV2PromptOverride the messages for this step. Use this for context management or message injection.

Key Features

  • Durable Execution: Agents can be interrupted and resumed without losing state
  • Flexible Tool Implementation: Tools can be implemented as workflow steps for automatic retries, or as regular workflow-level logic
  • Stream Processing: Handles streaming responses and tool calls in a structured way
  • Workflow Native: Fully integrated with Workflow DevKit for production-grade reliability

Good to Know

  • Tools can be implemented as workflow steps (using "use step" for automatic retries), or as regular workflow-level logic
  • Tools can use core library features like sleep() and Hooks within their execute functions
  • The agent processes tool calls iteratively until completion
  • The stream() method returns { messages } containing the full conversation history, including initial messages, assistant responses, and tool results
  • The prepareStep callback runs before each step and can modify the model or messages dynamically

Examples

Basic Agent with Tools

import { DurableAgent } from '@workflow/ai/agent';
import { getWritable } from 'workflow';
import { z } from 'zod';
import type { UIMessageChunk } from 'ai';

async function getWeather({ location }: { location: string }) {
  "use step";
  // Fetch weather data
  const response = await fetch(`https://api.weather.com?location=${location}`);
  return response.json();
}

async function weatherAgentWorkflow(userQuery: string) {
  'use workflow';

  const agent = new DurableAgent({
    model: 'anthropic/claude-haiku-4.5',
    tools: {
      getWeather: {
        description: 'Get current weather for a location',
        inputSchema: z.object({ location: z.string() }),
        execute: getWeather,
      },
    },
    system: 'You are a helpful weather assistant. Always provide accurate weather information.',
  });

  await agent.stream({
    messages: [
      {
        role: 'user',
        content: userQuery,
      },
    ],
    writable: getWritable<UIMessageChunk>(),
  });
}

Multiple Tools

import { DurableAgent } from '@workflow/ai/agent';
import { getWritable } from 'workflow';
import { z } from 'zod';
import type { UIMessageChunk } from 'ai';

async function getWeather({ location }: { location: string }) {
  "use step";
  return `Weather in ${location}: Sunny, 72°F`;
}

async function searchEvents({ location, category }: { location: string; category: string }) {
  "use step";
  return `Found 5 ${category} events in ${location}`;
}

async function multiToolAgentWorkflow(userQuery: string) {
  'use workflow';

  const agent = new DurableAgent({
    model: 'anthropic/claude-haiku-4.5',
    tools: {
      getWeather: {
        description: 'Get weather for a location',
        inputSchema: z.object({ location: z.string() }),
        execute: getWeather,
      },
      searchEvents: {
        description: 'Search for upcoming events in a location',
        inputSchema: z.object({ location: z.string(), category: z.string() }),
        execute: searchEvents,
      },
    },
  });

  await agent.stream({
    messages: [
      {
        role: 'user',
        content: userQuery,
      },
    ],
    writable: getWritable<UIMessageChunk>(),
  });
}

Multi-turn Conversation

import { DurableAgent } from '@workflow/ai/agent';
import { z } from 'zod';

async function searchProducts({ query }: { query: string }) {
  "use step";
  // Search product database
  return `Found 3 products matching "${query}"`;
}

async function multiTurnAgentWorkflow() {
  'use workflow';

  const agent = new DurableAgent({
    model: 'anthropic/claude-haiku-4.5',
    tools: {
      searchProducts: {
        description: 'Search for products',
        inputSchema: z.object({ query: z.string() }),
        execute: searchProducts,
      },
    },
  });

  const writable = getWritable<UIMessageChunk>();

  // First user message
  //   - Result is streamed to the provided `writable` stream
  //   - Message history is returned in `messages` for LLM context
  let { messages } = await agent.stream({
    messages: [
      { role: 'user', content: 'Find me some laptops' }
    ],
    writable,
  });

  // Continue the conversation with the accumulated message history
  const result = await agent.stream({
    messages: [
      ...messages,
      { role: 'user', content: 'Which one has the best battery life?' }
    ],
    writable,
  });

  // result.messages now contains the complete conversation history
  return result.messages;
}

Tools with Workflow Library Features

import { DurableAgent } from '@workflow/ai/agent';
import { sleep, defineHook, getWritable } from 'workflow';
import { z } from 'zod';
import type { UIMessageChunk } from 'ai';

// Define a reusable hook type
const approvalHook = defineHook<{ approved: boolean; reason: string }>();

async function scheduleTask({ delaySeconds }: { delaySeconds: number }) {
  // Note: No "use step" for this tool call,
  // since `sleep()` is a workflow level function
  await sleep(`${delaySeconds}s`);
  return `Slept for ${delaySeconds} seconds`;
}

async function requestApproval({ message }: { message: string }) {
  // Note: No "use step" for this tool call either,
  // since hooks are awaited at the workflow level

  // Utilize a Hook for Human-in-the-loop approval
  const hook = approvalHook.create({
    metadata: { message }
  });

  console.log(`Approval needed - token: ${hook.token}`);

  // Wait for the approval payload
  const approval = await hook;

  if (approval.approved) {
    return `Request approved: ${approval.reason}`;
  } else {
    throw new Error(`Request denied: ${approval.reason}`);
  }
}

async function agentWithLibraryFeaturesWorkflow(userRequest: string) {
  'use workflow';

  const agent = new DurableAgent({
    model: 'anthropic/claude-haiku-4.5',
    tools: {
      scheduleTask: {
        description: 'Pause the workflow for the specified number of seconds',
        inputSchema: z.object({
          delaySeconds: z.number(),
        }),
        execute: scheduleTask,
      },
      requestApproval: {
        description: 'Request approval for an action',
        inputSchema: z.object({ message: z.string() }),
        execute: requestApproval,
      },
    },
  });

  await agent.stream({
    messages: [{ role: 'user', content: userRequest }],
    writable: getWritable<UIMessageChunk>(),
  });
}

Dynamic Context with prepareStep

Use prepareStep to modify settings before each step in the agent loop:

import { DurableAgent } from '@workflow/ai/agent';
import { getWritable } from 'workflow';
import type { UIMessageChunk } from 'ai';

async function agentWithPrepareStep(userMessage: string) {
  'use workflow';

  const agent = new DurableAgent({
    model: 'openai/gpt-4.1-mini', // Default model
    system: 'You are a helpful assistant.',
  });

  await agent.stream({
    messages: [{ role: 'user', content: userMessage }],
    writable: getWritable<UIMessageChunk>(),
    prepareStep: async ({ stepNumber, messages }) => {
      // Switch to a stronger model for complex reasoning after initial steps
      if (stepNumber > 2 && messages.length > 10) {
        return {
          model: 'anthropic/claude-sonnet-4.5',
        };
      }

      // Trim context if messages grow too large
      if (messages.length > 20) {
        return {
          messages: [
            messages[0], // Keep system message
            ...messages.slice(-10), // Keep last 10 messages
          ],
        };
      }

      return {}; // No changes
    },
  });
}

Message Injection with prepareStep

Inject messages from external sources (like hooks) before each LLM call:

import { DurableAgent } from '@workflow/ai/agent';
import { getWritable, defineHook } from 'workflow';
import type { UIMessageChunk } from 'ai';

const messageHook = defineHook<{ message: string }>();

async function agentWithMessageQueue(initialMessage: string) {
  'use workflow';

  const messageQueue: Array<{ role: 'user'; content: string }> = [];

  // Listen for incoming messages via hook
  const hook = messageHook.create();
  hook.then(({ message }) => {
    messageQueue.push({ role: 'user', content: message });
  });

  const agent = new DurableAgent({
    model: 'anthropic/claude-haiku-4.5',
    system: 'You are a helpful assistant.',
  });

  await agent.stream({
    messages: [{ role: 'user', content: initialMessage }],
    writable: getWritable<UIMessageChunk>(),
    prepareStep: ({ messages }) => {
      // Inject queued messages before the next step
      if (messageQueue.length > 0) {
        const newMessages = messageQueue.splice(0);
        return {
          messages: [
            ...messages,
            ...newMessages.map(m => ({
              role: m.role,
              content: [{ type: 'text' as const, text: m.content }],
            })),
          ],
        };
      }
      return {};
    },
  });
}

See Also