← All guides

Claude Agent SDK Quickstart: Build Your First Agent in 15 Minutes

Step-by-step guide to building a working Claude agent with tool use using the Anthropic SDK — Python and TypeScript, with copy-paste code.

Claude Agent SDK Quickstart: Build Your First Agent in 15 Minutes

An agent is a Claude model that can call tools — functions you define — in a loop until it completes a task. This guide walks from zero to a working agent with two tools (web search and unit converter) in Python or TypeScript.

Prerequisites: Anthropic API key, Python 3.11+ or Node.js 18+.

What you're building

A research assistant that:

  1. Accepts a question
  2. Decides whether to search the web or convert a unit
  3. Calls the tool
  4. Uses the result to answer (or calls another tool)
  5. Returns a final answer

Python version

Step 1: Install the SDK

pip install anthropic
export ANTHROPIC_API_KEY=sk-ant-your-key-here

Step 2: Define your tools

Tools are Python functions with JSON Schema descriptions. Claude reads the description to decide when to call each tool.

import anthropic
import json

client = anthropic.Anthropic()

TOOLS = [
    {
        "name": "web_search",
        "description": "Search the web for current information. Use when the question requires recent facts or data.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "The search query"}
            },
            "required": ["query"]
        }
    },
    {
        "name": "unit_converter",
        "description": "Convert between common units. Supports: km/miles, kg/lbs, celsius/fahrenheit, usd/krw.",
        "input_schema": {
            "type": "object",
            "properties": {
                "value": {"type": "number", "description": "The numeric value to convert"},
                "from_unit": {"type": "string", "description": "Source unit"},
                "to_unit": {"type": "string", "description": "Target unit"}
            },
            "required": ["value", "from_unit", "to_unit"]
        }
    }
]

def web_search(query: str) -> str:
    # In production: call Brave API, Serper, or SerpAPI with your key
    return f"[Mock result for '{query}'] Claude Haiku 4.5 costs $1/1M input, $5/1M output (April 2026)."

CONVERSION_TABLE: dict[tuple[str, str], float] = {
    ("km", "miles"): 0.621371,
    ("miles", "km"): 1.60934,
    ("kg", "lbs"): 2.20462,
    ("lbs", "kg"): 0.453592,
    ("usd", "krw"): 1365.0,
    ("krw", "usd"): 1 / 1365.0,
}

def unit_converter(value: float, from_unit: str, to_unit: str) -> str:
    if from_unit == "celsius" and to_unit == "fahrenheit":
        return str(round(value * 9 / 5 + 32, 4))
    if from_unit == "fahrenheit" and to_unit == "celsius":
        return str(round((value - 32) * 5 / 9, 4))
    factor = CONVERSION_TABLE.get((from_unit, to_unit))
    if factor is None:
        return f"No conversion available from {from_unit} to {to_unit}"
    return str(round(value * factor, 4))

def run_tool(name: str, inputs: dict) -> str:
    if name == "web_search":
        return web_search(inputs["query"])
    if name == "unit_converter":
        return unit_converter(inputs["value"], inputs["from_unit"], inputs["to_unit"])
    return f"Unknown tool: {name}"

Step 3: Build the agent loop

def run_agent(user_question: str, max_turns: int = 10) -> str:
    messages = [{"role": "user", "content": user_question}]

    for turn in range(max_turns):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=4096,
            tools=TOOLS,
            messages=messages,
        )
        print(f"Turn {turn + 1}: stop_reason={response.stop_reason}")

        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text
            return "No text response"

        if response.stop_reason == "tool_use":
            messages.append({"role": "assistant", "content": response.content})
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    print(f"  Calling: {block.name}({block.input})")
                    result = run_tool(block.name, block.input)
                    print(f"  Result: {result[:80]}")
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })
            messages.append({"role": "user", "content": tool_results})
            continue

        break  # Unhandled stop_reason

    return "Max turns reached."


if __name__ == "__main__":
    question = "Convert 100 km to miles. Also, what is Claude's current Haiku API pricing?"
    print(run_agent(question))

Step 4: Run it

python agent.py

Expected output:

Turn 1: stop_reason=tool_use
  Calling: unit_converter({'value': 100, 'from_unit': 'km', 'to_unit': 'miles'})
  Result: 62.1371
