# How to Add a New Tool Definition with Zod Schemas in Freebuff

> Learn to add a new tool definition in Freebuff using Zod schemas for input validation and an async execute function. Simplify your custom tool integration.

- Repository: [Codebuff/freebuff](https://github.com/CodebuffAI/freebuff)
- Tags: how-to-guide
- Published: 2026-08-20

---

**To add a custom tool in Freebuff, use `getCustomToolDefinition` with a Zod schema for input validation and an async `execute` function that returns `ToolResultOutput[]`.**

Freebuff's SDK makes it straightforward to extend the platform with custom tools that agents can invoke during runs. A custom tool definition combines **Zod schema validation** with a typed execution function, letting you safely integrate external APIs or bespoke logic into your AI workflows. This guide walks through the complete implementation based on the CodebuffAI/freebuff source code.

## The Three Parts of a Custom Tool Definition

Creating a viable tool requires three distinct components that work together:

1. **Unique tool name** — must not conflict with built-in tools defined in `ToolName`
2. **Zod schema** — validates and types the tool's input parameters
3. **Execute function** — contains your implementation logic

All three are wrapped by the `getCustomToolDefinition` helper located in [[`sdk/src/custom-tool.ts`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/custom-tool.ts)](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/custom-tool.ts).

### Choosing a Tool Name

Before defining your tool, verify your chosen name doesn't collide with built-in tools. The exhaustive list lives in [[`agents/types/tools.ts`](https://github.com/CodebuffAI/freebuff/blob/main/agents/types/tools.ts)](https://github.com/CodebuffAI/freebuff/blob/main/agents/types/tools.ts):

```typescript
// agents/types/tools.ts
export type ToolName =
  | 'file_read'
  | 'file_write'
  | 'bash_command'
  | 'ls'
  // ... additional built-in tools

```

Pick any string not present in this union. The runtime will reject duplicate registrations.

### Creating the Zod Schema

The `inputSchema` field accepts any `z.ZodType<Args, Input>`. Freebuff automatically converts this to JSON Schema for LLM consumption:

```typescript
import { z } from 'zod/v4';

const weatherSchema = z.object({
  city: z.string().min(1, 'City name required'),
  units: z.enum(['metric', 'imperial']).default('metric'),
});

```

Zod's inference gives you fully typed arguments in the `execute` function without manual type duplication.

## Complete Implementation Example

Here's a production-ready custom tool that fetches weather data, assembled step by step:

```typescript
// 1️⃣ Import the SDK and Zod
import { CodebuffClient, getCustomToolDefinition } from '@codebuff/sdk';
import { z } from 'zod/v4';

// 2️⃣ Define the custom tool using Zod schemas
const weatherTool = getCustomToolDefinition({
  toolName: 'fetch_weather',                     // ── unique identifier
  description: 'Fetch current weather for a city',
  inputSchema: z.object({                       // ── Zod schema for validation
    city: z.string().min(1, 'City name required'),
    units: z.enum(['metric', 'imperial']).default('metric'),
  }),
  exampleInputs: [{ city: 'San Francisco', units: 'metric' }],

  // 3️⃣ Implement execute with parsed, typed arguments
  execute: async ({ city, units }) => {
    const apiKey = process.env.OPENWEATHER_API_KEY;
    const resp = await fetch(
      `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(
        city,
      )}&units=${units}&appid=${apiKey}`,
    );
    const data = await resp.json();

    // Return structured output the LLM can consume
    return [
      {
        type: 'json' as const,
        value: {
          temperature: data.main.temp,
          description: data.weather[0].description,
        },
      },
    ];
  },
});

```

The `execute` function receives arguments already validated against your Zod schema. It must return `Promise<ToolResultOutput[]>`, where each output has a `type` field (`'json'`, `'text'`, `'file'`, etc.) plus type-specific data.

## Registering and Running Your Tool

To make the tool available during a run, pass it in `customToolDefinitions`. To let an agent actually invoke it, include the name in `toolNames`:

```typescript
const client = new CodebuffClient({
  apiKey: process.env.CODEBUFF_API_KEY,
  cwd: process.cwd(),
});

// Optional: define an agent aware of the custom tool
const sentimentAgent = {
  id: 'sentiment-analyzer',
  model: 'google/gemini-3.1-flash-lite',
  toolNames: ['fetch_weather'],   // <-- agent can now call this tool
  instructionsPrompt: `
    Analyze sentiment of the user's message.
    If the user mentions a location, call fetch_weather and include weather in the analysis.
  `,
};

const { output } = await client.run({
  agent: 'sentiment-analyzer',
  prompt: 'I am in Paris and feeling a bit gloomy.',
  agentDefinitions: [sentimentAgent],
  customToolDefinitions: [weatherTool], // <<< tool registration
  handleEvent: (e) => console.log('Event →', JSON.stringify(e)),
});

```

## Reference: Key Source Files

| File | Purpose |
|------|---------|
| [[`sdk/src/custom-tool.ts`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/custom-tool.ts)](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/custom-tool.ts) | `getCustomToolDefinition` helper and `CustomToolDefinition` type |
| [[`agents/types/tools.ts`](https://github.com/CodebuffAI/freebuff/blob/main/agents/types/tools.ts)](https://github.com/CodebuffAI/freebuff/blob/main/agents/types/tools.ts) | Built-in `ToolName` union for collision checking |
| [[`sdk/examples/readme-example-2.ts`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/examples/readme-example-2.ts)](https://github.com/CodebuffAI/freebuff/blob/main/sdk/examples/readme-example-2.ts) | Working `fetch_api_data` custom tool example |
| [[`sdk/e2e/custom-agents/weather-agent.e2e.test.ts`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/e2e/custom-agents/weather-agent.e2e.test.ts)](https://github.com/CodebuffAI/freebuff/blob/main/sdk/e2e/custom-agents/weather-agent.e2e.test.ts) | E2E test demonstrating full integration |

## Summary

- **Use `getCustomToolDefinition`** from `@codebuff/sdk` to build type-safe tool definitions with Zod schemas
- **Verify uniqueness** against `ToolName` in [`agents/types/tools.ts`](https://github.com/CodebuffAI/freebuff/blob/main/agents/types/tools.ts) before naming your tool
- **Structure `execute`** to return `ToolResultOutput[]` with typed outputs (`json`, `text`, `file`)
- **Register in two places**: `customToolDefinitions` array for availability, agent's `toolNames` for invocation permission
- **Leverage Zod inference**—no manual type declarations needed for arguments

## Frequently Asked Questions

### What Zod schema versions are supported?

Freebuff uses Zod v4 as shown in its examples. Import from `zod/v4` for compatibility. The `inputSchema` accepts any `z.ZodType`, including objects, unions, intersections, and transformed types.

### Can I return multiple output types from one tool execution?

Yes. The `execute` function returns `ToolResultOutput[]`, allowing mixed output types. For example, return both a JSON payload and a file attachment in the same array: `[{ type: 'json', value: data }, { type: 'file', path: '/tmp/output.log' }]`.

### What happens if my Zod schema validation fails?

Since Freebuff converts your Zod schema to JSON Schema for the LLM, invalid arguments generated by the model trigger a validation error before `execute` runs. The runtime handles this gracefully, typically requesting corrected arguments from the LLM rather than crashing your client.

### Do I need to register the tool with both `customToolDefinitions` and `toolNames`?

`customToolDefinitions` makes the tool exist in the runtime; `toolNames` within an agent definition grants that agent permission to call it. For agentless runs or when using built-in agents, you might only need `customToolDefinitions`. For custom agents, both are typically required.