Claude Tool Use: How I Give My AI Agents Real-World Capabilities

Alejandro Rioja
Alejandro Rioja
11 min read
TL;DR

Claude tool use lets your agent take actions — not just generate text. You define tools as JSON schemas, Claude decides when to call them, and your code executes the real-world action. The loop is three steps: send message → receive tool_use block → execute and return result. I've shipped this in 15+ production agents on Cloudflare Workers. The failure mode is almost never the AI — it's ambiguous tool results coming back.

Free newsletter

Every Wednesday. 28,400+ operators. Zero fluff.

Table of contents

Open Table of contents

Why tool use changes what an agent can do

Without tools, an agent can only generate text. That’s useful for summarization, drafting, and classification — but it’s not what most business automations actually need. Business automations need to look things up, write to databases, call APIs, send messages.

Tool use is how you give Claude that access. You define a set of tools as JSON schemas. Claude reads the schemas, decides which tool to call and with what arguments, and returns a structured tool_use content block. Your code runs the actual function. Claude gets the result and decides what to do next — including calling another tool or producing a final text response.

The key: Claude decides when and whether to call a tool. You define the capabilities. The model reasons about when to use them.

How the API flow works

The tool use loop has three steps. You’ll run through this loop once or multiple times depending on how many tool calls the model makes.

Step 1: Send your message with tools defined

typescript
const response = await anthropic.messages.create({
  model: "claude-haiku-4-5-20251001",
  max_tokens: 1024,
  tools: [
    {
      name: "check_court_availability",
      description:
        "Check if a court is available at a given date, time, and duration",
      input_schema: {
        type: "object",
        properties: {
          date: {
            type: "string",
            description: "Date in YYYY-MM-DD format",
          },
          time: {
            type: "string",
            description: "Start time in HH:MM format (24h)",
          },
          duration_minutes: {
            type: "number",
            description: "Duration of the booking in minutes",
          },
        },
        required: ["date", "time", "duration_minutes"],
      },
    },
  ],
  messages: [
    {
      role: "user",
      content: "Is a court available tomorrow at 2pm for 90 minutes?",
    },
  ],
});

Step 2: Check if Claude wants to call a tool

typescript
if (response.stop_reason === "tool_use") {
  const toolUseBlock = response.content.find(
    (block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
  );

  if (!toolUseBlock) throw new Error("Expected tool_use block");

  // Run your actual function
  const toolResult = await checkCourtAvailability(
    toolUseBlock.input as CourtAvailabilityInput
  );

  // Step 3: Return the result to Claude
  const finalResponse = await anthropic.messages.create({
    model: "claude-haiku-4-5-20251001",
    max_tokens: 1024,
    tools: [
      /* same tools as before */
    ],
    messages: [
      {
        role: "user",
        content: "Is a court available tomorrow at 2pm for 90 minutes?",
      },
      { role: "assistant", content: response.content },
      {
        role: "user",
        content: [
          {
            type: "tool_result",
            tool_use_id: toolUseBlock.id,
            content: JSON.stringify(toolResult),
          },
        ],
      },
    ],
  });

  // finalResponse.content now has the text answer
}

That’s the entire pattern. Three API interactions per tool call: define tools → receive tool_use block → return result.

Real example: the Pickleland availability checker

Pickleland is a pickleball facility. We get booking inquiries on Facebook Messenger, in comments, and via a chatbot. The question is almost always some variation of “are you open Saturday at 3pm?” or “can I book a court for my group of 8?”

The availability checker agent uses tool use to look up the actual booking system in real time rather than giving a canned response.

Here’s the full agent — simplified but production-accurate:

typescript
// workers/availability-checker.ts
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();

const AVAILABILITY_TOOLS: Anthropic.Tool[] = [
  {
    name: "check_availability",
    description:
      "Check court availability for a date, time, and group size. Returns available courts and their prices.",
    input_schema: {
      type: "object",
      properties: {
        date: { type: "string", description: "YYYY-MM-DD" },
        start_time: { type: "string", description: "HH:MM (24h)" },
        duration_minutes: { type: "number" },
        players: { type: "number", description: "Number of players" },
      },
      required: ["date", "start_time", "duration_minutes"],
    },
  },
  {
    name: "get_pricing",
    description:
      "Get current pricing for court rentals and open play sessions",
    input_schema: {
      type: "object",
      properties: {
        session_type: {
          type: "string",
          enum: ["court_rental", "open_play", "clinics"],
        },
      },
      required: ["session_type"],
    },
  },
];

export async function handleInquiry(
  userMessage: string,
  env: Env
): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ];

  // Agentic loop — keep going until stop_reason is "end_turn"
  while (true) {
    const response = await anthropic.messages.create({
      model: "claude-haiku-4-5-20251001",
      max_tokens: 512,
      system:
        "You are the booking assistant for Pickleland, a pickleball facility in Pflugerville, TX. " +
        "Use the tools to look up real availability and pricing. Never make up availability or prices. " +
        "If the customer wants to book, direct them to pickleland.com/book.",
      tools: AVAILABILITY_TOOLS,
      messages,
    });

    // Push the assistant's response into message history
    messages.push({ role: "assistant", content: response.content });

    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find(
        (b): b is Anthropic.TextBlock => b.type === "text"
      );
      return (
        textBlock?.text ??
        "I wasn't able to answer that — please call us directly."
      );
    }

    if (response.stop_reason === "tool_use") {
      // Process ALL tool calls in this response (Claude can request multiple at once)
      const toolResults: Anthropic.ToolResultBlockParam[] = [];

      for (const block of response.content) {
        if (block.type !== "tool_use") continue;

        let result: unknown;

        switch (block.name) {
          case "check_availability":
            result = await checkAvailability(
              block.input as AvailabilityInput,
              env
            );
            break;
          case "get_pricing":
            result = await getPricing(block.input as PricingInput, env);
            break;
          default:
            result = { error: `Unknown tool: ${block.name}` };
        }

        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: JSON.stringify(result),
        });
      }

      // Return all tool results in a single user message
      messages.push({ role: "user", content: toolResults });
    }
  }
}

Two things to call out here.

The agentic loop. I keep going until stop_reason === "end_turn". Claude might call check_availability, decide it needs pricing too, call get_pricing, and then produce the final answer — that’s three API calls for one user message. The loop handles this without any special logic.

Multiple tool calls per turn. Claude can return multiple tool_use blocks in one response. I process all of them and return all results in a single user message. If you process them one at a time and return them individually, you break the conversation flow and waste tokens.

Real example: the lead research agent

My consulting brand uses a research agent that enriches inbound leads before I talk to them. When someone fills out a contact form, the agent looks up their company and extracts what I need to know before the call.

The tool definitions for this one include a write tool — and that’s where the pattern gets interesting:

typescript
const RESEARCH_TOOLS: Anthropic.Tool[] = [
  {
    name: "search_company",
    description: "Search for information about a company",
    input_schema: {
      type: "object",
      properties: {
        company_name: { type: "string" },
        website: { type: "string", description: "Company website if known" },
      },
      required: ["company_name"],
    },
  },
  {
    name: "save_research",
    description:
      "Save the completed research summary to Airtable. Call this when all research is complete.",
    input_schema: {
      type: "object",
      properties: {
        company_summary: { type: "string" },
        estimated_size: {
          type: "string",
          enum: ["1-10", "11-50", "51-200", "200+"],
        },
        likely_use_case: { type: "string" },
        priority: { type: "string", enum: ["high", "medium", "low"] },
        notes: { type: "string" },
      },
      required: [
        "company_summary",
        "estimated_size",
        "likely_use_case",
        "priority",
      ],
    },
  },
];

save_research is what I call a write tool — its purpose isn’t to get information, it’s to commit Claude’s output to a database in structured form. I use this pattern instead of trying to parse JSON from a text response. Claude knows when the research is done and calls save_research with properly typed fields. I never write a parser.

This is the cleanest application of tool use: define a “final action” tool with the exact schema you want, and Claude delivers structured output through the tool call. No text parsing, no regex, no JSONSchema validation of freeform output.

One tool vs. many

The instinct when starting with tool use is to build one giant tool that does everything. Resist this. Small, focused tools are better for three reasons:

  1. Claude reasons better about small tools. A tool called get_court_status that returns availability is easier for the model to reason about than a tool called manage_facility that takes a mode parameter and branches internally.

  2. Small tools are easier to test. Each tool is a TypeScript function you can unit-test independently of the LLM. You should — tool bugs are hard to debug inside a live conversation.

  3. Claude can parallelize small tools. If two tools don’t depend on each other, Claude may call them in the same response and you process them in parallel. This only works if the tools are genuinely independent.

