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:
- Unique tool name — must not conflict with built-in tools defined in
ToolName - Zod schema — validates and types the tool's input parameters
- 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
getCustomToolDefinitionfrom@codebuff/sdkto build type-safe tool definitions with Zod schemas - Verify uniqueness against
ToolNameinagents/types/tools.tsbefore naming your tool - Structure
executeto returnToolResultOutput[]with typed outputs (json,text,file) - Register in two places:
customToolDefinitionsarray for availability, agent'stoolNamesfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →