Skip to content

Structured Output

Atlas supports schema-based responses for extracting structured data from AI responses.

Prism Reference

Atlas uses Prism's schema system under the hood. For detailed schema documentation, see Prism Structured Output and Prism Schemas.

What is Structured Output?

Instead of free-form text, structured output returns data in a predefined format:

php
use Atlasphp\Atlas\Atlas;
use Atlasphp\Atlas\Schema\Schema;

$response = Atlas::agent('analyzer')
    ->withSchema(
        Schema::object('sentiment', 'Sentiment analysis result')
            ->enum('sentiment', 'The sentiment', ['positive', 'negative', 'neutral'])
            ->number('confidence', 'Confidence score from 0 to 1')
    )
    ->chat('I love this product!');

echo $response->structured['sentiment'];   // "positive"
echo $response->structured['confidence'];  // 0.95

Atlas Schema Builder

Atlas provides a fluent Schema Builder that creates Prism schemas with less boilerplate and automatic required field tracking.

Basic Usage

php
use Atlasphp\Atlas\Schema\Schema;

// Inline - auto-builds when passed to withSchema()
$response = Atlas::agent('extractor')
    ->withSchema(
        Schema::object('contact', 'Contact information')
            ->string('name', 'Full name')
            ->string('email', 'Email address')
            ->number('age', 'Age in years')
    )
    ->chat('Extract: John Smith, john@example.com, 30 years old');

// Or build separately for reuse
$schema = Schema::object('contact', 'Contact information')
    ->string('name', 'Full name')
    ->string('email', 'Email address')
    ->build();

The Schema Builder automatically tracks required fields and builds valid Prism schema objects. All fields are required by default, matching OpenAI's recommended practice.

Property Types

php
// String
->string('name', 'The person\'s full name')

// Number (float/decimal)
->number('score', 'A score between 0 and 100')

// Integer (alias for number)
->integer('count', 'Number of items')

// Boolean
->boolean('is_valid', 'Whether the input is valid')

// Enum
->enum('status', 'The current status', ['pending', 'approved', 'rejected'])

// String array
->stringArray('tags', 'List of relevant tags')

// Number array
->numberArray('scores', 'List of scores')

// Object array
->array('items', 'Order items', fn($s) => $s
    ->string('name', 'Item name')
    ->number('quantity', 'Quantity')
)

// Nested object
->object('address', 'Mailing address', fn($s) => $s
    ->string('street', 'Street address')
    ->string('city', 'City name')
)

Optional and Nullable Fields

php
Schema::object('user', 'User profile')
    ->string('name', 'Full name')                        // required
    ->string('email', 'Email address')                   // required
    ->string('phone', 'Phone number')->optional()        // NOT required
    ->string('notes', 'Optional notes')->nullable()      // NOT required + can be null
    ->build()
  • ->optional() — Removes the field from required fields
  • ->nullable() — Makes the field nullable (implies optional)

Using Prism Schemas Directly

Atlas accepts Prism schema objects directly. If you're already familiar with Prism or need advanced schema features, you can use Prism's schema classes:

php
use Atlasphp\Atlas\Atlas;
use Prism\Prism\Schema\ObjectSchema;
use Prism\Prism\Schema\NumberSchema;
use Prism\Prism\Schema\EnumSchema;

$schema = new ObjectSchema(
    name: 'sentiment',
    description: 'Sentiment analysis result',
    properties: [
        new EnumSchema('sentiment', 'The sentiment', ['positive', 'negative', 'neutral']),
        new NumberSchema('confidence', 'Confidence score from 0 to 1'),
    ],
    requiredFields: ['sentiment', 'confidence'],
);

$response = Atlas::agent('analyzer')
    ->withSchema($schema)
    ->chat('I love this product!');

Available Prism schema types:

  • StringSchema — Text values
  • NumberSchema — Integers and floats
  • BooleanSchema — True/false values
  • ArraySchema — Lists of items
  • EnumSchema — Predefined set of options
  • ObjectSchema — Nested structures

Structured Output Modes

By default, Atlas uses the provider's native structured output mode. For OpenAI, this requires all fields to be required. If you need optional fields, use JSON mode.

JSON Mode (for Optional Fields)

Use ->usingJsonMode() when your schema has optional fields:

php
$response = Atlas::agent('extractor')
    ->withSchema(
        Schema::object('contact', 'Contact info')
            ->string('name', 'Full name')
            ->string('email', 'Email')
            ->string('phone', 'Phone number')->optional()
    )
    ->usingJsonMode()  // Required for optional fields with OpenAI
    ->chat('Extract: John at john@example.com');

