Agentic Systems

Tool Use & Function Calling

How language models invoke external tools: the request -> tool_use -> execute -> tool_result cycle, JSON Schema tool definitions, parallel tool calls, structured outputs, and error handling — with spec-accurate examples from the Anthropic Messages API and OpenAI function calling, and how MCP generalizes per-app tool wiring into one standard.

A language model on its own produces only text. Tool-use (equivalently function-calling) is the mechanism that turns that text generator into something that can act: you describe a set of operations the model is allowed to invoke, and on each turn the model may emit a structured request to run one of them. Your code executes the operation and feeds the result back, and the model continues. This is the engine inside the agent loop — every "action" an agent takes in the world is, mechanically, a tool call.

The crucial property is that the model never executes anything itself. It emits a structured request; your application (or, for some providers, the provider's infrastructure) runs the code and returns the output. Anthropic's docs frame this as a contract: "You specify what operations are available and what shape their inputs and outputs take; Claude decides when and how to call them." Engineers with classical API experience can treat it like any other typed interface — define the schema, handle the callback, return a result — except the caller on the other side is a model choosing which function to invoke based on the conversation.

The tool-call cycle

Every provider implements the same four-beat loop, differing only in field names and message shapes:

  1. Request. You send the conversation plus a list of tool definitions.
  2. tool_use. Instead of (or alongside) a final answer, the model returns a structured call: a tool name and a JSON arguments object. The response carries a signal that this happened — Anthropic sets stop_reason: "tool_use"; OpenAI populates a tool_calls array.
  3. Execute. Your code reads the tool name and arguments, runs the corresponding function, and captures the output.
  4. tool_result. You append the model's call and your result to the conversation and send it back. The model uses the result to produce a final answer — or to call another tool, repeating from step 2.

The loop is driven by your application, not the model: each tool call is a full round trip. Anthropic's canonical shape is a while loop keyed on stop_reason — "while stop_reason == "tool_use", execute the tools and continue the conversation" — exiting on any terminal stop reason ("end_turn", "max_tokens", "stop_sequence", or "refusal"). One stop reason is not an exit: "pause_turn" signals that a server-side tool loop (e.g. web search) ran long and should be resumed — re-send the conversation unchanged to continue, rather than break out of the loop.

sequenceDiagram
    participant App as Your App
    participant Model as LLM
    participant Tool as Tool / API
    App->>Model: messages + tool definitions
    Model-->>App: stop_reason "tool_use"
tool_use block (name, input) App->>Tool: execute(name, input) Tool-->>App: raw output App->>Model: same messages + assistant turn
+ tool_result (tool_use_id, content) Model-->>App: final text (stop_reason "end_turn")

Tool definitions: a name, a description, and a JSON Schema

A tool-schema is how you tell the model what a tool does and what arguments it accepts. Across providers it has three load-bearing parts: a name, a natural-language description, and a parameter schema written in JSON Schema. The description is not decoration — Anthropic calls detailed descriptions "by far the most important factor in tool performance," recommending at least 3-4 sentences covering what the tool does, when to use it (and when not to), and what each parameter means.

In the Anthropic Messages API, a client tool is defined by name, description, and input_schema (the name must match ^[a-zA-Z0-9_-]{1,64}$):

{
  "name": "get_weather",
  "description": "Get the current weather in a given location",
  "input_schema": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "The city and state, e.g. San Francisco, CA"
      },
      "unit": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "The unit of temperature, either 'celsius' or 'fahrenheit'"
      }
    },
    "required": ["location"]
  }
}

OpenAI function calling carries the same information under different keys. A function tool nests the definition under a function object with name, description, and parameters (the JSON Schema), tagged with "type": "function":

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather in a given location",
    "parameters": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "description": "The city and state, e.g. San Francisco, CA"
        }
      },
      "required": ["location"],
      "additionalProperties": false
    },
    "strict": true
  }
}

Because parameters are plain JSON Schema, you get its full expressive range — types, enum constraints, nested objects, required lists, descriptions per field. The model is steered by both the schema and the prose descriptions: the schema constrains shape, the descriptions teach intent.

A note on field names. input_schema (Anthropic) and parameters (OpenAI) name the same thing — a JSON Schema object for the tool's arguments. Mixing them up is the most common porting bug. The wrapping differs too: Anthropic puts name/description/ input_schema at the top level of each tool; OpenAI's Chat Completions API (shown above) wraps them in {"type": "function", "function": {...}}. OpenAI's newer Responses API drops that nesting and flattens name/description/parameters to the top level alongside "type": "function" — the same fields, no inner function object. The examples on this page use the Chat Completions shape throughout.

