{"course":{"slug":"building-with-ai","title":"Building with AI","url":"https://addaly.com/learn/building-with-ai","level":"intermediate","track":"build"},"lesson":{"slug":"prompts-in-version-control","title":"Prompts belong in version control","url":"https://addaly.com/learn/building-with-ai/prompts-in-version-control","position":18,"of":80,"minutes":8,"answer":"A prompt is source code that ships behaviour, so version it in files, render it through a strict template that escapes your own delimiters, log the rendered bytes with the model string.","keyIdea":"A prompt is source code that ships behaviour, so version it in files, render it through a strict template that escapes your own delimiters, log the rendered bytes with the model string, and diff two versions over the same cases before switching."},"body":{"format":"markdown","text":"## A prompt change is a deploy\n\nEditing a prompt changes the behaviour of your product for every user immediately. That is the definition of a deploy, and it deserves the same machinery: a diff, a review, a version number, a test run, and a way back.\n\nWhat happens instead, almost everywhere, is that prompts live as string literals scattered through the codebase, or in a spreadsheet, or in a vendor's web console where there is no history at all. Then quality drops, nobody can say what changed, and the honest answer to \"what was the prompt on Tuesday?\" is that it is gone.\n\n## Files, not literals\n\nPut each prompt in its own file, with a version in the name or in front matter:\n\n```\nprompts/\n  support_reply.v4.md\n  classify_ticket.v2.md\n  extract_invoice.v7.md\n```\n\nA markdown file diffs cleanly, reviews properly in a pull request, and can carry a short header saying what changed and why. Load it at startup, cache it in memory, and pin the version in your configuration so a rollback is a one-line change.\n\nThe pin matters. `support_reply.latest` is how you get a change you did not intend on a Friday evening. An explicit `v4` means the version in production is a fact recorded in the repository.\n\n## Templating, with the escaping done properly\n\nUse a real template engine (Jinja, Handlebars, whatever your stack has) rather than f-strings and concatenation.\n\n```jinja\n{% raw %}You answer strictly from the passages below.\n\n<passages>\n{% for p in passages %}[{{ p.id }}] {{ p.text }}\n{% endfor %}</passages>\n\n<question>{{ question }}</question>{% endraw %}\n```\n\nTwo real dangers this addresses.\n\n**Injection at the template layer.** If a user's message itself contains `</passages>` or `{{ something }}`, naive substitution lets them close your structure or reach your template variables. Templating with autoescaping off is fine for prompts, but you must still strip or escape your own delimiter strings from user input. It is a two-line function and it removes the cheapest attack there is.\n\n**Silent empty variables.** An f-string that receives `None` produces the word `None` in the middle of your prompt. A template engine configured to be strict raises instead. A prompt that silently renders `Customer name: None` will produce answers addressed to a customer called None, and it will do it in production for a week before anyone notices.\n\n## Log the rendered prompt, not the template\n\nWhen an answer is wrong, the template tells you almost nothing. What you need is the exact bytes that were sent.\n\nStore, per call: the prompt version, the model string returned by the API, the rendered prompt (or a hash of it plus the variables), the response id, and token usage. That record is what turns \"the assistant said something strange yesterday\" from a shrug into a five-minute investigation.\n\nOne caution: rendered prompts contain user data, so they inherit your retention and privacy rules. Truncate, redact identifiers, and set an expiry. A prompt log that keeps customer messages forever is a data-protection problem wearing a debugging costume.\n\n## Changing a prompt safely\n\nThe procedure is short and almost nobody follows it:\n\n1. Copy `v4` to `v5` and edit `v5`. Never edit a version that is live.\n2. Run both versions over the same fixed set of cases.\n3. Compare on the metrics you defined, plus cost and latency, plus the specific case you were trying to fix.\n4. Look at the cases that changed from correct to incorrect. There are always some.\n5. Ship behind a flag, at a small percentage, with the old version one config change away.\n\nStep four is the one that pays. A prompt edit that fixes eight failures and breaks five is a change most teams would ship without knowing, because they only looked at the eight.\n\n## Two habits that compound\n\n**Never edit a prompt in the vendor console.** A playground is for exploring. The moment a prompt is used by anything, its home is the repository, and the console version is a copy that will silently diverge.\n\n**Write the reason in the commit message.** `support_reply v4: refuse to quote prices not on the site, after ticket #8812`. Six months later, the person wondering why that odd line exists is you, and the alternative to a commit message is deleting it and finding out.\n\nNone of this is sophisticated. It is the ordinary discipline of treating a prompt as source code, which it is: it is the part of your program that determines what your product says."},"check":{"question":"Answers degrade after a prompt edit, and the team cannot reproduce the old behaviour. Which practice would have prevented this most directly?","options":[{"text":"Logging every response's token usage so cost changes are visible.","correct":false,"tempting":"Captures spend rather than behaviour, and would show that something changed without letting anyone restore what was there before."},{"text":"Keeping the prompt in the vendor console where non-engineers can edit it quickly.","correct":false,"tempting":"Optimises for speed of change while removing history, review and rollback, which is precisely the failure described."},{"text":"Creating a new versioned prompt file rather than editing the live one, with the version pinned in config so the previous behaviour is one change away.","correct":true},{"text":"Adding more examples to the prompt so its behaviour is more stable.","correct":false,"tempting":"Might reduce variance in outputs but does nothing to recover a previous version that was never recorded."}]},"prev":{"slug":"steering-refusals-and-tone","title":"Scope, refusals, and a voice that stays put","url":"https://addaly.com/learn/building-with-ai/steering-refusals-and-tone"},"next":{"slug":"writing-for-many-languages","title":"Building for users who do not write in one language","url":"https://addaly.com/learn/building-with-ai/writing-for-many-languages"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Prompts belong in version control', in Building with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-with-ai/prompts-in-version-control (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Prompts belong in version control","courseTitle":"Building with AI","module":{"index":2,"slug":"prompting-as-engineering","title":"Prompting as engineering"},"answer":null,"keyIdea":"A prompt is source code that ships behaviour, so version it in files, render it through a strict template that escapes your own delimiters, log the rendered bytes with the model string, and diff two versions over the same cases before switching.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"a-prompt-change-is-a-deploy","runs":[{"text":"A prompt change is a deploy"}]},{"t":"p","runs":[{"text":"Editing a prompt changes the behaviour of your product for every user immediately. That is the definition of a deploy, and it deserves the same machinery: a diff, a review, a version number, a test run, and a way back."}]},{"t":"p","runs":[{"text":"What happens instead, almost everywhere, is that prompts live as string literals scattered through the codebase, or in a spreadsheet, or in a vendor’s web console where there is no history at all. Then quality drops, nobody can say what changed, and the honest answer to “what was the prompt on Tuesday?” is that it is gone."}]},{"t":"h2","id":"files-not-literals","runs":[{"text":"Files, not literals"}]},{"t":"p","runs":[{"text":"Put each prompt in its own file, with a version in the name or in front matter:"}]},{"t":"code","lang":"","label":"Text","text":"prompts/\n  support_reply.v4.md\n  classify_ticket.v2.md\n  extract_invoice.v7.md"},{"t":"p","runs":[{"text":"A markdown file diffs cleanly, reviews properly in a pull request, and can carry a short header saying what changed and why. Load it at startup, cache it in memory, and pin the version in your configuration so a rollback is a one-line change."}]},{"t":"p","runs":[{"text":"The pin matters. "},{"text":"support_reply.latest","code":true},{"text":" is how you get a change you did not intend on a Friday evening. An explicit "},{"text":"v4","code":true},{"text":" means the version in production is a fact recorded in the repository."}]},{"t":"h2","id":"templating-with-the-escaping-done-properly","runs":[{"text":"Templating, with the escaping done properly"}]},{"t":"p","runs":[{"text":"Use a real template engine (Jinja, Handlebars, whatever your stack has) rather than f-strings and concatenation."}]},{"t":"code","lang":"jinja","label":"Jinja","text":"{% raw %}You answer strictly from the passages below.\n\n<passages>\n{% for p in passages %}[{{ p.id }}] {{ p.text }}\n{% endfor %}</passages>\n\n<question>{{ question }}</question>{% endraw %}"},{"t":"p","runs":[{"text":"Two real dangers this addresses."}]},{"t":"p","runs":[{"text":"Injection at the template layer.","b":true},{"text":" If a user’s message itself contains "},{"text":"</passages>","code":true},{"text":" or "},{"text":"{{ something }}","code":true},{"text":", naive substitution lets them close your structure or reach your template variables. Templating with autoescaping off is fine for prompts, but you must still strip or escape your own delimiter strings from user input. It is a two-line function and it removes the cheapest attack there is."}]},{"t":"p","runs":[{"text":"Silent empty variables.","b":true},{"text":" An f-string that receives "},{"text":"None","code":true},{"text":" produces the word "},{"text":"None","code":true},{"text":" in the middle of your prompt. A template engine configured to be strict raises instead. A prompt that silently renders "},{"text":"Customer name: None","code":true},{"text":" will produce answers addressed to a customer called None, and it will do it in production for a week before anyone notices."}]},{"t":"h2","id":"log-the-rendered-prompt-not-the-template","runs":[{"text":"Log the rendered prompt, not the template"}]},{"t":"p","runs":[{"text":"When an answer is wrong, the template tells you almost nothing. What you need is the exact bytes that were sent."}]},{"t":"p","runs":[{"text":"Store, per call: the prompt version, the model string returned by the API, the rendered prompt (or a hash of it plus the variables), the response id, and token usage. That record is what turns “the assistant said something strange yesterday” from a shrug into a five-minute investigation."}]},{"t":"p","runs":[{"text":"One caution: rendered prompts contain user data, so they inherit your retention and privacy rules. Truncate, redact identifiers, and set an expiry. A prompt log that keeps customer messages forever is a data-protection problem wearing a debugging costume."}]},{"t":"h2","id":"changing-a-prompt-safely","runs":[{"text":"Changing a prompt safely"}]},{"t":"p","runs":[{"text":"The procedure is short and almost nobody follows it:"}]},{"t":"ol","start":1,"items":[[{"text":"Copy "},{"text":"v4","code":true},{"text":" to "},{"text":"v5","code":true},{"text":" and edit "},{"text":"v5","code":true},{"text":". Never edit a version that is live."}],[{"text":"Run both versions over the same fixed set of cases."}],[{"text":"Compare on the metrics you defined, plus cost and latency, plus the specific case you were trying to fix."}],[{"text":"Look at the cases that changed from correct to incorrect. There are always some."}],[{"text":"Ship behind a flag, at a small percentage, with the old version one config change away."}]]},{"t":"p","runs":[{"text":"Step four is the one that pays. A prompt edit that fixes eight failures and breaks five is a change most teams would ship without knowing, because they only looked at the eight."}]},{"t":"h2","id":"two-habits-that-compound","runs":[{"text":"Two habits that compound"}]},{"t":"p","runs":[{"text":"Never edit a prompt in the vendor console.","b":true},{"text":" A playground is for exploring. The moment a prompt is used by anything, its home is the repository, and the console version is a copy that will silently diverge."}]},{"t":"p","runs":[{"text":"Write the reason in the commit message.","b":true},{"text":" "},{"text":"support_reply v4: refuse to quote prices not on the site, after ticket #8812","code":true},{"text":". Six months later, the person wondering why that odd line exists is you, and the alternative to a commit message is deleting it and finding out."}]},{"t":"p","runs":[{"text":"None of this is sophisticated. It is the ordinary discipline of treating a prompt as source code, which it is: it is the part of your program that determines what your product says."}]}],"check":{"question":[{"text":"Answers degrade after a prompt edit, and the team cannot reproduce the old behaviour. Which practice would have prevented this most directly?"}],"options":[{"text":[{"text":"Logging every response’s token usage so cost changes are visible."}],"correct":false,"tempting":[{"text":"Captures spend rather than behaviour, and would show that something changed without letting anyone restore what was there before."}]},{"text":[{"text":"Keeping the prompt in the vendor console where non-engineers can edit it quickly."}],"correct":false,"tempting":[{"text":"Optimises for speed of change while removing history, review and rollback, which is precisely the failure described."}]},{"text":[{"text":"Creating a new versioned prompt file rather than editing the live one, with the version pinned in config so the previous behaviour is one change away."}],"correct":true,"tempting":null},{"text":[{"text":"Adding more examples to the prompt so its behaviour is more stable."}],"correct":false,"tempting":[{"text":"Might reduce variance in outputs but does nothing to recover a previous version that was never recorded."}]}],"order":[1,2,0,3],"why":null},"prev":{"slug":"steering-refusals-and-tone","title":"Scope, refusals, and a voice that stays put","minutes":8},"next":{"slug":"writing-for-many-languages","title":"Building for users who do not write in one language","minutes":9}}}