Skip to content

Structured output

Structured output makes an agent return a validated object instead of text. You define a schema, attach it to an agent or to a single call, and read the object from the result. For a worked example, see Return structured output.

Strands doesn’t ask the model to write JSON in its reply. It turns your schema into a tool, and the model returns the object by calling that tool with the field values as input. The tool gives the model a typed input schema to fill in, and gives Strands a way to send validation errors back through the agent loop.

AGENT LOOP AS A TOOL TOOL CALL ERRORS VALID Your schema Model calls the tool Structured output tool validates the arguments Validated object on the agent result

For each call with a schema:

  1. Strands adds a structured output tool, built from your schema, to the agent’s tools.
  2. The model works as usual and can call your other tools along the way.
  3. The model calls the structured output tool with the field values.
  4. Strands validates that input. If it passes, the invocation ends and the object is on result.structured_outputresult.structuredOutput. When you stream the call, it’s on the final result event.
  5. Strands removes the structured output tool when the invocation ends.

A schema lists the fields the object must have. Strands builds the tool from it, so the model reads every field description.

A schema is a Pydantic model. The tool is named after the class, so a TicketTriage schema produces a TicketTriage tool. The class docstring becomes the tool description, and each Field(description=...) describes that field to the model.

Strands adds an instruction to the tool description telling the model to call the tool last, after any other tools it needs.

A schema set when you create the agent applies to every call. A schema passed to a call applies to that call only and takes precedence over the agent’s schema.

from pydantic import BaseModel
from strands import Agent
class CustomerInfo(BaseModel):
name: str
email: str
class RefundRequest(BaseModel):
order_id: str
amount: float
agent = Agent(structured_output_model=CustomerInfo, callback_handler=None)
customer = agent("This is Dana Ruiz, dana@example.com.").structured_output
refund = agent(
"Please refund order 4512. It was $49.99.",
structured_output_model=RefundRequest,
).structured_output
print(customer)
print(refund)

Typical output:

name='Dana Ruiz' email='dana@example.com'
order_id='4512' amount=49.99

Agent.structured_output() and Agent.structured_output_async() are deprecated and emit a DeprecationWarning. Pass the schema to the call instead, as shown above.

An agent schema applies to every call, including turns that only build up context. If some calls should answer in plain text, pass the schema per call instead.

A schema can hold rules the model won’t always meet on the first try, like a check that a date is in the future. When the input fails validation, Strands doesn’t raise. It returns the validation errors to the model as the tool result, and the model can call the tool again with corrected values.

from datetime import date
from pydantic import BaseModel, field_validator
from strands import Agent
class FollowUp(BaseModel):
"""A follow-up task for a support ticket."""
action: str
due_date: date
@field_validator("due_date")
@classmethod
def must_be_in_future(cls, value: date) -> date:
if value <= date.today():
raise ValueError(f"due_date must be after {date.today().isoformat()}")
return value
agent = Agent(callback_handler=None)
result = agent(
"Schedule a follow-up call with the customer next Tuesday.",
structured_output_model=FollowUp,
)
print(result.structured_output)

Typical output:

action='Schedule a follow-up call with the customer' due_date=datetime.date(2026, 10, 20)

The model doesn’t know today’s date, so its first guess can fail the check:

Example:
1. Model calls the structured output tool with a due date of 2025-07-22
2. Validation fails, and the model receives "due_date must be after 2026-10-12"
3. Model calls the tool again with 2026-10-13
4. Validation passes, and the invocation returns the object

Put rules like this in the schema, not in your prompt. The model sees the exact error message, so write it as an instruction the model can act on. The model can retry as often as it needs; to cap the retries, see Limit attempts.

Sometimes the model ends its turn with a text reply and never calls the structured output tool. Strands then retries once with the structured output tool required, so the model’s only option is to call it.

Before the retry, Strands adds a user message to the conversation: You must format the previous response as structured output. To replace it, pass structured_output_prompt to the agent or to a single call. A value passed to a call takes precedence over the agent’s value.

from pydantic import BaseModel
from strands import Agent
class Contact(BaseModel):
name: str
email: str
agent = Agent(
structured_output_prompt="Return the contact details you found as a Contact.",
callback_handler=None,
)
result = agent("Reach Dana Ruiz at dana@example.com.", structured_output_model=Contact)
print(result.structured_output)

Typical output:

name='Dana Ruiz' email='dana@example.com'

If the retry still doesn’t return valid structured output, the call raises StructuredOutputExceptionStructuredOutputError. Catch it where your application can fall back. Return structured output shows an example. To retry again instead of raising, set an attempt limit.

To cap how many times the model can fail to produce structured output, set

limits['structured_output_attempts']limits.structuredOutputAttempts

on the call. Each model response that fails counts as one attempt: a structured output tool call that fails validation, or a forced retry that doesn’t return valid structured output. With a limit set, Strands retries a failed forced retry instead of raising. Calls to your other tools before the forced retry don’t count.

In TypeScript, a forced retry that a content filter or guardrail stops also counts as a failed attempt and is retried within the limit. In Python, that retry ends the call with its own stop reason, such as content_filteredcontentFiltered.

When the limit is reached, the call stops before the next model call. The result has the stop reason

limit_structured_output_attemptslimitStructuredOutputAttempts

and no structured output.

from pydantic import BaseModel
from strands import Agent
class Contact(BaseModel):
name: str
email: str
agent = Agent(callback_handler=None)
result = agent(
"Find the contact details in this note: call Dana back tomorrow.",
structured_output_model=Contact,
limits={"structured_output_attempts": 3},
)
if result.stop_reason == "limit_structured_output_attempts":
print("No valid contact after 3 attempts")
else:
print(result.structured_output)

The limit covers the whole request, like the other invocation limits. Agents that run as tools in the same request count toward it, and passing result.invocation_contextresult.invocationContext to a later call continues it.