Tool use β also called function calling β is how you give Claude the ability to interact with the real world. Instead of a static Q&A exchange, you define a set of βtoolsβ (functions your code can run), describe them to Claude in JSON Schema format, and let Claude decide when to call them. Claude returns a structured call with the right arguments; your code executes the function and sends the result back. The loop continues until Claude has what it needs to give a final answer.
This is the foundation of every serious AI agent. Without tool use, Claude is a text transformer. With it, Claude becomes an orchestrator that can query databases, call APIs, read files, search the web, or control your infrastructure. This tutorial uses the official anthropic Python SDK and covers everything from a single tool call to a multi-step agentic loop youβd ship in production.
How Tool Use Works at the API Level
The flow is straightforward once you see it:
- You send Claude a list of tool definitions (name, description, input schema) alongside the userβs message.
- If Claude decides a tool is needed, it returns a
tool_usecontent block instead of plain text. - Your code executes the function and sends back a
tool_resultmessage. - Claude reads the result and either calls another tool or produces its final answer.
One critical thing to internalize: Claude never executes code. It only produces structured output describing what it wants to call and with what arguments. Your code is the executor. This separation keeps you in full control of every side effect.
Environment Setup
Install the SDK and set your API key:
pip install anthropic python-dotenv
# .env
ANTHROPIC_API_KEY=sk-ant-...
import anthropic
import json
import os
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic()
Your First Tool: A Currency Converter
Start concrete. Weβll give Claude a currency conversion tool, then ask it a question that requires using it.
tools = [
{
"name": "convert_currency",
"description": "Convert an amount from one currency to another using current exchange rates.",
"input_schema": {
"type": "object",
"properties": {
"amount": {"type": "number", "description": "The amount to convert"},
"from_currency": {"type": "string", "description": "Source currency code, e.g. USD, EUR, GBP"},
"to_currency": {"type": "string", "description": "Target currency code"}
},
"required": ["amount", "from_currency", "to_currency"]
}
}
]
def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
rates = {"USD": 1.0, "EUR": 0.92, "GBP": 0.79, "JPY": 149.5}
usd_amount = amount / rates.get(from_currency, 1)
converted = usd_amount * rates.get(to_currency, 1)
return {"result": round(converted, 2), "from": from_currency, "to": to_currency}
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "How much is 500 EUR in USD and GBP?"}]
)
print(f"Stop reason: {response.stop_reason}") # "tool_use"
When stop_reason is "tool_use", Claude wants to call your function. Extract the block, execute it, and send the result back:
tool_results = []
for block in response.content:
if block.type == "tool_use":
if block.name == "convert_currency":
result = convert_currency(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
final_response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
messages=[
{"role": "user", "content": "How much is 500 EUR in USD and GBP?"},
{"role": "assistant", "content": response.content},
{"role": "user", "content": tool_results}
]
)
print(final_response.content[0].text)
Building a Proper Agentic Loop
The two-call example above handles one tool. Real agents chain multiple calls β Claude reads a file, finds a reference, fetches another file, writes a summary. Hereβs the reusable loop pattern you should reach for:
def run_agent(user_message: str, tools: list, dispatcher, max_iterations: int = 10) -> str:
messages = [{"role": "user", "content": user_message}]
iteration = 0
while iteration < max_iterations:
iteration += 1
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=tools,
messages=messages
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = dispatcher(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
messages.append({"role": "user", "content": tool_results})
return "Max iterations reached."
The max_iterations guard is non-optional in production. An unbounded loop is a real cost and latency risk. Cap it, log when it fires, and tune the limit based on what your agent actually needs.
Real-World Example: A File System Research Agent
Hereβs something youβd actually deploy: an agent that lists files, reads them, and writes a summary to disk. This is the core of any document processing pipeline.
FS_TOOLS = [
{
"name": "list_files",
"description": "List files in a directory. Returns names and sizes.",
"input_schema": {
"type": "object",
"properties": {"directory": {"type": "string"}},
"required": ["directory"]
}
},
{
"name": "read_file",
"description": "Read the full text content of a file.",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"]
}
},
{
"name": "write_file",
"description": "Write content to a file, overwriting if it exists.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"}
},
"required": ["path", "content"]
}
}
]
def fs_dispatcher(name: str, inputs: dict) -> dict:
try:
if name == "list_files":
entries = os.listdir(inputs["directory"])
files = [
{"name": e, "size": os.path.getsize(os.path.join(inputs["directory"], e))}
for e in entries
if os.path.isfile(os.path.join(inputs["directory"], e))
]
return {"files": files}
elif name == "read_file":
with open(inputs["path"], "r") as f:
return {"content": f.read()}
elif name == "write_file":
with open(inputs["path"], "w") as f:
f.write(inputs["content"])
return {"success": True, "path": inputs["path"]}
except Exception as ex:
return {"error": str(ex)}
return {"error": f"Unknown tool: {name}"}
# Run it
result = run_agent(
"List the markdown files in ./docs, read each one, and write a combined summary to ./docs/SUMMARY.md",
FS_TOOLS,
fs_dispatcher
)
print(result)
On a docs directory with five files, Claude will call list_files once, read_file five times, then write_file once β all autonomously, with correct paths inferred from the directory listing. No orchestration glue on your side beyond the loop.
Parallel Tool Calls
Claude 3.5+ can emit multiple tool calls in a single assistant message when it recognizes theyβre independent. Instead of reading three files sequentially (three round trips), Claude batches them. Execute them concurrently to cut latency proportionally:
import asyncio
async def dispatch_parallel(tool_uses: list, dispatcher) -> list:
async def run_one(tu):
result = await asyncio.to_thread(dispatcher, tu.name, tu.input)
return {
"type": "tool_result",
"tool_use_id": tu.id,
"content": json.dumps(result)
}
return await asyncio.gather(*[run_one(tu) for tu in tool_uses])
Claude will parallelize when it recognizes calls are independent β you just need your dispatcher to support concurrent execution. For I/O-bound tools like API calls or database queries, this is a straightforward win.
Streaming Tool Use
For production UIs where you want to show live progress, use the streaming API. The SDK accumulates tool use blocks for you automatically:
with client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=2048,
tools=FS_TOOLS,
messages=messages
) as stream:
for event in stream:
if event.type == "content_block_start":
if event.content_block.type == "tool_use":
print(f"\n-> Calling: {event.content_block.name}", flush=True)
elif event.type == "text":
print(event.text, end="", flush=True)
final = stream.get_final_message() # Fully assembled tool_use blocks here
Error Handling: When Tools Fail
Tools fail. Files donβt exist, APIs time out, queries return empty results. Tell Claude explicitly when something goes wrong:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps({"error": "File not found: /docs/missing.md"}),
"is_error": True
})
Claude adapts: it might try a different path, ask for clarification, or explain why the task canβt complete. You donβt need explicit retry logic at the orchestration layer β Claude handles it conversationally. What you do need is structured error output so Claude gets actionable information, not a raw Python traceback.
Hardware for Running the Tool Dispatch Layer
If your agent tools call locally hosted services β a self-hosted Postgres, a private n8n instance, a local vector database β you want a machine running 24/7 with minimal power draw. The Beelink EQ12 Mini PC (~$189) is exactly right for this: always-on, fanless at idle, and fast enough for I/O-bound agent workloads. Pair it with 32GB DDR4 RAM (~$59) and you can also run a small Ollama model alongside it for offline fallback inference when the Claude API is unavailable.
For agents that persist large knowledge bases between sessions β a research agent accumulating retrieved documents or a local vector store β fast random read/write matters. A WD Black 2TB NVMe SSD (~$129) keeps SQLite and ChromaDB queries snappy even as your corpus grows into the tens of thousands of entries.
Best Practices for Production Tool Use
Write Tool Descriptions Like API Documentation
Claude uses your descriptions to decide when and how to call each tool. Vague descriptions produce wrong calls. Precise descriptions specify what the tool returns, what edge cases exist, and what formats or units the arguments expect. The description is the contract β treat it that way.
Keep Tools Focused and Composable
A single database_query tool that accepts arbitrary SQL is harder for Claude to use correctly than list_users, get_user_by_id, and search_users_by_email. Narrow tools produce better results because Claude can express intent precisely. Theyβre also easier to test and audit in isolation.
Log the Full Trace
Tool calls are side effects. When an agent does something unexpected, you need the full trace: tool name, inputs, output, timestamp, and which message triggered the call. JSON Lines appended per tool call works well β query with jq later to understand agent behavior patterns over time.
Constrain Scope via System Prompts
Tell Claude in the system prompt what itβs allowed to do and what it isnβt: βYou are a read-only research assistant. Never call write_file unless the user explicitly asks for a saved output.β Claude respects system-level constraints reliably. This is much cleaner than trying to validate tool calls inside your dispatcher.
Wiring Tool Use into n8n
If youβre running n8n on your local infrastructure (covered in my n8n + Claude automation workflows guide), the cleanest pattern is: expose your tool dispatcher as a FastAPI service, then define Claude tools that call its endpoints over localhost. Your n8n workflows trigger the agent via webhook or schedule, the agent calls your FastAPI tools, and results flow back through n8n for further processing β Slack notifications, database writes, email reports, whatever the downstream workflow needs. This keeps the agentic logic in Python where itβs easy to test, while n8n handles the trigger, notification, and routing layer itβs purpose-built for.
What to Build Next
Tool use is the primitive everything else builds on. Once youβre comfortable with the loop pattern here, the interesting territory opens up:
- RAG + tools: Add a
search_knowledge_basetool backed by a vector store so Claude retrieves relevant context before doing anything else. - Multi-agent coordination: Build a coordinator agent that spawns specialized sub-agents with different tool sets β one for web research, one for code generation, one for output formatting.
- Vision tools: Give Claude a
take_screenshottool and build UI automation without the full computer-use API overhead. - Prompt caching: If your tool definitions are long and stable, cache them with the
cache_controlbeta header and cut token costs significantly on repeated calls.
The fundamentals here β define tools in JSON Schema, loop until end_turn, dispatch in parallel where possible, handle errors with structured output, guard with a max iteration count β apply across all of it. Get this pattern solid and everything else is a variation on the theme. All code in this post is tested against claude-sonnet-4-5 using the anthropic Python SDK 0.34+.