Use Multiple Tools

Su Suriya Ravichandran Updated 10 Nov 2026
8 min read ·Lesson 95 of 200

Tools Work Best as a Set

Real assistants rarely have just one tool. A study assistant might look up enrollments, search the catalog, show course details, and list upcoming sessions. With several tools available, Claude can:

  • choose the right tool for each part of a request,
  • combine tools in parallel when parts are independent (Lesson 84), and
  • chain tools across steps, using one tool's result as another's input (Lesson 88).

The tool runner (Lesson 92) handles all of this automatically. What remains for you is designing the tool set and giving each tool the context it needs.


Designing a Tool Set

Give Each Tool a Distinct Job

When two tools overlap, Claude may choose inconsistently between them. Make responsibilities clear, and say in each docstring when to use a different tool instead (Lesson 93):

ToolJobPoints to
search_coursesFind courses by topicget_course_details for one course's details
get_course_detailsDetails for one known coursesearch_courses to find slugs
get_enrollmentsThe student's own courses and progresssearch_courses for new courses
get_upcoming_sessionsThe student's booked live sessionsnone

Design Tools That Chain

Tools often feed each other. search_courses returns slugs, and get_course_details takes a slug. Using the same identifiers in outputs and inputs lets Claude chain tools without guessing (Lesson 82).

Name Tools Consistently

Use a consistent verb + object pattern (get_…, search_…, book_…). When tools come from several services, namespace them, for example github_list_prs and slack_send_message (Lesson 82).

Keep the Set Focused

Every tool definition is sent with every request and adds input tokens (Lesson 84). Give each feature only the tools it needs:

TOOLSETS = {
    "catalog_help": ["search_courses", "get_course_details"],
    "study_assistant": ["get_enrollments", "get_upcoming_sessions", "search_courses", "get_course_details"],
}

For very large tool libraries, tool search lets Claude load definitions on demand (Lesson 154).


Giving Tools Context: Factories and Closures

With the tool runner, Claude supplies a tool's arguments, and the runner calls your function with them. But some information must come from your application, never from Claude: the current user's identity, their permissions, a database connection (Lesson 86).

A clean way to provide it is a factory function that creates the tools for a specific user. The tools are defined inside the factory, so they can use the factory's variables. Python functions defined inside another function keep access to its variables; such functions are called closures.

def make_tools(user_id: str) -> list:
    @beta_tool
    def get_enrollments() -> str:
        """Return the current student's enrolled courses with progress, as JSON.

        Use it for questions about the student's own courses or progress.
        It takes no input: the student is identified automatically.
        """
        return json.dumps(ENROLLMENTS.get(user_id, []))    # user_id comes from the factory

    return [get_enrollments]

Why this is safe: get_enrollments has no user_id parameter, so Claude cannot choose whose data to read, whether by mistake or because of a prompt injection (Lesson 40). The user is fixed when your code creates the tools for the current request.


Practical Example: A Multi-Tool Study Assistant

This example builds a tool set per user with a factory, runs questions that need several tools, and shows how Claude combines and chains them.

"""multi_tools.py: a set of tools bound to the current user, run together."""

import json

from anthropic import Anthropic, beta_tool

COURSES = [
    {"slug": "django-101", "title": "Django for Beginners", "level": "beginner", "weeks": 6, "topic": "django"},
    {"slug": "django-apis", "title": "Advanced Django APIs", "level": "advanced", "weeks": 4, "topic": "django"},
    {"slug": "sql-basics", "title": "SQL Basics", "level": "beginner", "weeks": 3, "topic": "sql"},
]
ENROLLMENTS = {"meena": [{"slug": "django-101", "progress": 65}, {"slug": "sql-basics", "progress": 100}],
               "arjun": [{"slug": "sql-basics", "progress": 20}]}
SESSIONS = {"meena": [{"slug": "django-101", "day": "2026-11-12", "start": "19:00"}], "arjun": []}


def make_tools(user_id: str) -> list:
    """Create the study-assistant tools for one user."""

    @beta_tool
    def get_enrollments() -> str:
        """Return the current student's enrolled courses with progress percentages, as JSON.

        Use it for questions about the student's own courses, progress, or what to study next.
        It takes no input: the student is identified automatically.
        """
        return json.dumps(ENROLLMENTS.get(user_id, []))

    @beta_tool
    def get_upcoming_sessions() -> str:
        """Return the current student's booked live sessions, as JSON with slug, day, and start time.

        Use it when the student asks about their schedule or booked sessions.
        It takes no input: the student is identified automatically. Returns an empty list if none.
        """
        return json.dumps(SESSIONS.get(user_id, []))

    @beta_tool
    def search_courses(topic: str) -> str:
        """Search the GOCOURSE catalog for courses on a topic.

        Use it when the student asks which courses exist or what to take next.
        Do not use it for the student's own enrollments; use get_enrollments instead.
        Returns courses as JSON with slug, title, and level.

        Args:
            topic: Subject to search for, e.g. 'Django' or 'SQL'
        """
        matches = [{k: c[k] for k in ("slug", "title", "level")}
                   for c in COURSES if topic.lower() in c["topic"]]
        return json.dumps(matches) if matches else f"No courses found for {topic!r}."

    @beta_tool
    def get_course_details(slug: str) -> str:
        """Get details for one course by its slug, including its duration in weeks.

        Use it when the student wants details about a specific course.
        Do not use it to find courses; use search_courses to get slugs first.
        Returns the course as JSON.

        Args:
            slug: Course slug, e.g. 'django-101'
        """
        for course in COURSES:
            if course["slug"] == slug:
                return json.dumps(course)
        raise ValueError(f"No course with slug {slug!r}. Use search_courses to find valid slugs.")

    return [get_enrollments, get_upcoming_sessions, search_courses, get_course_details]


