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

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).

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):

// 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:

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:

// 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:

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) getCustomToolDefinition helper and CustomToolDefinition type
[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) 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) 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →