Chapter 05 · 10 min read
Give it tools
Define tools as typed Python functions and let the SDK's tool runner do the loop.
Tools are what turn a model that talks into a worker that acts. A tool is a function in your code with a name, a description and a schema for its inputs. Claude decides when to call it; your code runs it and sends back the result.
How a tool call works
- You send the conversation and the list of tools.
- Claude replies with
stop_reason: "tool_use"and one or moretool_useblocks (the tool name and its inputs). - Your code runs each tool and sends the results back as
tool_resultblocks. - Repeat until Claude answers without asking for a tool.
That repeat is the "agent loop". You can write it yourself, but the SDK's tool runner does it for you.
Tools as Python functions, with the tool runner
import anthropic
from anthropic import beta_tool
client = anthropic.Anthropic()
@beta_tool
def get_repair_status(job_number: str) -> str:
"""Look up a repair by its job number. Call this whenever a customer asks
about a repair they have already booked in.
Args:
job_number: The job number on the customer's receipt, e.g. FD-10482.
"""
job = repairs.find(job_number) # your own code
if job is None:
return "No repair found with that job number."
return f"{job.device}: {job.status}. Ready: {job.ready_on or 'not yet known'}."
runner = client.beta.messages.tool_runner(
model="claude-opus-5-5",
max_tokens=16000,
system=SYSTEM,
tools=[get_repair_status],
messages=[{"role": "user", "content": "Is FD-10482 ready yet?"}],
)
for message in runner: # one message per turn; stops when Claude is done
for block in message.content:
if block.type == "text":
print(block.text)
The decorator builds the input schema from the type hints and the docstring, so the docstring is not decoration: it is what Claude reads to decide when to use the tool.
The same tool in TypeScript
The TypeScript SDK has the same tool runner. Tools are defined with a Zod schema (or plain JSON Schema with betaTool) and a run function:
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic();
const getRepairStatus = betaZodTool({
name: "get_repair_status",
description: "Look up a repair by its job number. Call this whenever a customer asks about a repair they have already booked in.",
inputSchema: z.object({
job_number: z.string().describe("The job number on the receipt, e.g. FD-10482"),
}),
run: async ({ job_number }) => {
const job = await repairs.find(job_number); // your own code
return job ? `${job.device}: ${job.status}` : "No repair found with that job number.";
},
});
const finalMessage = await client.beta.messages.toolRunner({
model: "claude-opus-5-5",
max_tokens: 16000,
system: SYSTEM,
tools: [getRepairStatus],
messages: [{ role: "user", content: "Is FD-10482 ready yet?" }],
});
for (const block of finalMessage.content) {
if (block.type === "text") console.log(block.text);
}
Designing good tools
- Say when to call it, not only what it does: "Call this whenever a customer asks about a repair they have already booked in."
- Few, focused tools. Five clear tools beat twenty overlapping ones. If two tools could answer the same question, merge them.
- Return plain, useful text. Return what a person would need to answer, not a raw database row with forty fields.
- Return errors as results. When something fails, return a clear message ("No repair found with that job number") so Claude can ask the customer to check it. In a manual loop, set
is_error: trueon the result. - One tool per risky action. A dedicated
book_slotorsend_emailtool can be checked, logged and gated. A general "run any command" tool cannot (see the guardrails chapter).
Exact inputs with strict mode
When you define a tool's schema yourself, add "strict": True and Claude's inputs are guaranteed to match it exactly. The schema then needs "additionalProperties": False and a required list:
book_slot_tool = {
"name": "book_slot",
"description": "Book a drop-off slot. Call only after the customer has chosen a slot from check_slots.",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"slot_id": {"type": "string"},
"customer_email": {"type": "string"},
},
"required": ["slot_id", "customer_email"],
"additionalProperties": False,
},
}
Several tools at once
Claude can ask for several tools in one turn (for example, the repair status and the free slots together). Run them, then send all the results back in a single message. The tool runner does this for you; in a manual loop, do not split them across messages, or Claude learns to stop doing it.
Forcing a tool
On Claude Opus 5.5 and Sonnet 5.5 you cannot force a specific tool call (tool_choice of any or tool returns an error). Leave tool_choice on auto and say in the job description which tool to use when. If you only wanted the tool to get JSON back, use structured outputs instead.
Tools Anthropic runs for you
Some tools run on Anthropic's side, so there is nothing for you to execute: web search, web fetch and code execution in a sandbox. You declare them in the same tools list. For a support worker you usually do not want web search at all: everything it says should come from your own systems.