Rich Signatures, Lean Schemas
When signatures hit 5+ fields, JSON Schema overhead eats hundreds of tokens per call. BAML keeps them compact—no retraining needed.
Vicente Reig Rincon de Arellano
Fractional Engineering Lead •
I find writing Signatures instead of Prompts similar to modeling databases with ActiveRecord. You use objects to model the world as you want your prompt to see it. Start simple, add complexity as needed, and the framework handles the details.
Starting Simple
Here’s a basic signature - just input and output:
class SentimentAnalysis < DSPy::Signature
description "Classify sentiment of a given sentence"
class Sentiment < T::Enum
enums do
Positive = new('positive')
Negative = new('negative')
Neutral = new('neutral')
end
end
input do
const :text, String, description: "The text to analyze"
end
output do
const :sentiment, Sentiment, description: "Sentiment classification"
const :confidence, Float, description: "Confidence score between 0 and 1"
end
end
# Configure DSPy with your LLM
DSPy.configure do |c|
c.lm = DSPy::LM.new(
'openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY']
)
end
# Use the signature
predictor = DSPy::Predict.new(SentimentAnalysis)
sentiment = predictor.call(text: "This book was super fun to read!")
puts sentiment.sentiment.serialize # => "positive"
puts sentiment.confidence # => 0.95
Clean. Minimal. The LM receives a compact schema that fits in a few lines.
Signatures Get Richer
But real applications need more structure. Task decomposition, for example:
class TaskDecomposition < DSPy::Signature
description "Autonomously analyze a research topic and define optimal subtasks"
input do
const :topic, String, description: "The main research topic to investigate"
const :context, String, description: "Any additional context or constraints"
end
output do
const :subtasks, T::Array[String], description: "Research subtasks with clear objectives"
const :task_types, T::Array[String], description: "Type classification for each task"
const :priority_order, T::Array[Integer], description: "Priority rankings (1-5 scale)"
const :estimated_effort, T::Array[Integer], description: "Effort estimates in hours"
const :dependencies, T::Array[String], description: "Task dependency relationships"
const :agent_requirements, T::Array[String], description: "Suggested agent types/skills"
end
end
# Use the signature
predictor = DSPy::Predict.new(TaskDecomposition)
result = predictor.call(
topic: "Build user authentication system",
context: "Focus on security best practices and Rails integration"
)
# Access structured results
puts "Subtasks:"
result.subtasks.each_with_index do |task, i|
puts " #{i+1}. #{task} (#{result.estimated_effort[i]}h, priority: #{result.priority_order[i]})"
end
Six output fields with descriptions. Nested types. This is where schemas start creeping into your prompts.
The Schema Problem
With Enhanced Prompting (the default mode in DSPy.rb), schemas are embedded directly in prompts. Here’s what the LM actually receives for TaskDecomposition
:
{
"$schema": "http://json-schema.org/draft-06/schema#",
"type": "object",
"properties": {
"subtasks": {
"type": "array",
"items": {"type": "string"},
"description": "Research subtasks with clear objectives"
},
"task_types": {
"type": "array",
"items": {"type": "string"},
"description": "Type classification for each task"
},
"priority_order": {
"type": "array",
"items": {"type": "integer"},
"description": "Priority rankings (1-5 scale)"
},
"estimated_effort": {
"type": "array",
"items": {"type": "integer"},
"description": "Effort estimates in hours"
},
"dependencies": {
"type": "array",
"items": {"type": "string"},
"description": "Task dependency relationships"
},
"agent_requirements": {
"type": "array",
"items": {"type": "string"},
"description": "Suggested agent types/skills"
}
},
"required": ["subtasks", "task_types", "priority_order", "estimated_effort", "dependencies", "agent_requirements"]
}
1,378 characters. ~345 tokens. Every. Single. Call.
For rich signatures, JSON Schema verbosity becomes a real cost. Each API call carries hundreds of tokens just describing the output structure.
BAML: The Simple Fix
DSPy.rb v0.28.2 adds BAML schema format support via the sorbet-baml
gem. BAML provides the same information compactly:
class TaskDecomposition {
subtasks string[]
task_types string[]
priority_order int[]
estimated_effort int[]
dependencies string[]
agent_requirements string[]
}
200 characters. ~50 tokens. 85.5% savings.
Same structure. Same type safety. Same validation. But 295 fewer tokens per call.
Verified Performance
From our integration tests across multiple signatures:
Token Comparison by Schema Format
Signature | JSON Schema | BAML Schema |
---|---|---|
TaskDecomposition (6 fields) | 345 tokens | 50 tokens |
ResearchExecution (6 fields) | 287 tokens | 49 tokens |
Aggregate (all tests) | 632 tokens | 99 tokens |
Side-by-side comparison: Red bars show JSON Schema token consumption, green bars show BAML Schema savings. TaskDecomposition: 85.5% reduction (345→50 tokens). ResearchExecution: 83.0% reduction (287→49 tokens). Aggregate: 84.4% reduction (632→99 tokens) across all tests.
Detailed Performance Breakdown
TaskDecomposition (6 fields):
- JSON Schema: 1,378 chars (~345 tokens)
- BAML Schema: 200 chars (~50 tokens)
- Savings: 85.5% (~295 tokens/call)
ResearchExecution (6 fields):
- JSON Schema: 1,148 chars (~287 tokens)
- BAML Schema: 195 chars (~49 tokens)
- Savings: 83.0% (~238 tokens/call)
Aggregate across all tests:
- JSON Schema: 2,526 chars (~632 tokens)
- BAML Schema: 395 chars (~99 tokens)
- Savings: 84.4% (~533 tokens/call)
Quality: 100% identical outputs across all tests.
No training needed. No optimization required. Your baseline signatures just got more efficient.
How to Use
One configuration change:
DSPy.configure do |c|
c.lm = DSPy::LM.new(
'openai/gpt-4o-mini',
schema_format: :baml
)
end
# Use any signature - BAML is automatic
predictor = DSPy::Predict.new(TaskDecomposition)
result = predictor.call(
topic: "Build user authentication",
context: "Focus on security best practices"
)
Works with all providers in Enhanced Prompting mode: OpenAI, Anthropic, Gemini, Ollama.
When It Matters
BAML shines with:
- Complex signatures (5+ fields, nested types)
- High-volume applications (the savings compound)
- Cost-sensitive projects (every token counts)
For simple 1-3 field signatures, the difference is negligible.
If you’re using OpenAI’s Structured Outputs mode (structured_outputs: true
), schemas are sent via API instead of in prompts - BAML has no effect there since schemas never appear in the prompt.
Requirements
The sorbet-baml
gem is automatically included with DSPy.rb:
# Gemfile
gem 'dspy'
BAML generation is automatic from your Sorbet type signatures - no additional setup needed.