{"slug": "claude-s-tool-use-and-tool-result-content-blocks-end-to-end", "title": "Claude's tool_use and tool_result Content Blocks, End to End", "summary": "Anthropic's Claude Messages API implements tool use as two content block types and a developer-written loop, not a special mode. The protocol requires echoing tool_use IDs in tool_result blocks, supports multiple tool calls per turn, and returns errors via is_error flags. The full agent loop is: call, check stop_reason, execute tool_use blocks, append assistant turn and tool results, and repeat until stop_reason is not 'tool_use'.", "body_md": "Tool use on the Messages API is not a special mode. It is two content block types and a loop you write yourself, and the entire protocol fits on one page.\n\nTools are declared in a top-level `tools`\n\narray. Each entry has a name, a description, and a JSON Schema describing its input. The description is not documentation — it is the only thing the model has to decide when the tool applies, so it does more work than the schema does.\n\n```\n{\n  \"model\": \"claude-opus-4-6\",\n  \"max_tokens\": 1024,\n  \"tools\": [\n    {\n      \"name\": \"get_deploy_status\",\n      \"description\": \"Get the status of a deployment by its id. Use when the user asks whether a deploy succeeded, failed, or is still running.\",\n      \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"deploy_id\": {\"type\": \"string\", \"description\": \"The deploy id, e.g. dpl_8f21\"}\n        },\n        \"required\": [\"deploy_id\"]\n      }\n    }\n  ],\n  \"messages\": [\n    {\"role\": \"user\", \"content\": \"Did dpl_8f21 go out cleanly?\"}\n  ]\n}\n```\n\nThe model does not call anything. It returns a response whose `content`\n\narray contains a block of type `tool_use`\n\n, and whose `stop_reason`\n\nis `\"tool_use\"`\n\n:\n\n```\n{\n  \"id\": \"msg_01Xy…\",\n  \"role\": \"assistant\",\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"Let me check that deploy.\"\n    },\n    {\n      \"type\": \"tool_use\",\n      \"id\": \"toolu_01A9k3…\",\n      \"name\": \"get_deploy_status\",\n      \"input\": {\"deploy_id\": \"dpl_8f21\"}\n    }\n  ],\n  \"stop_reason\": \"tool_use\",\n  \"usage\": {\"input_tokens\": 412, \"output_tokens\": 68}\n}\n```\n\n`id`\n\n— a `toolu_`\n\n-prefixed identifier for this specific call. You must echo it back. It is not the tool name and it is not stable across calls.`name`\n\n— which tool to run. Dispatch on this.`input`\n\n— already-parsed JSON matching your schema, delivered as an object rather than a string. There is no second parse step.Note the text block sitting beside the tool call. A single assistant turn can contain prose and one or more tool calls together, so code that assumes `content[0]`\n\nis the tool call breaks the first time the model narrates what it is doing.\n\nTwo messages get appended. First the assistant turn, verbatim, including the `tool_use`\n\nblock. Then a *user* turn containing the results — `tool_result`\n\nis a user-role block, not a role of its own.\n\n```\n\"messages\": [\n  {\"role\": \"user\", \"content\": \"Did dpl_8f21 go out cleanly?\"},\n  {\n    \"role\": \"assistant\",\n    \"content\": [\n      {\"type\": \"text\", \"text\": \"Let me check that deploy.\"},\n      {\"type\": \"tool_use\", \"id\": \"toolu_01A9k3…\", \"name\": \"get_deploy_status\",\n       \"input\": {\"deploy_id\": \"dpl_8f21\"}}\n    ]\n  },\n  {\n    \"role\": \"user\",\n    \"content\": [\n      {\n        \"type\": \"tool_result\",\n        \"tool_use_id\": \"toolu_01A9k3…\",\n        \"content\": \"{\\\"state\\\":\\\"failed\\\",\\\"stage\\\":\\\"migrate\\\",\\\"exit_code\\\":1}\"\n      }\n    ]\n  }\n]\n```\n\n`tool_use_id`\n\nmust equal the `id`\n\nfrom the corresponding `tool_use`\n\nblock. That pairing is what the API validates, and a mismatch is a 400. `content`\n\nmay be a plain string or an array of blocks — an array lets you return an image as well as text, which is how screenshot-producing tools work.\n\nFor a failure, do not invent a fake success and do not drop the block. Return the result with `is_error`\n\nset, and let the model decide what to do:\n\n```\n{\n  \"type\": \"tool_result\",\n  \"tool_use_id\": \"toolu_01A9k3…\",\n  \"content\": \"Deploy id not found. Ids look like dpl_XXXX.\",\n  \"is_error\": true\n}\n```\n\nSend the whole array back — same tools, same model — and the model answers from the tool output, with `stop_reason`\n\nback to `end_turn`\n\n:\n\n```\n{\n  \"role\": \"assistant\",\n  \"content\": [\n    {\"type\": \"text\",\n     \"text\": \"No — dpl_8f21 failed at the migrate stage with exit code 1.\"}\n  ],\n  \"stop_reason\": \"end_turn\"\n}\n```\n\nThe loop is therefore: call, check `stop_reason`\n\n, execute any `tool_use`\n\nblocks, append assistant turn and tool results, call again. Repeat until `stop_reason`\n\nis not `\"tool_use\"`\n\n. That is the entire agent loop, and every framework that offers one is wrapping these four steps.\n\nStreaming changes how a tool call arrives, and it is the difference that breaks integrations ported from the buffered path. In a buffered response, `input`\n\nis handed to you as a parsed object. In a stream, it arrives as string fragments across many events:\n\n```\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\n       \"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01A9k3…\",\n                        \"name\":\"get_deploy_status\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\n       \"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"deploy_\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\n       \"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"id\\\": \\\"dpl_8f21\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n```\n\nThree consequences. First, `content_block_start`\n\ngives you the `id`\n\nand the `name`\n\nimmediately with an *empty* `input`\n\n— which is enough to show the user “checking deploy status” before the arguments exist, and is the reason streamed agents can narrate what they are about to do.\n\nSecond, the fragments are not individually valid JSON. `{\"deploy_`\n\nparses as nothing. You must accumulate the `partial_json`\n\nstrings for that block index and parse once, at `content_block_stop`\n\n. Attempting a parse per delta produces an exception on almost every event, and the tempting fix — swallowing those exceptions — hides the real parse failure when there is one.\n\nThird, you cannot dispatch early. The arguments are only complete at `content_block_stop`\n\n, so the earliest safe execution point is there, not on `message_stop`\n\nand certainly not mid-stream. If the stream ends with `stop_reason: \"max_tokens\"`\n\nbefore that stop event arrives, the accumulated string is a truncated object and the call cannot be run at all — the correct handling is to treat it as a failed turn rather than to guess at the missing fields.\n\n``` php\nconst partials = new Map();   // index -> accumulated json string\n\nfor await (const ev of stream) {\n  if (ev.type === \"content_block_start\" && ev.content_block.type === \"tool_use\") {\n    partials.set(ev.index, { meta: ev.content_block, json: \"\" });\n  }\n  if (ev.type === \"content_block_delta\" && ev.delta.type === \"input_json_delta\") {\n    partials.get(ev.index).json += ev.delta.partial_json;\n  }\n  if (ev.type === \"content_block_stop\" && partials.has(ev.index)) {\n    const { meta, json } = partials.get(ev.index);\n    // Empty string is legitimate: a tool with no required inputs.\n    const input = json.length ? JSON.parse(json) : {};\n    queue.push({ id: meta.id, name: meta.name, input });\n  }\n}\n```\n\nNote the empty-string case. A tool whose schema has no required properties can produce a `tool_use`\n\nblock with no `input_json_delta`\n\nevents at all, and `JSON.parse(\"\")`\n\nthrows. The computer-use `screenshot`\n\naction is close to this shape, which is why the bug tends to appear first in agents that take screenshots.\n\n`tool_result`\n\nwithout first appending the assistant message that contained the `tool_use`\n\nis a 400: the result references an id that is not in the conversation. Append the response `content`\n\narray verbatim rather than extracting the text.`tool_use`\n\nblocks. Every one of them needs a matching `tool_result`\n\n, and all of those results belong in a A fourth one appears once you turn reasoning on, and it is worth naming because it produces a 400 mentioning a block type you never deliberately created. On a thinking-enabled model the assistant turn that contains the `tool_use`\n\nalso contains a `thinking`\n\nblock, and that block has to be echoed back in the history exactly as it arrived — including its `signature`\n\nfield. Stripping it because it looked like debug output, or reserialising it through a formatter that reorders keys, breaks the signature and the request is rejected. The rule that avoids this is the rule that avoids the first failure above: append the response `content`\n\narray verbatim rather than rebuilding a message from the parts you recognise.\n\nOne design note that pays for itself. The model reads `tool_result.content`\n\nas text, so its shape is a prompt-design decision rather than a serialisation detail. Returning a raw stack trace gets you a model that reasons about a stack trace; returning `Deploy dpl_8f21 failed at stage \"migrate\" (exit 1). Logs: …`\n\ngets you one that answers the question. Errors especially benefit from being written for a reader — an `is_error`\n\nresult saying what valid input looks like is frequently enough for the model to fix its own call on the next turn, which turns a hard failure into one extra round trip.", "url": "https://wpnews.pro/news/claude-s-tool-use-and-tool-result-content-blocks-end-to-end", "canonical_source": "https://dev.to/multigrid/claudes-tooluse-and-toolresult-content-blocks-end-to-end-3nli", "published_at": "2026-08-12 22:20:58+00:00", "updated_at": "2026-08-12 22:46:20.082588+00:00", "lang": "en", "topics": ["large-language-models", "ai-agents", "developer-tools"], "entities": ["Anthropic", "Claude", "Messages API"], "alternates": {"html": "https://wpnews.pro/news/claude-s-tool-use-and-tool-result-content-blocks-end-to-end", "markdown": "https://wpnews.pro/news/claude-s-tool-use-and-tool-result-content-blocks-end-to-end.md", "text": "https://wpnews.pro/news/claude-s-tool-use-and-tool-result-content-blocks-end-to-end.txt", "jsonld": "https://wpnews.pro/news/claude-s-tool-use-and-tool-result-content-blocks-end-to-end.jsonld"}}