Back to guides
1
5 min

Tool Design

Building the Hands of Your Agent

Why Tools Matter

An AI agent without tools is just a chatbot. It can talk about looking up a customer's account, but it can't actually do it. Tools are what turn language models from conversationalists into operators.

The quality of your agent is bounded by the quality of its tools. A brilliant planning loop with sloppy tool definitions will hallucinate parameters, misinterpret results, and fail silently. A simple loop with well-designed tools will reliably get work done.

The principle: Design tools for the LLM, not for a human developer. The model reads your tool's name, description, and schema to decide when and how to call it. Every word matters.

Anatomy of a Tool

Every tool has four parts:

ComponentPurposeExample
NameHow the LLM refers to it — must be unambiguous`search_crm_contacts` not `search`
DescriptionWhen to use it and what it returns — the LLM's decision guide"Search CRM contacts by name, email, or company. Returns up to 10 matching contacts with their deal history."
Input SchemaJSON Schema defining exact parameters`{ name?: string, email?: string, company?: string, limit?: number }`
Execute FunctionThe actual implementation that runs when calledDatabase query, API call, computation
const searchContactsTool = {
  name: "search_crm_contacts",
  description:
    "Search CRM contacts by name, email, or company. " +
    "Returns up to 10 matching contacts with their deal history. " +
    "Use this when the user asks about a specific person or account.",
  parameters: {
    type: "object" as const,
    properties: {
      name: { type: "string", description: "Contact's full or partial name" },
      email: { type: "string", description: "Contact's email address" },
      company: { type: "string", description: "Company name to filter by" },
      limit: { type: "number", description: "Max results (default 10, max 50)" },
    },
    required: [],
  },
  execute: async (params: {
    name?: string;
    email?: string;
    company?: string;
    limit?: number;
  }) => {
    const results = await db.contacts.search({
      ...params,
      limit: Math.min(params.limit ?? 10, 50),
    });
    return { contacts: results, total: results.length };
  },
};

JSON Schema for Input Validation

The LLM generates tool call arguments as JSON. Your schema is both documentation and validation:

  • Use `description` on every property. The model reads these to understand what values to pass. "Contact's full or partial name" is far better than leaving the field undescribed.
  • Use `enum` for constrained choices. If a field only accepts "high", "medium", or "low", say so. The model won't guess wrong.
  • Use `required` deliberately. Only require what's truly mandatory. Optional parameters with sensible defaults give the model flexibility.
  • Limit nesting. Deeply nested schemas confuse models. Flatten when possible.
  • {
      "type": "object",
      "properties": {
        "priority": {
          "type": "string",
          "enum": ["low", "medium", "high", "critical"],
          "description": "Ticket priority level"
        },
        "assignee_email": {
          "type": "string",
          "format": "email",
          "description": "Email of the team member to assign this ticket to"
        }
      },
      "required": ["priority"]
    }

    Output Contracts

    Tool outputs should be structured, predictable, and informative:

  • Return objects, not strings. { success: true, contact_id: "abc-123" } beats "Contact created successfully."
  • Include metadata. How many results? Was the list truncated? What page are we on?
  • Use consistent shapes. Every tool that returns a list should use { items: [...], total: number }. Every tool that mutates should return { success: boolean, id: string }.
  • Include enough context. If the model searched for contacts, return names and companies — not just IDs. The model needs context to form its next response.
  • Error Handling

    Tools will fail. Networks time out, APIs return 500s, users ask for things that don't exist. Your error handling determines whether the agent recovers or crashes:

    execute: async (params) => {
      try {
        const result = await externalApi.call(params);
        return { success: true, data: result };
      } catch (error) {
        if (error.status === 404) {
          return { success: false, error: "not_found",
            message: "No contact found with that email. Try searching by name instead." };
        }
        if (error.status === 429) {
          return { success: false, error: "rate_limited",
            message: "API rate limit reached. Try again in 30 seconds." };
        }
        return { success: false, error: "internal",
          message: "Failed to search contacts. The CRM service may be down." };
      }
    }

    Key rule: Never throw exceptions from tool execution. Always return a structured error the model can reason about. The model can then tell the user what happened and suggest alternatives.

    Tool Registry Pattern

    As your agent grows, you'll have dozens of tools. A registry pattern keeps them organized and enables dynamic discovery:

    class ToolRegistry {
      private tools = new Map<string, Tool>();
    
      register(tool: Tool) {
        this.tools.set(tool.name, tool);
      }
    
      // Return schemas for the LLM to choose from
      getSchemas(): ToolSchema[] {
        return Array.from(this.tools.values()).map((t) => ({
          name: t.name,
          description: t.description,
          parameters: t.parameters,
        }));
      }
    
      // Execute a tool by name
      async execute(name: string, params: unknown) {
        const tool = this.tools.get(name);
        if (!tool) return { success: false, error: `Unknown tool: ${name}` };
        return tool.execute(params);
      }
    }

    This pattern lets you add tools per user role, per module, or per context — the agent only sees what it needs. In the capstone, you'll build a registry that exposes different tool sets depending on whether the user is a sales rep, a manager, or an admin.

    This is chapter 1 of Production AI Agents.

    Get the full hands-on course for $100 and build the complete system. Your projects become your portfolio.

    View course details