Use Multiple Tools
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):
| Tool | Job | Points to |
|---|---|---|
search_courses | Find courses by topic | get_course_details for one course's details |
get_course_details | Details for one known course | search_courses to find slugs |
get_enrollments | The student's own courses and progress | search_courses for new courses |
get_upcoming_sessions | The student's booked live sessions | none |
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
make_tools(user_id)defines four tools inside the function. Each one can useuser_id, but none of them has auser_idparameter, so Claude cannot change whose data is read.- Each request gets its own tool set:
make_tools("meena")andmake_tools("arjun")create separate tool objects bound to different users. Meena's question returned her session; Arjun's returned an empty list. - Docstrings divide responsibilities, and point to the right alternative tool, so Claude chooses consistently.
- Parallel calls: in step 1, Claude called three independent tools at once. The runner ran all of them and returned all results together.
- Chaining: in step 2, Claude used a slug from
search_courses,django-apis, as input toget_course_details, because outputs and inputs share the same identifiers. max_iterations=8bounds 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
- Add a
book_session(slug, day, start)tool to the factory (reuse the validation from Lesson 94), bound to the current user. - Create a
TOOLSETSdictionary and a functiontools_for(feature, user_id)that returns only the tools named for that feature. - Ask Meena's assistant "Which courses have I finished?" and check which tools Claude calls.
- Explain why
make_tools()must be called per request in a web application, instead of once at startup.
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.