惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
博客园 - 三生石上(FineUI控件)
S
Securelist
U
Unit 42
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Simon Willison's Weblog
Simon Willison's Weblog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
B
Blog
T
Tenable Blog
The Hacker News
The Hacker News
The Register - Security
The Register - Security
IT之家
IT之家
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
P
Privacy & Cybersecurity Law Blog
博客园_首页
T
Tailwind CSS Blog
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tor Project blog
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
A
Arctic Wolf
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
V2EX
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
V
Visual Studio Blog
月光博客
月光博客
爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

Workflow SDK Documentation

Patterns for Defining Tools Human-in-the-Loop Building Durable AI Agents Queueing User Messages Resumable Streams Sleep, Suspense, and Scheduling Streaming Updates from Tools API Reference Workflow Globals Changelog Resilient run start Cookbook Building a World Deploying Astro Express Fastify Hono Getting Started NestJS Next.js Nitro Nuxt Python SvelteKit Vite corrupted-event-log fetch-in-workflow hook-conflict Errors node-js-module-in-workflow serialization-failed start-invalid-workflow-function step-not-registered timeout-in-workflow webhook-invalid-respond-with-value webhook-response-not-sent workflow-not-registered Errors & Retrying Hooks & Webhooks Idempotency Foundations Serialization Starting Workflows Streaming Versioning Workflows and Steps How the Directives Work Encryption Event Sourcing Framework Integrations Understanding Directives Migration Guides Migrating from AWS Step Functions Migrating from Inngest Migrating from Temporal Observability Testing Server-Based Testing createHook createWebhook defineHook FatalError fetch getStepMetadata getWorkflowMetadata getWritable workflow RetryableError sleep @workflow/vitest @workflow/ai WorkflowChatTransport getHookByToken getRun getWorld workflow/api resumeHook resumeWebhook Chat Session Modeling runtime-decryption-failed Upgrading Workflows abort-signal-timeout-in-workflow Cancellation How Cancellation Works Internal Serializable AbortController and AbortSignal Eager Processing of Steps & Incremental Event Replay TanStack Start Agent Cancellation Sequential & Parallel Execution Workflow Composition Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK Migrating from trigger.dev Secure Credential Handling Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK
DurableAgent
2026-05-31 · via Workflow SDK Documentation

Create AI agents that maintain state, call tools, and handle interruptions gracefully.

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",
    instructions: "You are a helpful weather assistant.",
    temperature: 0.7,
    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>();

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

  // result contains messages, steps, and optional structured output
  console.log(result.messages);
}

Class

NameTypeDescription
modelany
toolsTBaseToolsThe tool set configured for this agent.
instructionsany
generationSettingsany
toolChoiceany
telemetryany
experimentalContextany
prepareStepany
constructorOnStepFinishany
constructorOnFinishany
generate() => void
stream<TTools extends TBaseTools = TBaseTools, OUTPUT = never, PARTIAL_OUTPUT = never>(options: DurableAgentStreamOptions<TTools, OUTPUT, PARTIAL_OUTPUT>) => Promise<...>

DurableAgentOptions

