Skip to main content

Using createTool()

The createTool() function is the recommended way to create custom tools. It provides automatic schema generation from Zod schemas and handles all the boilerplate for you.

Basic Tool Creation

import { createTool } from '@iqai/adk';
import { z } from 'zod';

const calculatorTool = createTool({
  name: 'calculator',
  description: 'Performs basic arithmetic operations',
  schema: z.object({
    operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
    a: z.number().describe('First number'),
    b: z.number().describe('Second number')
  }),
  fn: ({ operation, a, b }) => {
    switch (operation) {
      case 'add': return { result: a + b };
      case 'subtract': return { result: a - b };
      case 'multiply': return { result: a * b };
      case 'divide': return { result: b !== 0 ? a / b : 'Cannot divide by zero' };
    }
  }
});

Tool Without Parameters

For tools that don’t need parameters, you can omit the schema:
const timestampTool = createTool({
  name: 'get_timestamp',
  description: 'Returns the current timestamp in milliseconds',
  fn: () => ({ timestamp: Date.now() })
});

Async Tools

Both sync and async functions are supported:
const fetchDataTool = createTool({
  name: 'fetch_data',
  description: 'Fetch data from an API',
  schema: z.object({
    url: z.string().url().describe('API endpoint URL')
  }),
  fn: async ({ url }) => {
    const response = await fetch(url);
    const data = await response.json();
    return { data, status: response.status };
  }
});

Using ToolContext

Access session state, memory, and other runtime information:
const saveSettingTool = createTool({
  name: 'save_setting',
  description: 'Save a user setting to the session',
  schema: z.object({
    key: z.string().describe('Setting name'),
    value: z.string().describe('Setting value')
  }),
  fn: ({ key, value }, context) => {
    // Access and modify session state
    context.state.set(key, value);
    
    // Get session information
    const userId = context.session.userId;
    const sessionId = context.session.id;
    
    return {
      success: true,
      userId,
      sessionId,
      message: `Saved ${key} = ${value}`
    };
  }
});

Complex Schema Example

const createUserTool = createTool({
  name: 'create_user',
  description: 'Create a new user in the system',
  schema: z.object({
    username: z.string().min(3).max(20).describe('Username (3-20 characters)'),
    email: z.string().email().describe('Valid email address'),
    age: z.number().int().min(13).describe('User age (must be 13 or older)'),
    role: z.enum(['user', 'admin', 'moderator']).default('user'),
    preferences: z.object({
      theme: z.enum(['light', 'dark']).default('light'),
      notifications: z.boolean().default(true)
    }).optional().describe('User preferences')
  }),
  fn: async (user) => {
    // Zod automatically validates the input
    // Type is inferred: { username: string, email: string, age: number, ... }
    
    // Save to database
    const userId = await database.users.create(user);
    
    return {
      success: true,
      userId,
      message: `User ${user.username} created successfully`
    };
  }
});

Retry Configuration

const resilientApiTool = createTool({
  name: 'resilient_api',
  description: 'Call external API with automatic retries',
  shouldRetryOnFailure: true,
  maxRetryAttempts: 5,
  schema: z.object({
    endpoint: z.string()
  }),
  fn: async ({ endpoint }) => {
    const response = await fetch(endpoint);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  }
});

Extending BaseTool

For advanced use cases, extend the BaseTool class directly:

Basic BaseTool Extension

import { BaseTool, ToolContext, Type } from '@iqai/adk';
import type { FunctionDeclaration } from '@iqai/adk';

class WeatherTool extends BaseTool {
  private apiKey: string;

  constructor(apiKey: string) {
    super({
      name: 'get_weather',
      description: 'Get current weather for a location',
      isLongRunning: false,
      shouldRetryOnFailure: true,
      maxRetryAttempts: 3
    });
    this.apiKey = apiKey;
  }

  getDeclaration(): FunctionDeclaration {
    return {
      name: this.name,
      description: this.description,
      parameters: {
        type: Type.OBJECT,
        properties: {
          location: {
            type: Type.STRING,
            description: 'City name or coordinates'
          },
          units: {
            type: Type.STRING,
            description: 'Temperature units',
            enum: ['celsius', 'fahrenheit'],
            default: 'celsius'
          }
        },
        required: ['location']
      }
    };
  }

  async runAsync(
    args: { location: string; units?: string },
    context: ToolContext
  ): Promise<any> {
    const { location, units = 'celsius' } = args;
    
    try {
      const response = await fetch(
        `https://api.weather.com/data?location=${location}&units=${units}&key=${this.apiKey}`
      );
      
      if (!response.ok) {
        return {
          error: `Weather API returned ${response.status}`,
          location
        };
      }
      
      const data = await response.json();
      
      // Store in session state
      context.state.set('last_weather_check', {
        location,
        timestamp: Date.now()
      });
      
      return {
        location,
        temperature: data.temp,
        conditions: data.conditions,
        humidity: data.humidity,
        units
      };
    } catch (error) {
      return {
        error: error instanceof Error ? error.message : 'Unknown error',
        location
      };
    }
  }
}

// Usage
const weatherTool = new WeatherTool(process.env.WEATHER_API_KEY!);

Long-Running Operations

For operations that take significant time:
class DataProcessingTool extends BaseTool {
  constructor() {
    super({
      name: 'process_large_dataset',
      description: 'Process large datasets asynchronously',
      isLongRunning: true,  // Mark as long-running
      shouldRetryOnFailure: false
    });
  }

  getDeclaration(): FunctionDeclaration {
    return {
      name: this.name,
      description: this.description,
      parameters: {
        type: Type.OBJECT,
        properties: {
          datasetId: {
            type: Type.STRING,
            description: 'ID of the dataset to process'
          },
          operation: {
            type: Type.STRING,
            description: 'Processing operation to perform',
            enum: ['transform', 'aggregate', 'filter']
          }
        },
        required: ['datasetId', 'operation']
      }
    };
  }

