Interview prep
RefactoringComposing prompts

Extract Versioned Template

Extrair Template Versionado

Extract Versioned Template is an AI refactoring for LLM systems, in the Composing prompts group. Gather scattered prompt fragments into one template with a name, version, and variables.

Element
05/35
Level
Beginner
Read
5 min
Updated
On this page 8
  1. 01Problem
  2. 02Solution
  3. 03Before and after
  4. 04Why it works
  5. 05Safe mechanics
  6. 06Risks
  7. 07Challenge
  8. 08Check your understanding
  9. ··Quick answers

Problem

The prompt is scattered in pieces across the code, unversioned, or tuned to one model in a way nobody can change safely. These are the Scattered Prompt Strings, Unversioned Prompt, and Model-Pinned Prompt smells.

Picture the company’s SDR. The first message to a lead is born in four files: the persona in persona.py, the tone in tone.py, the quarter’s campaign in campaigns/q3.py, and an if in writer.py that hides the price when the lead is enterprise. Nobody on the team can paste the full prompt into a chat and say “this is what the model sees”.

One Monday the reply rate drops. You open the trace and see only the model call. No name, no version, no rendered text. Which of the four files changed? Is the Q3 campaign still live in October? Did the price if fire for the wrong lead? Without an artifact, you are guessing.

The destination pattern for this refactoring is the Prompt Template Registry.

Solution

Gather the pieces into a single template, with a name, an immutable version, and declared variables. Code loads the template by name and version, fills in the variables, and records the version on every run.

Before and after

-# persona.py, tone.py, campaigns/q3.py, writer.py
-prompt = BASE + tone(lead.segment) + CAMPAIGN + f"Lead: {lead.name}. " \
-         + ("Do not mention price. " if lead.is_enterprise else "")
+# prompts/sdr_first_message/v14.yaml
+# name: sdr_first_message
+# version: 14
+# model: claude-sonnet-4-5
+# variables: [lead_name, company, tone, pricing_policy, campaign]
+# template: |
+#   You are Julia, a consultant at the Company.
+#   Tone: {{ tone }}.
+#   Active campaign: {{ campaign }}.
+#   Pricing policy for this lead: {{ pricing_policy }}.
+#   Lead: {{ lead_name }}, {{ company }}.
+
+tpl = prompts.get("sdr_first_message", version=14)
+prompt = tpl.render(
+    lead_name=lead.name, company=lead.company, tone=tone_for(lead.segment),
+    pricing_policy=pricing_policy_for(lead), campaign=active_campaign())
+trace.set("prompt.name", tpl.name)
+trace.set("prompt.version", tpl.version)

Before: the prompt only exists at run time.

flowchart LR
  A["persona.py"]:::tool --> J["1. Concatenation"]:::tool
  B["tone.py"]:::tool --> J
  C["campaign.py"]:::tool --> J
  J --> M(["2. Prompt with no name or version"]):::llm

After: a versioned template, filled with declared variables.

flowchart LR
  R[("1. Registry: sdr_first_message v14")]:::store --> T["2. Render with variables"]:::tool
  V(["Lead data"]):::io --> T
  T --> M(["3. Model"]):::llm
  M --> L[("4. Trace with name and version")]:::store
  1. The whole template lives in one place, with a name and a version.
  2. Code only supplies the values of the declared variables. Tone and pricing policy are computed in Python, not in the YAML.
  3. The model receives a prompt that can be read and reproduced.
  4. Each run records which version it used.

Worked example

Take a realistic lead: Marina, at Indústria Norte (a Brazilian manufacturer), segment manufacturing, enterprise contract. In the before, writer.py concatenates four strings and if lead.is_enterprise appends “Do not mention price”. The final text exists only in that instant.

In the after, tone_for("manufacturing") returns "formal", pricing_policy_for(lead) returns "Do not mention price or a range.", and active_campaign() returns the live campaign text. Template v14 receives five values and produces a reproducible prompt. The span looks like this:

prompt.name=sdr_first_message  prompt.version=14
lead_name=Marina  company=Indústria Norte  tone=formal

A week later the reply rate drops again. You filter the trace by prompt.version=14, grab the rendered prompt, and compare it with a case that used to work. If the text is identical, the problem is not the prompt: it is the lead, the model, or the channel. If it differs, you know exactly what changed.

Check +20 XP on first tryAn SDR message went out with an informal tone to an enterprise lead. The call's trace has no prompt.name and no prompt.version. What is the first diagnosis?

Why it works

A versioned template turns the prompt into an artifact: you can read it whole, review it in a pull request, compare two versions, and roll back. Declared variables make explicit what changes from one request to the next; everything else is fixed and testable.

The version on the trace closes the loop. A bad answer points to the exact text that produced it, and the eval can compare a new version with the previous one before the switch. Migrating models becomes creating a new template version for the new model and comparing the two, instead of rewriting scattered if model == ... checks.

