Skip to main content
This page covers common patterns for defining tools in AI agents using Workflow DevKit. With DurableAgent, we typically model tools as steps. These can range from simple function calls to entire multi-day workflows.

Basic Tool Definition

Tools in DurableAgent follow the same structure as AI SDK tools:
workflows/chat/steps/tools.ts
import { z } from "zod";

export const tools = {
  getWeather: {
    description: "Get weather for a location",
    inputSchema: z.object({
      location: z.string().describe("City name"),
    }),
    execute: getWeatherStep,
  },
};

async function getWeatherStep({ location }: { location: string }) {
  "use step";

  // Automatically retried on failure
  const response = await fetch(`https://api.weather.com/${location}`);
  return response.json();
}
The key difference from standard AI SDK tools is the "use step" directive, which makes the tool execution durable with automatic retries.

Accessing Message Context

Tools receive the full conversation context as a second argument:
workflows/chat/steps/tools.ts
import type { LanguageModelV2Prompt } from "@ai-sdk/provider";

async function analyzeConversation(
  { topic }: { topic: string },
  { messages, toolCallId }: { messages: LanguageModelV2Prompt; toolCallId: string }
) {
  "use step";

  // Access the full conversation history
  const userMessages = messages.filter((m) => m.role === "user");
  const messageCount = userMessages.length;

  return {
    topic,
    messageCount,
    summary: `Analyzed ${messageCount} user messages about ${topic}`,
  };
}
The context object provides:
  • messages: The full conversation as LanguageModelV2Prompt
  • toolCallId: Unique identifier for this tool invocation
  • experimental_context: Custom context passed to the agent

Writing to Streams

As discussed in Streaming Updates from Tools, tools can write custom data to the stream for progress updates:
workflows/chat/steps/tools.ts
import { getWritable } from "workflow";
import type { UIMessageChunk } from "ai";

async function searchFlights(
  { from, to }: { from: string; to: string },
  { toolCallId }: { toolCallId: string }
) {
  "use step";

  const writable = getWritable<UIMessageChunk>();
  const writer = writable.getWriter();

  try {
    // Emit progress updates
    await writer.write({
      type: "data-search-progress",
      id: toolCallId,
      data: { status: "searching", from, to },
    });

    const flights = await fetchFlights(from, to);

    // Emit each result as it's found
    for (const flight of flights) {
      await writer.write({
        type: "data-found-flight",
        id: `${toolCallId}-${flight.id}`,
        data: flight,
      });
    }

    return { flights, count: flights.length };
  } finally {
    writer.releaseLock();
  }
}
Create a reusable helper for stream writing:
workflows/chat/utils/stream.ts
import { getWritable } from "workflow";
import type { UIMessageChunk } from "ai";

export async function writeToStream(data: UIMessageChunk) {
  "use step";

  const writable = getWritable<UIMessageChunk>();
  const writer = writable.getWriter();
  try {
    await writer.write(data);
  } finally {
    writer.releaseLock();
  }
}

Step-Level vs Workflow-Level Tools

Tools can be implemented at either the step level or workflow level, with different capabilities:
CapabilityStep-Level ("use step")Workflow-Level ("use workflow")
getWritable()
Automatic retries
Side-effects (API calls)
sleep()
createWebhook()
defineHook().create()

Step-Level Tools

Best for I/O operations that need automatic retries:
lineNumbers
async function fetchUserData({ userId }: { userId: string }) {
  "use step";

  // Automatically retried on network errors
  const response = await fetch(`https://api.example.com/users/${userId}`);
  if (!response.ok) throw new Error("Failed to fetch user");
  
  return response.json();
}

Workflow-Level Tools

Best for orchestration that needs workflow primitives:
lineNumbers
import { sleep } from "workflow";

async function scheduledTask({ delayMs }: { delayMs: number }) {
  // No "use step" - this is workflow-level
  
  await sleep(delayMs);
  
  // Call a step for the actual I/O
  return await performTaskStep();
}

async function performTaskStep() {
  "use step";
  
  // I/O with automatic retries
  const response = await fetch("https://api.example.com/task");
  return response.json();
}