Available Modes

php
// Let Atlas choose the best mode (default)
->usingAutoMode()

// Use native JSON schema (faster, but all fields must be required for OpenAI)
->usingNativeMode()

// Use JSON mode (allows optional fields, works with all providers)
->usingJsonMode()

When to use JSON mode:

  • Your schema has ->optional() fields
  • You need flexibility in what the model returns
  • You're working with providers that don't support native structured output

Complex Examples

Nested Schema with Optional Fields

php
$response = Atlas::agent('order-extractor')
    ->withSchema(
        Schema::object('order', 'Order details')
            ->string('id', 'Order ID')
            ->object('customer', 'Customer info', fn($s) => $s
                ->string('name', 'Customer name')
                ->string('email', 'Email address')->optional()
            )
            ->array('items', 'Order items', fn($s) => $s
                ->string('name', 'Item name')
                ->number('quantity', 'Quantity')
                ->number('price', 'Unit price')->optional()
            )
    )
    ->usingJsonMode()
    ->chat($orderText);

Classification Schema

php
$response = Atlas::agent('classifier')
    ->withSchema(
        Schema::object('classification', 'Content classification')
            ->enum('category', 'Content category', ['support', 'sales', 'feedback', 'other'])
            ->number('confidence', 'Classification confidence')
            ->stringArray('tags', 'Relevant tags')->optional()
    )
    ->usingJsonMode()
    ->chat('I want to return my order and get a refund');

// $response->structured = ['category' => 'support', 'confidence' => 0.92, 'tags' => ['refund', 'return']]

Data Extraction Schema

php
$response = Atlas::agent('extractor')
    ->withSchema(
        Schema::object('contact', 'Extracted contact')
            ->string('name', 'Full name')
            ->string('email', 'Email address')->optional()
            ->string('phone', 'Phone number')->optional()
    )
    ->usingJsonMode()
    ->chat('Contact: John Smith, john@example.com, 555-1234');

// $response->structured = ['name' => 'John Smith', 'email' => 'john@example.com', 'phone' => '555-1234']

Summary Schema

php
$response = Atlas::agent('summarizer')
    ->withSchema(
        Schema::object('summary', 'Article summary')
            ->string('title', 'Suggested title')
            ->string('summary', 'Brief summary (2-3 sentences)')
            ->stringArray('key_points', 'Key takeaways')
            ->enum('sentiment', 'Overall sentiment', ['positive', 'negative', 'neutral'])
    )
    ->chat($articleText);

With Variables and Messages

php
$response = Atlas::agent('extractor')
    ->withMessages($messages)
    ->withVariables(['user_name' => 'John'])
    ->withSchema(
        Schema::object('data', 'Extracted data')
            ->string('field', 'The field')
    )
    ->chat('Extract the data');

$data = $response->structured;

Checking Responses

php
use Atlasphp\Atlas\Atlas;
use Atlasphp\Atlas\Schema\Schema;

$response = Atlas::agent('extractor')
    ->withSchema(
        Schema::object('person', 'Person details')
            ->string('name', 'Full name')
            ->string('email', 'Email address')
    )
    ->chat('Extract from: John Smith can be reached at john@example.com');

if ($response->structured !== null) {
    $person = $response->structured;
    echo "Name: {$person['name']}";    // "John Smith"
    echo "Email: {$person['email']}";  // "john@example.com"
} else {
    // Handle case where extraction failed
    echo "Could not extract person data";
}

Best Practices

1. Provide Clear Descriptions

php
// Good - specific descriptions
->string('email', 'A valid email address in format user@domain.com')

// Less helpful
->string('email', 'Email')

2. Use Required Fields Appropriately

Only mark fields as optional if they're truly optional:

php
Schema::object('user', 'User profile')
    ->string('name', 'Full name')         // Essential
    ->string('email', 'Email address')    // Essential
    ->string('phone', 'Phone number')->optional()  // Nice to have

3. Match Schema to Agent Purpose

Design your agent's system prompt to produce the expected structure:

php
use Atlasphp\Atlas\Agents\AgentDefinition;

class DataExtractorAgent extends AgentDefinition
{
    public function systemPrompt(): ?string
    {
        return <<<PROMPT
        You extract structured data from text.
        Focus on finding the specific fields requested.
        If a field cannot be determined, omit it from the response.
        PROMPT;
    }
}

Next Steps

Released under the MIT License.