Loading...
    • Build
    • Admin
    • Models & pricing
    • Client SDKs
    • API Reference
    Search...
    ⌘K
    First steps
    Intro to ClaudeQuickstart
    Building with Claude
    Features overviewUsing the Messages APIHandling stop reasons
    Model capabilities
    Extended thinkingAdaptive thinkingEffortFast mode (beta: research preview)Structured outputsCitationsStreaming MessagesBatch processingSearch resultsStreaming refusalsMultilingual supportEmbeddings
    Tools
    OverviewHow tool use worksWeb search toolWeb fetch toolCode execution toolAdvisor toolMemory toolBash toolComputer use toolText editor tool
    Tool infrastructure
    Tool referenceTool searchProgrammatic tool callingFine-grained tool streaming
    Context management
    Context windowsCompactionContext editingPrompt cachingToken counting
    Working with files
    Files APIPDF supportImages and vision
    Skills
    OverviewQuickstartBest practicesSkills for enterpriseSkills in the API
    MCP
    Remote MCP serversMCP connector
    Prompt engineering
    OverviewPrompting best practicesConsole prompting tools
    Test and evaluate
    Define success and build evaluationsUsing the Evaluation Tool in ConsoleReducing latency
    Strengthen guardrails
    Reduce hallucinationsIncrease output consistencyMitigate jailbreaksReduce prompt leak
    Resources
    Glossary
    Release notes
    Claude Platform
    Console
    Log in
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...
    Loading...

    Solutions

    • AI agents
    • Code modernization
    • Coding
    • Customer support
    • Education
    • Financial services
    • Government
    • Life sciences

    Partners

    • Amazon Bedrock
    • Google Cloud's Vertex AI

    Learn

    • Blog
    • Courses
    • Use cases
    • Connectors
    • Customer stories
    • Engineering at Anthropic
    • Events
    • Powered by Claude
    • Service partners
    • Startups program

    Company

    • Anthropic
    • Careers
    • Economic Futures
    • Research
    • News
    • Responsible Scaling Policy
    • Security and compliance
    • Transparency

    Learn

    • Blog
    • Courses
    • Use cases
    • Connectors
    • Customer stories
    • Engineering at Anthropic
    • Events
    • Powered by Claude
    • Service partners
    • Startups program

    Help and security

    • Availability
    • Status
    • Support
    • Discord

    Terms and policies

    • Privacy policy
    • Responsible disclosure policy
    • Terms of service: Commercial
    • Terms of service: Consumer
    • Usage policy
    Documentation

    Tutorial: Build a tool-using agent

    A guided walkthrough from a single tool call to a production-ready agentic loop.

    Was this page helpful?

    • Ring 1: Single tool, single turn
    • Ring 2: The agentic loop
    • Ring 3: Multiple tools, parallel calls
    • Ring 4: Error handling
    • Ring 5: The Tool Runner SDK abstraction
    • What you built
    • Next steps

    This tutorial builds a calendar-management agent in five concentric rings. Each ring is a complete, runnable program that adds exactly one concept to the ring before it. By the end you will have written the agentic loop by hand and then replaced it with the Tool Runner SDK abstraction.

    The example tool is create_calendar_event. Its schema uses nested objects, arrays, and optional fields, so you will see how Claude handles realistic input shapes rather than a single flat string.

    Every ring runs standalone. Copy any ring into a fresh file and it will execute without the code from earlier rings.

    Ring 1: Single tool, single turn

    The smallest possible tool-using program: one tool, one user message, one tool call, one result. The code is heavily commented so you can map each line to the tool use lifecycle.

    The request sends a tools array alongside the user message. When Claude decides to call a tool, the response comes back with stop_reason: "tool_use" and a tool_use content block containing the tool name, a unique id, and the structured input. Your code executes the tool, then sends the result back in a tool_result block whose tool_use_id matches the id from the call.

    #!/bin/bash
    # Ring 1: Single tool, single turn.
    # Source for <CodeSource> in build-a-tool-using-agent.mdx.
    
    # Define one tool as a JSON fragment. The input_schema is a JSON Schema
    # object describing the arguments Claude should pass when it calls this
    # tool. This schema includes nested objects (recurrence), arrays
    # (attendees), and optional fields, which is closer to real-world tools
    # than a flat string argument.
    TOOLS='[
      {
        "name": "create_calendar_event",
        "description": "Create a calendar event with attendees and optional recurrence.",
        "input_schema": {
          "type": "object",
          "properties": {
            "title": {"type": "string"},
            "start": {"type": "string", "format": "date-time"},
            "end": {"type": "string", "format": "date-time"},
            "attendees": {
              "type": "array",
              "items": {"type": "string", "format": "email"}
            },
            "recurrence": {
              "type": "object",
              "properties": {
                "frequency": {"enum": ["daily", "weekly", "monthly"]},
                "count": {"type": "integer", "minimum": 1}
              }
            }
          },
          "required": ["title", "start", "end"]
        }
      }
    ]'
    
    USER_MSG="Schedule a 30-minute sync with [email protected] and [email protected] next Monday at 10am."
    
    # Send the user's request along with the tool definition. Claude decides
    # whether to call the tool based on the request and the tool description.
    RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \
      -H "x-api-key: $ANTHROPIC_API_KEY" \
      -H "anthropic-version: 2023-06-01" \
      -H "content-type: application/json" \
      -d "$(jq -n \
        --argjson tools "$TOOLS" \
        --arg msg "$USER_MSG" \
        '{
          model: "claude-opus-4-6",
          max_tokens: 1024,
          tools: $tools,
          tool_choice: {type: "auto", disable_parallel_tool_use: true},
          messages: [{role: "user", content: $msg}]
        }')")
    
    # When Claude calls a tool, the response has stop_reason "tool_use"
    # and the content array contains a tool_use block alongside any text.
    echo "stop_reason: $(echo "$RESPONSE" | jq -r '.stop_reason')"
    
    # Find the tool_use block. A response may contain text blocks before the
    # tool_use block, so filter by type rather than assuming position.
    TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")')
    TOOL_USE_ID=$(echo "$TOOL_USE" | jq -r '.id')
    echo "Tool: $(echo "$TOOL_USE" | jq -r '.name')"
    echo "Input: $(echo "$TOOL_USE" | jq -c '.input')"
    
    # Execute the tool. In a real system this would call your calendar API.
    # Here the result is hardcoded to keep the example self-contained.
    RESULT='{"event_id": "evt_123", "status": "created"}'
    
    # Send the result back. The tool_result block goes in a user message and
    # its tool_use_id must match the id from the tool_use block above. The
    # assistant's previous response is included so Claude has the full history.
    ASSISTANT_CONTENT=$(echo "$RESPONSE" | jq '.content')
    FOLLOWUP=$(curl -s https://api.anthropic.com/v1/messages \
      -H "x-api-key: $ANTHROPIC_API_KEY" \
      -H "anthropic-version: 2023-06-01" \
      -H "content-type: application/json" \
      -d "$(jq -n \
        --argjson tools "$TOOLS" \
        --arg msg "$USER_MSG" \
        --argjson assistant "$ASSISTANT_CONTENT" \
        --arg tool_use_id "$TOOL_USE_ID" \
        --arg result "$RESULT" \
        '{
          model: "claude-opus-4-6",
          max_tokens: 1024,
          tools: $tools,
          tool_choice: {type: "auto", disable_parallel_tool_use: true},
          messages: [
            {role: "user", content: $msg},
            {role: "assistant", content: $assistant},
            {role: "user", content: [
              {type: "tool_result", tool_use_id: $tool_use_id, content: $result}
            ]}
          ]
        }')")
    
    # With the tool result in hand, Claude produces a final natural-language
    # answer and stop_reason becomes "end_turn".
    echo "stop_reason: $(echo "$FOLLOWUP" | jq -r '.stop_reason')"
    echo "$FOLLOWUP" | jq -r '.content[] | select(.type == "text") | .text'

    What to expect

    Output
    stop_reason: tool_use
    Tool: create_calendar_event
    Input: {'title': 'Sync', 'start': '2026-03-30T10:00:00', 'end': '2026-03-30T10:30:00', 'attendees': ['[email protected]', '[email protected]']}
    stop_reason: end_turn
    I've scheduled your 30-minute sync with Alice and Bob for next Monday at 10am.

    The first stop_reason is tool_use because Claude is waiting for the calendar result. After you send the result, the second stop_reason is end_turn and the content is natural language for the user.

    Ring 2: The agentic loop

    Ring 1 assumed Claude would call the tool exactly once. Real tasks often need several calls: Claude might create an event, read the confirmation, then create another. The fix is a while loop that keeps running tools and feeding results back until stop_reason is no longer "tool_use".

    The other change is conversation history. Instead of rebuilding the messages array from scratch on each request, keep a running list and append to it. Every turn sees the complete prior context.

    What to expect

    Output
    I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.

    The loop may run once or several times depending on how Claude breaks down the task. Your code no longer needs to know in advance.

    Ring 3: Multiple tools, parallel calls

    Agents rarely have just one capability. Add a second tool, list_calendar_events, so Claude can check the existing schedule before creating something new.

    When Claude has multiple independent tool calls to make, it may return several tool_use blocks in a single response. Your loop needs to process all of them and send back all results together in one user message. Iterate over every tool_use block in response.content, not just the first.

    What to expect

    Output
    I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.

    For more on concurrent execution and ordering guarantees, see Parallel tool use.

    Ring 4: Error handling

    Tools fail. A calendar API might reject an event with too many attendees, or a date might be malformed. When a tool raises an error, send the error message back with is_error: true instead of crashing. Claude reads the error and can retry with corrected input, ask the user for clarification, or explain the limitation.

    What to expect

    Output
    I tried to schedule the all-hands but the calendar only allows 10 attendees per event. I can split this into two sessions, or you can let me know which 10 people to prioritize.

    The is_error flag is the only difference from a successful result. Claude sees the flag and the error text, and responds accordingly. See Handle tool calls for the full error-handling reference.

    Ring 5: The Tool Runner SDK abstraction

    Rings 2 through 4 wrote the same loop by hand: call the API, check stop_reason, run tools, append results, repeat. The Tool Runner does this for you. Define each tool as a function, pass the list to tool_runner, and retrieve the final message once the loop completes. Error wrapping, result formatting, and conversation management are handled internally.

    The Python SDK uses the @beta_tool decorator to infer the schema from type hints and the docstring. The TypeScript SDK uses betaZodTool with a Zod schema.

    Tool Runner is available in the Python, TypeScript, and Ruby SDKs. The Shell and CLI tabs show a note instead of code; keep the Ring 4 loop for shell-based scripts.

    What to expect

    Output
    I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.

    The output is identical to Ring 3. The difference is in the code: roughly half the lines, no manual loop, and the schema lives next to the implementation.

    What you built

    You started with a single hardcoded tool call and ended with a production-shaped agent that handles multiple tools, parallel calls, and errors, then collapsed all of that into the Tool Runner. Along the way you saw every piece of the tool-use protocol: tool_use blocks, tool_result blocks, tool_use_id matching, stop_reason checking, and is_error signaling.

    Next steps

    Sharpen your schemas

    Schema specification and best practices.

    Tool Runner deep dive

    The full SDK abstraction reference.

    Troubleshooting

    Fix common tool-use errors.

    #!/bin/bash
    # Ring 2: The agentic loop.
    # Source for <CodeSource> in build-a-tool-using-agent.mdx.
    
    TOOLS='[
      {
        "name": "create_calendar_event",
        "description": "Create a calendar event with attendees and optional recurrence.",
        "input_schema": {
          "type": "object",
          "properties": {
            "title": {"type": "string"},
            "start": {"type": "string", "format": "date-time"},
            "end": {"type": "string", "format": "date-time"},
            "attendees": {"type": "array", "items": {"type": "string", "format": "email"}},
            "recurrence": {
              "type": "object",
              "properties": {
                "frequency": {"enum": ["daily", "weekly", "monthly"]},
                "count": {"type": "integer", "minimum": 1}
              }
            }
          },
          "required": ["title", "start", "end"]
        }
      }
    ]'
    
    run_tool() {
      local name="$1"
      local input="$2"
      if [ "$name" = "create_calendar_event" ]; then
        local title=$(echo "$input" | jq -r '.title')
        jq -n --arg title "$title" '{event_id: "evt_123", status: "created", title: $title}'
      else
        echo "{\"error\": \"Unknown tool: $name\"}"
      fi
    }
    
    # Keep the full conversation history in a JSON array so each turn sees prior context.
    MESSAGES='[{"role": "user", "content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: [email protected], [email protected], [email protected]."}]'
    
    call_api() {
      curl -s https://api.anthropic.com/v1/messages \
        -H "x-api-key: $ANTHROPIC_API_KEY" \
        -H "anthropic-version: 2023-06-01" \
        -H "content-type: application/json" \
        -d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \
          '{model: "claude-opus-4-6", max_tokens: 1024, tools: $tools, tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: $messages}')"
    }
    
    RESPONSE=$(call_api)
    
    # Loop until Claude stops asking for tools. Each iteration runs the requested
    # tool, appends the result to history, and asks Claude to continue.
    while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
      TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")')
      TOOL_NAME=$(echo "$TOOL_USE" | jq -r '.name')
      TOOL_INPUT=$(echo "$TOOL_USE" | jq -c '.input')
      TOOL_USE_ID=$(echo "$TOOL_USE" | jq -r '.id')
      RESULT=$(run_tool "$TOOL_NAME" "$TOOL_INPUT")
    
      ASSISTANT_CONTENT=$(echo "$RESPONSE" | jq '.content')
      MESSAGES=$(echo "$MESSAGES" | jq \
        --argjson assistant "$ASSISTANT_CONTENT" \
        --arg tool_use_id "$TOOL_USE_ID" \
        --arg result "$RESULT" \
        '. + [
          {role: "assistant", content: $assistant},
          {role: "user", content: [{type: "tool_result", tool_use_id: $tool_use_id, content: $result}]}
        ]')
    
      RESPONSE=$(call_api)
    done
    
    echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'
    #!/bin/bash
    # Ring 3: Multiple tools, parallel calls.
    # Source for <CodeSource> in build-a-tool-using-agent.mdx.
    
    TOOLS='[
      {
        "name": "create_calendar_event",
        "description": "Create a calendar event with attendees and optional recurrence.",
        "input_schema": {
          "type": "object",
          "properties": {
            "title": {"type": "string"},
            "start": {"type": "string", "format": "date-time"},
            "end": {"type": "string", "format": "date-time"},
            "attendees": {"type": "array", "items": {"type": "string", "format": "email"}},
            "recurrence": {
              "type": "object",
              "properties": {
                "frequency": {"enum": ["daily", "weekly", "monthly"]},
                "count": {"type": "integer", "minimum": 1}
              }
            }
          },
          "required": ["title", "start", "end"]
        }
      },
      {
        "name": "list_calendar_events",
        "description": "List all calendar events on a given date.",
        "input_schema": {
          "type": "object",
          "properties": {"date": {"type": "string", "format": "date"}},
          "required": ["date"]
        }
      }
    ]'
    
    run_tool() {
      case "$1" in
        create_calendar_event)
          jq -n --arg title "$(echo "$2" | jq -r '.title')" '{event_id: "evt_123", status: "created", title: $title}' ;;
        list_calendar_events)
          echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;
        *)
          echo "{\"error\": \"Unknown tool: $1\"}" ;;
      esac
    }
    
    MESSAGES='[{"role": "user", "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts."}]'
    
    call_api() {
      curl -s https://api.anthropic.com/v1/messages \
        -H "x-api-key: $ANTHROPIC_API_KEY" \
        -H "anthropic-version: 2023-06-01" \
        -H "content-type: application/json" \
        -d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \
          '{model: "claude-opus-4-6", max_tokens: 1024, tools: $tools, messages: $messages}')"
    }
    
    RESPONSE=$(call_api)
    
    while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
      # A single response can contain multiple tool_use blocks. Process all of
      # them and return all results together in one user message.
      TOOL_RESULTS='[]'
      while read -r block; do
        NAME=$(echo "$block" | jq -r '.name')
        INPUT=$(echo "$block" | jq -c '.input')
        ID=$(echo "$block" | jq -r '.id')
        RESULT=$(run_tool "$NAME" "$INPUT")
        TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$RESULT" \
          '. + [{type: "tool_result", tool_use_id: $id, content: $result}]')
      done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")')
    
      MESSAGES=$(echo "$MESSAGES" | jq \
        --argjson assistant "$(echo "$RESPONSE" | jq '.content')" \
        --argjson results "$TOOL_RESULTS" \
        '. + [{role: "assistant", content: $assistant}, {role: "user", content: $results}]')
    
      RESPONSE=$(call_api)
    done
    
    echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'
    #!/bin/bash
    # Ring 4: Error handling.
    # Source for <CodeSource> in build-a-tool-using-agent.mdx.
    
    TOOLS='[
      {
        "name": "create_calendar_event",
        "description": "Create a calendar event with attendees and optional recurrence.",
        "input_schema": {
          "type": "object",
          "properties": {
            "title": {"type": "string"},
            "start": {"type": "string", "format": "date-time"},
            "end": {"type": "string", "format": "date-time"},
            "attendees": {"type": "array", "items": {"type": "string", "format": "email"}},
            "recurrence": {
              "type": "object",
              "properties": {
                "frequency": {"enum": ["daily", "weekly", "monthly"]},
                "count": {"type": "integer", "minimum": 1}
              }
            }
          },
          "required": ["title", "start", "end"]
        }
      },
      {
        "name": "list_calendar_events",
        "description": "List all calendar events on a given date.",
        "input_schema": {
          "type": "object",
          "properties": {"date": {"type": "string", "format": "date"}},
          "required": ["date"]
        }
      }
    ]'
    
    run_tool() {
      case "$1" in
        create_calendar_event)
          local count=$(echo "$2" | jq '.attendees | length // 0')
          if [ "$count" -gt 10 ]; then
            echo "ERROR: Too many attendees (max 10)"
            return 1
          fi
          jq -n --arg title "$(echo "$2" | jq -r '.title')" '{event_id: "evt_123", status: "created", title: $title}' ;;
        list_calendar_events)
          echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;
        *)
          echo "ERROR: Unknown tool: $1"
          return 1 ;;
      esac
    }
    
    EMAILS=$(seq 0 14 | sed 's/.*/user&@example.com/' | paste -sd, -)
    MESSAGES="[{\"role\": \"user\", \"content\": \"Schedule an all-hands with everyone: $EMAILS\"}]"
    
    call_api() {
      curl -s https://api.anthropic.com/v1/messages \
        -H "x-api-key: $ANTHROPIC_API_KEY" \
        -H "anthropic-version: 2023-06-01" \
        -H "content-type: application/json" \
        -d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \
          '{model: "claude-opus-4-6", max_tokens: 1024, tools: $tools, messages: $messages}')"
    }
    
    RESPONSE=$(call_api)
    
    while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
      TOOL_RESULTS='[]'
      while read -r block; do
        NAME=$(echo "$block" | jq -r '.name')
        INPUT=$(echo "$block" | jq -c '.input')
        ID=$(echo "$block" | jq -r '.id')
        if OUTPUT=$(run_tool "$NAME" "$INPUT"); then
          TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \
            '. + [{type: "tool_result", tool_use_id: $id, content: $result}]')
        else
          # Signal failure so Claude can retry or ask for clarification.
          TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \
            '. + [{type: "tool_result", tool_use_id: $id, content: $result, is_error: true}]')
        fi
      done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")')
    
      MESSAGES=$(echo "$MESSAGES" | jq \
        --argjson assistant "$(echo "$RESPONSE" | jq '.content')" \
        --argjson results "$TOOL_RESULTS" \
        '. + [{role: "assistant", content: $assistant}, {role: "user", content: $results}]')
    
      RESPONSE=$(call_api)
    done
    
    echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text'
    #!/bin/bash
    # Ring 5: The Tool Runner SDK abstraction.
    # Source for <CodeSource> in build-a-tool-using-agent.mdx.
    
    # The Tool Runner SDK abstraction is available in the Python, TypeScript,
    # and Ruby SDKs. There is no equivalent for raw curl requests. Switch to
    # the Python or TypeScript tab to see Ring 5, or keep the Ring 4 loop as
    # your shell implementation.