> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.thatdevguy.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Anvil Agent Tools: Zod Schemas and Execute Functions

> Define Anvil agent tools with Zod schemas and execute functions. The runtime validates inputs and feeds results back to the model in the tool loop.

Tools are the primary way your agent interacts with the outside world — fetching data, querying databases, calling APIs, and performing computations. Each tool you pass to `defineAgent` is advertised to the model, and the runtime handles the full call-validate-execute cycle automatically.

## The `AgentTool` interface

Every tool you define implements this interface:

```ts theme={null}
interface AgentTool<Input = unknown> {
  name: string;
  description: string;
  zodSchema?: z.ZodType<Input>;
  inputSchema?: JsonSchema;
  sideEffect?: boolean;
  execute: (input: Input, meta: ToolExecMeta) => Promise<unknown> | unknown;
}
```

<ParamField body="name" type="string" required>
  A unique identifier for the tool. The model uses this name when requesting a call. Use `snake_case` — most model providers require it.
</ParamField>

<ParamField body="description" type="string" required>
  A plain-English description of what the tool does. The model reads this to decide when to call the tool, so write it from the model's perspective: what information does the tool provide, and when should it be used?
</ParamField>

<ParamField body="zodSchema" type="z.ZodType<Input>">
  A Zod schema for the tool's input. **Preferred over `inputSchema`** — the runtime converts it to JSON Schema automatically (to advertise to the model) and validates the model's input against it before calling `execute`. If validation fails, the model receives a descriptive error result instead of a crash.
</ParamField>

<ParamField body="inputSchema" type="JsonSchema">
  A raw JSON Schema for the tool's input. Use this if you cannot express the input as a Zod schema. If both `zodSchema` and `inputSchema` are present, `zodSchema` is used for validation and `inputSchema` is ignored.
</ParamField>

<ParamField body="sideEffect" type="boolean">
  Annotates the tool as one that mutates external state (sends an email, writes to a database, calls a payment API, etc.). This is a documentation signal — use it to communicate intent to other developers reading the agent definition.
</ParamField>

<ParamField body="execute" type="(input: Input, meta: ToolExecMeta) => Promise<unknown> | unknown" required>
  The async function that runs when the model calls this tool. Return any serializable value — it becomes the tool result fed back to the model. Throw an error to return an error result.
</ParamField>

## The `ToolExecMeta` argument

`execute` receives a second `meta` argument with runtime context:

<ParamField body="meta.callId" type="string">
  The unique ID for this tool invocation, assigned by the model. Useful for correlating logs and traces.
</ParamField>

<ParamField body="meta.signal" type="AbortSignal | undefined">
  The request's `AbortSignal`. Pass it to any `fetch` calls or async work inside the tool so that a client disconnect cancels in-flight I/O.
</ParamField>

<ParamField body="meta.requestApproval" type="(payload?: unknown) => never">
  Throw this to suspend the run and request human approval before proceeding (human-in-the-loop). The run is checkpointed and the client receives a `suspended` event. Resume with `resumeAgent` after the operator approves.
</ParamField>

## Complete tool example

```ts theme={null}
import { defineAgent } from 'anvil/agent';
import { LlmClient, AnthropicDriver } from 'anvil/llm';
import { z } from 'zod';

const client = new LlmClient({
  drivers: [new AnthropicDriver({ apiKey: process.env.ANTHROPIC_API_KEY })],
  defaultModel: 'claude-opus-4-8',
});

export default defineAgent({
  client,
  system: 'You are a helpful assistant. Use the provided tools to answer questions.',
  maxIterations: 5,
  tools: [
    {
      name: 'search_products',
      description: 'Search the product catalog by keyword. Returns a list of matching products with name, price, and stock status.',
      zodSchema: z.object({
        query: z.string().describe('The search keyword or phrase'),
        limit: z.number().int().min(1).max(20).default(5).describe('Maximum results to return'),
      }),
      execute: async ({ query, limit }, meta) => {
        const results = await fetch(
          `/internal/products?q=${encodeURIComponent(query)}&limit=${limit}`,
          { signal: meta.signal },
        ).then((r) => r.json());
        return results;
      },
    },
    {
      name: 'place_order',
      description: 'Place an order for a product. Only call this after confirming the product ID and quantity with the user.',
      sideEffect: true, // annotates this tool as one that mutates external state
      zodSchema: z.object({
        productId: z.string(),
        quantity: z.number().int().min(1),
      }),
      execute: async ({ productId, quantity }, meta) => {
        // Request human approval before placing the order
        if (quantity > 10) {
          meta.requestApproval({ productId, quantity, reason: 'Large order requires approval' });
        }
        const order = await createOrder(productId, quantity);
        return { orderId: order.id, status: 'placed' };
      },
    },
  ],
});
```

## Input validation

The runtime validates tool inputs against `zodSchema` before calling `execute`. You never need to validate inside the function body:

```ts theme={null}
// The runtime already ensures `city` is a non-empty string when execute runs.
{
  name: 'get_weather',
  description: 'Get the current weather for a city',
  zodSchema: z.object({ city: z.string().min(1) }),
  execute: ({ city }) => fetchWeather(city),
}
```

If the model sends invalid input (a missing required field, a number where a string is expected), the runtime returns a descriptive Zod error message to the model as the tool result. The model can then correct its call on the next iteration.

<Warning>
  Always use `zodSchema` for user-facing tools. Without it, malformed model output reaches `execute` unvalidated. Raw `inputSchema` provides no runtime protection — it only shapes what the model is told.
</Warning>

## Preventing infinite loops with `maxIterations`

By default the agent runtime caps the model↔tool loop at **10 iterations**. Each call to the model counts as one iteration, regardless of how many tools it calls in that turn. Set `maxIterations` on `defineAgent` to tighten or loosen this limit:

```ts theme={null}
export default defineAgent({
  client,
  maxIterations: 3, // stop after at most 3 model calls
  tools: [...],
});
```

When the cap is reached the runtime emits a `final` event with whatever text the model produced last. The `stoppedAtCap` flag in `AgentRunResult` tells you whether the run ended this way.

<Tip>
  Set `maxIterations` to a small value (2–3) for tools that should complete in one or two steps, and only raise it for complex multi-step workflows. Smaller caps reduce latency and cost when a runaway tool loop occurs.
</Tip>

## Standalone tools and `anvil mcp`

Tools defined inline in `defineAgent` are only available to that agent route. If you also want a tool served via the MCP protocol (for use with Claude Desktop or other MCP clients), define it as a standalone tool in `server/tools/`:

```ts theme={null}
// server/tools/wordCount.ts
import { z } from 'zod';

export const description = 'Count the words in a piece of text';
export const inputSchema = z.object({ text: z.string() });

export default async function wordCount(args: { text: string }) {
  const words = args.text.trim().split(/\s+/).filter(Boolean);
  return { words: words.length, characters: args.text.length };
}
```

Running `anvil mcp` serves both `server/tools/*.ts` files and any MCP-exposed route-derived tools from the same endpoint. Route-level tools defined in `defineAgent` are not automatically MCP-exposed — only `server/tools/` files and routes with `meta.mcp.expose: true` appear there.
