6Tool use with Claude

Handling message blocks

5 min read855 words

You pass tools=[get_current_datetime_schema] into client.messages.create(), send a user message asking for the current time, and get back a response — but not the shape of response you're used to. Up to now, response.content[0].text has been the only field you cared about. With tools in the mix, the response contains multiple blocks, and treating them like a plain text response breaks everything downstream. This lesson is about reading them correctly and preserving them properly in history.

Making a tool-enabled call

The API call itself is almost the same as before — just add a tools parameter:

messages = []
messages.append({
    "role": "user",
    "content": "What is the exact time, formatted as HH:MM:SS?"
})

response = client.messages.create(
    model=model,
    max_tokens=1000,
    messages=messages,
    tools=[get_current_datetime_schema],
)

tools takes a list of schemas — one or more. Claude sees those schemas as part of its system context and knows it can request any of them. Nothing else in the call changes.

What comes back is where things get different.

Multi-block assistant messages

When Claude decides to use a tool, the response contains multiple content blocks instead of a single text block. Typically you'll see:

  • A text block — human-readable text like "I'll check the current time for you." Sometimes this block is short or missing entirely; Claude can choose to go straight to the tool call without narrating.
  • A tool_use block — a structured request telling you which tool to call and what arguments to use.

The tool_use block carries:

  • id — a unique string like toolu_01XY..., used to match up the eventual tool result.
  • name — the tool Claude wants to call, e.g. "get_current_datetime".
  • input — a dict of arguments, already parsed from JSON into a Python object.
  • type — always "tool_use", for discrimination alongside text blocks.

In code:

>>> response.content
[
    TextBlock(type="text", text="Let me check the current time."),
    ToolUseBlock(
        type="tool_use",
        id="toolu_01XYZ...",
        name="get_current_datetime",
        input={"date_format": "%H:%M:%S"},
    ),
]

Two blocks, two different shapes, same list. This is why you can't treat the response as a plain text stream anymore — half of what Claude wants to communicate is now encoded in structured blocks, not prose.

Preserve the entire content list in history

Here's the single most common mistake people make when they first hit tool use:

# DON'T do this with tool-enabled messages
messages.append({
    "role": "assistant",
    "content": response.content[0].text  # drops the tool_use block!
})

That line compiles, runs, and breaks everything, because on the next API call you'll send tool results back but Claude will have no memory of requesting them — you threw the tool_use block away.

Preserve the full content list, not just the text:

messages.append({
    "role": "assistant",
    "content": response.content,  # full list, all blocks
})

Why this works: Claude's API accepts an assistant message whose content is a list of block objects exactly as the model emitted them. The conversation history round-trips cleanly — when you send the next request, Claude sees its own previous tool call in context and understands that the next user message contains the result.

The rule: when tools are in play, always append response.content — the whole thing — to your messages list. Never reduce it to a string.

The complete flow

With tools, the conversation shape is a five-step dance:

  1. You send a user message and a tools list.
  2. Claude responds with an assistant message containing text and tool_use blocks.
  3. You extract the tool use block, execute the corresponding function on your server, and capture the result.
  4. You send a follow-up request containing the whole history plus a new user message carrying a tool_result block matched to the tool_use ID.
  5. Claude responds with a final assistant message incorporating the result into a natural answer for the user.

Step 4 is the subject of the next lesson. The crucial thing for this lesson is that every step preserves the block structure exactly as received — you don't collapse multi-block messages into strings anywhere in the pipeline.

Helper functions need a refresh

The add_user_message and add_assistant_message helpers you wrote back in Module 3 assume plain strings. They still work for simple cases, but for tool-enabled calls you need to pass block lists (or full Message objects) through. A couple of lessons from now you'll refactor them to handle both. For now, recognise that when you see content=response.content in a code sample, it's intentional — that's the block list being preserved whole.

Key Takeaways

  • 1Enabling tools adds a tools parameter to client.messages.create() — otherwise the call is structurally the same.
  • 2Tool-enabled responses return multi-block assistant messages containing at least one text block and one or more tool_use blocks.
  • 3A tool_use block carries id, name, input, and type='tool_use' — the id is the key you use to match a future tool_result back to the request.
  • 4Always append response.content (the full block list) to your messages history, not response.content[0].text — dropping blocks throws away the tool call Claude just made.
  • 5Existing helper functions that assume plain-string content need to be updated to pass multi-block content through unchanged.