NameTypeDescription
modelstring | (() => Promise<LanguageModelV3>)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 LanguageModelV3 instance.
toolsTToolsA 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.
instructionsstring | SystemModelMessage | SystemModelMessage[]Agent instructions. Can be a string, a SystemModelMessage, or an array of SystemModelMessages. Supports provider-specific options (e.g., caching) when using the SystemModelMessage form.
systemstringOptional system prompt to guide the agent's behavior. **Deprecated**: Use instructions instead.
toolChoiceToolChoice<TTools>The tool choice strategy. Default: 'auto'.
experimental_telemetryTelemetrySettingsOptional telemetry configuration (experimental).
experimental_contextunknownDefault context that is passed into tool execution for every stream call on this agent. Per-stream experimental_context values passed to stream() override this default. Experimental (can break in patch releases).
prepareStepPrepareStepCallback<TTools>Default callback function called before each step in the agent loop. Use this to modify settings, manage context, or inject messages dynamically for every stream call on this agent instance. Per-stream prepareStep values passed to stream() override this default.
onStepFinishStreamTextOnStepFinishCallback<ToolSet>Callback function to be called after each step completes.
onFinishStreamTextOnFinishCallback<ToolSet, never>Callback that is called when the LLM response and all request tool executions are finished.
maxOutputTokensnumberMaximum number of tokens to generate.
temperaturenumberTemperature setting. The range depends on the provider and model. It is recommended to set either temperature or topP, but not both.
topPnumberNucleus sampling. This is a number between 0 and 1. E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered. It is recommended to set either temperature or topP, but not both.
topKnumberOnly sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.
presencePenaltynumberPresence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty.
frequencyPenaltynumberFrequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty.
stopSequencesstring[]Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences.
seednumberThe seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results.
maxRetriesnumberMaximum number of retries. Set to 0 to disable retries. Note: In workflow context, retries are typically handled by the workflow step mechanism.
abortSignalAbortSignalAbort signal for cancelling the operation.
headersRecord<string, string | undefined>Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
providerOptionsSharedV3ProviderOptionsAdditional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider.

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 (with an auto-generated messageId) at the beginning of the stream. Defaults to true. Set to false when you write custom UIMessageChunks to the writable stream **before** calling agent.stream(). The auto-generated start chunk would otherwise create a second message in the UI because its messageId differs from the one established by your earlier chunks.
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.
maxStepsnumberMaximum number of sequential LLM calls (steps), e.g. when you use tool calls. A maximum number can be set to prevent infinite loops in the case of misconfigured tools. By default, it's unlimited (the agent loops until completion).
toolChoiceToolChoice<TTools>The tool choice strategy. Default: 'auto'. Overrides the toolChoice from the constructor if provided.
activeToolsNoInfer<keyof TTools>[]Limits the tools that are available for the model to call without changing the tool call and result types in the result.
experimental_telemetryTelemetrySettingsOptional telemetry configuration (experimental).
experimental_contextunknownContext that is passed into tool execution. Experimental (can break in patch releases).
experimental_outputOutputSpecification<OUTPUT, PARTIAL_OUTPUT>Optional specification for parsing structured outputs from the LLM response. Use Output.object({ schema }) for structured output or Output.text() for text output.
includeRawChunksbooleanWhether to include raw chunks from the provider in the stream. When enabled, you will receive raw chunks with type 'raw' that contain the unprocessed data from the provider. This allows access to cutting-edge provider features not yet wrapped by the AI SDK. Defaults to false.
experimental_repairToolCallToolCallRepairFunction<TTools>A function that attempts to repair a tool call that failed to parse.
experimental_transformStreamTextTransform<TTools> | StreamTextTransform<TTools>[]Optional stream transformations. They are applied in the order they are provided. The stream transformations must maintain the stream structure for streamText to work correctly.
experimental_downloadDownloadFunctionCustom download function to use for URLs. By default, files are downloaded if the model does not support the URL for the given media type.
onStepFinishStreamTextOnStepFinishCallback<TTools>Callback function to be called after each step completes.
onErrorStreamTextOnErrorCallbackCallback that is invoked when an error occurs during streaming. You can use it to log errors.
onFinishStreamTextOnFinishCallback<TTools, OUTPUT>Callback that is called when the LLM response and all request tool executions (for tools that have an execute function) are finished.
onAbortStreamTextOnAbortCallback<TTools>Callback that is called when the operation is aborted.
prepareStepPrepareStepCallback<TTools>Callback function called before each step in the agent loop. Use this to modify settings, manage context, or inject messages dynamically. Overrides the agent-level prepareStep for this stream when provided.
collectUIMessagesbooleanIf true, accumulates UIMessage[] during streaming. The accumulated messages will be available in the uiMessages property of the result. This is useful when you need the final UIMessage representation after streaming completes, without having to re-read the stream.
timeoutnumberTimeout in milliseconds for the stream operation. When specified, creates an AbortSignal that will abort the operation after the given time. If both timeout and abortSignal are provided, whichever triggers first will abort.
maxOutputTokensnumberMaximum number of tokens to generate.
temperaturenumberTemperature setting. The range depends on the provider and model. It is recommended to set either temperature or topP, but not both.
topPnumberNucleus sampling. This is a number between 0 and 1. E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered. It is recommended to set either temperature or topP, but not both.
topKnumberOnly sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.
presencePenaltynumberPresence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty.
frequencyPenaltynumberFrequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty.
stopSequencesstring[]Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences.
seednumberThe seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results.
maxRetriesnumberMaximum number of retries. Set to 0 to disable retries. Note: In workflow context, retries are typically handled by the workflow step mechanism.
abortSignalAbortSignalAbort signal for cancelling the operation.
headersRecord<string, string | undefined>Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
providerOptionsSharedV3ProviderOptionsAdditional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider.

