Skip to main content

Overview

defineTool() is a helper function for creating type-safe server-side tools that execute in your runtime environment. It provides a clean API for defining tool schemas and execution logic with full TypeScript type inference.

Function Signature

function defineTool<TParameters extends z.ZodTypeAny>(config: {
  name: string;
  description: string;
  parameters: TParameters;
  execute: (args: z.infer<TParameters>) => Promise<unknown>;
}): ToolDefinition<TParameters>

Parameters

name
string
required
Unique identifier for the tool. Used by the LLM to call the tool.Convention: Use camelCase or snake_case.
name: 'searchDatabase'
// or
name: 'search_database'
description
string
required
Human-readable description of what the tool does. This is shown to the LLM to help it decide when to use the tool.Best practices:
  • Be specific about what the tool does
  • Mention the type of data it returns
  • Include any important constraints or requirements
description: 'Search the product database by name or category. Returns up to 10 matching products with their IDs, names, and prices.'
parameters
z.ZodTypeAny
required
Zod schema defining the tool’s input parameters. Provides type safety and validation.
import { z } from 'zod';

parameters: z.object({
  query: z.string().describe('Search query'),
  category: z.string().optional().describe('Filter by category'),
  limit: z.number().default(10).describe('Maximum results')
})
execute
(args: z.infer<TParameters>) => Promise<unknown>
required
Async function that executes the tool logic. Receives validated parameters and returns the result.
  • Parameters are automatically validated against the Zod schema
  • Return value should be JSON-serializable
  • Errors are caught and returned as error responses
execute: async ({ query, category, limit }) => {
  const results = await database.search(query, {
    category,
    limit
  });
  return results;
}

Return Value

Returns a ToolDefinition object that can be used with BuiltInAgent:
interface ToolDefinition<TParameters extends z.ZodTypeAny> {
  name: string;
  description: string;
  parameters: TParameters;
  execute: (args: z.infer<TParameters>) => Promise<unknown>;
}

Usage Examples

Basic Tool

import { defineTool } from '@copilotkit/agent';
import { z } from 'zod';

const getCurrentTime = defineTool({
  name: 'getCurrentTime',
  description: 'Get the current time in a specific timezone',
  parameters: z.object({
    timezone: z.string().describe('IANA timezone name (e.g., "America/New_York")')
  }),
  execute: async ({ timezone }) => {
    const date = new Date();
    const formatter = new Intl.DateTimeFormat('en-US', {
      timeZone: timezone,
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      timeZoneName: 'short'
    });
    return {
      time: formatter.format(date),
      timezone
    };
  }
});

Database Query Tool

const searchProducts = defineTool({
  name: 'searchProducts',
  description: 'Search the product catalog by name, description, or category',
  parameters: z.object({
    query: z.string().describe('Search query'),
    category: z.enum(['electronics', 'clothing', 'books', 'home']).optional(),
    minPrice: z.number().optional().describe('Minimum price in USD'),
    maxPrice: z.number().optional().describe('Maximum price in USD'),
    limit: z.number().default(10).describe('Maximum number of results')
  }),
  execute: async ({ query, category, minPrice, maxPrice, limit }) => {
    const results = await db.products.findMany({
      where: {
        OR: [
          { name: { contains: query, mode: 'insensitive' } },
          { description: { contains: query, mode: 'insensitive' } }
        ],
        ...(category && { category }),
        ...(minPrice && { price: { gte: minPrice } }),
        ...(maxPrice && { price: { lte: maxPrice } })
      },
      take: limit
    });
    
    return {
      count: results.length,
      products: results.map(p => ({
        id: p.id,
        name: p.name,
        price: p.price,
        category: p.category
      }))
    };
  }
});

API Integration Tool

const getWeatherForecast = defineTool({
  name: 'getWeatherForecast',
  description: 'Get weather forecast for a location for the next 7 days',
  parameters: z.object({
    location: z.string().describe('City name or coordinates (lat,lon)'),
    units: z.enum(['metric', 'imperial']).default('metric')
  }),
  execute: async ({ location, units }) => {
    const apiKey = process.env.WEATHER_API_KEY;
    const response = await fetch(
      `https://api.weather.com/forecast?location=${encodeURIComponent(location)}&units=${units}&key=${apiKey}`
    );
    
    if (!response.ok) {
      throw new Error(`Weather API error: ${response.statusText}`);
    }
    
    const data = await response.json();
    return {
      location: data.location,
      forecast: data.daily.slice(0, 7).map(day => ({
        date: day.date,
        temperature: {
          high: day.temp.max,
          low: day.temp.min
        },
        conditions: day.weather[0].description,
        precipitation: day.pop
      }))
    };
  }
});

File System Tool

const readFile = defineTool({
  name: 'readFile',
  description: 'Read contents of a file from the project directory',
  parameters: z.object({
    path: z.string().describe('Relative path to the file'),
    encoding: z.enum(['utf8', 'base64']).default('utf8')
  }),
  execute: async ({ path, encoding }) => {
    const fs = require('fs').promises;
    const pathModule = require('path');
    
    // Security: Ensure path is within project directory
    const safePath = pathModule.join(process.cwd(), path);
    if (!safePath.startsWith(process.cwd())) {
      throw new Error('Path must be within project directory');
    }
    
    const content = await fs.readFile(safePath, encoding);
    return {
      path,
      content,
      encoding,
      size: content.length
    };
  }
});

Complex Validation Tool

const createUser = defineTool({
  name: 'createUser',
  description: 'Create a new user account with validation',
  parameters: z.object({
    email: z.string().email().describe('User email address'),
    name: z.string().min(2).max(100).describe('Full name'),
    age: z.number().int().min(13).max(120).describe('User age'),
    preferences: z.object({
      newsletter: z.boolean().default(false),
      notifications: z.boolean().default(true),
      theme: z.enum(['light', 'dark', 'auto']).default('auto')
    }).optional()
  }),
  execute: async ({ email, name, age, preferences }) => {
    // Check if email already exists
    const existingUser = await db.users.findUnique({
      where: { email }
    });
    
    if (existingUser) {
      return {
        success: false,
        error: 'Email already registered'
      };
    }
    
    // Create user
    const user = await db.users.create({
      data: {
        email,
        name,
        age,
        preferences: preferences || {}
      }
    });
    
    return {
      success: true,
      user: {
        id: user.id,
        email: user.email,
        name: user.name
      }
    };
  }
});

Tool with Dependencies

class DatabaseService {
  async search(query: string) { /* ... */ }
}

function createSearchTool(db: DatabaseService) {
  return defineTool({
    name: 'searchDatabase',
    description: 'Search the database',
    parameters: z.object({
      query: z.string()
    }),
    execute: async ({ query }) => {
      return await db.search(query);
    }
  });
}

// Usage
const db = new DatabaseService();
const searchTool = createSearchTool(db);

const agent = new BuiltInAgent({
  model: 'openai/gpt-4o',
  apiKey: process.env.OPENAI_API_KEY,
  tools: [searchTool]
});

Using Tools with BuiltInAgent

import { BuiltInAgent, defineTool } from '@copilotkit/agent';
import { z } from 'zod';

const tools = [
  defineTool({
    name: 'getInventory',
    description: 'Get current inventory levels',
    parameters: z.object({
      productId: z.string()
    }),
    execute: async ({ productId }) => {
      return await inventory.get(productId);
    }
  }),
  
  defineTool({
    name: 'updateInventory',
    description: 'Update inventory quantity',
    parameters: z.object({
      productId: z.string(),
      quantity: z.number().int().positive()
    }),
    execute: async ({ productId, quantity }) => {
      return await inventory.update(productId, quantity);
    }
  })
];

const agent = new BuiltInAgent({
  model: 'openai/gpt-4o',
  apiKey: process.env.OPENAI_API_KEY,
  prompt: 'You are an inventory management assistant.',
  tools,
  maxSteps: 5  // Allow multiple tool calls
});

Type Safety

defineTool provides full TypeScript type inference:
const tool = defineTool({
  name: 'example',
  description: 'Example tool',
  parameters: z.object({
    name: z.string(),
    age: z.number()
  }),
  execute: async ({ name, age }) => {
    // TypeScript knows:
    // - name is string
    // - age is number
    return { name, age };
  }
});

// Type error if parameters don't match:
execute: async ({ name, age, invalid }) => {
  //                          ^^^^^^^ 
  // Error: Object literal may only specify known properties
}

Error Handling

Errors thrown in execute are caught and returned as error responses:
const tool = defineTool({
  name: 'riskyOperation',
  description: 'Operation that might fail',
  parameters: z.object({
    id: z.string()
  }),
  execute: async ({ id }) => {
    const item = await database.findById(id);
    
    if (!item) {
      throw new Error('Item not found');
    }
    
    if (!item.available) {
      throw new Error('Item is not available');
    }
    
    return item;
  }
});

// Errors are caught and returned as:
// { error: 'Item not found' }

Validation

Parameters are automatically validated against the Zod schema:
const tool = defineTool({
  name: 'validateInput',
  description: 'Tool with validation',
  parameters: z.object({
    email: z.string().email(),
    age: z.number().int().min(0).max(120)
  }),
  execute: async ({ email, age }) => {
    // If we reach here, validation passed
    return { email, age };
  }
});

// Invalid input like { email: "invalid", age: -5 } 
// will be rejected before execute() is called

Best Practices

  1. Clear descriptions: Help the LLM understand when to use the tool
  2. Specific parameters: Use Zod’s rich validation features
  3. Add descriptions to parameters: Use .describe() for better LLM understanding
  4. Return structured data: Return objects, not primitive values
  5. Handle errors gracefully: Throw descriptive errors
  6. Keep tools focused: One tool should do one thing well
  7. Use TypeScript: Leverage type inference for safety

Common Patterns

Optional Parameters with Defaults

parameters: z.object({
  query: z.string(),
  limit: z.number().default(10),
  offset: z.number().default(0)
})

Enums for Fixed Values

parameters: z.object({
  status: z.enum(['pending', 'approved', 'rejected']),
  priority: z.enum(['low', 'medium', 'high']).default('medium')
})

Nested Objects

parameters: z.object({
  user: z.object({
    name: z.string(),
    email: z.string().email()
  }),
  settings: z.object({
    theme: z.enum(['light', 'dark']),
    notifications: z.boolean()
  })
})

Arrays

parameters: z.object({
  tags: z.array(z.string()).min(1).max(10),
  ids: z.array(z.number().int()).optional()
})

Union Types

parameters: z.object({
  identifier: z.union([
    z.string().uuid(),
    z.number().int().positive()
  ])
})

See Also