An optimizer can spend minutes and provider tokens finding better instructions and demonstrations. DSPy.rb v0.20.0 added storage primitives so applications do not have to discard that work.
There are two APIs. They solve different problems.
Save Module State to JSON
Every DSPy::Module has save(path). It writes the module’s to_h result as formatted JSON and creates the parent directory when needed.
program.save("artifacts/support_classifier.json")
The base implementation only records the class name and an empty state:
{
class_name: "SupportClassifier",
state: {}
}
Useful persistence therefore depends on the module. Built-in modules can override to_h; custom modules should serialize the state required to reproduce their behavior.
Module#save does not provide a corresponding universal Module.load. Ruby cannot safely reconstruct an arbitrary object graph from a class name. Treat the file as a state artifact and implement an explicit loader for your module when you need one.
Store Optimization Results
DSPy::Storage::ProgramStorage keeps program state, optimization metadata, and a history index:
require "dspy/storage/program_storage"
storage = DSPy::Storage::ProgramStorage.new(
storage_path: "./dspy_storage"
)
saved = storage.save_program(
optimized_program,
optimization_result,
metadata: {
dataset: "support-v2",
git_sha: ENV["GIT_SHA"]
}
)
puts saved.program_id
The storage directory contains one JSON file per program under programs/ and a history.json index. list_programs and get_history read that index.
storage.list_programs.each do |entry|
puts [entry[:program_id], entry[:best_score], entry[:saved_at]].join("\t")
end
Loading a Program
loaded = storage.load_program(saved.program_id)
if loaded
program = loaded.program
result = loaded.optimization_result
metadata = loaded.metadata
end
Loading is deliberately constrained. The stored class_name must resolve to a loaded Ruby constant, and that class must implement .from_h.
ProgramStorage does not call an arbitrary module’s to_h. Its current serializer extracts a small common state: the signature class name, prompt instruction, and few-shot examples when the program exposes them. Built-in program classes that implement compatible .from_h methods can reconstruct that state. A custom module with additional instance state needs its own persistence boundary; Module#save plus an explicit loader is the clearer option.
Persist model identifiers, instructions, demonstrations, and configuration that affect behavior. Do not serialize credentials.
load_program returns nil when the file is absent or deserialization fails. If an application must distinguish those cases, validate the artifact and class compatibility before deployment rather than relying on a late load.
Export, Import, and Deletion
Program storage can move selected artifacts between files and remove old entries:
storage.export_programs([saved.program_id], "release/programs.json")
storage.import_programs("release/programs.json")
storage.delete_program(saved.program_id)
An export is JSON, not a self-contained Ruby package. The destination still needs the same program class and compatible .from_h behavior.
Storage Manager
DSPy::Storage::StorageManager wraps ProgramStorage for optimizer results. It can auto-save an optimized_program, find saved programs by score, age, tags, optimizer, or signature class, and retain a configured number of artifacts.
require "dspy/storage/storage_manager"
config = DSPy::Storage::StorageManager::StorageConfig.new
config.storage_path = "./dspy_storage"
config.max_stored_programs = 25
manager = DSPy::Storage::StorageManager.new(config: config)
manager.save_optimization_result(
optimization_result,
tags: ["support", "candidate"],
description: "GEPA run on support-v2"
)
This is filesystem storage with a JSON history file. It has no locking or transactional database behind it. Coordinate writers yourself, and use object storage or a database when several processes must publish artifacts concurrently.
What to Version
A saved prompt artifact is only one part of an LM program. Record enough context to explain a result later:
- DSPy.rb and Ruby versions
- model identifier and provider configuration
- optimizer and metric
- dataset or fixture version
- application commit
- custom module serialization version
ProgramStorage records DSPy.rb and Ruby versions automatically. The application must supply the rest as metadata.
Evaluate an artifact before promoting it, and evaluate the loaded artifact again to catch incompatible code or serialization changes.