arrow_backAI Engineering / Structured Outputs & Tool Use
Level: Beginner–Intermediate Updated: August 2026

Structured Outputs & Tool Use

The step that turns a "chat" into a "product": getting an LLM to return valid JSON and call tools. Without it, you can't build software on top of a language model.

Why it's critical

Free text is great for humans, but code can't work with it. If the model answers "the lead looks solid, about 85, worth following up" — your software doesn't know what the score is. But if it returns {"score": 85, "priority": "high"} — you can feed that straight into a database, a condition, or the next step in a flow.

Structured Outputs are the ability to force the model to return a fixed, valid data structure. Tool Use (or Function Calling) is the next step — the model doesn't just return data, it chooses which function to call and with which parameters. These two are the foundation of any serious automation, agent or integration.

bolt
Rule of thumb

If the LLM's output feeds the next step in the system (rather than going straight to a user's eyes) — it must be structured and validated.

JSON & Structured Output

The basic way: ask for JSON in the prompt and define a schema. Modern models have a dedicated mode that guarantees valid output. Example with OpenAI (Python):

from openai import OpenAI
client = OpenAI()

resp = client.chat.completions.create(
    model="gpt-5.6",
    response_format={"type": "json_schema", "json_schema": {
        "name": "lead_score",
        "schema": {
            "type": "object",
            "properties": {
                "score": {"type": "integer", "minimum": 0, "maximum": 100},
                "priority": {"type": "string", "enum": ["high", "medium", "low"]},
                "reason": {"type": "string"}
            },
            "required": ["score", "priority", "reason"],
            "additionalProperties": False
        }
    }},
    messages=[
        {"role": "system", "content": "Score the lead quality from the details."},
        {"role": "user", "content": "A SaaS company, 50 employees, defined budget, high urgency."}
    ],
)
import json
data = json.loads(resp.choices[0].message.content)
print(data["score"], data["priority"])

The schema guarantees score is a number between 0 and 100 and priority is one of the allowed values. That's far more reliable than "ask for JSON" in the prompt alone.

Tool Use / Function Calling

Here the model decides on its own which tools to call. You define the available tools, and the model returns which one to call and with which parameters. Example with Anthropic (Claude):

import anthropic
client = anthropic.Anthropic()

tools = [{
    "name": "get_weather",
    "description": "Returns the current weather in a given city",
    "input_schema": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"]
    }
}]

msg = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "What's the weather in Tel Aviv?"}],
)

for block in msg.content:
    if block.type == "tool_use":
        print(block.name, block.input)   # get_weather {'city': 'Tel Aviv'}
        # here you run the real function and return the result to the model

The full flow: (1) the model asks to call a tool; (2) your code runs the real function; (3) you return the result to the model; (4) the model composes a final answer. This is exactly the mechanism behind AI agents and MCP.

Validation & Retry — don't trust, verify

Even with json_schema, it's worth validating on your side before feeding the system. In Python people use Pydantic; in TypeScript — Zod.

from pydantic import BaseModel, Field, ValidationError

class LeadScore(BaseModel):
    score: int = Field(ge=0, le=100)
    priority: str
    reason: str

def parse_with_retry(client, messages, tries=2):
    for attempt in range(tries):
        raw = call_model(client, messages)          # call the model
        try:
            return LeadScore.model_validate_json(raw)  # validate
        except ValidationError as e:
            # return the error to the model and ask it to fix
            messages.append({"role": "user",
                "content": f"The output was invalid: {e}. Return valid JSON only."})
    raise RuntimeError("failed to get valid output")

This pattern — try, validate, and if it fails return the error to the model and ask for a fix — is an industry standard. It turns a fragile system into a stable one.

Tips & common mistakes

rocket_launch

Next step

Now that the output is structured — the next step is to connect knowledge (RAG) and measure quality (Evals).