  async runAsync(
    args: { datasetId: string; operation: string },
    context: ToolContext
  ): Promise<any> {
    const { datasetId, operation } = args;
    
    // Start async processing
    const jobId = await this.startProcessingJob(datasetId, operation);
    
    // Return immediately with resource ID
    return {
      jobId,
      status: 'processing',
      message: 'Job started. Use check_job_status to monitor progress.'
    };
  }

  private async startProcessingJob(
    datasetId: string,
    operation: string
  ): Promise<string> {
    // Implementation
    return 'job-' + Date.now();
  }
}

Custom Validation

class ValidatedTool extends BaseTool {
  constructor() {
    super({
      name: 'validated_operation',
      description: 'Operation with custom validation'
    });
  }

  getDeclaration(): FunctionDeclaration {
    return {
      name: this.name,
      description: this.description,
      parameters: {
        type: Type.OBJECT,
        properties: {
          email: { type: Type.STRING, description: 'Email address' },
          amount: { type: Type.NUMBER, description: 'Transaction amount' }
        },
        required: ['email', 'amount']
      }
    };
  }

  validateArguments(args: Record<string, any>): boolean {
    // Call parent validation first
    if (!super.validateArguments(args)) {
      return false;
    }

    // Custom validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(args.email)) {
      console.error('Invalid email format');
      return false;
    }

    if (args.amount <= 0 || args.amount > 10000) {
      console.error('Amount must be between 0 and 10000');
      return false;
    }

    return true;
  }

  async runAsync(
    args: { email: string; amount: number },
    context: ToolContext
  ): Promise<any> {
    // Arguments are pre-validated
    return { success: true, ...args };
  }
}

Stateful Tools

class SessionCounterTool extends BaseTool {
  constructor() {
    super({
      name: 'increment_counter',
      description: 'Increment a session counter'
    });
  }

  getDeclaration(): FunctionDeclaration {
    return {
      name: this.name,
      description: this.description,
      parameters: {
        type: Type.OBJECT,
        properties: {
          counterName: {
            type: Type.STRING,
            description: 'Name of the counter to increment'
          },
          incrementBy: {
            type: Type.INTEGER,
            description: 'Amount to increment by',
            default: 1
          }
        },
        required: ['counterName']
      }
    };
  }

  async runAsync(
    args: { counterName: string; incrementBy?: number },
    context: ToolContext
  ): Promise<any> {
    const { counterName, incrementBy = 1 } = args;
    
    // Get current value from session state
    const currentValue = context.state.get(counterName, 0);
    const newValue = currentValue + incrementBy;
    
    // Update state
    context.state.set(counterName, newValue);
    
    // Get all counters
    const allCounters = context.state.getAll();
    
    return {
      counterName,
      previousValue: currentValue,
      newValue,
      allCounters
    };
  }
}

Tool Configuration Options

When creating tools, you can configure:
name
string
required
Tool name (alphanumeric and underscores only)
description
string
required
Clear description of what the tool does (minimum 3 characters)
isLongRunning
boolean
default:"false"
Whether the tool returns immediately with a resource ID
shouldRetryOnFailure
boolean
default:"false"
Enable automatic retry logic for transient failures
maxRetryAttempts
number
default:"3"
Maximum number of retry attempts

Best Practices

Naming Conventions

// Good: Clear, descriptive names
const getUserProfile = createTool({ name: 'get_user_profile', ... });
const sendEmail = createTool({ name: 'send_email', ... });
const calculateTotal = createTool({ name: 'calculate_total', ... });

// Avoid: Vague or unclear names
const process = createTool({ name: 'process', ... });  // Too vague
const doStuff = createTool({ name: 'do_stuff', ... });  // Not descriptive

Schema Descriptions

Use Zod’s .describe() to help the LLM understand parameters:
const tool = createTool({
  name: 'book_flight',
  description: 'Book a flight reservation',
  schema: z.object({
    departure: z.string().describe('Departure airport code (e.g., LAX, JFK)'),
    arrival: z.string().describe('Arrival airport code (e.g., LAX, JFK)'),
    date: z.string().describe('Departure date in YYYY-MM-DD format'),
    passengers: z.number().int().min(1).max(9).describe('Number of passengers (1-9)')
  }),
  fn: async (args) => { /* ... */ }
});

Error Handling

Return errors as part of the result object:
const tool = createTool({
  name: 'api_call',
  description: 'Call external API',
  schema: z.object({ url: z.string() }),
  fn: async ({ url }) => {
    try {
      const response = await fetch(url);
      
      if (!response.ok) {
        return {
          error: `HTTP ${response.status}: ${response.statusText}`,
          url
        };
      }
      
      const data = await response.json();
      return { success: true, data };
    } catch (error) {
      return {
        error: error instanceof Error ? error.message : 'Unknown error',
        url
      };
    }
  }
});

Using Memory and Artifacts

const memoryTool = createTool({
  name: 'search_past_conversations',
  description: 'Search through past conversation history',
  schema: z.object({
    query: z.string().describe('Search query')
  }),
  fn: async ({ query }, context) => {
    // Search memory
    const memories = await context.searchMemory(query);
    
    // List artifacts
    const artifacts = await context.listArtifacts();
    
    return {
      memories: memories.map(m => ({
        content: m.content,
        relevance: m.score
      })),
      artifacts,
      query
    };
  }
});

Next Steps

Built-in Tools

Explore all available built-in tools

Function Tools

Learn about automatic function wrapping

Build docs developers (and LLMs) love