Structured output
Get a typed Pydantic model back from an agent instead of free-form text, and validate it with output validators that can ask the model to retry.
By default an agent returns text. result.output is a string and print(result) shows what the model said. That is fine for chat, but most real workflows want a typed object: a parsed report, an extraction, a decision with named fields.
Pass output=SomePydanticModel to Agent and the agent returns an instance of that model. The fields are validated by Pydantic, so once you read result.output you are holding a fully-typed object — not a blob of JSON you have to parse yourself.
Asking for a typed result
Define a Pydantic BaseModel, hand the class to output=, and read the fields off result.output.
from pydantic import BaseModel
from agentroute import Agent
class WeatherReport(BaseModel):
city: str
temp_c: float
conditions: str
agent = Agent(
name="weather",
model="claude-sonnet-4",
output=WeatherReport,
)
result = agent.run("What's the weather in Paris right now?")
print(type(result.output)) # <class '__main__.WeatherReport'>
print(result.output.city) # "Paris"
print(result.output.temp_c) # 18.0result.output is a WeatherReport, so result.output.temp_c is a float and your editor autocompletes the fields. The rest of the Result is unchanged — result.usage still reports tokens and cost, result.messages still holds the full transcript.
While the agentic loop runs, the model may call tools several times before it produces a final answer. result.output is only populated once the loop returns. If the agent is interrupted (for example, an approval is denied), result.interrupted is True and result.output may be None.
How output_mode works
output_mode controls how the agent asks the model for the final answer. It has three values:
| Parameter | Type | Default | Description |
|---|---|---|---|
output_mode | str | "auto" |
|
In tool mode, the agent exposes your output model to the model as a tool whose arguments are the model's fields. The model "calls" that tool to deliver the final answer, and the arguments are validated into your BaseModel. This is what you get with output_mode="auto" whenever output= is set.
In text mode, the agent just returns the model's text and result.output is a str. This is the default when no output is set.
Because the default is "auto", you almost never set output_mode yourself — passing output=WeatherReport is enough to switch the agent into structured tool mode. Set it explicitly only when you want to override that behavior.
# These two agents behave the same way.
Agent(name="a", model="claude-sonnet-4", output=WeatherReport)
Agent(name="b", model="claude-sonnet-4", output=WeatherReport, output_mode="tool")
# Force plain text even though an output type is declared.
Agent(name="c", model="claude-sonnet-4", output=WeatherReport, output_mode="text")Validating output with retries
Pydantic guarantees the shape of the output, but not that the values make sense. A temp_c of 500.0 parses fine — it is still a float. To catch implausible values, register an output validator.
An output validator is a function (ctx: Context, output: OutputModel) -> OutputModel. Register it with the @agent.output_validator decorator. Return the output to accept it, or raise Retry with a hint to send the model back for another attempt.
from pydantic import BaseModel
from agentroute import Agent, Context, Retry
class WeatherReport(BaseModel):
city: str
temp_c: float
conditions: str
agent = Agent(
name="weather",
model="claude-sonnet-4",
output=WeatherReport,
retries=2,
)
@agent.output_validator
def sanity(ctx: Context, output: WeatherReport) -> WeatherReport:
"""Reject physically implausible temperatures."""
if not -90.0 <= output.temp_c <= 60.0:
raise Retry(
f"temp_c={output.temp_c} is implausible for {output.city}. "
"Provide a realistic surface temperature in Celsius."
)
return output
result = agent.run("What's the weather in Paris right now?")
print(result.output.city) # "Paris"
print(result.output.temp_c) # 18.0 — guaranteed within rangeWhen sanity raises Retry, the loop catches the signal, formats the message as a tool-role message, and continues so the model can correct itself. The hint text is what the model sees, so make it specific and actionable.
Retry is not an ErrorAgent — it is deliberately not a user-visible failure. Raising it asks the model to try again; it does not surface to your caller unless retries run out. You can raise Retry from a tool body too, with the same effect.
How Retry and the retries parameter interact
The retries parameter on the agent bounds how many times a Retry can be honored before the loop gives up. Each Retry consumes one of the agent's retry budget; once retries is exhausted, the loop stops retrying.
| Parameter | Type | Default | Description |
|---|---|---|---|
retries | int | 1 | How many times a |
Validators run in registration order. You can register more than one with repeated @agent.output_validator decorators; the output must pass every validator. The first one to raise Retry short-circuits the round and sends the hint back to the model.
Retry is for "the answer is wrong, try again." It is bounded by retries. That is separate from the loop's own ceilings: ErrorMaxTurns (too many tool-calling turns, max_turns) and ErrorBudget (cost over max_cost) are real failures that propagate to your caller. See Errors and retries for the full picture.
Validators can read context
Because the validator receives the Context, it can check the output against your dependencies or the current retry count. Context is never sent to the model, so this is purely server-side logic.
@agent.output_validator
def must_match_requested_city(ctx: Context, output: WeatherReport) -> WeatherReport:
requested = ctx.deps["city"]
if output.city.lower() != requested.lower():
raise Retry(f"Report the weather for {requested}, not {output.city}.")
return output
result = agent.run("Weather please", deps={"city": "Paris"})Next steps
The full signature for output, output_mode, retries, and output_validator.
How Retry, max_turns, and max_cost differ and when each surfaces.
What lives on Context and how it reaches your validators and tools.
Retry, ErrorMaxTurns, ErrorBudget, and the ErrorAgent base class.