Prompt versioning and catalog management
On this page
Lesson updated November 3, 2026 based on OpenAI documentation:
temperatureandtop_pare only accepted with a reasoning effort ofnone, whereas GPT-5.6 models reason by default (medium). Calls that set these parameters must therefore specifyreasoning={"effort": "none"}(Responses API) orreasoning_effort="none"(Chat Completions). Sources: Using GPT-6 · GPT-5.6 Terra.
Prompt versioning and catalog management
In production, your prompts evolve just as much as your code. Without versioning, you lose track of changes, cannot roll back, and have no idea which version is running in production. This lesson shows you how to manage prompts as software artifacts.
Why version prompts
A prompt change can radically alter your application’s behavior. Unlike code, these changes are often subtle and hard to detect without automated tests: a rephrased sentence, a moved word, and the model starts misclassifying one in ten edge cases. No standard unit test will catch this gap, but support will flag it three weeks later as a vague complaint about misrouted tickets.
Versioning addresses four distinct needs. First, traceability: knowing who changed what and when turns a sterile team debate into a simple history check. Second, rollback: when a regression appears, the immediate goal isn’t to understand why but to restore known behavior. Third, performance comparison: this requires older versions to remain executable exactly as they were, with their original model and settings. Finally, auditing: for systems subject to the GDPR (General Data Protection Regulation) or other compliance frameworks, you must demonstrate which instructions were in force on a given date—a screenshot won’t suffice.
Catalog structure
The PromptCatalog class meets these needs with minimal infrastructure: one folder per prompt, one JSON file per version, numbered sequentially. Each file bundles the system prompt text along with the associated model and temperature—a prompt tuned to 0.1 and the same prompt at 0.7 are not the same thing, and locking them together prevents restoring text while forgetting its settings.
The status field tracks the lifecycle. A version starts as draft, allowing preparation and testing without affecting live traffic. Calling activate switches it to active and automatically demotes the previous version to deprecated, ensuring only one version serves traffic at any time. This method makes rollback trivial: reactivating a prior version is a single call, not a deployment. Your application calls get_active in production, while list_versions supports reviews and audits.
import json
from datetime import datetime
from pathlib import Path
class PromptCatalog:
"""Prompt catalog manager with versioning."""
def __init__(self, catalog_dir: str = "prompts/"):
self.catalog_dir = Path(catalog_dir)
self.catalog_dir.mkdir(parents=True, exist_ok=True)
def save_prompt(self, name: str, system_prompt: str,
model: str, temperature: float,
metadata: dict = None) -> str:
"""Saves a new version of a prompt."""
prompt_dir = self.catalog_dir / name
prompt_dir.mkdir(exist_ok=True)
# Determine version number
versions = sorted(prompt_dir.glob("v*.json"))
version = len(versions) + 1
version_str = f"v{version:03d}"
# Save
prompt_data = {
"name": name,
"version": version_str,
"created_at": datetime.now().isoformat(),
"model": model,
"temperature": temperature,
"system_prompt": system_prompt,
"metadata": metadata or {},
"status": "draft" # draft | active | deprecated
}
filepath = prompt_dir / f"{version_str}.json"
filepath.write_text(json.dumps(prompt_data, indent=2,
ensure_ascii=False))
return version_str
def get_prompt(self, name: str,
version: str = "latest") -> dict:
"""Retrieves a prompt by name and version."""
prompt_dir = self.catalog_dir / name
if version == "latest":
versions = sorted(prompt_dir.glob("v*.json"))
if not versions:
raise FileNotFoundError(f"Prompt '{name}' not found")
filepath = versions[-1]
else:
filepath = prompt_dir / f"{version}.json"
return json.loads(filepath.read_text())
def get_active(self, name: str) -> dict:
"""Retrieves the active version of a prompt."""
prompt_dir = self.catalog_dir / name
for filepath in sorted(prompt_dir.glob("v*.json"),
reverse=True):
data = json.loads(filepath.read_text())
if data["status"] == "active":
return data
raise FileNotFoundError(
f"No active version for '{name}'"
)
def activate(self, name: str, version: str):
"""Activates a version and deactivates others."""
prompt_dir = self.catalog_dir / name
for filepath in prompt_dir.glob("v*.json"):
data = json.loads(filepath.read_text())
if data["version"] == version:
data["status"] = "active"
elif data["status"] == "active":
data["status"] = "deprecated"
filepath.write_text(json.dumps(data, indent=2,
ensure_ascii=False))
def list_versions(self, name: str) -> list[dict]:
"""Lists all versions of a prompt."""
prompt_dir = self.catalog_dir / name
versions = []
for filepath in sorted(prompt_dir.glob("v*.json")):
data = json.loads(filepath.read_text())
versions.append({
"version": data["version"],
"status": data["status"],
"created_at": data["created_at"],
"model": data["model"]
})
return versions
Using the catalog in your application
On the caller side, the rule is simple: your code contains no prompt text; it requests the active version from the catalog. This keeps prompts modifiable without touching application code, and prevents thirty-line strings from getting buried in the middle of a function. Metadata saved during storage—author, description, changelog—has no effect on execution but makes the history readable six months later, when no one remembers why the wording changed.
The critical detail lies at the end of classifier_ticket: the returned response includes prompt_version. Log this field with every result, and incident analysis becomes possible—you’ll know that Tuesday afternoon’s misclassified tickets all came from v004, without cross-referencing deployment timestamps.
from openai import OpenAI
client = OpenAI()
catalog = PromptCatalog("prompts/")
# Save a prompt
catalog.save_prompt(
name="ticket-classifier",
system_prompt="""You are a support ticket classifier.
Categorize each ticket as: bug, feature, question, billing.
Respond in JSON with category, priority (1-5), summary.""",
model="gpt-5.6-terra",
temperature=0.1,
metadata={
"author": "marie.dupont",
"description": "Automatic classification of Zendesk tickets",
"changelog": "Initial version"
}
)
# In production: use the active version
def classifier_ticket(ticket_text: str) -> dict:
"""Classifies a ticket using the active prompt."""
prompt_config = catalog.get_active("ticket-classifier")
response = client.responses.create(
model=prompt_config["model"],
reasoning={"effort": "none"}, # temperature requires effort none
instructions=prompt_config["system_prompt"],
input=ticket_text,
temperature=prompt_config["temperature"]
)
return {
"result": json.loads(response.output_text),
"prompt_version": prompt_config["version"]
}
Versioning with Git
For teams, store your prompts in a dedicated Git repository. The tree below follows a simple convention: one folder per prompt, containing its definition, test cases, and changelog. Keeping tests alongside the prompt they verify isn’t cosmetic—a pull request modifies both together, and the reviewer judges the change on evidence rather than declared intent.
prompts/
ticket-classifier/
prompt.yaml
tests/
test_cases.json
CHANGELOG.md
invoice-extractor/
prompt.yaml
tests/
test_cases.json
CHANGELOG.md
YAML replaces JSON here for a specific reason: prompts are multiline texts, and the | syntax displays them exactly as sent, without escaping or stray \n characters. A Git diff on this file reads like a prose diff—the reviewer sees that a sentence was added. The output schema travels in the same file, so the complete configuration for a call fits in a single artifact.
# prompts/ticket-classifier/prompt.yaml
name: ticket-classifier
model: gpt-5.6-terra
temperature: 0.1
max_output_tokens: 500
system_prompt: |
You are a support ticket classifier.
Categorize each ticket as: bug, feature, question, billing.
Respond in JSON with category, priority (1-5), summary.
schema:
type: object
properties:
category:
type: string
enum: [bug, feature, question, billing]
priority:
type: integer
summary:
type: string
required: [category, priority, summary]
additionalProperties: false
Best practices
What separates a living catalog from an archive folder comes down to a few housekeeping rules. Keep one prompt per file: mixing multiple prompts in a single document makes diffs unreadable and rollbacks impossible to target. Enforce a changelog documenting not what changed—the diff shows that—but why: “added instruction on multilingual tickets following support feedback” is worth ten lines of technical description.
Systematically associate a test suite with each prompt; otherwise, users will be the first to tell you when a modification breaks established behavior. Route prompt changes through the same code review process as the code itself: a prompt that goes to production without review produces the same surprises as unreviewed code. Verify that rollback takes less than a minute, and test it on a quiet day rather than during an incident. Finally, mark deployed versions with tags to unambiguously link observed behavior to a specific repository state.
Testing the catalog with a rollback
Set up a PromptCatalog for three real prompts from your application, then record two or three versions of each by evolving the instructions in small steps—a tone instruction, an added category. Versioning proves its worth on these fine-grained changes, because they’re the ones you won’t remember making. Set up activation and deactivation, and attach a test suite to each prompt.
Finish with the most instructive exercise: deliberately activate a degraded version, observe the regression via tests, and execute the rollback while timing it. If the operation exceeds one minute, fix the procedure before adding a single prompt to the catalog—a slow rollback isn’t a rollback, it’s a prolonged incident.
Key takeaways
- Prompts are software artifacts that require versioning
- Each version is a JSON or YAML file with metadata
- Status (draft, active, deprecated) enables gradual deployment
- Store prompts in Git with tests and a changelog
- Rollback must be instant in case of regression