Overview
A production prompt is a function with named slots, not a free-form string. Separate the role, instructions, examples, retrieved context, and user input into delimited regions so the model can tell each apart, store the template with the code that calls it, and bump a version when the body changes.
Use one delimiter convention per template
Pick one convention and keep it for every template.
- XML tags (
<instructions>,<context>,<user_input>): recommended by Anthropic for Claude because they reduce misinterpretation when a prompt mixes instructions, context, examples, and variable input. OpenAI documents them too. - Markdown headings (
## Instructions): readable in diffs and editors; OpenAI documents Markdown headers as a boundary. ---fences: acceptable for short prompts; ambiguous when the body contains markdown.
Mixing conventions in one template reads as noise. Use consistent, descriptive tag names across all templates.
Ship one canonical skeleton
The system message holds the stable role and policy. The user message carries regions in an order that serves both caching and long-context quality: stable instructions and examples first, retrieved documents next, the user input last.
SYSTEM:
{role_and_policy}
USER:
<instructions>
{task_instructions}
{output_format_spec}
</instructions>
<examples>
{few_shot_examples}
</examples>
<context>
{retrieved_context}
</context>
<user_input>
{user_input}
</user_input>Keep empty regions as empty tags so the shape stays constant and diffs stay readable. A constant shape also keeps the prefix cacheable; see prompt-caching-strategies.
Fill named placeholders, never concatenate
Concatenating user text into the instruction region is the root cause of most prompt-injection bugs. The caller fills {user_input} and {retrieved_context} at call time; the model sees them only inside their own tags. Escape or encode untrusted values before substitution; see prompt-injection-defense. Single-brace placeholders collide with JSON in the template, so use a template engine or a distinct marker such as {{user_input}}.
Keep the system message stable
The system message holds role, policy, and the output contract, and should not change per request. Per-request data belongs in the user message. A stable system message is also what gets cached; see system-prompt.
Declare the output region
If the output is structured, declare the schema in the template and enforce it with provider structured outputs; see structured-output and output-constraints.
Store and version templates in the repo
prompts/
classifier.v3.prompt.md
classifier.v3.examples.json
evals/classifier.v3.cases.jsonA versioned filename lets two prompts run in parallel during a rollout, and the eval cases move with the prompt; see prompt-evals. Log the rendered prompt during development to catch unfilled placeholders.