DurableAgentStreamResult

The result returned from the stream() method:

NameTypeDescription
messagesModelMessage[]The final messages including all tool calls and results.
stepsStepResult<TTools>[]Details for all steps.
toolCallsToolCall[]The tool calls from the last step. Includes all tool calls regardless of whether they were executed. When the agent stops because a tool without an execute function was called, this array will contain those calls. Compare with toolResults to find unresolved tool calls that need client-side handling: ``ts const unresolved = result.toolCalls.filter( tc => !result.toolResults.some(tr => tr.toolCallId === tc.toolCallId) ); ` This matches the AI SDK's GenerateTextResult.toolCalls` behavior.
toolResultsToolResult[]The tool results from the last step. Only includes results for tools that were actually executed (server-side or provider-executed). Tools without an execute function will NOT have entries here. This matches the AI SDK's GenerateTextResult.toolResults behavior.
experimental_outputOUTPUTThe generated structured output. It uses the experimental_output specification. Only available when experimental_output is specified.
uiMessagesUIMessage<unknown, UIDataTypes, UITools>[]The accumulated UI messages from the stream. Only available when collectUIMessages is set to true in the stream options.

GenerationSettings

Settings that control model generation behavior. These can be set on the constructor or overridden per-stream call:

NameTypeDescription
maxOutputTokensnumberMaximum number of tokens to generate.
temperaturenumberTemperature setting. The range depends on the provider and model. It is recommended to set either temperature or topP, but not both.
topPnumberNucleus sampling. This is a number between 0 and 1. E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered. It is recommended to set either temperature or topP, but not both.
topKnumberOnly sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.
presencePenaltynumberPresence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty.
frequencyPenaltynumberFrequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty.
stopSequencesstring[]Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences.
seednumberThe seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results.
maxRetriesnumberMaximum number of retries. Set to 0 to disable retries. Note: In workflow context, retries are typically handled by the workflow step mechanism.
abortSignalAbortSignalAbort signal for cancelling the operation.
headersRecord<string, string | undefined>Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
providerOptionsSharedV3ProviderOptionsAdditional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider.

PrepareStepInfo

Information passed to the prepareStep callback:

NameTypeDescription
modelstring | (() => Promise<LanguageModelV3>)The current model configuration (string or function). The function should return a LanguageModelV3 instance.
stepNumbernumberThe current step number (0-indexed).
stepsStepResult<TTools>[]All previous steps with their results.
messagesLanguageModelV3PromptThe messages that will be sent to the model. This is the LanguageModelV3Prompt format used internally.
experimental_contextunknownThe context passed via the experimental_context setting (experimental).

PrepareStepResult

Return type from the prepareStep callback:

NameTypeDescription
modelstring | (() => Promise<LanguageModelV3>)Override the model for this step. The function should return a LanguageModelV3 instance.
systemstringOverride the system message for this step.
messagesLanguageModelV3PromptOverride the messages for this step. Use this for context management or message injection.
toolChoiceToolChoice<ToolSet>Override the tool choice for this step.
activeToolsstring[]Override the active tools for this step. Limits the tools that are available for the model to call.
experimental_contextunknownContext that is passed into tool execution. Experimental. Changing the context will affect the context in this step and all subsequent steps.
maxOutputTokensnumberMaximum number of tokens to generate.
temperaturenumberTemperature setting. The range depends on the provider and model. It is recommended to set either temperature or topP, but not both.
topPnumberNucleus sampling. This is a number between 0 and 1. E.g. 0.1 would mean that only tokens with the top 10% probability mass are considered. It is recommended to set either temperature or topP, but not both.
topKnumberOnly sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.
presencePenaltynumberPresence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The presence penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty.
frequencyPenaltynumberFrequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The frequency penalty is a number between -1 (increase repetition) and 1 (maximum penalty, decrease repetition). 0 means no penalty.
stopSequencesstring[]Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated. Providers may have limits on the number of stop sequences.
seednumberThe seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results.
maxRetriesnumberMaximum number of retries. Set to 0 to disable retries. Note: In workflow context, retries are typically handled by the workflow step mechanism.
abortSignalAbortSignalAbort signal for cancelling the operation.
headersRecord<string, string | undefined>Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
providerOptionsSharedV3ProviderOptionsAdditional provider-specific options. They are passed through to the provider from the AI SDK and enable provider-specific functionality that can be fully encapsulated in the provider.