There is a cache win too. Providers such as Anthropic and OpenAI cache the stable prefix of the prompt. If the persona, the base tone, and the rules sit at the start of the template, and only lead_name, campaign, and the date change at the end, the cache hits. Scattered concatenation mixes what changes with what does not, and the cache misses the prefix.

A common mistake: moving the Python if into the YAML. {{ "formal" if segment == "manufacturing" else "friendly" }} recreates Scattered Prompt Strings, now hidden inside the template. Compute tone in code and pass the result.

Safe mechanics

  1. Measure. Run the eval with the current prompt. Also keep a few really assembled prompts, to compare byte by byte later.
  2. Gather the pieces and write the complete prompt for each relevant combination (manufacturing enterprise, startup with visible pricing, and so on).
  3. Identify what actually varies and turn it into declared variables. Complex conditions become variables computed in code (pricing_policy, tone), not logic in the template.
  4. Create the template with a name, a version, and a variable list, somewhere with history. A file in the repo is enough. Tools such as LangSmith and Langfuse also version prompts, if the team already uses them.
  5. Replace the assembly with loading and rendering the template, failing if a variable is missing. Do not use a silent default: a missing variable is a bug, not “empty text”.
  6. Check that the rendered prompt matches the previously assembled one for the test combinations. Spaces and line breaks count.
  7. Record name and version on every span. If you can, also record a hash of the rendered text.
  8. Measure again with the eval. From here on, every change is a new version: v15, not an edit on top of v14.

How to version for real

Immutable version means: the file v14.yaml is never edited after it is published. A one-word tweak becomes v15.yaml. Production points at a number, not at latest.

When the model changes, you do not reuse v14 “and hope”. You create v15 with the text adjusted to the new model, run the eval on both, and only then promote. That is the treatment for Model-Pinned Prompt: the text and the model travel together, but out in the open.

Check +20 XP on first tryThe team put Tone: {{ 'formal' if segment == 'manufacturing' else 'friendly' }} in the YAML. The prompt now lives in a single file. Is the refactoring done?

Risks

  • Too much logic in the template. Ifs and loops inside the template recreate the problem. Compute in code and pass it as a variable.
  • Invisible differences. Spaces and line breaks change the rendering. Compare prompts before and after byte by byte on the test cases.
  • “Latest” in production. Always loading the newest version gives up control. Pin the version in production and promote new versions after the eval.
  • A variable that grows without a limit. If campaign can receive a whole PDF, the template becomes a bottomless bag. Declare type and max size in the registry, and reject values that miss.

Challenge

Which parts of these snippets become template variables, and which stay fixed?

prompt = "You answer HR questions for the Company. "
prompt += f"The employee's name is {emp.name} and they work in {emp.state}. "
if emp.state == "SP":
    prompt += "Account for the January 25 city holiday. "
prompt += "Be brief. Today is " + date.today().isoformat() + "."
Show solution

Fixed: the role, “be brief”. Variables: employee_name, local_holidays (a list computed in code from the state), and today. The date should go at the end of the prompt, or into the user message, so it does not break the prompt cache.

The if emp.state == "SP" does not go into the YAML. In code, something like holidays_for(emp.state) returns "January 25 (São Paulo)" or an empty list. São Paulo (the Brazilian state, abbreviated SP) has a city holiday that other states do not. The template only interpolates.

name: hr_assistant
version: 3
variables: [employee_name, local_holidays, today]
template: |
  You answer HR questions for the Company. Be brief.
  Employee: {{ employee_name }}.
  Local holidays to consider: {{ local_holidays }}.
  Today's date: {{ today }}.

Check your understanding

Check +20 XP on first tryProduction loads prompts.get('sdr_first_message', version='latest'). A pull request publishes v15, the eval has not run yet, and SDR messages change immediately. What is the mistake?

Check +20 XP on first tryYou are switching the SDR model from Sonnet to a smaller model. Template v14 was tuned on Sonnet. What is the safest next step?

Check +20 XP on first tryThe renderer finds campaign in the template, but the code did not pass that value. What should the code do?

Check +20 XP on first tryThe team wants to put today's date at the start of the system prompt, right after the persona, so the model 'knows what day it is'. The persona prefix is cached by the provider. What happens?

Quick answers

What is Extract Versioned Template?

Extract Versioned Template is an AI refactoring for LLM systems, in the Composing prompts group. Gather scattered prompt fragments into one template with a name, version, and variables.

What problem does Extract Versioned Template solve?

It treats Model-Pinned Prompt, Scattered Prompt Strings, and Unversioned Prompt.

Which pattern does Extract Versioned Template lead to?

The Prompt Template Registry design pattern.

Go further

Relations

Search