An agent that returns raw text is fine for chat. But in production, your downstream code needs typed data — objects it can validate, store, and route without string parsing. Strands solves this with Zod schemas on the agent constructor, giving you type-safe structured output with automatic validation and retries.
Consider what happens without it: your agent produces a response, and your application code has to parse free-form text into usable data. You write regex, you handle edge cases, you pray the model doesn't change its formatting. One day it wraps JSON in markdown backticks. Another day it adds a preamble. Your parser breaks at 2am.
Structured output eliminates this entirely. You define a Zod schema, and Strands guarantees the agent's output conforms to it — or throws a typed error you can handle. The model is constrained at the API level to produce valid JSON matching your schema.
Pass a Zod schema as structuredOutputSchema on the Agent constructor. The agent's final response will be validated against this schema:
import { Agent } from '@strands-agents/sdk'
import { z } from 'zod'
const sentimentSchema = z.object({
sentiment: z.enum(['positive', 'negative', 'neutral']),
confidence: z.number().min(0).max(1),
reasoning: z.string(),
keywords: z.array(z.string()),
})
const analyzer = new Agent({
name: 'sentiment-analyzer',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: `Analyze the sentiment of the given text.
Identify the overall sentiment, your confidence level, reasoning, and key words.`,
structuredOutputSchema: sentimentSchema,
})
The schema is both a runtime validator and a type definition. TypeScript infers the output type from the schema — no manual type declarations needed.
When you invoke an agent with a structured output schema, the result includes a structuredOutput field with full type inference:
const result = await analyzer.invoke(
'The new feature launch exceeded all expectations. Users love it!'
)
// result.structuredOutput is fully typed:
// { sentiment: 'positive' | 'negative' | 'neutral', confidence: number, ... }
const { sentiment, confidence, reasoning, keywords } = result.structuredOutput
console.log(`Sentiment: ${sentiment} (${(confidence * 100).toFixed(0)}% confident)`)
console.log(`Keywords: ${keywords.join(', ')}`)
console.log(`Reasoning: ${reasoning}`)
No JSON.parse. No type assertions. No "I hope the model returned what I asked for." The schema enforces it, and TypeScript knows the shape at compile time.
When the model produces output that doesn't match your schema, Strands throws a StructuredOutputError. This is distinct from other errors — it means the model responded, but the response didn't validate:
import { StructuredOutputError } from '@strands-agents/sdk'
try {
const result = await analyzer.invoke('Analyze this review...')
// Use result.structuredOutput safely
} catch (error) {
if (error instanceof StructuredOutputError) {
console.error('Model output failed validation:', error.validationErrors)
console.error('Raw output was:', error.rawOutput)
// Decide: retry, fall back to a default, or escalate
} else {
// Other errors: network, rate limit, etc.
throw error
}
}
In practice, StructuredOutputError is rare with well-designed schemas because Strands uses the model's constrained generation mode. But it can happen with complex Zod refinements that the model can't satisfy (e.g., a .refine() that checks business logic the model doesn't know about). Always handle it.
Zod supports .refine() for custom validation logic beyond type checks. Strands integrates with this: when a refinement fails, the framework can automatically retry the model with feedback about what went wrong.
const actionItemSchema = z.object({
title: z.string().min(5, 'Title must be at least 5 characters'),
assignee: z.string(),
dueDate: z.string().refine(
(d) => !isNaN(Date.parse(d)),
'Must be a valid ISO date string'
),
priority: z.enum(['high', 'medium', 'low']),
context: z.string().refine(
(c) => c.split(' ').length >= 10,
'Context must be at least 10 words for downstream processing'
),
})
const extractor = new Agent({
name: 'action-item-extractor',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: 'Extract action items from meeting transcripts.',
structuredOutputSchema: actionItemSchema,
structuredOutputRetries: 2, // Retry up to 2 times on validation failure
})
When the model produces output where context is only 6 words, Strands catches the refinement failure and re-prompts the model with the error message: "Context must be at least 10 words for downstream processing." The model corrects itself and retries — no manual intervention.
Structured output and tools work together naturally. The agent uses tools to gather information, then produces a structured result from what it found:
import { Agent, tool } from '@strands-agents/sdk'
import { z } from 'zod'
const lookupUser = tool({
name: 'lookupUser',
description: 'Look up a user by email address',
schema: z.object({ email: z.string().email() }),
handler: async ({ email }) => {
// Simulate database lookup
return { name: 'Alice Chen', role: 'engineer', team: 'Platform' }
},
})
const lookupTickets = tool({
name: 'lookupTickets',
description: 'Get open tickets assigned to a user',
schema: z.object({ userName: z.string() }),
handler: async ({ userName }) => {
return [
{ id: 'PLAT-123', title: 'Fix auth timeout', priority: 'high' },
{ id: 'PLAT-456', title: 'Update SDK docs', priority: 'low' },
]
},
})
const reportSchema = z.object({
userName: z.string(),
team: z.string(),
openTickets: z.number(),
highPriorityCount: z.number(),
summary: z.string(),
})
const statusReporter = new Agent({
name: 'status-reporter',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: `Look up the user and their tickets, then produce a status summary.`,
tools: [lookupUser, lookupTickets],
structuredOutputSchema: reportSchema,
})
const result = await statusReporter.invoke('Status report for alice@company.com')
// result.structuredOutput: { userName, team, openTickets, highPriorityCount, summary }
The agent loops through tools (lookup user → lookup tickets), then produces the structured output as its final response. The schema constrains only the final output — tool calls remain flexible.
You can stream events from an agent with structured output. The events flow normally during execution (tool calls, intermediate reasoning), and the typed structuredOutput is available on the final result:
const result = await statusReporter.invoke('Status for alice@company.com', {
streaming: true,
onEvent: (event) => {
switch (event.type) {
case 'toolCall':
console.log(`Calling: ${event.toolName}`)
break
case 'textChunk':
// Intermediate reasoning (not the structured output)
process.stdout.write(event.text)
break
}
},
})
// After streaming completes, structured output is available
console.log('Report:', result.structuredOutput)
Structured output is available only after the stream completes — it's the final validated result. You can't incrementally build the typed object from stream chunks. Use streaming for progress feedback (tool calls, reasoning), not for partial structured data.
Let's build a complete example: an agent that takes raw meeting transcript text and extracts structured action items with assignees, due dates, and priorities.
import { Agent } from '@strands-agents/sdk'
import { z } from 'zod'
// Define the output schema
const meetingOutputSchema = z.object({
meetingTitle: z.string(),
date: z.string(),
attendees: z.array(z.string()),
actionItems: z.array(z.object({
id: z.number(),
title: z.string().min(5),
assignee: z.string(),
dueDate: z.string().refine(
(d) => !isNaN(Date.parse(d)),
'Must be a valid ISO date'
),
priority: z.enum(['high', 'medium', 'low']),
context: z.string(),
})),
decisions: z.array(z.object({
decision: z.string(),
rationale: z.string(),
})),
nextMeeting: z.string().optional(),
})
// Create the agent
const meetingProcessor = new Agent({
name: 'meeting-processor',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: `You process meeting transcripts into structured data.
Extract:
- Meeting metadata (title, date, attendees)
- Action items with assignees, due dates, and priorities
- Key decisions with rationale
- Next meeting date if mentioned
Be precise with dates (ISO format). Infer priority from urgency language.
If no due date is mentioned, use 1 week from the meeting date.`,
structuredOutputSchema: meetingOutputSchema,
structuredOutputRetries: 2,
})
// Use it
const transcript = `
Team sync - July 3, 2026
Attendees: Alice, Bob, Carol
Alice: The auth service is timing out under load. We need to fix this before
the launch next Tuesday. Bob, can you take this?
Bob: Yes, I'll have a fix by Monday. I'll also update the load test suite.
Carol: I've decided we're going with Redis for the session cache instead of
DynamoDB. The latency numbers are 3x better for our access pattern.
Alice: Good call. Let's meet again Thursday to review Bob's fix.
`
const result = await meetingProcessor.invoke(transcript)
const { actionItems, decisions, nextMeeting } = result.structuredOutput
// Fully typed — send to your task management system
for (const item of actionItems) {
console.log(`[${item.priority.toUpperCase()}] ${item.title}`)
console.log(` Assigned to: ${item.assignee}, due: ${item.dueDate}`)
}
// Decisions are structured too
for (const d of decisions) {
console.log(`Decision: ${d.decision}`)
console.log(` Why: ${d.rationale}`)
}
The output is guaranteed to match your schema. Feed it directly to a project management API, store it in a database with typed columns, or pass it to another agent in a workflow. No parsing, no prayer.
structuredOutputSchema guarantees typed, validated agent responses — no string parsing.result.structuredOutput — zero manual type declarations.StructuredOutputError gives you typed error handling when validation fails.structuredOutputRetries + Zod refinements enable auto-correction — the model sees the validation error and tries again.structuredOutputRetries: 2 produces output where a Zod .refine() fails. What happens?Structured Output — Strands Agents Docs — Full API reference for structured output schemas, retries, and error handling. ~8 min read.