Skip to main content
Structured output constrains the LLM to return JSON matching your defined schema. Instead of parsing free-form text, you get predictable data structures that integrate seamlessly with your application.

Quick Start

Define a DataModel and use it as your response schema:
use LarAgent\Core\Abstractions\DataModel;
use LarAgent\Attributes\Desc;

class WeatherInfo extends DataModel
{
    #[Desc('The city name')]
    public string $city;
    
    #[Desc('Temperature in Celsius')]
    public float $temperature;
    
    #[Desc('Weather condition')]
    public string $condition;
}
class WeatherAgent extends Agent
{
    protected $responseSchema = WeatherInfo::class;
    
    public function instructions()
    {
        return 'Provide weather information for the requested location.';
    }
}
$weather = WeatherAgent::ask('What is the weather in Paris?');

echo $weather->city;        // 'Paris'
echo $weather->temperature; // 18.5
echo $weather->condition;   // 'Partly cloudy'

Setting Up Schemas

Using a DataModel Class

The recommended approach for type-safe responses:
class ProductExtractor extends Agent
{
    protected $responseSchema = ProductInfo::class;
}

// Response is automatically a ProductInfo instance
$product = ProductExtractor::ask('Extract: MacBook Air M3 - $1,099');
echo $product->name;  // 'MacBook Air M3'
echo $product->price; // 1099

Using an Array Schema

For simple cases or dynamic schemas:
class ProductExtractor extends Agent
{
    protected $responseSchema = [
        'type' => 'object',
        'properties' => [
            'name' => ['type' => 'string', 'description' => 'Product name'],
            'price' => ['type' => 'number', 'description' => 'Price in dollars'],
        ],
        'required' => ['name', 'price'],
        'additionalProperties' => false,
    ];
}

// Response is an associative array
$product = ProductExtractor::ask('Extract: MacBook Air M3 - $1,099');
// ['name' => 'MacBook Air M3', 'price' => 1099]

Using the structuredOutput() Method

For dynamic schemas or complex logic, override the structuredOutput() method:
class DynamicAgent extends Agent
{
    public function structuredOutput()
    {
        // Dynamically choose schema based on context
        if ($this->requiresDetailedResponse()) {
            return DetailedReport::generateSchema();
        }
        
        return SummaryReport::generateSchema();
    }
    
    // Enable automatic DataModel reconstruction
    public function getResponseSchemaClass(): ?string
    {
        return $this->requiresDetailedResponse() 
            ? DetailedReport::class 
            : SummaryReport::class;
    }
}
When you override structuredOutput(), also override getResponseSchemaClass() to enable automatic DataModel reconstruction. Or skip if you want response returned as array.

Runtime Schema

Set the schema at runtime using the fluent API:
$response = MyAgent::for('session')
    ->responseSchema(ProductInfo::class)
    ->respond('Extract product info from: iPhone 15 - $799');

DataModel Basics

DataModels are PHP classes that define your expected response structure.

Property Types

class UserProfile extends DataModel
{
    public string $name;           // Text
    public int $age;               // Integer
    public float $score;           // Decimal
    public bool $isActive;         // Boolean
    public array $tags;            // Generic array
    public ?string $nickname;      // Nullable (optional)
    public UserStatus $status;     // Enum (string-backed or int-backed)
    public Address $address;       // Nested DataModel
    public SkillArray $skills;     // DataModelArray: collection of DataModels
}

The #[Desc] Attribute

Add descriptions to guide the LLM on what each field should contain:
use LarAgent\Attributes\Desc;

class ContactInfo extends DataModel
{
    #[Desc('Full legal name of the person')]
    public string $name;
    
    #[Desc('Valid email address format')]
    public string $email;
    
    #[Desc('Phone number with country code')]
    public ?string $phone = null;
}
Descriptive #[Desc] attributes significantly improve the quality and accuracy of extracted data.

Optional Properties

Use nullable types or default values:
class Article extends DataModel
{
    #[Desc('Article title')]
    public string $title;
    
    #[Desc('Article subtitle (optional)')]
    public ?string $subtitle = null;
    
    #[ExcludeFromSchema]
    public string $status = 'draft';
}
Use #[ExcludeFromSchema] to exclude properties from the schema passed to the LLM. These properties won’t be affected by the AI response β€” they remain completely controlled by you with their default values and defined methods.

Advanced DataModels

DataModels support powerful features for complex data structures:
  • Nested DataModels β€” Embed DataModels within other DataModels for hierarchical data
  • DataModelArray β€” Type-safe collections of DataModels
  • Polymorphic Collections β€” Collections containing different DataModel types with discriminators

DataModels In-Depth

Learn about nested structures, collections, polymorphic arrays, and advanced DataModel patterns.

Real-World Example

A complete example analyzing product reviews:
App/DataModels/ReviewSentiment.php
// Represents sentiment analysis for a single review
class ReviewSentiment extends DataModel
{
    #[Desc('Overall sentiment: positive, negative, or neutral')]
    public string $sentiment;
    
    #[Desc('Confidence score from 0.0 to 1.0')]
    public float $confidence;
    
    #[Desc('Key points from the review')]
    public array $keyPoints;
}
App/DataModels/ReviewSentimentArray.php
// Typed collection of ReviewSentiment instances
class ReviewSentimentArray extends DataModelArray
{
    public static function allowedModels(): array
    {
        return [ReviewSentiment::class];
    }
}
App/DataModels/ReviewAnalysis.php
// Complete analysis result containing all reviews and summary
class ReviewAnalysis extends DataModel
{
    #[Desc('Product being reviewed')]
    public string $productName;
    
    #[Desc('Average sentiment score')]
    public float $averageScore;
    
    #[Desc('Individual review analyses')]
    public ReviewSentimentArray $reviews;
    
    #[Desc('Summary recommendation')]
    public string $recommendation;
}
App/AiAgents/ReviewAnalyzerAgent.php
// Agent that analyzes product reviews using the ReviewAnalysis schema
class ReviewAnalyzerAgent extends Agent
{
    protected $model = 'gpt-4o';
    protected $responseSchema = ReviewAnalysis::class;
    
    public function instructions()
    {
        return 'Analyze product reviews and provide sentiment analysis.';
    }
}

$reviews = <<<REVIEWS
1. "Great product! Fast shipping, excellent quality."
2. "Decent but overpriced. Works as expected."
3. "Disappointed. Broke after a week."
REVIEWS;

// Analyze reviews and get structured response
$analysis = ReviewAnalyzerAgent::ask("Analyze these reviews:\n{$reviews}");

echo "Product: {$analysis->productName}\n";
echo "Average Score: {$analysis->averageScore}\n";
echo "Recommendation: {$analysis->recommendation}\n";

foreach ($analysis->reviews as $review) {
    echo "- {$review->sentiment} ({$review->confidence})\n";
}


Best Practices

Use DataModels for reusable, type-safe schemas with IDE autocompletion.
Add descriptive #[Desc] attributes to guide the LLM.
Use DataModelArray instead of raw arrays for typed collections.
Keep DataModels focused β€” split complex structures into nested models.
Avoid deeply nested schemas (4+ levels) as they can reduce response accuracy.
Always include critical fields in required β€” only omit truly optional properties.

Next Steps