The exception: tools that need access to a lot of shared internal state. If the function needs 10 variables from the same data source, one tool with a richer schema beats 10 tools that each hit the database separately.

My rule of thumb: start with one tool per distinct capability. Merge tools only when you see Claude calling them together on every single request.

Cost implications

Tool use adds tokens. Every tool definition goes into the system prompt context. Every tool_use block and tool_result block consumes tokens in the conversation history. For a multi-turn agentic loop, this compounds quickly.

For the Pickleland availability checker, a typical conversation runs 3–4 API calls total (initial message + 1–2 tool calls + final answer), each processing 600–900 tokens. At Haiku pricing, this runs under $0.001 per inquiry. As I cover in the AI agent cost math post, Haiku handles well-defined tool-calling tasks reliably and is 10× cheaper than Sonnet for the same token volume.

The lead research agent runs on Sonnet because the judgment calls — prioritizing a lead, estimating fit — require more reasoning capability than Haiku delivers on open-ended inputs. The math still works because it runs infrequently (a few times per week, not thousands per day). Model choice follows task complexity, not personal preference.

The failure mode nobody talks about

The most common failure I see in production tool use isn’t Claude calling the wrong tool. It’s the tool returning something Claude can’t reason about clearly.

If your tool returns a raw database object with 40 fields, Claude gets confused about which fields matter. If your tool throws an exception (which surfaces as a Worker crash rather than a tool result), the loop breaks silently. If your tool returns null when it means “no results,” Claude doesn’t know whether to retry or give up.

Three rules for tool results:

Return lean, explicit results. { available: true, courts: ["Court 3", "Court 5"], price_per_hour: 20 } — not the full database row.

Catch errors inside the tool function and return them as structured results. { error: "booking system timeout", retry: true } — not a thrown exception that crashes the Worker.

Make “no results” explicit. { available: false, next_available: "2026-07-23T14:00:00Z" } — not null or an empty array with no context.

Claude reasons much better about clear signals than ambiguous return values. Every hour I’ve spent debugging tool use in production has been about unclear results, not model reasoning.

The operator’s bottom line

Tool use is the feature that turns Claude from a text generator into an operator. Define focused tools with clear input schemas. Handle all tool_use blocks in a single response to the model. Run the agentic loop until stop_reason === "end_turn". Return clean, lean results from your tool functions — not raw data objects, not thrown exceptions, not ambiguous nulls.

The model handles the reasoning. Your code handles the real-world actions. Keep those two jobs cleanly separated and the architecture stays maintainable even as you add tools.

If you’re building your first tool-use agent, start with the availability checker pattern above — one tool, one purpose, one agentic loop. Ship that. Then add the second tool.


Related: The agent stack I use to run 30+ production agents · Haiku vs Sonnet: the cost math for agent tasks · Event-triggered vs scheduled agents: which pattern for which job

Building a tool-use agent and hitting a wall? Get in touch — I design and build production agent architectures for operator teams.

FAQ

Does Claude tool use work with all models?

Yes — tool use is supported on all current Claude models. Claude Haiku handles well-defined tools with clear schemas reliably and is the cheapest option for high-volume task types. Sonnet handles more ambiguous or open-ended tool-calling decisions better. Start with Haiku; move up if output quality isn’t sufficient.

What’s the difference between Claude tool use and OpenAI function calling?

Mechanically identical. OpenAI coined “function calling”; Anthropic calls it “tool use.” In both cases: you define JSON schemas, the model returns structured calls, your code executes the function. The API shape differs but the concept is the same.

Can Claude call multiple tools in a single response?

Yes. Claude may return multiple tool_use blocks in one assistant response. Process all of them and return all results in one user message. See the agentic loop pattern in the Pickleland example above — the for loop over response.content handles this correctly.

How many tools should I define per agent?

I stay under 8–10 tools per agent. Beyond that, I’ve seen Claude occasionally pick the wrong tool on the first attempt, which wastes tokens on a correction loop. If you need more than 10 capabilities, split the agent into multiple agents with specialized tool sets rather than building one agent that knows everything.

Should I use tool use to get structured output?

Yes — the save_research write-tool pattern is cleaner than asking Claude to return JSON in a text block and then parsing it. Define a “final action” tool with the exact schema you want. Claude calls it with properly typed fields when it’s done. No parser needed.

Keep reading

Related posts

Keep reading

Get the AI playbook in your inbox

Every Wednesday. 28,400+ operators. Zero fluff.

↵ to see all results esc esc to close