Whetstone.
Designing a Claude ApplicationTool use, the integration primitive
Module 2, Lesson 228 min

Tool use, the integration primitive

Tool use is how Claude stops being a text box and starts being an integration. It is also where most of the fiddly, testable detail lives, and where two things that sound like they belong together turn out to live in completely different places.

The loop, in five moves

You send tools, each with a name, a description, and an input_schema in JSON Schema. Claude replies with stop_reason: "tool_use" and one or more tool_use blocks. You execute. You append the assistant message verbatim, then a user message carrying tool_result blocks keyed by tool_use_id. You call again.

One turn of the loop, the part people get wrong
[
  { "role": "user", "content": "Weather in Exeter and Totnes?" },
  { "role": "assistant", "content": [
      { "type": "tool_use", "id": "tu_1", "name": "get_weather", "input": { "city": "Exeter" } },
      { "type": "tool_use", "id": "tu_2", "name": "get_weather", "input": { "city": "Totnes" } }
  ]},
  { "role": "user", "content": [
      { "type": "tool_result", "tool_use_id": "tu_1", "content": "17C, rain" },
      { "type": "tool_result", "tool_use_id": "tu_2", "content": "18C, cloud" }
  ]}
]

Parallel tool use is GA, so two calls in one assistant message is normal, not exotic. Both results go back in one user message.

The bit that feels wrong: results your own code produced go in a user message. You wrote the weather lookup, so surely you are not the user. But the roles here are not about authorship, they are about direction. Everything travelling into the model is the user side, whoever generated it, and once you read the roles as inbound and outbound rather than human and machine, tool_result stops looking misplaced.

description is not documentation. It is the only thing the model has to decide whether this tool is the right one, and a vague description is the most common cause of a model calling the wrong tool. Write it like a spec, not a comment.

tool_choice controls whether, not how

Four settings. auto lets the model decide. any forces a tool call but leaves the choice open. tool with a name forces one specific tool. none forbids tools this turn.

That is the whole job of tool_choice. It picks which tool runs. It has no opinion about schemas.

Strict tool use, and the flag placement question

Strict tool use is GA, no beta header required. It constrains the generated tool input to your schema rather than merely encouraging it.

Correct placement
"tools": [
  {
    "name": "create_ticket",
    "description": "Open a support ticket. Use only after the user confirms.",
    "input_schema": { "type": "object", "properties": { "title": { "type": "string" } }, "required": ["title"] },
    "strict": true
  }
]

Tools you do not have to define

Anthropic ships two kinds of tool, and the split is who executes the call, which is a different axis from GA-versus-beta and a favourite thing to conflate.

Server tools. Anthropic runs them, and no handler code appears in your application: web search, web fetch, code execution, tool search. All GA. (The advisor tool and the MCP connector are also server tools, both still beta.)

Anthropic-schema client tools. Anthropic publishes the schema and trains the model on it, but your application still executes every call and returns the tool_result: memory, bash, text editor, all GA, plus computer use, which is beta.

Two cost notes that make good questions. Web search bills at 10 dollars per 1000 searches. Web fetch is free. So a design that scrapes a known URL should reach for fetch, and one that genuinely needs discovery pays for search.

Tool search deserves a specific mention: it exists because a large tool catalogue eats your context window before the conversation even starts. If your design has forty tools, the answer is not a bigger prompt.

Practice

Try it yourself

Recall

Where strict lives

One flag, one location, and the wrong location is the whole question.

Where does the strict flag for strict tool use go, and where does it emphatically not go, and is there anything it does not work with?

Reveal answer

strict: true goes on the TOOL DEFINITION, as a sibling of name, description and input_schema. It does not go on tool_choice. tool_choice selects which tool gets used (auto, any, tool, none) and has nothing to do with schema strictness. Strict tool use is GA and needs no beta header, but it is NOT available on mcp_toolset.

Quiz

Two tool calls in one turn

