Build your first AI agent with tool use (Claude API)
This guide is for developers who have made a basic Claude API call (if you haven't, start with Call the Claude API in Python) and want to build a real agent: Claude decides which of your tools to call, your code runs them, and a loop continues until the job is done. You will write a little over 100 lines of Python, and each term is explained when it first appears. The tool use syntax and model IDs were checked against Anthropic's docs on October 16, 2026. Model names change, so check the models page before you ship.
- Time
- About 35 minutes
- Checked
If a button name on the screen does not match this guide, follow the action. These products rename buttons often. This is an advanced guide: it uses code, and each technical term is explained the first time it appears. Model names and prices change, so check the linked docs before you ship.
Steps
Know what an agent is
Tool use (also called function calling) lets Claude ask your program to run a function you describe. Claude never runs your code itself: it replies with a structured request, your code does the work, and you send the result back. An agent is just that exchange in a loop. Claude asks for a tool, you run it, Claude looks at the result and either asks for another tool or gives its final answer. The code that repeats this is the agent loop.
Set up the project
Create an API key in the Claude Console under API keys and store it in an environment variable, never in code. Then make a project with a virtual environment and install the official SDK (a library that wraps the API's web requests):
export ANTHROPIC_API_KEY="your-key-here" mkdir claude-agent && cd claude-agent python3 -m venv .venv && source .venv/bin/activate pip install anthropicYou'll build one file,
agent.py, in three parts. Paste each part below the last one.Describe your tools
A tool definition has three fields:
name(letters, numbers,_or-),description(plain English: what it does, when to use it, what it returns), andinput_schema(a JSON Schema, a standard way to describe the arguments and their types). The description matters most, because it's how Claude decides when to call the tool. This agent gets two small tools that need no outside service: one reads the clock, one does date math.import json from datetime import date, datetime, timedelta from zoneinfo import ZoneInfo import anthropic client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY MODEL = "claude-sonnet-5-5" MAX_TURNS = 10 # stop condition: never let the loop run forever tools = [ { "name": "get_current_time", "description": ( "Returns the current date, time, and weekday in one IANA time zone, " "such as America/Chicago. Use it whenever the answer depends on " "today's date or the current time. It does not convert between zones." ), "input_schema": { "type": "object", "properties": { "timezone": { "type": "string", "description": "IANA time zone name, e.g. America/Chicago", } }, "required": ["timezone"], }, }, { "name": "add_days", "description": ( "Adds a number of days to a calendar date and returns the new date " "and its weekday. Use a negative number to go back in time. " "The start date must be in YYYY-MM-DD format." ), "input_schema": { "type": "object", "properties": { "start_date": {"type": "string", "description": "Date as YYYY-MM-DD"}, "days": {"type": "integer", "description": "Days to add (negative subtracts)"}, }, "required": ["start_date", "days"], }, }, ]claude-sonnet-5-5is a fast, mid-priced current model. Check the models page for the latest IDs.Write the functions the tools run
These are ordinary Python functions.
run_toollooks up the function by the name Claude asked for and returns its output as text. If something goes wrong, it returns the error message and a flag instead of crashing, so Claude can see what happened and try again.def get_current_time(timezone): now = datetime.now(ZoneInfo(timezone)) return json.dumps({"now": now.isoformat(timespec="minutes"), "weekday": now.strftime("%A")}) def add_days(start_date, days): result = date.fromisoformat(start_date) + timedelta(days=days) return json.dumps({"date": result.isoformat(), "weekday": result.strftime("%A")}) TOOL_FUNCTIONS = {"get_current_time": get_current_time, "add_days": add_days} def run_tool(name, tool_input): """Run one tool. Returns (output_text, is_error).""" if name not in TOOL_FUNCTIONS: return f"Unknown tool: {name}", True try: return TOOL_FUNCTIONS[name](**tool_input), False except Exception as error: # report the problem to Claude instead of crashing return f"{type(error).__name__}: {error}", TrueWrite the agent loop
Every API response has a
stop_reason: why Claude stopped writing."tool_use"means Claude wants a tool. Its reply then contains one or moretool_useblocks, each with anid, the toolname, and aninputthat matches your schema. You run each one and answer with ausermessage oftool_resultblocks. Each result carries the matchingtool_use_idand your output ascontent, plusis_error: trueif the tool failed. Any other stop reason ends the loop.MAX_TURNSis your safety stop, so a confused agent can't call tools (and spend money) forever.def run_agent(question): messages = [{"role": "user", "content": question}] for turn in range(1, MAX_TURNS + 1): response = client.messages.create( model=MODEL, max_tokens=1024, tools=tools, messages=messages, ) print(f"turn {turn}: stop_reason={response.stop_reason}") # Anything other than "tool_use" means Claude is done (or stopped for another reason). if response.stop_reason != "tool_use": answer = "".join(block.text for block in response.content if block.type == "text") return answer, response.stop_reason # Keep Claude's full reply, including its tool_use blocks, in the history. messages.append({"role": "assistant", "content": response.content}) # Run every tool Claude asked for and send all results back in ONE user message. results = [] for block in response.content: if block.type == "tool_use": output, failed = run_tool(block.name, block.input) print(f" {block.name}({block.input}) -> {output}") results.append( { "type": "tool_result", "tool_use_id": block.id, "content": output, "is_error": failed, } ) messages.append({"role": "user", "content": results}) return f"Stopped after {MAX_TURNS} turns without a final answer.", "max_turns" if __name__ == "__main__": answer, reason = run_agent("What date is 45 days from today in Chicago, and what weekday is that?") print(f"\n[{reason}]\n{answer}")Run it and read the trace
Run
python agent.py. You should see Claude callget_current_timefirst, thenadd_dayswith today's date, then stop withstop_reason=end_turnand a final answer. Claude may call both tools in a single turn, or one per turn. The loop handles either case, because it answers everytool_useblock in the reply. The printed lines are your trace, a record of each step, and the first place to look when an agent misbehaves.Handle every way the loop can end
end_turnis the normal finish.max_tokensmeans the reply hit yourmax_tokenscap and was cut off. If the last block is a half-writtentool_use, retry with a higher cap.refusalmeans Claude declined the request.max_turnsis the label this script uses when it hits its ownMAX_TURNSlimit. It isn't an API value. In a real app, treat everything exceptend_turnas "not finished" and log it instead of showing it as an answer. (If you later add Anthropic-run server tools such as web search, also handlepause_turnby sending the conversation back so Claude can continue.)Check the answer yourself
Look at the trace. Did Claude pass the right time zone? Did
add_daysget today's date and45? Check the final date against a calendar. An agent can call the right tools and still word the answer wrong, so check the final answer against the tool outputs. Before you give an agent tools that change things (send email, delete files, spend money), add a step where a person approves each of those actions.
Tips
Send the assistant's full
response.contentback unchanged. Dropping itstool_useblocks, or sending a tool result without its matchingtool_use_id, causes a 400 error.In the message with your tool results, put the
tool_resultblocks first and don't add text after them. Extra text after results can make Claude end its turn early.Treat tool output that comes from outside (web pages, emails, uploads) as untrusted. Keep it inside
tool_resultblocks, never in your system prompt, because it may contain instructions meant to hijack the agent.On Claude Opus 5.5 and Sonnet 5.5, forcing a tool with
tool_choiceset toanyortoolreturns a 400 error. Leave it on the default (auto) and say in the prompt when a tool should be used.On Windows,
ZoneInfoneeds time zone data: runpip install tzdataif you get a ZoneInfoNotFoundError.
Common questions
Is there a way to skip writing the loop?
Yes. The Python SDK has a beta Tool Runner (client.beta.messages.tool_runner) that builds tool definitions from functions marked with @beta_tool and runs the loop for you. Writing the loop by hand once shows you what it does, and you'll need that knowledge when you add approvals, logging, or limits.
How is this different from an MCP server?
Here, tools are Python functions inside your own program. An MCP server packages tools so any MCP-capable app, such as Claude Desktop, can use them. See Build your first MCP server for that route.
How much does an agent run cost?
Every turn is a normal API call. Your tool definitions, the growing conversation, and every tool_use and tool_result block count as tokens, so long loops get more expensive with each turn. Keep MAX_TURNS low while testing. Use prompt caching when the same long instructions repeat, and check usage in the Console.
How do I know my agent is good, not just working once?
Write a few test questions with known answers and run them after every change. The evals guide shows how.
More guides
BeginnerClaudeDocuments
Explain a bill or official letter
Paste a bill or letter into free Claude and get plain English, the deadline, and what is actually being asked.
About 10 minutes using Claude
BeginnerClaudeWriting
Write a clear email
Tell free Claude who you are writing to and what you need. You get a short draft, and you send it yourself.
About 8 minutes using Claude
BeginnerClaudeWriting
Rewrite resume bullets from real work
Ask free Claude for three resume bullets that use only the facts you provide, with blanks where a number is missing.
About 12 minutes using Claude
Occasional notes
Hear when a new guide is up
A short note when a guide is added. No account, and no emails until this form is connected to a real list.