Claude for Developers

Lesson 6 of 14

Tool use: giving Claude hands

A language model alone can only describe the world. Give it tools and it can query your database, call your APIs, and act — with Claude deciding when and with what arguments, and your code deciding what actually happens. That division of labour is the heart of every agent you will ever build, so this lesson builds it from the wire format up.

A tool is a name, a description, and a schema

python
tools = [{
    "name": "get_order_status",
    "description": (
        "Look up the current status of a customer order by its ID. "
        "Call this whenever the user asks where an order is, whether it shipped, "
        "or anything about a specific order's state. Do not answer order questions from memory."
    ),
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string", "description": "The order ID, e.g. ORD-12345"},
        },
        "required": ["order_id"],
        "additionalProperties": False,
    },
}]

Read the description again, because it is the most important string in tool use: it says what the tool does and when to call it. Claude decides tool invocation almost entirely from descriptions, and the difference between "looks up order status" and the version above is the difference between a tool that under-triggers and one that fires exactly when it should. Write descriptions as if onboarding a new colleague: purpose, trigger conditions, and what not to do instead.

The loop: request, execute, return, repeat

Tool use is a conversation pattern. Claude answers with a tool_use block instead of text; you execute the function and send back a tool_result; Claude continues with the result in hand:

python
messages = [{"role": "user", "content": "Where is order ORD-9931?"}]

while True:
    response = client.messages.create(
        model="claude-opus-5", max_tokens=16000, tools=tools, messages=messages,
    )
    if response.stop_reason != "tool_use":
        break                                   # Claude is done — final answer in content
    messages.append({"role": "assistant", "content": response.content})
    results = []
    for block in response.content:
        if block.type == "tool_use":
            output = run_tool(block.name, block.input)      # your code, your rules
            results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
    messages.append({"role": "user", "content": results})

Three rules keep this loop correct. Echo the full assistant content back — the tool_use blocks must be in history or the API rejects the follow-up. Match every tool_use_id — each result names the call it answers. And when Claude requests several tools in one turn — it does this deliberately, in parallel — execute them all and return all results in a single user message; splitting them across messages trains the model out of parallelism.

When a tool fails, return that as data, not an exception: {"type": "tool_result", "tool_use_id": ..., "content": "Error: no such order", "is_error": True}. Claude reads the error and adapts — retries with a fix, tries another route, or tells the user — which is exactly what you want an agent to do with failure.

You rarely write that loop by hand

The SDKs ship a tool runner that owns the loop — you supply decorated functions, it handles schemas, execution, and feeding results back until Claude finishes:

python
from anthropic import beta_tool

@beta_tool
def get_order_status(order_id: str) -> str:
    """Look up the current status of a customer order by its ID."""
    return lookup(order_id)

runner = client.beta.messages.tool_runner(
    model="claude-opus-5", max_tokens=16000,
    tools=[get_order_status],
    messages=[{"role": "user", "content": "Where is ORD-9931?"}],
)
for message in runner:
    ...   # each iteration yields a message; loop ends when Claude stops calling tools

The runner is not a black box: each iteration yields the assistant message before tools execute, so approval gates, logging, and result rewriting all fit inside it. Reserve the hand-written loop for when you need control the runner genuinely doesn't expose.

Tools Anthropic runs for you

Some tools need no implementation at all — declare them and Anthropic executes server-side: web search and web fetch (current information with citations), and code execution (a sandboxed Python environment Claude uses for math, data analysis, and file generation). One wrinkle worth knowing before it bites: long server-tool turns can pause with stop_reason: "pause_turn" — re-send the conversation as-is and the server resumes where it left off.

What to take into the next lesson

Descriptions steer, schemas constrain (make them strict), the loop is mechanical enough to delegate to the runner, errors are data, and parallel calls come back in one message. You now have the full request-side toolkit — models, prompts, schemas, tools. The next two lessons turn to what production actually runs on: making context cheap, and making responses flow.

← Previous