An assistant message comes back containing two tool_use blocks. What does the next request need?

  1. ATwo separate follow-up requests, one tool_result each
  2. BOne user message containing two tool_result blocks, one per tool_use id
  3. COne assistant message containing two tool_result blocks
  4. DA single tool_result containing both outputs concatenated
Show answer

Correct answer: B — One user message containing two tool_result blocks, one per tool_use id

Parallel tool calls arrive as multiple tool_use blocks inside one assistant message, and every one of them needs a matching tool_result keyed by its tool_use id, all inside a single following user message. tool_result blocks go in a user message, never an assistant one, which kills the third option. Splitting into two requests breaks the alternation and leaves the first request with an unanswered tool call.

Quiz

Spot the defect in this loop turn

A hand-rolled loop appends this after executing the tool. It does not work.

What the client appended
[
  { "role": "user", "content": "Weather in Exeter?" },
  { "role": "assistant", "content": [
      { "type": "tool_use", "id": "tu_1", "name": "get_weather", "input": { "city": "Exeter" } }
  ]},
  { "role": "assistant", "content": [
      { "type": "tool_result", "tool_use_id": "tu_1", "content": "17C, rain" }
  ]}
]
  1. AThe tool_use block is missing the required output field that carries the result
  2. BThe tool_result is missing its name field, so the call cannot be matched to a tool
  3. CTwo assistant messages cannot appear consecutively under any circumstances
  4. DThe tool_result is in an assistant message, and tool_result blocks belong in a user message
Show answer

Correct answer: D — The tool_result is in an assistant message, and tool_result blocks belong in a user message

Results your own code produced still go back in a user message. That feels backwards the first time, because you generated the value and you are not the user, but the protocol treats everything you send in as the user side of the exchange regardless of where it came from. The tempting answer is the consecutive assistant messages, which is a real symptom, though it is a consequence of the actual defect rather than the defect itself. A tool_result is keyed by tool_use_id alone and carries no name field.

Quiz

Forcing a tool call

The requirement is that the model must call one of your tools on this turn and must not answer in prose. Which tool_choice do you set?

  1. A{ type: auto }
  2. B{ type: none }
  3. C{ type: any }
  4. D{ type: tool, name: the_only_tool }
Show answer

Correct answer: C — { type: any }

any means it must call a tool but may choose which. That matches the requirement exactly. tool plus a name is the tempting near-miss: it also forces a call, but it removes the model's choice entirely, which over-constrains a requirement that said one of your tools rather than this specific tool. auto permits a prose answer and none forbids tools outright.

Quiz

Fill the blank without getting the level wrong

You want the generated tool input constrained to the schema rather than merely encouraged toward it. One line is missing.

Where does strict: true go?
{
  "tool_choice": { "type": "auto" },
  "tools": [
    {
      "name": "create_ticket",
      "description": "Open a support ticket. Use only after the user confirms.",
      "input_schema": { "type": "object", "properties": { "title": { "type": "string" } } },
      ___
    }
  ]
}
  1. Astrict: true, at the marked position inside the tool definition
  2. Bstrict: true, moved up into the tool_choice object
  3. Cstrict: true, as a new top-level request parameter
  4. DThere is no such flag; strictness comes from the beta header
Show answer

Correct answer: A — strict: true, at the marked position inside the tool definition

Strictness is a property of the schema being enforced, so it lives with the schema, on the tool definition, beside name and description and input_schema. Moving it to tool_choice is the tempting error and the reason this gets tested, because tool_choice sounds like the place all tool configuration goes and its own name suggests it is making a decision about how the tool is used. It is not: tool_choice decides which tool runs, never how its arguments are validated. Strict tool use is also GA, so there is no beta header involved at all.

Check

Inventory what you do not have to build

List which of your planned tools Anthropic already defines, and mark each one by who executes it.

You should see

You have checked your tool list against the GA server tools (web search, web fetch, code execution, tool search), which you can delete outright because Anthropic runs them, and separately against the GA Anthropic-schema client tools (memory, bash, text editor), where you drop the schema but still write the executor. You also noted that web search bills at 10 dollars per 1000 searches while web fetch is free.

Sign in to track your progress →