Controlling whether the model calls a tool

By default the model decides per turn. Both providers expose a knob to force or forbid tool use. Anthropic's tool_choice has four options: auto (model decides — the default when tools are present), any (must use some tool), tool (must use a named tool, e.g. {"type": "tool", "name": "get_weather"}), and none (no tools — the default when no tools are supplied). OpenAI exposes an analogous tool_choice. Note that with Anthropic's any or tool, the API prefills the assistant turn to force a call, so the model emits no natural-language preamble before the tool call.

The tool_use block and the tool_result

When Claude calls a tool, the response has stop_reason: "tool_use" and its content array contains a tool-use-block with type: "tool_use", a unique id, the tool name, and an input object conforming to the tool's input_schema. A turn can mix a text block (Claude explaining itself) with the call:

{
  "id": "msg_01Aq9w938a90dw8q",
  "model": "claude-opus-4-8",
  "stop_reason": "tool_use",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "I'll check the current weather in San Francisco for you."
    },
    {
      "type": "tool_use",
      "id": "toolu_01A09q90qw90lq917835lq9",
      "name": "get_weather",
      "input": { "location": "San Francisco, CA", "unit": "celsius" }
    }
  ]
}

You run the tool, then continue the conversation with a user message whose content holds a tool-result block. It carries type: "tool_result", a tool_use_id matching the id of the call it answers, and a content field (a string, or a list of text / image / document blocks):

{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "15 degrees"
    }
  ]
}

Two formatting rules from the spec are easy to trip over: the tool_result block must immediately follow its tool_use (no messages in between), and within the user message the tool_result blocks must come first in the content array — any text comes after. Violating either returns a 400 error.

Anthropic folds tool messages into the ordinary user/assistant roles. OpenAI uses a dedicated tool role instead. The model returns a tool_calls array where each entry has an id, "type": "function", and a function object whose arguments is a JSON-encoded string (not a parsed object) — so you must JSON.parse it before use:

{
  "id": "call_12345xyz",
  "type": "function",
  "function": {
    "name": "get_weather",
    "arguments": "{\"location\":\"Paris, France\"}"
  }
}

You return the output as a message with "role": "tool", a tool_call_id referencing that id, and the result in content:

{
  "role": "tool",
  "tool_call_id": "call_12345xyz",
  "content": "15 degrees"
}

Parallel tool calls

When a request needs several independent operations — weather in three cities, three files read — the model can ask for them all at once. Parallel-tool-calls means a single assistant turn carries multiple tool_use blocks. The API does not prescribe an execution order: you may run the calls concurrently (asyncio.gather, Promise.all) for lower latency, or sequentially if they share state or have ordering requirements.

The hard rule is the response shape: return one tool_result for each tool_use block, all together in the next single user message. Splitting them across separate user messages is the most common mistake — Anthropic notes it actually "teaches" the model to stop batching, reducing future parallelism. If you choose not to run a call (say a prior write failed), still return a tool_result for it with is_error: true and a short explanation, so every tool_use_id is answered.

{
  "role": "assistant",
  "content": [
    { "type": "tool_use", "id": "toolu_01", "name": "get_weather", "input": { "location": "San Francisco, CA" } },
    { "type": "tool_use", "id": "toolu_02", "name": "get_weather", "input": { "location": "New York, NY" } }
  ]
}
{
  "role": "user",
  "content": [
    { "type": "tool_result", "tool_use_id": "toolu_01", "content": "San Francisco: 68F, partly cloudy" },
    { "type": "tool_result", "tool_use_id": "toolu_02", "content": "New York: 45F, clear skies" }
  ]
}

Parallelism can be disabled: Anthropic exposes disable_parallel_tool_use (which caps the model at one tool per turn), and OpenAI exposes parallel_tool_calls, which when set to false "ensures exactly zero or one tool is called."

Structured outputs

Sometimes you don't want the model to act — you want a guaranteed-shape JSON object back (extract fields from a document, return a typed classification). Structured-output is this guarantee: the model's output is constrained to conform to a JSON Schema you supply. There are two related surfaces for it.