def ask(user_id: str, question: str) -> None:
    client = Anthropic()
    runner = client.beta.messages.tool_runner(
        model="claude-opus-5-5",
        max_tokens=4096,
        max_iterations=8,
        system="You are the GOCOURSE study assistant. Use the tools; never guess what they can tell you. "
               "Keep answers under 80 words.",
        tools=make_tools(user_id),
        messages=[{"role": "user", "content": question}],
    )
    print(f"[{user_id}] Q: {question}")
    last = None
    for step, message in enumerate(runner, start=1):
        calls = [f"{b.name}({b.input})" for b in message.content if b.type == "tool_use"]
        if calls:
            print(f"  step {step}: {', '.join(calls)}")
        last = message
    print("  A:", "".join(b.text for b in last.content if b.type == "text"), "\n")


if __name__ == "__main__":
    ask("meena", "What's on my schedule, and how many weeks is the next Django course after the one I'm taking?")
    ask("arjun", "What's on my schedule?")

Example output (Claude's exact steps and wording vary):

[meena] Q: What's on my schedule, and how many weeks is the next Django course after the one I'm taking?
  step 1: get_upcoming_sessions({}), get_enrollments({}), search_courses({'topic': 'Django'})
  step 2: get_course_details({'slug': 'django-apis'})
  A: You have a live Django for Beginners session on 12 November at 19:00. The next course after it is Advanced Django APIs, which takes 4 weeks.

[arjun] Q: What's on my schedule?
  step 1: get_upcoming_sessions({})
  A: You don't have any live sessions booked right now. Would you like help finding one for SQL Basics?

How It Works

  1. make_tools(user_id) defines four tools inside the function. Each one can use user_id, but none of them has a user_id parameter, so Claude cannot change whose data is read.
  2. Each request gets its own tool set: make_tools("meena") and make_tools("arjun") create separate tool objects bound to different users. Meena's question returned her session; Arjun's returned an empty list.
  3. Docstrings divide responsibilities, and point to the right alternative tool, so Claude chooses consistently.
  4. Parallel calls: in step 1, Claude called three independent tools at once. The runner ran all of them and returned all results together.
  5. Chaining: in step 2, Claude used a slug from search_courses, django-apis, as input to get_course_details, because outputs and inputs share the same identifiers.
  6. max_iterations=8 bounds the run, as in Lesson 92.

Common Mistakes

Letting Claude Supply Identity

What happens: a user_id parameter lets the model read or change another user's data.

Fix: bind identity in a factory (closure) or another application-controlled context.

Overlapping Tools

What happens: Claude switches between similar tools unpredictably.

Fix: give each tool a distinct job, and say in each docstring when to use another tool.

Mismatched Identifiers Between Tools

What happens: Claude cannot chain tools, or guesses identifiers.

Fix: return the same identifiers that other tools accept as input.

Sending Every Tool to Every Feature

What happens: higher costs and more chances for wrong tool choices.

Fix: define tool sets per feature.

Sharing User-Bound Tools Between Users

What happens: a tool set created for one user is reused for another user's request, exposing the first user's data.

Fix: create tools per request (or per user session), never as global objects bound to a user.


Best Practices

  • Design tools as a coherent set with distinct responsibilities.
  • Point to alternatives in each docstring.
  • Use shared identifiers so tools can be chained.
  • Bind application context with factories, not parameters.
  • Create user-bound tools per request.
  • Send focused tool sets per feature.
  • Let Claude run independent tools in parallel, and limit iterations.

Exercise

  1. Add a book_session(slug, day, start) tool to the factory (reuse the validation from Lesson 94), bound to the current user.
  2. Create a TOOLSETS dictionary and a function tools_for(feature, user_id) that returns only the tools named for that feature.
  3. Ask Meena's assistant "Which courses have I finished?" and check which tools Claude calls.
  4. Explain why make_tools() must be called per request in a web application, instead of once at startup.
Show answers for steps 2 and 4

Step 2:

TOOLSETS = {
    "catalog_help": ["search_courses", "get_course_details"],
    "study_assistant": ["get_enrollments", "get_upcoming_sessions", "search_courses", "get_course_details"],
}


def tools_for(feature: str, user_id: str) -> list:
    wanted = set(TOOLSETS[feature])
    return [tool for tool in make_tools(user_id) if tool.name in wanted]

Each decorated tool has a name taken from its function name, so filtering by name selects the right subset.

Step 4: the tools returned by make_tools(user_id) are bound to one specific user through the closure. A web application serves many users; if the tools were created once at startup, they would be bound to a single user, and every request would read that user's data. Creating them per request, with the user from the request's authentication, ensures each user's tools access only their own data.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Use Multiple Tools and get answers drawn from it.

Signed-in readers only.