Primitive · The Tool-Use Primitive
A calculator as a tool
You sent Claude a math question and it answered from memory. Now you want it to answer from code — code you control, code that is always correct. The gap between "Claude answers" and "Claude runs your function" is exactly one JSON schema and one extra API call.
What a tool actually is
A tool is a JSON schema you attach to a Messages API request. The schema tells Claude what functions exist, what arguments they take, and what they return. Claude does not execute the function. Claude decides whether to call it and, when it does, emits a tool_use content block containing the function name and a validated input dict. Your code reads that block, runs the function, and hands the result back. The model never touches your runtime.
Here is the smallest possible tool definition:
# scripts/calculator.py
import anthropic, json
client = anthropic.Anthropic()
tools = [{
"name": "add",
"description": "Add two integers and return their sum.",
"input_schema": {
"type": "object",
"properties": {
"a": {"type": "integer", "description": "First addend"},
"b": {"type": "integer", "description": "Second addend"}
},
"required": ["a", "b"]
}
}]
def add(a: int, b: int) -> int:
return a + b
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=256,
tools=tools,
messages=[{"role": "user", "content": "What is 41 plus 1?"}]
)
print(response.stop_reason) # → tool_use
print(response.content) # → [ToolUseBlock(id='...', name='add', input={'a': 41, 'b': 1})]
The response has stop_reason: "tool_use" and its content array contains a ToolUseBlock. That block has three fields you will use every time: id (a string like "toolu_01..."), name (the function name), and input (a dict matching your schema). Nothing about this block changes across any tool you will ever build.
Completing the round-trip
Once you have the tool call, you run the function and send the result back as a second request. That second request must include both the original assistant turn and a new user turn carrying the result:
# scripts/calculator.py (continued)
tool_block = next(b for b in response.content if b.type == "tool_use")
result = add(**tool_block.input) # → 42
final = client.messages.create(
model="claude-opus-4-5",
max_tokens=256,
tools=tools,
messages=[
{"role": "user", "content": "What is 41 plus 1?"},
{"role": "assistant", "content": response.content}, # full assistant turn
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_block.id,
"content": str(result)
}]
}
]
)
print(final.content[0].text) # → "41 plus 1 is 42."
The assistant turn in the second request is response.content verbatim — not just the text, the full list of content blocks. Discarding it and sending only the tool_result is the single most common beginner error. The API will accept the malformed request but Claude will not know what it asked, and the answer will be incoherent.
Why start with addition
Addition has no side effects, no authentication, no error states, and no ambiguity. The result is always correct or obviously wrong. This means every bug you encounter is in the envelope — the tool definition, the round-trip structure, the result format — not in the tool logic. Learn the envelope here, with no noise, before adding any real complexity.
If you get stuck
Schema validation error on the first request: The input_schema must have "type": "object" at the top level. A bare {"type": "integer"} for a single-argument tool will fail. Always wrap in an object even if you have one parameter.
stop_reason is "end_turn" instead of "tool_use": The model decided not to call the tool. Usually the prompt does not require computation — try phrasing the question so the answer is a specific number, not something the model can guess from training data. "What is 41 plus 1?" works because the model recognizes the tool is the right channel. "How much is forty-one and one?" sometimes does not.
tool_result gets a validation error: The content field of a tool_result block must be a string (or a list of content blocks for richer returns). Passing an integer directly raises a validation error. Cast to str().
Cross-reference
If the two-turn structure feels unfamiliar, the api-fundamentals course, lesson l2, covers multi-turn message shape in detail. This lesson extends that directly — the only new element is tool_use blocks in the assistant turn and tool_result blocks in the user turn.
Deliverable
scripts/calculator.py — a self-contained script that defines the add tool, sends one prompt, executes the function, completes the round-trip, and prints the final text response. Thirty lines. No imports beyond anthropic. When you run it, the last line of output is "41 plus 1 is 42." or equivalent.
Further reading
- "The Widget That Knows What a Gunshot Sounds Like" — a worked case for when a substring-match function is the right tool and an LLM is the wrong one, and where the line falls between them.
Try these prompts
- Define a JSON schema for a multiply tool that takes two integers and returns their product. Include the description field, required array, and one field description example.
- Write the Python code to extract the tool_use block from a Messages API response and call the corresponding function.
- Explain why the content field of a tool_result block must be a string, not an integer, and what error results from passing the integer directly.
Sign in to keep your place and tick off lessons. Reading stays open either way.