Skip to main content
Lesson 14 of 16 · Prompt Engineering for Developers

Prompt versioning and catalog management

Lesson 14 of 16 Updated November 3, 2026 Developer
On this page
  1. Prompt versioning and catalog management
  2. Why version prompts
  3. Catalog structure
  4. Using the catalog in your application
  5. Versioning with Git
  6. Best practices
  7. Testing the catalog with a rollback
  8. Key takeaways

Lesson updated November 3, 2026 based on OpenAI documentation: temperature and top_p are only accepted with a reasoning effort of none, whereas GPT-5.6 models reason by default (medium). Calls that set these parameters must therefore specify reasoning={"effort": "none"} (Responses API) or reasoning_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