Skip to content

Return structured output

An agent answers in text, which works when a person reads it. When your code needs to act on the answer, text is hard to parse. Structured output returns an object instead. You describe the fields you need in a schema, and the agent returns an object with those fields, validated against the schema.

A schema lists each field’s name, type, and description. In Python, write it as a Pydantic model, which installs with strands-agents. In TypeScript, write it as a Zod schema and install Zod with npm install zod.

This agent reads a customer support message and returns the category, priority, and summary that your code needs to route the ticket:

from typing import Literal
from pydantic import BaseModel, Field
from strands import Agent
from strands.types.exceptions import StructuredOutputException
class TicketTriage(BaseModel):
"""Routing decision for one customer support message."""
category: Literal["billing", "bug", "account", "other"] = Field(
description="The team that should handle the message"
)
priority: Literal["low", "medium", "high"] = Field(
description="high when the customer is blocked or losing money"
)
summary: str = Field(description="One sentence describing the problem")
def triage(message: str) -> TicketTriage:
# A fresh agent per message keeps tickets from sharing conversation history
agent = Agent(callback_handler=None)
try:
result = agent(message, structured_output_model=TicketTriage)
except StructuredOutputException:
# Send anything the agent can't classify to a person
return TicketTriage(category="other", priority="medium", summary=message)
return result.structured_output
ticket = triage("I was charged twice this month and need one of the charges refunded.")
print(f"{ticket.priority} | {ticket.category} | {ticket.summary}")

invoke_async takes the same argument.

Typical output:

high | billing | Customer was charged twice this month and is requesting a refund.

TicketTriage lists the fields the routing code needs. category and priority accept only fixed values, so the agent can only pick a team and a priority you’ve defined. The model reads each field description, so describe what a field means, not only its type.

Pass the schema as structured_output_modelstructuredOutputSchema and read the object from result.structured_outputresult.structuredOutput. In Python, the object is an instance of your model. In TypeScript, cast it to the z.infer type.

If the agent doesn’t return an object, the call raises StructuredOutputExceptionStructuredOutputError. triage catches it and returns a ticket in the other category for a person to review.

Invalid field values don’t raise. Strands sends the validation errors back to the agent, and the agent corrects them.

The agent can call tools before it returns the object. Here, it looks up the customer’s name and plan by email:

from typing import Literal
from pydantic import BaseModel
from strands import Agent, tool
@tool
def lookup_customer(email: str) -> dict:
"""Look up a customer's name and plan by email address."""
customers = {"dana@example.com": {"name": "Dana Ruiz", "plan": "enterprise"}}
return customers.get(email, {"name": "unknown", "plan": "free"})
class CustomerTicket(BaseModel):
customer_name: str
plan: Literal["free", "pro", "enterprise"]
priority: Literal["low", "medium", "high"]
agent = Agent(tools=[lookup_customer], callback_handler=None)
result = agent(
"From dana@example.com: our dashboard has been down for an hour.",
structured_output_model=CustomerTicket,
)
print(result.structured_output)

Typical output:

customer_name='Dana Ruiz' plan='enterprise' priority='high'

A schema applies only to the call that passes it, so earlier turns return text as usual. The agent fills the fields from the whole conversation:

from pydantic import BaseModel
from strands import Agent
class Escalation(BaseModel):
problem: str
steps_tried: list[str]
agent = Agent(callback_handler=None)
agent("Hi, I can't log in to my account.")
agent("I reset my password twice, and the reset email never arrives.")
result = agent(
"Summarize this conversation for the escalation team.",
structured_output_model=Escalation,
)
escalation = result.structured_output
print(escalation.problem)
for step in escalation.steps_tried:
print(f"- {step}")

Typical output:

The user is unable to log in and is not receiving password reset emails.
- User reset their password twice but never received the reset email.
- Agent suggested checking the spam folder for the reset email.

Pass the schema to a stream the same way. Text arrives in chunks as the model writes it, and the object arrives once, on the final result:

import asyncio
from pydantic import BaseModel
from strands import Agent
class ReplyDraft(BaseModel):
subject: str
body: str
async def main():
agent = Agent(callback_handler=None)
async for event in agent.stream_async(
"Draft a reply apologizing for a delayed refund.",
structured_output_model=ReplyDraft,
):
if "data" in event:
print(event["data"], end="", flush=True)
elif "result" in event:
draft = event["result"].structured_output
print(f"\nSubject: {draft.subject}")
asyncio.run(main())

Typical output:

I'd be happy to help draft a reply apologizing for a delayed refund!
Subject: Apology for Your Delayed Refund