Hybrid Tools

Combine both approaches for complex tools:
lineNumbers
import { sleep } from "workflow";

// Workflow-level: orchestrates the flow
async function retryWithBackoff({ url }: { url: string }) {
  const delays = [1000, 5000, 10000];
  
  for (const delay of delays) {
    try {
      return await fetchWithTimeout(url);
    } catch (error) {
      await sleep(delay);
    }
  }
  
  throw new Error("All retries exhausted");
}

// Step-level: handles I/O
async function fetchWithTimeout(url: string) {
  "use step";
  
  const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
  return response.json();
}

Complex Tool Patterns

Multi-Step Tools

Break complex operations into multiple steps:
lineNumbers
async function processDocument(
  { documentId }: { documentId: string },
  { toolCallId }: { toolCallId: string }
) {
  // Workflow-level orchestration
  const writable = getWritable<UIMessageChunk>();
  
  // Step 1: Download
  const document = await downloadDocument(documentId);
  await writeProgress(writable, toolCallId, "downloaded");
  
  // Step 2: Process
  const processed = await processDocumentStep(document);
  await writeProgress(writable, toolCallId, "processed");
  
  // Step 3: Upload
  const result = await uploadResult(processed);
  await writeProgress(writable, toolCallId, "uploaded");
  
  return result;
}

async function downloadDocument(id: string) {
  "use step";
  const response = await fetch(`https://api.example.com/docs/${id}`);
  return response.arrayBuffer();
}

async function processDocumentStep(data: ArrayBuffer) {
  "use step";
  // Process the document
  return { processed: true, size: data.byteLength };
}

async function uploadResult(result: any) {
  "use step";
  const response = await fetch("https://api.example.com/results", {
    method: "POST",
    body: JSON.stringify(result),
  });
  return response.json();
}

async function writeProgress(
  writable: WritableStream,
  toolCallId: string,
  status: string
) {
  "use step";
  const writer = writable.getWriter();
  try {
    await writer.write({
      type: "data-progress",
      id: toolCallId,
      data: { status },
    });
  } finally {
    writer.releaseLock();
  }
}

Conditional Tools

Implement conditional logic at the workflow level:
lineNumbers
async function conditionalFetch({ url, useCache }: { url: string; useCache: boolean }) {
  if (useCache) {
    const cached = await getCachedData(url);
    if (cached) return cached;
  }
  
  return await fetchFreshData(url);
}

async function getCachedData(url: string) {
  "use step";
  // Check cache
  const response = await fetch(`https://cache.example.com/${encodeURIComponent(url)}`);
  if (!response.ok) return null;
  return response.json();
}

async function fetchFreshData(url: string) {
  "use step";
  const response = await fetch(url);
  return response.json();
}

Error Handling

Tools can throw errors that are automatically retried (for steps) or returned to the agent:
lineNumbers
import { RetryableError } from "workflow";

async function rateLimitedApi({ endpoint }: { endpoint: string }) {
  "use step";
  
  const response = await fetch(endpoint);
  
  if (response.status === 429) {
    const retryAfter = response.headers.get("Retry-After");
    throw new RetryableError("Rate limited", {
      retryAfter: retryAfter ? parseInt(retryAfter) * 1000 : "1m",
    });
  }
  
  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }
  
  return response.json();
}
Learn more in Errors and Retries.

Tool Result Types

Tools can return different types of results:
lineNumbers
// String result
async function getWeather({ location }: { location: string }) {
  "use step";
  return `Weather in ${location}: Sunny, 72°F`;
}

// Object result
async function searchFlights(params: { from: string; to: string }) {
  "use step";
  return {
    flights: [...],
    count: 5,
    cheapest: { price: 299 },
  };
}

// Error result (returned to agent)
async function bookFlight({ flightId }: { flightId: string }) {
  "use step";
  
  const response = await fetch(`https://api.example.com/book/${flightId}`, {
    method: "POST",
  });
  
  if (!response.ok) {
    return {
      error: true,
      message: "Booking failed: Flight is full",
    };
  }
  
  return { success: true, confirmationCode: "ABC123" };
}

Build docs developers (and LLMs) love