TelemetrySettings

Configuration for observability and telemetry:

NameTypeDescription
isEnabledbooleanEnable or disable telemetry. Defaults to true.
recordInputsbooleanEnable or disable input recording. Enabled by default. You might want to disable input recording to avoid recording sensitive information, to reduce data transfers, or to increase performance.
recordOutputsbooleanEnable or disable output recording. Enabled by default. You might want to disable output recording to avoid recording sensitive information, to reduce data transfers, or to increase performance.
functionIdstringIdentifier for this function. Used to group telemetry data by function.
metadataRecord<string, string | number | boolean | (string | number | boolean)[] | null | undefined>Additional information to include in the telemetry data.
tracerunknownCustom tracer for the telemetry.

Callbacks

StreamTextOnFinishCallback

Called when streaming completes:

NameTypeDescription
event{ readonly steps: StepResult<TTools>[]; readonly messages: ModelMessage[]; readonly text: string; readonly finishReason: FinishReason; readonly totalUsage: LanguageModelUsage; readonly experimental_context: unknown; readonly experimental_output: OUTPUT; }

StreamTextOnErrorCallback

Called when an error occurs:

NameTypeDescription
event{ error: unknown; }

StreamTextOnAbortCallback

Called when the operation is aborted:

NameTypeDescription
event{ readonly steps: StepResult<TTools>[]; }

Advanced Types

ToolCallRepairFunction

Function to repair malformed tool calls:

NameTypeDescription
options{ toolCall: LanguageModelV3ToolCall; tools: TTools; error: unknown; messages: LanguageModelV3Prompt; }

LanguageModelV3ToolCall | Promise<LanguageModelV3ToolCall | null> | null

StreamTextTransform

Transform applied to the stream:

NameTypeDescription
options{ tools: TTools; stopStream: () => void; }

TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>

OutputSpecification

Specification for structured output parsing:

NameTypeDescription
namestring
responseFormatPromiseLike<{ type: "text"; } | { type: "json"; schema?: JSONSchema7; name?: string; description?: string; } | undefined>
parsePartialOutput(options: { text: string; }) => Promise<{ partial: PARTIAL; } | undefined>
parseCompleteOutput(options: { text: string; }, context: { response: LanguageModelResponseMetadata; usage: LanguageModelUsage; finishReason: FinishReason; }) => Promise<...>
  • 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 SDK for production-grade reliability
  • AI SDK Parity: Supports the same options as AI SDK's streamText including generation settings, callbacks, and structured output
  • 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 or maxSteps is reached
  • Default maxSteps is unlimited - set a value to limit the number of LLM calls
  • The stream() method returns { messages, steps, toolCalls, toolResults, experimental_output, uiMessages } containing the full conversation history, step details, tool call details, optional structured output, and optionally accumulated UI messages
  • Use collectUIMessages: true to accumulate UIMessage[] during streaming, useful for persisting conversation state without re-reading the stream
  • The prepareStep callback runs before each step and can modify model, messages, generation settings, tool choice, and context
  • Generation settings (temperature, maxOutputTokens, etc.) can be set on the constructor and overridden per-stream call
  • Use activeTools to limit which tools are available for a specific stream call
  • The onFinish callback is called when all steps complete; onAbort is called if aborted

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,
      },
    },
    instructions: "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 { getWritable } from "workflow";
import type { UIMessageChunk } from "ai";
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
    instructions: "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",
    instructions: "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 {};
    },
  });
}

Generation Settings

Configure model generation parameters at the constructor or stream level:

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