Turn 2: stop_reason=tool_use
  Calling: web_search({'query': 'Claude Haiku API pricing 2026'})
  Result: [Mock result...
Turn 3: stop_reason=end_turn
100 km equals 62.14 miles. Claude Haiku 4.5 costs $1/1M input tokens...

TypeScript version

Step 1: Install the SDK

npm init -y
npm install @anthropic-ai/sdk zod
npm install -D typescript tsx @types/node
export ANTHROPIC_API_KEY=sk-ant-your-key-here

Step 2: Define tools

// agent.ts
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";

const client = new Anthropic();

const WebSearchInput = z.object({ query: z.string() });
const UnitConverterInput = z.object({
  value: z.number(),
  from_unit: z.string(),
  to_unit: z.string(),
});

const TOOLS: Anthropic.Messages.Tool[] = [
  {
    name: "web_search",
    description: "Search the web for current information.",
    input_schema: {
      type: "object",
      properties: { query: { type: "string", description: "The search query" } },
      required: ["query"],
    },
  },
  {
    name: "unit_converter",
    description: "Convert between units: km/miles, kg/lbs, celsius/fahrenheit, usd/krw.",
    input_schema: {
      type: "object",
      properties: {
        value: { type: "number", description: "Value to convert" },
        from_unit: { type: "string" },
        to_unit: { type: "string" },
      },
      required: ["value", "from_unit", "to_unit"],
    },
  },
];

const CONVERSION_TABLE: Record<string, number> = {
  "km:miles": 0.621371,
  "miles:km": 1.60934,
  "kg:lbs": 2.20462,
  "lbs:kg": 0.453592,
  "usd:krw": 1365,
  "krw:usd": 1 / 1365,
};

function webSearch(query: string): string {
  return `[Mock result for '${query}'] Claude Haiku 4.5: $1/1M input, $5/1M output.`;
}

function unitConverter(value: number, fromUnit: string, toUnit: string): string {
  if (fromUnit === "celsius" && toUnit === "fahrenheit") {
    return String(+(value * 9 / 5 + 32).toFixed(4));
  }
  if (fromUnit === "fahrenheit" && toUnit === "celsius") {
    return String(+((value - 32) * 5 / 9).toFixed(4));
  }
  const factor = CONVERSION_TABLE[`${fromUnit}:${toUnit}`];
  if (factor === undefined) return `No conversion: ${fromUnit} to ${toUnit}`;
  return String(+(value * factor).toFixed(4));
}

function runTool(name: string, input: unknown): string {
  if (name === "web_search") {
    return webSearch(WebSearchInput.parse(input).query);
  }
  if (name === "unit_converter") {
    const { value, from_unit, to_unit } = UnitConverterInput.parse(input);
    return unitConverter(value, from_unit, to_unit);
  }
  return `Unknown tool: ${name}`;
}

Step 3: Agent loop

async function runAgent(question: string, maxTurns = 10): Promise<string> {
  const messages: Anthropic.Messages.MessageParam[] = [
    { role: "user", content: question },
  ];

  for (let turn = 0; turn < maxTurns; turn++) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 4096,
      tools: TOOLS,
      messages,
    });

    console.log(`Turn ${turn + 1}: stop_reason=${response.stop_reason}`);

    if (response.stop_reason === "end_turn") {
      const text = response.content.find((b) => b.type === "text");
      return text?.type === "text" ? text.text : "No text response";
    }

    if (response.stop_reason === "tool_use") {
      messages.push({ role: "assistant", content: response.content });
      const results: Anthropic.Messages.ToolResultBlockParam[] = [];

      for (const block of response.content) {
        if (block.type === "tool_use") {
          console.log(`  Calling: ${block.name}(${JSON.stringify(block.input)})`);
          const result = runTool(block.name, block.input);
          console.log(`  Result: ${result.slice(0, 80)}`);
          results.push({ type: "tool_result", tool_use_id: block.id, content: result });
        }
      }
      messages.push({ role: "user", content: results });
      continue;
    }

    break;
  }
  return "Max turns reached.";
}

runAgent("Convert 100 km to miles, and what is Claude Haiku's API pricing?").then(
  (answer) => { console.log("\n=== ANSWER ===\n" + answer); }
);

Step 4: Run it

npx tsx agent.ts

What just happened

The agent loop:

  1. Send user message + tool definitions to Claude
  2. Claude responds: end_turn (final answer) or tool_use (wants to call a tool)
  3. On tool_use: execute the tool, add result to messages, loop back to step 1
  4. On end_turn: return the text

This is the complete foundation. Every production agent adds on top of these four steps.


Next steps

Add streaming

with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=4096,
    tools=TOOLS,
    messages=messages,
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    response = stream.get_final_message()

Track cost per session

def log_cost(response: anthropic.types.Message) -> float:
    costs = {
        "claude-haiku-4-5": (1.0, 5.0),
        "claude-sonnet-4-6": (3.0, 15.0),
        "claude-opus-4-7": (5.0, 25.0),
    }
    input_rate, output_rate = costs.get(response.model, (3.0, 15.0))
    cost = (
        response.usage.input_tokens / 1_000_000 * input_rate
        + response.usage.output_tokens / 1_000_000 * output_rate
    )
    print(f"  Cost: ${cost:.4f}")
    return cost

Persist session history

import json, pathlib

def save_session(messages: list, session_id: str) -> None:
    pathlib.Path(f"sessions/{session_id}.json").write_text(json.dumps(messages))

def load_session(session_id: str) -> list:
    p = pathlib.Path(f"sessions/{session_id}.json")
    return json.loads(p.read_text()) if p.exists() else []

Common mistakes

Missing the assistant turn in message history: when Claude uses a tool, add Claude's full response (including tool_use blocks) to messages before adding the tool results. The API requires the complete alternating user/assistant pattern.

Tool results in the wrong role: tool results must go in a user message, not an assistant message.

No max_turns guard: without a ceiling, a buggy tool causes infinite loops and unbounded cost.

Unhandled stop_reason values: handle end_turn, tool_use, max_tokens, and stop_sequence.


FAQ

Do I need a special "Agent SDK" package? No. The anthropic Python package and @anthropic-ai/sdk npm package are sufficient. The agent loop is implemented using the standard messages API.

What is Claude Code's relationship to this agent pattern? Claude Code is itself an agent using this pattern. Its tools are Read, Write, Edit, Bash, and Glob.

Can I call multiple tools in one turn? Yes. Claude may return multiple tool_use blocks in one response. Collect all their results into one user message and continue.

How do I add human approval before tool execution? Check the tool name before calling run_tool(). If it requires approval, pause and prompt the user before proceeding.

Sources

  1. Anthropic tool use documentation — April 2026
  2. Anthropic Python SDK — April 2026
  3. Anthropic TypeScript SDK — April 2026
AI Disclosure: Drafted with Claude Code; all code examples tested with Anthropic SDK April 2026 versions.