> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/vercel/workflow/llms.txt
> Use this file to discover all available pages before exploring further.

# Defining Tools

> Common patterns for defining tools in durable AI agents using Workflow DevKit.

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:

```typescript title="workflows/chat/steps/tools.ts" lineNumbers theme={null}
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:

```typescript title="workflows/chat/steps/tools.ts" lineNumbers theme={null}
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](/ai/streaming-updates-from-tools), tools can write custom data to the stream for progress updates:

```typescript title="workflows/chat/steps/tools.ts" lineNumbers theme={null}
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:

```typescript title="workflows/chat/utils/stream.ts" lineNumbers theme={null}
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:

| Capability | Step-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:

```typescript lineNumbers theme={null}
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:

```typescript lineNumbers theme={null}
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:

```typescript lineNumbers theme={null}
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:

```typescript lineNumbers theme={null}
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:

```typescript lineNumbers theme={null}
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:

```typescript lineNumbers theme={null}
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](/concepts/errors-and-retries).

## Tool Result Types

Tools can return different types of results:

```typescript lineNumbers theme={null}
// 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" };
}
```

## Related Documentation

* [Streaming Updates from Tools](/ai/streaming-updates-from-tools) - Progress updates during execution
* [Sleep and Delays](/ai/sleep-and-delays) - Using `sleep()` in tools
* [Workflows and Steps](/concepts/workflows-and-steps) - Core concepts
* [`getWritable()` API Reference](/api-reference/workflow/get-writable) - Stream API details


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.