async function agentWithGenerationSettings() {
  "use workflow";

  // Set default generation settings in constructor
  const agent = new DurableAgent({
    model: "anthropic/claude-haiku-4.5",
    temperature: 0.7,
    maxOutputTokens: 2000,
    topP: 0.9,
  });

  // Override settings per-stream call
  await agent.stream({
    messages: [{ role: "user", content: "Write a creative story" }],
    writable: getWritable<UIMessageChunk>(),
    temperature: 0.9, // More creative for this call
    maxSteps: 1,
  });

  // Use different settings for a different task
  await agent.stream({
    messages: [{ role: "user", content: "Summarize this document precisely" }],
    writable: getWritable<UIMessageChunk>(),
    temperature: 0.1, // More deterministic
    maxSteps: 1,
  });
}

Limiting Steps with maxSteps

By default, the agent loops until completion. Use maxSteps to limit the number of LLM calls:

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

async function searchWeb({ query }: { query: string }) {
  "use step";
  return `Results for "${query}": ...`;
}

async function analyzeResults({ data }: { data: string }) {
  "use step";
  return `Analysis: ${data}`;
}

async function multiStepAgent() {
  "use workflow";

  const agent = new DurableAgent({
    model: "anthropic/claude-haiku-4.5",
    tools: {
      searchWeb: {
        description: "Search the web for information",
        inputSchema: z.object({ query: z.string() }),
        execute: searchWeb,
      },
      analyzeResults: {
        description: "Analyze search results",
        inputSchema: z.object({ data: z.string() }),
        execute: analyzeResults,
      },
    },
  });

  // Limit to 10 steps for safety on complex research tasks
  const result = await agent.stream({
    messages: [{ role: "user", content: "Research the latest AI trends and provide an analysis" }],
    writable: getWritable<UIMessageChunk>(),
    maxSteps: 10,
  });

  // Access step-by-step details
  console.log(`Completed in ${result.steps.length} steps`);
}

Callbacks for Monitoring

Use callbacks to monitor streaming progress, handle errors, and react to completion:

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

async function agentWithCallbacks() {
  "use workflow";

  const agent = new DurableAgent({
    model: "anthropic/claude-haiku-4.5",
  });

  await agent.stream({
    messages: [{ role: "user", content: "Hello!" }],
    writable: getWritable<UIMessageChunk>(),
    maxSteps: 5,

    // Called after each step completes
    onStepFinish: async (step) => {
      console.log(`Step finished: ${step.finishReason}`);
      console.log(`Tokens used: ${step.usage.totalTokens}`);
    },

    // Called when streaming completes
    onFinish: async ({ steps, messages }) => {
      console.log(`Completed with ${steps.length} steps`);
      console.log(`Final message count: ${messages.length}`);
    },

    // Called on errors
    onError: async ({ error }) => {
      console.error("Stream error:", error);
    },
  });
}

Structured Output

Parse structured data from the LLM response using Output.object:

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

async function agentWithStructuredOutput() {
  "use workflow";

  const agent = new DurableAgent({
    model: "anthropic/claude-haiku-4.5",
  });

  const result = await agent.stream({
    messages: [{ role: "user", content: "Analyze the sentiment of: 'I love this product!'" }],
    writable: getWritable<UIMessageChunk>(),
    experimental_output: Output.object({
      schema: z.object({
        sentiment: z.enum(["positive", "negative", "neutral"]),
        confidence: z.number().min(0).max(1),
        reasoning: z.string(),
      }),
    }),
  });

  // Access the parsed structured output
  console.log(result.experimental_output);
  // { sentiment: "positive", confidence: 0.95, reasoning: "..." }
}

Tool Choice Control

Control when and which tools the model can use:

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

async function agentWithToolChoice() {
  "use workflow";

  const agent = new DurableAgent({
    model: "anthropic/claude-haiku-4.5",
    tools: {
      calculator: {
        description: "Perform calculations",
        inputSchema: z.object({ expression: z.string() }),
        execute: async ({ expression }) => `Calculated: ${expression}`,
      },
      search: {
        description: "Search for information",
        inputSchema: z.object({ query: z.string() }),
        execute: async ({ query }) => `Results for: ${query}`,
      },
    },
    toolChoice: "auto", // Default: model decides
  });

  // Force the model to use a tool
  await agent.stream({
    messages: [{ role: "user", content: "What is 2 + 2?" }],
    writable: getWritable<UIMessageChunk>(),
    toolChoice: "required",
    maxSteps: 2,
  });

  // Prevent tool usage
  await agent.stream({
    messages: [{ role: "user", content: "Just chat with me" }],
    writable: getWritable<UIMessageChunk>(),
    toolChoice: "none",
  });

  // Force a specific tool
  await agent.stream({
    messages: [{ role: "user", content: "Calculate something" }],
    writable: getWritable<UIMessageChunk>(),
    toolChoice: { type: "tool", toolName: "calculator" },
    maxSteps: 2,
  });

  // Limit available tools for this call
  await agent.stream({
    messages: [{ role: "user", content: "Just search, don't calculate" }],
    writable: getWritable<UIMessageChunk>(),
    activeTools: ["search"],
    maxSteps: 2,
  });
}

