· 4 min read
Multi-agent systems are only as useful as the output they produce. A team of agents might perform brilliant research, synthesis, and review internally, but if the final output is unstructured text that downstream systems cannot parse, the entire pipeline breaks. Structuring agent output formats is not an afterthought — it is a core architectural decision that affects reliability, composability, and user experience.
The Claude Agent SDK provides several mechanisms for constraining and validating agent outputs. This guide covers practical patterns for defining output schemas, enforcing structure at every handoff point, and handling the inevitable cases where agents deviate from expected formats.
Structured output formats matter most when:
The most robust approach is to define output schemas using Zod and enforce them at the SDK level.
import { z } from "zod";
import { Agent } from "@anthropic-ai/agent-sdk";
const AgentReportSchema = z.object({
title: z.string().min(1).max(200),
summary: z.string().min(50).max(500),
sections: z.array(z.object({
heading: z.string(),
content: z.string(),
confidence: z.enum(["high", "medium", "low"]),
sources: z.array(z.object({
url: z.string().url().optional(),
title: z.string(),
relevance: z.number().min(0).max(1),
})),
})).min(1).max(10),
recommendations: z.array(z.string()).min(1),
metadata: z.object({
generatedAt: z.string(),
agentVersion: z.string(),
totalTokensUsed: z.number().optional(),
}),
});
type AgentReport = z.infer<typeof AgentReportSchema>;
const analysisAgent = new Agent({
name: "analyst",
model: "claude-sonnet-4-20250514",
instructions: `Analyze the provided data and produce a structured report.
Your output MUST be valid JSON matching the specified schema.
Do not include any text outside the JSON object.`,
outputSchema: AgentReportSchema,
});
const result = await analysisAgent.run(inputData);
// result.output is typed as AgentReport
When you specify an outputSchema, the SDK automatically validates the agent's response and will retry if the output does not conform. This eliminates an entire category of parsing errors.
For long-running agent tasks, you often want to stream partial results to the user. Structured outputs can be streamed section by section.
import { Agent, StreamEvent } from "@anthropic-ai/agent-sdk";
const SectionSchema = z.object({
heading: z.string(),
content: z.string(),
status: z.enum(["complete", "in-progress"]),
});
const streamingAgent = new Agent({
name: "report-writer",
model: "claude-sonnet-4-20250514",
instructions: "Write the report one section at a time. Output each section as a separate JSON object.",
streaming: true,
});
const stream = streamingAgent.stream(inputData);
for await (const event of stream) {
if (event.type === "partial_output") {
const section = SectionSchema.safeParse(event.data);
if (section.success) {
renderSection(section.data);
}
}
}
When multiple agents each produce a piece of the final output, you need an aggregation strategy. There are three common patterns.
Each agent produces output conforming to a sub-schema, and the orchestrator merges them.
const ResearchOutput = z.object({ findings: z.array(z.string()), sources: z.array(z.string()) });
const AnalysisOutput = z.object({ insights: z.array(z.string()), risks: z.array(z.string()) });
const RecommendationOutput = z.object({ actions: z.array(z.string()), priority: z.enum(["high", "medium", "low"]) });
const FinalReport = z.object({
research: ResearchOutput,
analysis: AnalysisOutput,
recommendations: RecommendationOutput,
});
// In the orchestrator
const research = await researchAgent.run(input);
const analysis = await analysisAgent.run(JSON.stringify(research.output));
const recommendations = await recommendationAgent.run(JSON.stringify({
research: research.output,
analysis: analysis.output,
}));
const finalReport: z.infer<typeof FinalReport> = {
research: research.output,
analysis: analysis.output,
recommendations: recommendations.output,
};
A dedicated agent receives all outputs and produces the final structured result.
const reducerAgent = new Agent({
name: "reducer",
model: "claude-sonnet-4-20250514",
instructions: `You receive outputs from multiple specialist agents.
Synthesize them into a single coherent report following the output schema exactly.
Resolve any contradictions by favoring higher-confidence findings.`,
outputSchema: FinalReportSchema,
});
Pre-define a template and have agents fill specific sections.
interface ReportTemplate {
executiveSummary: string | null;
marketAnalysis: string | null;
technicalAssessment: string | null;
financialProjection: string | null;
recommendation: string | null;
}
const template: ReportTemplate = {
executiveSummary: null,
marketAnalysis: null,
technicalAssessment: null,
financialProjection: null,
recommendation: null,
};
// Each agent fills its section
template.marketAnalysis = (await marketAgent.run(input)).output;
template.technicalAssessment = (await techAgent.run(input)).output;
// ...
Generated agent teams from Build Agents Store include output format specifications for each agent in the team. When you generate a team configuration, the system defines not just each agent's role and prompt, but also the expected output format at each stage.
This is critical for reliability. A customer support triage team, for example, might define output schemas that include a severity level, category classification, suggested response, and escalation flag — all as typed fields rather than free text. The reviewing agent can then programmatically verify that the triage agent's output includes all required fields before approving the response.
When designing your own output formats, prioritize: