Deep Agents
AgentOrchestratorRetrievalText2SQLToolbox

Structured Output

Type-safe responses from agents using Zod schemas

Why Structured Output?

Free-form text responses have limitations:

// Without structured output
const text = await generate(agent, 'Analyze sentiment', {}).text;
// text = "The sentiment appears to be positive with high confidence..."
// Now you need to parse this somehow

With structured output:

const SentimentSchema = z.object({
  sentiment: z.enum(['positive', 'negative', 'neutral']),
  confidence: z.number().min(0).max(1),
  reasoning: z.string(),
});

const { experimental_output } = await generate(agent, 'Analyze sentiment', {});
// experimental_output is typed as:
// { sentiment: 'positive' | 'negative' | 'neutral'; confidence: number; reasoning: string }

Benefits:

  • Type safety: TypeScript knows the response shape
  • Validation: Invalid responses are rejected
  • Reliability: Consistent format every time
  • Composability: Chain agents with predictable interfaces

Defining Schemas

Use Zod to define output schemas:

import z from 'zod';

// Simple schema
const AnalysisSchema = z.object({
  summary: z.string(),
  score: z.number(),
});

// With descriptions (helps the model)
const DetailedSchema = z.object({
  summary: z.string().describe('A 2-3 sentence summary'),
  findings: z.array(z.string()).describe('Key findings as bullet points'),
  confidence: z.number().min(0).max(1).describe('Confidence from 0 to 1'),
});

// With enums
const ClassificationSchema = z.object({
  category: z.enum(['bug', 'feature', 'question', 'other']),
  priority: z.enum(['low', 'medium', 'high', 'critical']),
});

// With optional fields
const FlexibleSchema = z.object({
  required: z.string(),
  optional: z.string().optional(),
  withDefault: z.string().default('default value'),
});

Using Output in Agents

Pass the schema to the output field:

import { agent, instructions, generate } from '@deepagents/agent';
import { groq } from '@ai-sdk/groq';
import z from 'zod';

const ReportSchema = z.object({
  summary: z.string(),
  findings: z.array(z.string()),
  recommendations: z.array(z.string()),
});

const analyst = agent({
  name: 'Analyst',
  model: groq('gpt-oss-20b'),
  output: ReportSchema,
  prompt: instructions({
    purpose: ['You analyze data and produce structured reports.'],
    routine: [
      'Review the provided data',
      'Identify key findings',
      'Generate actionable recommendations',
    ],
  }),
});

const { experimental_output } = await generate(analyst, data, {});

console.log(experimental_output.summary);
console.log(experimental_output.findings);       // string[]
console.log(experimental_output.recommendations); // string[]

Type Inference

Zod provides automatic type inference:

const PlanSchema = z.object({
  steps: z.array(z.object({
    action: z.string(),
    tool: z.string(),
    reasoning: z.string(),
  })),
  estimatedTime: z.number(),
});

// TypeScript infers this type:
type Plan = z.infer<typeof PlanSchema>;
// {
//   steps: { action: string; tool: string; reasoning: string }[];
//   estimatedTime: number;
// }

const planner = agent({
  output: PlanSchema,
  // ...
});

// experimental_output is typed as Plan
const { experimental_output } = await generate(planner, task, {});

Complex Schemas

Nested Objects

const CompanyAnalysisSchema = z.object({
  company: z.object({
    name: z.string(),
    ticker: z.string(),
    sector: z.string(),
  }),
  financials: z.object({
    revenue: z.number(),
    profit: z.number(),
    growth: z.number(),
  }),
  analysis: z.object({
    strengths: z.array(z.string()),
    weaknesses: z.array(z.string()),
    outlook: z.enum(['bullish', 'bearish', 'neutral']),
  }),
});

Discriminated Unions

const ResponseSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('success'),
    data: z.object({ result: z.string() }),
  }),
  z.object({
    type: z.literal('error'),
    error: z.object({ code: z.string(), message: z.string() }),
  }),
]);

Recursive Schemas

const TreeNodeSchema: z.ZodType<TreeNode> = z.lazy(() =>
  z.object({
    value: z.string(),
    children: z.array(TreeNodeSchema),
  })
);

How It Works

When you provide an output schema, two things happen:

1. AI SDK Output Mode

The execution uses Output.object() from the AI SDK:

// Internal (simplified from swarm.ts)
experimental_output: agent.output
  ? Output.object({ schema: agent.output })
  : undefined,

2. Model Wrapping

