Tool Design & Schema Safety
This module covers building reliable, cost-efficient tool integrations, versioning web search for dynamic filtering, enforcing strict JSON schemas on tool inputs, and understanding the compliance trade-offs for each approach.
1. Concepts: Tool Selection & Strict Mode
Before writing any code, understand how Claude decides which tool to call.
- Tool descriptions are the primary selection mechanism. Claude reads the
nameanddescriptionon each tool to choose. If two tools have overlapping purposes (e.g.analyze_contentvs.analyze_document), Claude may misroute the request, write descriptions that disambiguate exactly when each tool is appropriate. - Dynamic filtering with
web_search_20260209alongsidecode_execution_20260120lets Claude write Python that post-processes search results before they enter the context window. Lower token cost, higher signal. - Strict mode (
strict: True) uses grammar-constrained sampling to force tool inputs to match your JSON schema 100%. No trailing commas, no missing required fields, no surprise keys.
Standard web_search alone is ZDR-eligible. Adding code_execution_20260120 for dynamic filtering makes the entire request non-ZDR because execution state is held server-side. Communicate this trade-off to regulated clients before enabling it.
The web_search_20260209 tool version activates dynamic filtering, the older version does not. Always check the tool type version string, it is a direct exam signal for which capabilities are available.
2. Defining the Toolbox
Both web_search and code_execution are server-side built-ins, Anthropic runs them and feeds the results back to Claude. add_lead_to_crm is a client-side custom tool: when Claude calls it, your code is responsible for executing the action and returning the result.
Note the explicit name field on every entry. code_execution_20260120 requires name even though it's a built-in, omitting it returns a 400 BadRequestError.
# DEFINE THE TOOLS
# Note: Use 'strict: True' for the CRM tool to guarantee valid JSON.
# Note: 'code_execution' must have a name even if it's a built-in server tool.
tools = [
{
"type": "web_search_20260209",
"name": "web_search",
"max_uses": 5,
"allowed_domains": ["gartner.com", "forrester.com", "hbr.org"],
},
{
"type": "code_execution_20260120",
"name": "code_execution" # CRITICAL: required alongside type
},
{
"name": "add_lead_to_crm",
"description": "Adds a validated prospect to the enterprise CRM. Use only after industry research is complete.",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"company": {"type": "string"},
"contact_email": {"type": "string", "format": "email"},
"budget_range": {"type": ["string", "null"]}, # Nullable to prevent hallucinations
},
"required": ["company", "contact_email", "budget_range"],
"additionalProperties": False,
},
},
]
3. The Initial Request & stop_reason
Send the user's prospecting goal with the toolbox attached. Don't print just the final text, inspect stop_reason and walk the content blocks. On the first turn you should see tool_use, not end_turn.
# THE INITIAL REQUEST
# On Opus 4.7, 'thinking' is required for complex tasks like this.
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=8192,
thinking={"type": "adaptive"},
tools=tools,
messages=[{
"role": "user",
"content": "Find the top 5 AI consulting prospects in EMEA finance and add them to my CRM.",
}],
)
# SHOW OUTPUT
print(f"Stop Reason: {response.stop_reason}") # Expected: 'tool_use'
for block in response.content:
if block.type == "thinking":
print("Reasoning Signature Detected.")
if block.type == "tool_use":
print(f"Claude requested tool: {block.name}")
print(f"Parameters: {block.input}")
4. Handling the Agentic Loop
Web search runs server-side, Anthropic executes it and feeds the results back to Claude inside the same request. Your CRM write is client-side, your code must execute it and return a tool_result block before Claude can finish its turn.
- Turn 1: Claude calls
web_search. The API runs it server-side and silently injects results into Claude's context. - Turn 2: Claude reasons over the results and emits a
tool_useblock foradd_lead_to_crm. - Turn 3: your application performs the database write and sends a
tool_resultblock back. Claude generates the finalend_turnresponse.
def handle_tool_use(block):
if block.name == "add_lead_to_crm":
# crm.write(block.input) # your client-side action
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": "ok: lead written",
}
raise ValueError(f"unhandled client-side tool: {block.name}")
# Loop until Claude is done.
while response.stop_reason == "tool_use":
tool_results = [handle_tool_use(b) for b in response.content if b.type == "tool_use"]
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=8192,
thinking={"type": "adaptive"},
tools=tools,
messages=[
{"role": "user", "content": "Find the top 5 AI consulting prospects ..."},
{"role": "assistant", "content": response.content},
{"role": "user", "content": tool_results},
],
)
5. Final Verification & Citations
Once the loop terminates with end_turn, inspect the final text blocks. Web search automatically attaches citations to grounded claims, verify them so you can audit the sources Claude relied on.
# FINAL OUTPUT VERIFICATION
# Web search automatically provides citations.
for block in response.content:
if block.type == "text":
print("Final Answer:", block.text)
if hasattr(block, "citations") and block.citations:
print("Sources:", [c.url for c in block.citations])
- Safety: nullable types in the schema (
["string", "null"]) prevent Claude from inventing a budget figure when one isn't in the search results. - Efficiency:
code_execution_20260120is free when bundled withweb_searchorweb_fetch, so dynamic filtering costs nothing extra in token generation. - Compliance: that same bundling makes the request non-ZDR. Standard web search alone stays ZDR-eligible.
- Selection: tool descriptions, not names alone, drive Claude's routing. Disambiguate any tools whose purposes overlap.
Lab Exercise: Tool Design & Schema Safety
Self-driven lab Module2_Self_Driven_Lab.ipynbObjective: design a tool surface that routes correctly, validates inputs strictly, and makes compliance trade-offs explicit.
- Define web search, code execution, and CRM write tools with clear descriptions and versioned built-in tool types.
- Make the CRM schema strict and nullable where data may be absent.
- Trigger a tool-use turn, inspect the requested tool, and return a correctly keyed `tool_result`.
- Write a short ZDR note explaining what changes when dynamic filtering is enabled.
A tool definition and loop handler that can safely add validated leads.