Skip to main content

Structured Output

Structured output enables agents to return type-safe, validated data instead of plain text. This is essential for building reliable applications that integrate with your codebase.

Why Structured Output?

Benefits:
  • Type safety - Full TypeScript types inferred from schemas
  • Validation - Automatic validation of LLM responses
  • Reliability - Guaranteed output format
  • Integration - Easy to use in applications
  • Error handling - Graceful fallbacks when parsing fails

Basic Usage

From: packages/core/src/agent/agent.test.ts

Generate with Schema

import { Agent } from '@mastra/core/agent';
import { z } from 'zod';

const agent = new Agent({
  id: 'election-agent',
  name: 'Election Agent',
  instructions: 'You know about US presidential elections',
  model: 'openai/gpt-4o',
});

const result = await agent.generate(
  'Who won the 2012 US presidential election?',
  {
    structuredOutput: {
      schema: z.object({
        winner: z.string().describe('Name of the winner'),
        year: z.number().describe('Election year'),
        party: z.enum(['Democratic', 'Republican', 'Other']),
      }),
    },
  }
);

// Type-safe access
console.log(result.object.winner); // "Barack Obama"
console.log(result.object.year); // 2012
console.log(result.object.party); // "Democratic"

Stream Structured Output

const stream = await agent.stream(
  'Who won the 2012 election?',
  {
    structuredOutput: {
      schema: z.object({
        winner: z.string(),
        party: z.string(),
      }),
    },
  }
);

// Stream partial objects as they're generated
for await (const partial of stream.objectStream) {
  console.log('Partial:', partial);
  // { winner: "Barack" }
  // { winner: "Barack Obama" }
  // { winner: "Barack Obama", party: "Democratic" }
}

// Get final validated object
const finalObject = await stream.object;
console.log('Complete:', finalObject);

Schema Definition

Simple Schema

const weatherSchema = z.object({
  temperature: z.number().describe('Temperature in Fahrenheit'),
  condition: z.enum(['sunny', 'cloudy', 'rainy', 'snowy']),
  humidity: z.number().min(0).max(100),
  forecast: z.string().optional(),
});

const result = await agent.generate('What is the weather in NYC?', {
  structuredOutput: { schema: weatherSchema },
});

type Weather = z.infer<typeof weatherSchema>;
const weather: Weather = result.object;

Complex Nested Schema

const analysisSchema = z.object({
  summary: z.string().describe('Brief summary of findings'),
  sentiment: z.object({
    score: z.number().min(-1).max(1),
    label: z.enum(['positive', 'neutral', 'negative']),
    confidence: z.number().min(0).max(1),
  }),
  topics: z.array(
    z.object({
      name: z.string(),
      relevance: z.number(),
    })
  ).describe('Key topics mentioned'),
  metadata: z.object({
    wordCount: z.number(),
    readingTime: z.number().describe('Estimated reading time in minutes'),
  }),
});

const result = await agent.generate('Analyze this article: ...', {
  structuredOutput: { schema: analysisSchema },
});

Array Output

const listSchema = z.object({
  items: z.array(
    z.object({
      title: z.string(),
      description: z.string(),
      priority: z.enum(['low', 'medium', 'high']),
    })
  ),
  total: z.number(),
});

const result = await agent.generate(
  'List 5 steps to deploy a Next.js app',
  {
    structuredOutput: { schema: listSchema },
  }
);

result.object.items.forEach((item, i) => {
  console.log(`${i + 1}. ${item.title} [${item.priority}]`);
  console.log(`   ${item.description}`);
});

Configuration Options

Custom Instructions

Override the default structuring instructions:
const result = await agent.generate(
  'Extract key information',
  {
    structuredOutput: {
      schema: dataSchema,
      instructions: `Extract and structure the key information.
        Focus on accuracy over completeness.
        If a field is uncertain, use null.`,
    },
  }
);

Error Strategies

Handle parsing failures gracefully:
// Strict mode (default) - throws on error
const result = await agent.generate(prompt, {
  structuredOutput: {
    schema: mySchema,
    errorStrategy: 'strict',
  },
});

// Warn mode - logs warning but continues
const result = await agent.generate(prompt, {
  structuredOutput: {
    schema: mySchema,
    errorStrategy: 'warn',
  },
});

// Fallback mode - returns fallback value
const result = await agent.generate(prompt, {
  structuredOutput: {
    schema: mySchema,
    errorStrategy: 'fallback',
    fallbackValue: {
      status: 'error',
      data: null,
    },
  },
});

Custom Model

Use a different model for structuring:
import { openai } from '@ai-sdk/openai';

const result = await agent.generate(
  'Analyze this data',
  {
    structuredOutput: {
      schema: analysisSchema,
      model: openai('gpt-4o-mini'), // Faster, cheaper for extraction
    },
  }
);

JSON Prompt Injection

For models without native structured output:
const result = await agent.generate(prompt, {
  structuredOutput: {
    schema: mySchema,
    jsonPromptInjection: true, // Use prompt engineering instead of native support
  },
});

Real-World Examples

Data Extraction

import { Agent } from '@mastra/core/agent';
import { z } from 'zod';

const extractionAgent = new Agent({
  id: 'extractor',
  name: 'Data Extractor',
  instructions: 'Extract structured data from unstructured text',
  model: 'openai/gpt-4o',
});

const contactSchema = z.object({
  name: z.string(),
  email: z.string().email().nullable(),
  phone: z.string().nullable(),
  company: z.string().nullable(),
  role: z.string().nullable(),
});

const unstructuredText = `
  John Smith works as Senior Developer at Acme Corp.
  You can reach him at [email protected] or call 555-1234.
`;

const result = await extractionAgent.generate(unstructuredText, {
  structuredOutput: { schema: contactSchema },
});

console.log(result.object);
/*
{
  name: "John Smith",
  email: "[email protected]",
  phone: "555-1234",
  company: "Acme Corp",
  role: "Senior Developer"
}
*/

Classification

const classificationSchema = z.object({
  category: z.enum([
    'bug',
    'feature-request',
    'question',
    'documentation',
    'other'
  ]),
  priority: z.enum(['low', 'medium', 'high', 'urgent']),
  tags: z.array(z.string()).describe('Relevant tags'),
  assignee: z.string().nullable().describe('Suggested assignee'),
  confidence: z.number().min(0).max(1),
});

const result = await agent.generate(
  'The app crashes when I click the submit button on the payment form',
  {
    structuredOutput: { schema: classificationSchema },
  }
);

console.log(result.object);
/*
{
  category: "bug",
  priority: "high",
  tags: ["payment", "crash", "ui"],
  assignee: "frontend-team",
  confidence: 0.95
}
*/

Form Generation

const formSchema = z.object({
  fields: z.array(
    z.object({
      name: z.string(),
      label: z.string(),
      type: z.enum(['text', 'email', 'number', 'select', 'textarea']),
      required: z.boolean(),
      placeholder: z.string().optional(),
      options: z.array(z.string()).optional(),
      validation: z.object({
        min: z.number().optional(),
        max: z.number().optional(),
        pattern: z.string().optional(),
      }).optional(),
    })
  ),
  submitButton: z.string(),
});

const result = await agent.generate(
  'Create a contact form with name, email, message, and a dropdown for department',
  {
    structuredOutput: { schema: formSchema },
  }
);

// Use result to render a form
const form = result.object;
form.fields.forEach(field => {
  renderFormField(field);
});

API Response Transformation

const apiSchema = z.object({
  status: z.enum(['success', 'error']),
  data: z.object({
    users: z.array(
      z.object({
        id: z.string(),
        name: z.string(),
        email: z.string(),
        active: z.boolean(),
      })
    ),
  }),
  metadata: z.object({
    total: z.number(),
    page: z.number(),
    perPage: z.number(),
  }),
});