Passing Context to Tools

Use experimental_context to pass shared context to tool executions:

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

interface UserContext {
  userId: string;
  permissions: string[];
}

async function agentWithContext(userId: string) {
  "use workflow";

  const agent = new DurableAgent({
    model: "anthropic/claude-haiku-4.5",
    tools: {
      getUserData: {
        description: "Get user data",
        inputSchema: z.object({}),
        execute: async (_, { experimental_context }) => {
          const ctx = experimental_context as UserContext;
          return { userId: ctx.userId, permissions: ctx.permissions };
        },
      },
    },
  });

  await agent.stream({
    messages: [{ role: "user", content: "What are my permissions?" }],
    writable: getWritable<UIMessageChunk>(),
    maxSteps: 2,
    experimental_context: {
      userId,
      permissions: ["read", "write"],
    } as UserContext,
  });
}

Collecting UI Messages

Use collectUIMessages to accumulate UIMessage[] during streaming. This is useful when you need to persist the conversation without re-reading the run's output stream:

import { DurableAgent } from "@workflow/ai/agent";
import { getWritable } from "workflow";
import type { UIMessage, UIMessageChunk } from "ai";

async function agentWithUIMessages(userMessage: string) {
  "use workflow";

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

  const result = await agent.stream({
    messages: [{ role: "user", content: userMessage }],
    writable: getWritable<UIMessageChunk>(),
    collectUIMessages: true, 
  });

  // Access the accumulated UI messages
  const uiMessages: UIMessage[] = result.uiMessages ?? []; 

  // Persist messages to a database
  await saveConversation(uiMessages);

  return result;
}

async function saveConversation(messages: UIMessage[]) {
  "use step";
  // Save to database...
}

The uiMessages property is only available when collectUIMessages is set to true. When disabled, uiMessages is undefined.

Machine-Readable Tool Results

stream() returns tool call information you can inspect programmatically. Compare toolCalls with toolResults to find unresolved tool calls that need client-side handling:

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

async function checkOrderStatus({ orderId }: { orderId: string }) {
  "use step";
  return `Order ${orderId}: shipped`;
}

async function agentWithToolInspection(userMessage: string) {
  "use workflow";

  const agent = new DurableAgent({
    model: "anthropic/claude-haiku-4.5",
    tools: {
      checkOrderStatus: {
        description: "Check order status",
        inputSchema: z.object({ orderId: z.string() }),
        execute: checkOrderStatus,
      },
    },
  });

  const result = await agent.stream({
    messages: [{ role: "user", content: userMessage }],
    writable: getWritable<UIMessageChunk>(),
  });

  const unresolved = result.toolCalls.filter( 
    (tc) => !result.toolResults.some((tr) => tr.toolCallId === tc.toolCallId) 
  ); 

  if (unresolved.length > 0) {
    return {
      status: "needs-client-tools",
      unresolved,
    };
  }

  return {
    status: "complete",
    messages: result.messages,
    toolResults: result.toolResults,
  };
}

toolCalls and toolResults reflect the last step of the agent loop. Tools without an execute function will appear in toolCalls but not in toolResults, which is how you detect calls that need client-side handling.

Aborting Long-Running Streams

Use timeout to abort a stream automatically after a fixed duration:

abortSignal is not yet supported and will be available in a future release. Use timeout for now.

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

async function agentWithTimeout(userMessage: string) {
  "use workflow";

  const agent = new DurableAgent({
    model: "anthropic/claude-haiku-4.5",
  });

  await agent.stream({
    messages: [{ role: "user", content: userMessage }],
    writable: getWritable<UIMessageChunk>(),
    timeout: 30_000, 
  });
}