The model is wrapped to include JSON schema in its configuration:

// Internal (simplified from swarm.ts)
if (agent.output) {
  const json_schema = zodToJsonSchema(agent.output, {
    $refStrategy: 'root',
  });
  stepModel = wrapLanguageModel({
    model: stepModel,
    middleware: {
      transformParams: async ({ params }) => ({
        ...params,
        response_format: {
          type: 'json_schema',
          json_schema,
          name: `${agent.handoff.name}_output`,
        },
      }),
    },
  });
}

This ensures the model produces valid JSON matching your schema.

Accessing Structured Output

With generate()

const result = await generate(agent, input, context);

// Typed access
const output = result.experimental_output;
console.log(output.summary);

// Also available
console.log(result.text); // Raw text response

With execute()

const stream = execute(agent, input, context);
await stream.consumeStream();

// Access after streaming completes
const output = await stream.experimental_output;

Schema Design Tips

Use Descriptions

Descriptions guide the model:

const Schema = z.object({
  sentiment: z.enum(['positive', 'negative', 'neutral'])
    .describe('Overall emotional tone of the text'),
  confidence: z.number()
    .min(0).max(1)
    .describe('How confident the analysis is, from 0 (uncertain) to 1 (certain)'),
});

Keep It Focused

Smaller, focused schemas work better than large ones:

// Good: focused
const SentimentSchema = z.object({
  sentiment: z.enum(['positive', 'negative', 'neutral']),
  confidence: z.number(),
});

// Potentially problematic: too broad
const EverythingSchema = z.object({
  sentiment: z.enum(['positive', 'negative', 'neutral']),
  topics: z.array(z.string()),
  entities: z.array(z.object({ name: z.string(), type: z.string() })),
  summary: z.string(),
  keywords: z.array(z.string()),
  language: z.string(),
  // ... many more fields
});

Provide Examples in Prompts

Help the model with examples:

const agent = agent({
  output: ReportSchema,
  prompt: `
    Analyze the data and produce a report.

    Example output format:
    {
      "summary": "Brief overview of findings",
      "findings": ["Finding 1", "Finding 2"],
      "confidence": 0.85
    }
  `,
});

Real-World Examples

Research Bot Planner

const WebSearchPlanSchema = z.object({
  searches: z.array(z.object({
    reason: z.string()
      .describe('Why this search is important to the query'),
    query: z.string()
      .describe('The search term to use'),
  }))
  .describe('A list of web searches to perform'),
});

const planner = agent({
  name: 'PlannerAgent',
  model: openai('gpt-4.1'),
  output: WebSearchPlanSchema,
  prompt: instructions({
    purpose: ['Come up with a set of web searches to answer the query.'],
    routine: ['Output between 5 and 10 terms to query for.'],
  }),
});

Financial Report

const FinancialReportSchema = z.object({
  short_summary: z.string()
    .describe('A short 2-3 sentence executive summary'),
  markdown_report: z.string()
    .describe('The full markdown report'),
  follow_up_questions: z.array(z.string())
    .describe('Suggested follow-up questions for further research'),
});

const writer = agent({
  name: 'FinancialWriterAgent',
  model: groq('gpt-oss-20b'),
  output: FinancialReportSchema,
  prompt: instructions({
    purpose: ['Synthesize research into a long-form markdown report.'],
    routine: [
      'Create executive summary',
      'Write detailed analysis',
      'Suggest follow-up questions',
    ],
  }),
});

Verification Result

const VerificationSchema = z.object({
  verified: z.boolean()
    .describe('Whether the report seems coherent and plausible'),
  issues: z.string()
    .describe('If not verified, describe the main issues or concerns'),
});

const verifier = agent({
  name: 'VerificationAgent',
  model: groq('gpt-oss-20b'),
  output: VerificationSchema,
  prompt: instructions({
    purpose: ['Verify reports are internally consistent and well-sourced.'],
    routine: ['Point out any issues or uncertainties.'],
  }),
});

Agents as Tools with Output Extraction

When using agent.asTool(), you can extract structured output:

const summaryExtractor = async (result) => {
  return result.experimental_output.summary;
};

const writerWithTools = writerAgent.clone({
  tools: {
    fundamentals_analysis: financialsAgent.asTool({
      toolDescription: 'Get a financial metrics write-up',
      outputExtractor: summaryExtractor,
    }),
  },
});

The outputExtractor transforms the structured output before returning it to the calling agent.

Next Steps