# Structured Output

Structured output constrains the model’s final message so you can parse it into a **typed Python value**—a number, a `dataclass`, a Pydantic model, or the result of your own validator—instead of treating the reply as an opaque string.

## What you get on each turn

Every turn returns an [`AgentReply`](https://docs.ag2.ai/latest/docs/beta/agents/). Two surfaces matter for structured output:

| Surface | What it is |
| --- | --- |
| `reply.body` | Raw text from the model for that turn (a `str` or `None`). |
| `await reply.content()` | Parsed value according to the **response schema** in effect for that turn. |

If the model’s output cannot be parsed or fails validation, `content()` raises an error from the underlying parser (for example Pydantic’s validation errors). You can pass `retries` to automatically [re-ask the model](https://docs.ag2.ai/latest/docs/beta/structured_output/#validation-retries) on failure.

With the default **OpenAI** client, when the schema exposes a JSON Schema to the API, the client sends a structured `response_format` so the model is guided to emit JSON matching that schema. [`PromptedSchema`](https://docs.ag2.ai/latest/docs/beta/structured_output/#promptedschema-models-without-native-structured-output) is the escape hatch when the provider does not support that mechanism: the schema is injected into the system prompt instead, and `content()` still runs the same way afterward.

## When to use which tool

- **Pass a plain type** (`int`, `YourModel`, …) when the default schema name and description are enough.
- Use **`ResponseSchema`** when you want a clear **`name`** and **`description`** in the API payload so the model knows the role of the structured payload.
- Use **`@response_schema`** when you need **custom parsing**, normalization, or extra steps after JSON is read.
- Use **`PromptedSchema`** when your **model or endpoint does not support** native structured output.

## Quick start

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>``` | ```<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>agent = Agent(<br>    "assistant",<br>    prompt="You are a helpful assistant. Answer concisely.",<br>    config=OpenAIConfig("gpt-4o-mini"),<br>    response_schema=int,<br>)<br>reply = await agent.ask("How many bits are in a byte?")<br>print(reply.body)       # e.g. '8' — raw model text<br>result = await reply.content()<br>print(result)              # 8 — Python int<br>``` |

## Real-world examples

The following patterns mirror how structured output is used in applications: triage, extraction, and safe normalization.

### Classify a support ticket (Pydantic)

Route incoming text into fields your helpdesk or CRM already understands:

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>24<br>25<br>26<br>27<br>``` | ```<br>from typing import Annotated<br>from pydantic import BaseModel, Field<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>class TicketTriage(BaseModel):<br>    """Structured triage for a single support message."""<br>    category: Annotated[str, Field(description="e.g. billing, bug, account_access")]<br>    urgency: Annotated[str, Field(description="low, medium, or high")]<br>    summary_one_line: Annotated[str, Field(description="Max 120 characters", max_length=120)]<br>agent = Agent(<br>    "triage",<br>    prompt="You triage customer support messages. Be conservative with urgency.",<br>    config=OpenAIConfig("gpt-4o-mini"),<br>    response_schema=TicketTriage,<br>)<br>body = (<br>    "I was charged twice for Pro last week and I still can't export my reports. "<br>    "This is blocking our quarter close."<br>)<br>reply = await agent.ask(f"Classify this ticket:\n\n{body}")<br>triage = await reply.content()<br># triage.category, triage.urgency, triage.summary_one_line → use in routing rules<br>``` |

### Extract a delivery ETA window (dataclass)

Turn natural language into something your scheduling layer can consume:

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>``` | ```<br>from dataclasses import dataclass<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>@dataclass<br>class DeliveryWindow:<br>    day_label: str<br>    start_hour_local: int<br>    end_hour_local: int<br>    timezone: str<br>agent = Agent(<br>    "scheduler",<br>    prompt="Extract delivery windows as structured data only; use 24h integers for hours.",<br>    config=OpenAIConfig("gpt-4o-mini"),<br>    response_schema=DeliveryWindow,<br>)<br>reply = await agent.ask(<br>    "Customer said: drop off Tuesday between 2 and 5pm Pacific, before dinner."<br>)<br>window = await reply.content()<br>``` |

### Score a review on a fixed scale (primitive + clear prompt)

Use a primitive schema when the payload is a single JSON value and your prompt defines the scale:

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>``` | ```<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>agent = Agent(<br>    "reviews",<br>    prompt="You output a single integer 1–5 for overall satisfaction. No prose.",<br>    config=OpenAIConfig("gpt-4o-mini"),<br>    response_schema=int,<br>)<br>reply = await agent.ask(<br>    "Rate this review: 'Shipped fast, packaging was torn, product works great.'"<br>)<br>stars = await reply.content()<br>``` |

## Supported schema types

You can pass any type the stack can turn into a JSON Schema and parse back: primitives, `dataclass`, Pydantic models, unions, and more. Plain types are wrapped in an internal `ResponseSchema` instance for validation and API schema generation.

### Primitives

|     |     |
| --- | --- |
| ```<br>1<br>2<br>3<br>4<br>5<br>``` | ```<br>agent = Agent("assistant", config=config, response_schema=int)<br>reply = await agent.ask("What is 2 + 2?")<br>result = await reply.content()<br># 4 — int<br>``` |

### Dataclasses

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>``` | ```<br>from dataclasses import dataclass<br>@dataclass<br>class City:<br>    name: str<br>    population: int<br>agent = Agent("assistant", config=config, response_schema=City)<br>reply = await agent.ask("Give the city name and approximate population for Kyoto.")<br>result = await reply.content()<br>``` |

### Pydantic models

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>``` | ```<br>from pydantic import BaseModel<br>class Sentiment(BaseModel):<br>    label: str<br>    score: float<br>agent = Agent("assistant", config=config, response_schema=Sentiment)<br>reply = await agent.ask("Analyze: 'I love this product!'")<br>result = await reply.content()<br>``` |

### Unions

Use a union (`int | str`) or a tuple of types (`(int, str)`) when the model must return **one of several JSON shapes**.

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>``` | ```<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>config = OpenAIConfig("gpt-4o-mini")<br># int | str — e.g. a count, or "unknown" when the text does not say<br>agent = Agent(<br>    "extractor",<br>    prompt='Reply with JSON only: either an integer count or the string "unknown".',<br>    config=config,<br>    response_schema=int | str,<br>)<br>reply = await agent.ask("How many seats does the venue mention? (no number in text)")<br>result = await reply.content()<br># result is int or str, depending on the model output<br>``` |

### `ResponseSchema` (named payloads)

For clearer API metadata, construct a `ResponseSchema` with an explicit **`name`** and **`description`**:

|     |     |
| --- | --- |
| ```<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>``` | ```<br>from autogen.beta import Agent, ResponseSchema<br>schema = ResponseSchema(<br>    int | str,<br>    name="ByteWidth",<br>    description="The number of bits in one byte.",<br>)<br>agent = Agent("assistant", config=config, response_schema=schema)<br>``` |

Those fields are attached to the structured-output payload where the provider supports it, which helps the model treat the JSON as a named contract rather than a generic blob.

## Custom validation with `@response_schema`

Use the decorator when you need **logic beyond** “parse this JSON into a type”: clamping, regex cleanup, decoding wrapped JSON, or combining fields.

### Sync validator: clamp a numeric rating

|     |     |
| --- | --- |
| ```<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>``` | ```<br>from autogen.beta import Agent, response_schema<br>@response_schema<br>def parse_rating(content: str) -> int:<br>    """Parse a rating and clamp it to 1–5."""<br>    return max(1, min(5, int(content)))<br>agent = Agent("assistant", config=config, response_schema=parse_rating)<br>reply = await agent.ask("Rate this movie from 1 to 5.")<br>result = await reply.content()<br>``` |

### Async validator: enrich after JSON parse

|     |     |
| --- | --- |
| ```<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br>``` | ```<br>import json<br>@response_schema<br>async def fetch_and_validate(content: str) -> dict:<br>    """Validate and enrich the model's JSON response."""<br>    data = json.loads(content)<br>    data["validated"] = True<br>    return data<br>``` |

### Validation rules for `@response_schema`

The framework introspects your function with **fast_depents** (the same dependency-injection path as `@tool` callables). Parameters satisfied by injection - [`Variables`](https://docs.ag2.ai/latest/docs/beta/context/variables/), [`Depends`](https://docs.ag2.ai/latest/docs/beta/depends/), [`Inject`](https://docs.ag2.ai/latest/docs/beta/context/inject/), Context and similar—are **not** part of the JSON the model must produce. Every other parameter controls how the completion text is decoded and whether a JSON Schema is attached for native structured output.

### Accessing `Context`

Validators participate in the same dependency injection model as tools. Inject [`Context`](https://docs.ag2.ai/latest/docs/beta/context/variables/) to read `variables`, tie validation to session state, or perform lookups:

|     |     |
| --- | --- |
| ```<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br>``` | ```<br>from autogen.beta import Context, response_schema<br>@response_schema<br>def validate_with_context(content: str, context: Context) -> str:<br>    """Use context variables during validation."""<br>    language = context.variables.get("language", "en")<br>    return f"[{language}] {content}"<br>``` |

## `PromptedSchema` (models without native structured output)

Some models or providers do not support API-level structured output (no `response_format` JSON schema). `PromptedSchema` **injects the JSON Schema into the system prompt** and sets `json_schema` to `None` on the wire so the client does not request native structured mode. Validation still goes through the inner schema’s `validate` method.

### Custom prompt template

The default template asks for raw JSON only. Override it with a string that contains the `{schema}` placeholder:

|     |     |
| --- | --- |
| ```<br>1<br>2<br>3<br>4<br>``` | ```<br>PromptedSchema(<br>    int,<br>    prompt_template="Reply with JSON matching this schema:\n{schema}",<br>)<br>``` |

### Override schema per request

Pass `response_schema` to `ask()` (or `AgentReply.ask()`) to change the contract for **one turn** only. The agent’s default schema applies again on the next turn unless you override again:

|     |     |
| --- | --- |
| ```<br>1<br>2<br>3<br>4<br>``` | ```<br>agent = Agent("assistant", config=config)<br>turn = await agent.ask("How many seconds in a minute?", response_schema=int)<br>result = await turn.content()<br>#> 60 - int<br>turn2 = await turn.ask("Say hello.")<br>result2 = await turn2.content()<br>#> "Hello!" - str<br>``` |

## Validation retries

When the model's response fails schema validation, you can automatically **re-ask** the model instead of raising immediately. Pass the `retries` keyword to `content()`:

| Value | Behavior |
| --- | --- |
| `retries=0` (default) | No retries — raise on the first validation failure. |
| `retries=3` | Up to 3 re-asks after the initial attempt (4 total). |
| `retries=math.inf` | Re-ask indefinitely until the model produces a valid response. |

The `retries` parameter controls how many **re-asks** are allowed after the initial attempt. With `retries=3`, the initial response is validated; if it fails, the model is re-asked up to **3 more times** before the error is raised.
