Prompts Are Code: How to Version, Test, and Deploy Them
Your AI feature has a 200-line system prompt living in a string in app.py. That's tech debt. Here's how to treat prompts like first-class artifacts.
Your team's flagship AI feature is powered by a 200-line system prompt. It lives in a string literal in app.py. Every change is a code deploy. Nobody knows who edited it last. There are three commented-out variants from previous experiments.
This is the natural state of prompts at most companies. It's also the source of half their AI feature regressions.
Prompts are not strings. They're behavior specifications. Treat them like code: version them, test them, deploy them with care.
The problem with prompts in code
A prompt embedded in source code has these issues:
- Code review friction. A 50-line prompt change in a PR is hard to review next to a 5-line code change.
- No A/B testing. Switching prompts requires a deploy. Slow iteration.
- No rollback. If the new prompt regresses, you ship a fix and redeploy.
- Mixed concerns. Prompt engineers (often non-eng) can't iterate without bothering an engineer.
- Hidden in diffs.
git logfor the file is mixed code/prompt. Hard to see prompt history alone.
Some of these are tooling problems. Some are organizational.
Three approaches
1. Inline strings. Default. Tolerable for simple prompts.
2. Separate files. Prompts as .txt or .md files in the repo, loaded at runtime.
3. Prompt registry. Hosted service (LangSmith, PromptLayer, Helicone, or homegrown) where prompts have versions, deploys, and metrics.
Pick based on team size and prompt complexity.
The minimum: separate files
For most teams, this is enough:
/prompts
├── customer_support_v1.md
├── code_reviewer_v1.md
└── summarizer_v1.md
Loader:
import { readFileSync } from 'fs';
const promptCache = new Map<string, string>();
export function loadPrompt(name: string): string {
if (!promptCache.has(name)) {
promptCache.set(name, readFileSync(`prompts/${name}.md`, 'utf-8'));
}
return promptCache.get(name)!;
}
Benefits:
- Reviewable as standalone files
git logshows prompt-only history- Non-engineers can edit (just read/write a markdown file)
- Easy to reuse (same prompt across services)
This costs you 30 minutes to set up. Pays back forever.
Variables in prompts
Prompts often need dynamic values. Don't string-concat — use a template engine.
import Mustache from 'mustache';
const template = loadPrompt('customer_support');
const rendered = Mustache.render(template, {
customer_name: 'Sarah',
account_tier: 'pro',
recent_orders: orders,
});
Template:
You are a support agent for Acme Inc.
Customer: {{customer_name}}
Tier: {{account_tier}}
Recent orders:
{{#recent_orders}}
- {{id}}: {{status}}
{{/recent_orders}}
Help them.
Benefits over string concat:
- Variables visible at the top
- Engine errors on missing values (catches bugs early)
- Diff-friendly
Versioning
Once prompts are files, version them deliberately. Two patterns:
Pattern 1: filename versioning.
customer_support_v1.md
customer_support_v2.md
customer_support_v3.md (current)
Code references _v3. Old versions stay around for rollback.
Pattern 2: git tags.
git tag prompts/customer_support/v3
Code reads from a deployed bundle that has a specific version baked in.
Pattern 1 is simpler. Pattern 2 is cleaner but requires more tooling.
Prompt registry: when scale demands it
For larger teams (>10 prompts, multiple non-engineers iterating, frequent A/B tests):
A prompt registry is a hosted service that:
- Stores prompts with version history
- Supports A/B testing (route X% of traffic to v3, X% to v4)
- Tracks metrics per prompt version (latency, cost, eval scores)
- Allows updates without code deploys
Options:
- LangSmith / Langfuse — popular OSS-friendly options
- PromptLayer — purpose-built
- Helicone — proxy-based, good observability
- Roll your own — a DB table with versions + an API. Surprisingly easy.
Simple homegrown:
CREATE TABLE prompts (
name TEXT NOT NULL,
version INT NOT NULL,
content TEXT NOT NULL,
active BOOLEAN DEFAULT FALSE,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (name, version)
);
Loader checks active versions. Frontend lets PMs/prompt engineers create new versions and toggle active.
Testing prompts
Prompts need evals (covered in another post). Key requirements:
- Run on every prompt change (CI step)
- Compare new version vs. current production version
- Block merge if quality drops on critical metrics
# .github/workflows/eval-prompts.yml
on:
pull_request:
paths: ['prompts/**']
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm run eval -- --baseline=main --candidate=HEAD
- uses: actions/upload-artifact@v4
with:
name: eval-results
path: eval-output/
The eval comments on the PR with score deltas. You see the regression before merging.
Deployment strategies
Three patterns:
Big bang. New prompt replaces old. Fast iteration, full risk.
Canary. 1% → 10% → 50% → 100% over hours/days. Catches regressions you didn't catch in eval.
Shadow. New prompt runs alongside old; only old's output is shown to users. Compare outputs offline. Slower but very safe.
For high-stakes prompts (legal, financial, customer-facing): canary at minimum. For internal tools: big bang is fine.
The audit trail problem
Six months from now, you'll need to answer "what prompt was running when this customer got that response?" The answer requires logging:
- Request ID
- Prompt name + version
- Input (the user's message)
- Rendered prompt (with variables filled in)
- Model + parameters
- Output
This is a lot of data. Sample it (10% logging is usually fine) or store cheaply (S3 + Athena).
When a customer complains about an AI response, you can pull the exact prompt that was used. Without this, you're guessing.
Who edits prompts
This is an organizational question more than a technical one. Three models:
Engineers only. Default at small teams. Slow iteration but high quality.
Engineer-mediated. PMs / prompt engineers write Markdown changes; engineer reviews and merges. Decent balance.
Direct. Non-engineers edit prompts in a registry. Engineers review changes asynchronously. Fastest, requires good guardrails (eval CI, canary deploys).
Most teams should start at #2 and graduate to #3 as confidence grows.
The takeaway
A prompt embedded in code is tech debt. Pull it into a file, version it, test it on every change, deploy it with care. The investment is small (a day) and the leverage is huge — your prompt iteration loop goes from days to hours, with fewer regressions slipping into production.
Work with me
I consult with engineering teams on AI adoption, cloud architecture, and engineering effectiveness. If this post surfaced a challenge you're facing, let's talk.
Get in touch →Related posts
Explore more on these topics: