{"slug": "forcing-a-specific-tool-call-in-the-claude-api", "title": "Forcing a Specific Tool Call in the Claude API", "summary": "An engineer detailed how the Claude API's tool_choice parameter can be used to force a specific tool call, enabling developers to obtain structured JSON output by declaring a single tool and setting tool_choice to that tool. The technique guarantees a tool_use response with no preamble text, which is useful for extracting structured data from unstructured input, though it does not ensure the values are present in the source document.", "body_md": "`tool_choice`\n\ndecides whether the model may call a tool, must call some tool, must call one particular tool, or may not call any. The interesting part is not the parameter — it is what each setting removes from the response.\n\n`tool_choice`\n\nis a top-level object with a `type`\n\ndiscriminator. All four forms:\n\n```\n\"tool_choice\": {\"type\": \"auto\"}                              // default\n\"tool_choice\": {\"type\": \"any\"}                               // some tool, model picks\n\"tool_choice\": {\"type\": \"tool\", \"name\": \"get_deploy_status\"}  // this exact tool\n\"tool_choice\": {\"type\": \"none\"}                              // no tools this turn\n```\n\n`auto`\n\nis what you get when the parameter is omitted and `tools`\n\nis present. `none`\n\nis what you get when there are no tools at all, and setting it explicitly while leaving the `tools`\n\narray in place is the way to keep the tool definitions in the cached prefix while forbidding calls for one turn.\n\nWith `auto`\n\n, the model chooses. The response may contain only text, with `stop_reason: \"end_turn\"`\n\n:\n\n```\n{\n  \"content\": [{\"type\": \"text\", \"text\": \"Deploy ids look like dpl_XXXX — which one?\"}],\n  \"stop_reason\": \"end_turn\"\n}\n```\n\nOr text plus one or more `tool_use`\n\nblocks, with `stop_reason: \"tool_use\"`\n\n. Both are normal and your code has to handle both.\n\nWith `any`\n\nor `tool`\n\n, the model is required to call. `stop_reason`\n\nis `\"tool_use\"`\n\non every response, and — this is the part worth internalising — the preamble text disappears. A forced response is the tool call and nothing else:\n\n```\n// tool_choice: {\"type\": \"tool\", \"name\": \"get_deploy_status\"}\n{\n  \"content\": [\n    {\"type\": \"tool_use\", \"id\": \"toolu_01B7…\", \"name\": \"get_deploy_status\",\n     \"input\": {\"deploy_id\": \"dpl_8f21\"}}\n  ],\n  \"stop_reason\": \"tool_use\"\n}\n```\n\nThere is no text block to render, and if your UI expected one it now shows a blank turn. That is the trade: you gain a guaranteed shape and lose the model’s ability to say “I need more information first”.\n\nThe difference between `any`\n\nand `tool`\n\nis only who picks. With `any`\n\nand three tools declared, you will get exactly one of the three and you will not know which until you read `name`\n\n. With `tool`\n\nyou know before you send the request.\n\nThe most common reason to force a specific tool has nothing to do with tools. A tool is a JSON Schema the model must fill in, so declaring one tool and forcing it is a way to get a guaranteed object back:\n\n```\n{\n  \"model\": \"claude-opus-4-6\",\n  \"max_tokens\": 1024,\n  \"tools\": [{\n    \"name\": \"record_incident\",\n    \"description\": \"Record the structured fields of an incident report.\",\n    \"input_schema\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"severity\": {\"type\": \"string\", \"enum\": [\"sev1\", \"sev2\", \"sev3\"]},\n        \"component\": {\"type\": \"string\"},\n        \"customer_impact\": {\"type\": \"boolean\"}\n      },\n      \"required\": [\"severity\", \"component\", \"customer_impact\"]\n    }\n  }],\n  \"tool_choice\": {\"type\": \"tool\", \"name\": \"record_incident\"},\n  \"messages\": [{\"role\": \"user\", \"content\": \"<incident channel transcript>\"}]\n}\n```\n\nYou never execute `record_incident`\n\n. You read `content[0].input`\n\nand that is your parsed object — no fenced code blocks to strip, no leading “Here is the JSON” to discard. The `enum`\n\nconstrains `severity`\n\nto three values in a way a prompt instruction does not. This pattern predates structured outputs and remains useful because it works on every model that supports tools; see [getting JSON out via tool_choice](https://multigrid.ai/learn/claude-json-output-tool-choice) for the full technique.\n\nThere is one thing to check that the schema does not guarantee. A forced call means the model produced *a* value for each required field, not that the value was in the document. Asked to extract a severity from a transcript that never states one, the model must still emit one of the three enum members. The schema constrains the shape and nothing else, so a nullable field or an explicit `\"unknown\"`\n\nmember is usually worth adding — it gives the model somewhere honest to put the answer.\n\nThis is the failure that costs people real money, and it comes from treating `tool_choice`\n\nas configuration rather than as a per-request decision.\n\nForce a tool, get the call, execute it, append the `tool_result`\n\n, and send the conversation back with the same request-building code — which still sets `tool_choice: {\"type\": \"tool\"}`\n\n. The model is again required to call, so it calls again. You execute again, append again, and send again. Nothing errors. Every response is a valid 200 with `stop_reason: \"tool_use\"`\n\n, and the loop runs until your iteration cap catches it or, if there is no cap, until someone notices the spend.\n\n```\n// wrong: the same tool_choice on every iteration\nwhile (msg.stop_reason === \"tool_use\") {\n  msg = await client.messages.create({\n    model, max_tokens, tools, messages,\n    tool_choice: { type: \"tool\", name: \"record_incident\" },   // never relaxes\n  });\n}\n\n// right: force the first call only, then let the model finish\nlet toolChoice = { type: \"tool\", name: \"record_incident\" };\nwhile (true) {\n  const msg = await client.messages.create({\n    model, max_tokens, tools, messages, tool_choice: toolChoice,\n  });\n  toolChoice = { type: \"auto\" };            // relax after the first turn\n  if (msg.stop_reason !== \"tool_use\") break;\n  // … execute, append assistant turn and tool_result …\n}\n```\n\nThe same trap exists in a milder form with `{\"type\": \"any\"}`\n\n, which is worse to diagnose because the model varies which tool it calls and the transcript looks like exploration rather than a stuck loop.\n\nThe general rule: `tool_choice`\n\nbelongs to a turn, not to a conversation. If the reason you forced the call was to guarantee the first action, relax it the moment that action has happened. And if you are using the forced-tool pattern purely for extraction — one tool, no execution, read `input`\n\nand stop — then there is no second turn at all, and the loop is the bug rather than the design.\n\n`auto`\n\n.`auto`\n\n: the common shape is to force the initial call and then let the model decide whether it needs more.`tool_choice`\n\nis not part of the cached prefix in the way the `tools`\n\narray is, so you can vary it per request without paying to re-cache the tool definitions.Any `tool_choice`\n\nvalue can carry `disable_parallel_tool_use`\n\n, which caps the response at one `tool_use`\n\nblock:\n\n```\n\"tool_choice\": {\"type\": \"auto\", \"disable_parallel_tool_use\": true}\n```\n\nBy default the model may emit several calls in one turn, which is faster when the calls are independent and wrong when they are not — a tool that mutates state usually wants to see the result of call one before deciding on call two. Setting the flag serialises the loop at the cost of a round trip per call.", "url": "https://wpnews.pro/news/forcing-a-specific-tool-call-in-the-claude-api", "canonical_source": "https://dev.to/multigrid/forcing-a-specific-tool-call-in-the-claude-api-d5j", "published_at": "2026-08-12 22:20:42+00:00", "updated_at": "2026-08-12 22:46:26.412862+00:00", "lang": "en", "topics": ["large-language-models", "developer-tools"], "entities": ["Claude API", "Anthropic"], "alternates": {"html": "https://wpnews.pro/news/forcing-a-specific-tool-call-in-the-claude-api", "markdown": "https://wpnews.pro/news/forcing-a-specific-tool-call-in-the-claude-api.md", "text": "https://wpnews.pro/news/forcing-a-specific-tool-call-in-the-claude-api.txt", "jsonld": "https://wpnews.pro/news/forcing-a-specific-tool-call-in-the-claude-api.jsonld"}}