The first reuses the tool machinery. Anthropic's tool definitions accept strict: true, which "ensure[s] Claude's tool calls always match your schema exactly." OpenAI's function definitions accept the same strict: true flag; setting it, OpenAI says, "will ensure function calls reliably adhere to the function schema, instead of being best effort." For strict mode OpenAI requires the schema to set additionalProperties: false, mark every property as required, and have an object at the root.

The second is a response-format surface for when you want the model's final answer (not a tool call) to be JSON. OpenAI distinguishes the two: function calling is for "connecting the model to tools," while response format is for when you "want to structure the model's output when it responds to the user." OpenAI's response_format takes a json_schema object:

{
  "type": "json_schema",
  "json_schema": {
    "name": "weather_result",
    "schema": { "type": "object" },
    "strict": true
  }
}

The practical heuristic from Anthropic's docs: if you find yourself "writing a regex to extract a decision from model output, that decision should have been a tool call." Parsing free-form text to recover structured intent is a sign the structure belongs in the schema.

Error handling

Tools fail — networks drop, APIs rate-limit, arguments come out malformed — and the loop has a designed-in channel for it. In the Anthropic API you report a failed execution by returning the error text in the tool_result content with is_error: true:

{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
      "content": "ConnectionError: the weather service API is not available (HTTP 500)",
      "is_error": true
    }
  ]
}

The model reads the error and adapts — apologizing to the user, or retrying with corrected inputs. The docs advise writing instructive errors: instead of "failed", say "Rate limit exceeded. Retry after 60 seconds.", giving the model the context to recover. For a malformed call (e.g. a missing required parameter), returning an is_error result lets the model retry — Anthropic notes it "will retry 2-3 times with corrections before apologizing." A complementary defense is strict: true, which prevents schema-violating calls from being emitted in the first place.

Treat tool results as untrusted. Tool outputs often carry content from outside your control — web pages, inbound email, user uploads, third-party APIs. An attacker who can influence that content can embed instructions that try to redirect the model (indirect prompt injection). Anthropic recommends keeping untrusted content inside tool_result blocks rather than system prompts or plain user text. See ecosystem-ops.html for hardening.

How MCP generalizes tool wiring into a standard

Everything above describes tools wired per application: you hand-write definitions, you hold the execution code, you manage the loop. That works, but it doesn't compose — every agent must re-implement every integration, and a tool built for one app can't be reused by another. The Model Context Protocol (MCP) standardizes this layer so tools become portable across any compliant agent.

MCP runs over JSON-RPC 2.0. A server exposes a tools/list method that returns tool definitions, each with name, description, and an inputSchema (the same JSON-Schema-shaped field you write by hand today — note MCP's spelling is inputSchema, camelCase). A client calls a tool with tools/call, passing name and arguments:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": { "location": "New York" }
  }
}

The result mirrors the tool_result idea: a content array of typed blocks plus an isError boolean (note MCP uses camelCase isError, vs. Anthropic's is_error). The spec distinguishes the same two error classes the per-app world has — protocol errors (unknown tool, malformed request) returned as JSON-RPC errors, and tool execution errors (API failures, validation errors) returned in the result with isError: true so the model can self-correct:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"
      }
    ],
    "isError": false
  }
}

The payoff is decoupling: any MCP-speaking agent can discover and call any MCP server's tools without bespoke wiring, and the agent's own model-facing tool layer (Anthropic tool_use/tool_result, OpenAI tool_calls) is bridged to MCP's tools/call by the client runtime. MCP also goes beyond plain function calling — tools can declare an optional outputSchema and return structuredContent, pushing structured outputs across the wire. The protocol page, protocols-mcp.html, covers transports, discovery, and the rest of the surface.

Takeaways

  • Tool use is a contract: the model emits a structured request; your code executes and returns a result. The model never runs code itself.
  • A tool is a name + description + JSON Schema. The description drives selection quality as much as the schema drives input shape.
  • Field names differ by provider — Anthropic input_schema / tool_use / tool_result / is_error vs. OpenAI parameters / tool_calls (arguments as a JSON string) / role: "tool" — but the cycle is identical.
  • Parallel calls arrive as multiple tool_use blocks in one turn; return all results together in one message.
  • Structured outputs (strict: true, JSON-Schema response formats) reuse the same schema machinery to guarantee output shape.
  • MCP lifts per-app tool wiring into a standard (tools/list, tools/call, inputSchema, isError) so tools become portable across agents.