# Build your first AI agent with tool use (Claude API)

> Give Claude two Python tools, write the agent loop that runs them and sends results back, and add stop conditions so your agent always ends.

Level: Advanced
Tool: Claude
Category: Building
Time: About 35 minutes
Checked: 2026-10-16
Tools: [Claude Console](https://platform.claude.com), [Tool use overview](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview), [How tool use works (the agent loop)](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works), [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls), [Stop reasons](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)
HTML page: https://easyaihowtos.com/guides/build-your-first-ai-agent-with-tool-use

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](/guides/call-the-claude-api-with-prompt-caching)) 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.

## What you need

- Python 3.10 or newer and a terminal (the Anthropic Python SDK requires 3.10+)
- A Claude Console account with billing set up and an API key (API use is pay-as-you-go, separate from a claude.ai subscription)
- A first Claude API call that already works (the prompt caching guide walks you through one)

## Steps

### 1. 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**.

### 2. 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):
```bash
export ANTHROPIC_API_KEY="your-key-here"
mkdir claude-agent && cd claude-agent
python3 -m venv .venv && source .venv/bin/activate
pip install anthropic
```
You'll build one file, `agent.py`, in three parts. Paste each part below the last one.

### 3. 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), and `input_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.
```python
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-5` is a fast, mid-priced current model. Check the models page for the latest IDs.

### 4. Write the functions the tools run

These are ordinary Python functions. `run_tool` looks 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.
```python
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}", True
```

### 5. Write 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 more **`tool_use` blocks**, each with an `id`, the tool `name`, and an `input` that matches your schema. You run each one and answer with a `user` message of **`tool_result` blocks**. Each result carries the matching `tool_use_id` and your output as `content`, plus `is_error: true` if the tool failed. Any other stop reason ends the loop. `MAX_TURNS` is your safety stop, so a confused agent can't call tools (and spend money) forever.
```python
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}")
```

### 6. Run it and read the trace

Run `python agent.py`. You should see Claude call `get_current_time` first, then `add_days` with today's date, then stop with `stop_reason=end_turn` and a final answer. Claude may call both tools in a single turn, or one per turn. The loop handles either case, because it answers every `tool_use` block in the reply. The printed lines are your **trace**, a record of each step, and the first place to look when an agent misbehaves.

### 7. Handle every way the loop can end

`end_turn` is the normal finish. `max_tokens` means the reply hit your `max_tokens` cap and was cut off. If the last block is a half-written `tool_use`, retry with a higher cap. `refusal` means Claude declined the request. `max_turns` is the label this script uses when it hits its own `MAX_TURNS` limit. It isn't an API value. In a real app, treat everything except `end_turn` as "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 handle `pause_turn` by sending the conversation back so Claude can continue.)

### 8. Check the answer yourself

Look at the trace. Did Claude pass the right time zone? Did `add_days` get today's date and `45`? 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.content` back unchanged. Dropping its `tool_use` blocks, or sending a tool result without its matching `tool_use_id`, causes a 400 error.
- In the message with your tool results, put the `tool_result` blocks 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_result` blocks, 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_choice` set to `any` or `tool` returns a 400 error. Leave it on the default (`auto`) and say in the prompt when a tool should be used.
- On Windows, `ZoneInfo` needs time zone data: run `pip install tzdata` if you get a ZoneInfoNotFoundError.

## FAQ

### 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](/guides/build-an-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](/guides/call-the-claude-api-with-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](/guides/write-evals-for-an-llm-app) shows how.

---

This guide is from Easy AI How-To (https://easyaihowtos.com). It is independent of the tools named above. Screens and free-plan limits change, so check the date before you rely on a button name.
