DSPy Signatures anchor your app in a world where everything changes—prompting techniques, model families, even serialization formats. They’re the declarative contract for your prompt, so you never handcraft schemas or payloads again. JSON Schema and JSON payloads, however, bloat requests—especially when you’re shipping time-series data or long lists of structs that repeat every key. Starting today you can flip two symbols and keep Enhanced Prompting lean. Here’s the latest signature we used for the benchmark (now with nested structs and enums):

class TaskDecomposition < DSPy::Signature
  description "Autonomously analyze a research topic and define optimal subtasks with strategic prioritization"

  input do
    const :topic, String, description: "The main research topic to investigate"
    const :context, String, description: "Any additional context or constraints"
    const :complexity_level, ComplexityLevel,
      description: "Desired complexity level for task decomposition"
  end

  output do
    const :subtasks, T::Array[Task], description: "Autonomously defined research subtasks"
    const :task_types, T::Array[TaskType], description: "Type classification for each task"
    const :priority_order, T::Array[Integer], description: "Priority rankings (1-5 scale)"
    const :estimated_effort, T::Array[EstimatedEffortWithReasoning], description: "Effort estimates in hours with rationale"
    const :dependencies, T::Array[Task], description: "Task dependency relationships"
    const :agent_requirements, T::Array[String], description: "Suggested agent skills"
  end
end

The remaining cost has always been tokens: JSON Schema is verbose and JSON payloads repeat every key. Starting today, you can flip two symbols and trim Enhanced Prompting back down to size—even for signatures that emit nested structs, enums, and rationales.

TL;DR

DSPy.configure do |c|
  c.lm = DSPy::LM.new(
    'openai/gpt-4o-mini',
    api_key: ENV['OPENAI_API_KEY'],
    schema_format: :baml,
    data_format: :toon
  )
end

That’s it. Predictors, ChainOfThought, ReAct, and every DSPy module keep the same API; prompts just get cheaper.

Why TOON + BAML matters

Scenario JSON Schema + JSON Data BAML Schema + TOON Data
Signature guidance size 3,528 chars 608 chars
Sample input + output payload 2,063 chars 1,180 chars
Total prompt tokens (Enhanced Prompting) ~13,500 ~6,300

Source: examples/baml_vs_json_benchmark.rb, live run baml_benchmark_20251107_172759.json.

That reduction isn’t just abstract token math:

What the model feels

Where the savings show up

  1. Prediction prompts – any signature-backed Predict now emits TOON payloads, so even single-call apps get the 57% token cut.
  2. ReAct loops – every turn now shares tools, histories, and observations as TOON. Long multi-tool dialogues stop reprinting JSON hashes.
  3. Tool ecosystems – TOON preserves typing (thanks to Sorbet::Toon.decode), so tool outputs round-trip back into Sorbet structs without manual serialization glue.

FAQ

Do I need to use BAML and TOON together?
No. They’re independent toggles. Use schema_format: :baml when you want compact schema guidance, data_format: :toon when you want lean payloads. You can enable either one (or both) per LM.
Where’s the benchmarking code?
In examples/baml_vs_json_benchmark.rb. It ships with the repo and emits the same .json/.csv/.txt artifacts referenced here.
Does this rely on function calling or structured outputs?
No. Everything stays in Enhanced Prompting—you still write plain Predict, ChainOfThought, or ReAct code and parse completions the same way.
Can I combine TOON with provider-native structured outputs?
Not today. Provider structured outputs still expect JSON. TOON is purpose-built for Enhanced Prompting, so use it when you’re controlling the prompt yourself.
Will TOON break my ReAct tools or custom modules?
No. ReAct, toolsets, and other DSPy modules already understand data_format: :toon; they simply serialize histories, tools, and responses using Sorbet::Toon instead of JSON.

What’s the migration diff?

 DSPy.configure do |c|
-  c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
+  c.lm = DSPy::LM.new(
+    'openai/gpt-4o-mini',
+    api_key: ENV['OPENAI_API_KEY'],
+    schema_format: :baml,
+    data_format: :toon
+  )
 end

Flip the formats, keep your prompts declarative, and run TOON wherever Enhanced Prompting makes sense.