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:
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.95Atlas Schema Builder
Atlas provides a fluent Schema Builder that creates Prism schemas with less boilerplate and automatic required field tracking.
Basic Usage
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
// 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
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:
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 valuesNumberSchema— Integers and floatsBooleanSchema— True/false valuesArraySchema— Lists of itemsEnumSchema— Predefined set of optionsObjectSchema— 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:
$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
// 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
Examples
Example: Nested Schema with Optional Fields
$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);Example: Classification Schema
$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']]Example: Data Extraction Schema
$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']Example: Summary Schema
$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
$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
Chat operations return an AgentResponse that wraps Prism's response with agent context:
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');
// Check if structured and access data
if ($response->isStructured()) {
$person = $response->structured();
echo "Name: {$person['name']}"; // "John Smith"
echo "Email: {$person['email']}"; // "john@example.com"
}
// Backward compatible property access also works
echo $response->structured['name']; // Via __get magicBest Practices
1. Provide Clear Descriptions
// 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:
Schema::object('user', 'User profile')
->string('name', 'Full name') // Essential
->string('email', 'Email address') // Essential
->string('phone', 'Phone number')->optional() // Nice to have3. Match Schema to Agent Purpose
Design your agent's system prompt to produce the expected structure:
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;
}
}Pipeline Hooks
Structured output supports pipeline middleware for observability:
| Pipeline | Trigger |
|---|---|
structured.before_structured | Before structured output generation |
structured.after_structured | After structured output generation |
use Atlasphp\Atlas\Contracts\PipelineContract;
class LogStructuredOutput implements PipelineContract
{
public function handle(mixed $data, Closure $next): mixed
{
$result = $next($data);
Log::info('Structured output generated', [
'user_id' => $data['metadata']['user_id'] ?? null,
]);
return $result;
}
}
$registry->register('structured.after_structured', LogStructuredOutput::class);API Reference
// Schema factory
Schema::object(string $name, string $description): SchemaBuilder;
// SchemaBuilder property methods (all return SchemaProperty for chaining)
$builder->string(string $name, string $description): SchemaProperty;
$builder->number(string $name, string $description): SchemaProperty;
$builder->integer(string $name, string $description): SchemaProperty;
$builder->boolean(string $name, string $description): SchemaProperty;
$builder->enum(string $name, string $description, array $options): SchemaProperty;
$builder->stringArray(string $name, string $description): SchemaProperty;
$builder->numberArray(string $name, string $description): SchemaProperty;
$builder->object(string $name, string $description, callable $callback): SchemaProperty;
$builder->array(string $name, string $description, callable $callback): SchemaProperty;
$builder->build(): ObjectSchema;
// SchemaProperty modifiers
$property->optional(): SchemaProperty; // Remove from required fields
$property->nullable(): SchemaProperty; // Allow null values (implies optional)
// Agent executor schema methods
Atlas::agent('agent')
->withSchema(SchemaBuilder|ObjectSchema $schema)
->usingAutoMode() // Let Atlas choose (default)
->usingNativeMode() // Use native JSON schema
->usingJsonMode() // Use JSON mode (required for optional fields)
->chat(string $input): AgentResponse;
// AgentResponse structured output methods
$response->isStructured(): bool; // Check if response is structured
$response->structured(): ?array; // Get structured data (or null)
$response->structured; // Property access via __get (backward compatible)
// AgentResponse agent context
$response->agentKey(): string; // Agent key
$response->agentName(): string; // Agent name
$response->systemPrompt; // System prompt used
$response->metadata(): array; // Pipeline metadata
$response->variables(): array; // Variables used
$response->response; // Full Prism StructuredResponseNext Steps
- Chat — Chat API details
- Agents — Agent configuration
- Prism Schemas — Full schema reference