const result = await agent.generate(
  'Transform this CSV data into a paginated API response format',
  {
    structuredOutput: { schema: apiSchema },
  }
);

// Result matches your API contract exactly
return Response.json(result.object);

Test Case Generation

const testCaseSchema = z.object({
  testCases: z.array(
    z.object({
      name: z.string().describe('Test case name'),
      description: z.string(),
      given: z.string().describe('Initial state/setup'),
      when: z.string().describe('Action to perform'),
      then: z.string().describe('Expected result'),
      priority: z.enum(['low', 'medium', 'high']),
    })
  ),
});

const result = await agent.generate(
  'Generate test cases for a login form',
  {
    structuredOutput: { schema: testCaseSchema },
  }
);

result.object.testCases.forEach(test => {
  console.log(`Test: ${test.name}`);
  console.log(`  Given: ${test.given}`);
  console.log(`  When: ${test.when}`);
  console.log(`  Then: ${test.then}`);
});

Combining with Tools

From: packages/core/src/agent/agent.test.ts
import { createTool } from '@mastra/core/tools';

const searchTool = createTool({
  id: 'search',
  description: 'Search the knowledge base',
  inputSchema: z.object({ query: z.string() }),
  execute: async ({ query }) => {
    return { results: [...] };
  },
});

const agent = new Agent({
  id: 'research-agent',
  name: 'Research Agent',
  instructions: 'Research topics and provide structured summaries',
  model: 'openai/gpt-4o',
  tools: { searchTool },
});

// Agent can use tools AND return structured output
const result = await agent.generate(
  'Research quantum computing',
  {
    maxSteps: 5, // Allow tool calls
    structuredOutput: {
      schema: z.object({
        topic: z.string(),
        summary: z.string(),
        keyPoints: z.array(z.string()),
        sources: z.array(z.object({
          title: z.string(),
          url: z.string(),
        })),
        confidence: z.number(),
      }),
    },
  }
);

// Agent searched using tools, then returned structured data
console.log(result.object);
console.log('Tools used:', result.toolCalls.length);

Type Inference

Zod schemas provide full TypeScript types:
const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  age: z.number(),
  verified: z.boolean(),
});

// Infer TypeScript type from schema
type User = z.infer<typeof userSchema>;

const result = await agent.generate(prompt, {
  structuredOutput: { schema: userSchema },
});

// result.object is typed as User
const user: User = result.object;

// Type-safe access with autocomplete
console.log(user.name);
console.log(user.email);

// TypeScript error - property doesn't exist
// console.log(user.invalidField);

Best Practices

Help the LLM understand what each field represents:
const schema = z.object({
  temperature: z.number().describe('Temperature in Fahrenheit'),
  condition: z.string().describe('Weather condition (sunny, cloudy, etc.)'),
});
Ensure valid categories:
const schema = z.object({
  status: z.enum(['pending', 'approved', 'rejected']),
  priority: z.enum(['low', 'medium', 'high']),
});
Use .optional() or .nullable() for fields that may not exist:
const schema = z.object({
  name: z.string(),
  email: z.string().optional(),
  phone: z.string().nullable(),
});
Add constraints to ensure data quality:
const schema = z.object({
  age: z.number().min(0).max(150),
  email: z.string().email(),
  url: z.string().url(),
  rating: z.number().min(1).max(5),
});
Handle parsing failures gracefully:
const result = await agent.generate(prompt, {
  structuredOutput: {
    schema: mySchema,
    errorStrategy: 'fallback',
    fallbackValue: getDefaultValue(),
  },
});

Next Steps

Tools

Combine structured output with tools

Memory

Persist structured conversations

Workflows

Use structured output in workflows

API Reference

Complete API documentation

Build docs developers (and LLMs) love