Reusable App Instructions
Writing Reusable Application-Level Instructions
Once system instructions are separated from user data (Lesson 1), the next engineering problem appears quickly in any real application: the same instructions text gets needed in more than one place, and slightly different variants of it start to multiply. A web handler, a background worker, and a CLI debugging script all need "the support assistant" instructions. Left unmanaged, each one copies the string, and within a few months there are four slightly different versions of what should have been one policy. This lesson covers how to structure instructions so they are written once, reused correctly, and changed safely.
Why a Single Prompt String Does Not Scale
A single hardcoded instructions string works for a demo or a script with one call site. It stops working as soon as any of the following becomes true:
- More than one function or module needs the same instructions.
- The instructions need to vary slightly by context (for example, a stricter tone for a public-facing chatbot versus an internal tool using the same underlying task).
- The instructions need to be tested, reviewed, or changed independently of the code that calls the model.
The underlying reason is ordinary software engineering, not anything specific to language models: a value duplicated across multiple locations will eventually drift out of sync, because someone will edit one copy without realizing there are others. Instructions text is no different from a configuration constant or a SQL query string in this respect — it needs exactly one authoritative definition.
Organizing Instructions as a Module
The simplest fix that scales well for small-to-medium applications is a dedicated module that holds instruction constants, functions, or classes — nothing calling the OpenAI SDK directly:
# app/prompts/support.py
BILLING_ASSISTANT_INSTRUCTIONS = """You are a billing support assistant for Acme Cloud.
Rules:
- Only answer questions about billing, invoices, and subscription plans.
- If asked about anything else, say you can only help with billing topics
and suggest contacting general support.
- Keep responses under 150 words.
- Never invent specific dollar amounts; if you don't have the customer's
actual invoice data, ask for it instead of guessing.
"""
TECHNICAL_ASSISTANT_INSTRUCTIONS = """You are a technical support assistant for Acme Cloud.
Rules:
- Only answer questions about API usage, authentication, and error codes.
- Provide code examples in Python when relevant.
- If the question requires access to account-specific data you do not have,
say so explicitly rather than guessing.
"""
Callers import the constant they need instead of retyping instructions inline:
# app/handlers/billing_handler.py
from openai import OpenAI
from app.prompts.support import BILLING_ASSISTANT_INSTRUCTIONS
client = OpenAI()
def handle_billing_message(user_message: str) -> str:
response = client.responses.create(
model="gpt-5.6-terra",
instructions=BILLING_ASSISTANT_INSTRUCTIONS,
input=user_message,
)
return response.output_text
This is a small change in mechanics but a significant one in effect: app/prompts/support.py becomes the single file a reviewer opens to see every instruction the application sends, and the single file a writer edits to change assistant behavior. A pull request that changes BILLING_ASSISTANT_INSTRUCTIONS shows up as a clean diff against one well-defined string, instead of as scattered edits across every handler that happened to embed a copy.
Why Explicit Structure Inside the Instructions Matters
Beyond just centralizing the text, the internal structure of an instructions string affects how reliably the model follows it, and how maintainable the string is for humans editing it later. Compare an unstructured paragraph to a structured one:
# Harder to maintain and less reliable
UNSTRUCTURED = "You are a billing assistant, only answer billing questions, keep it short, and don't make up numbers, be polite too."
# Easier to maintain and more reliable
STRUCTURED = """You are a billing support assistant for Acme Cloud.
Scope:
- Billing, invoices, subscription plans only.
Constraints:
- Responses under 150 words.
- Never invent specific dollar amounts.
Tone:
- Polite and professional.
"""
The structured version works better for two independent reasons. First, models are trained on a large volume of structured technical text (documentation, configuration files, specifications), and tend to follow itemized, labeled constraints more consistently than a single run-on sentence carrying the same information — the itemization reduces ambiguity about which words are separate requirements versus incidental phrasing. Second, and just as important for a codebase, itemized structure is easier for a human to diff, review, and extend: adding a new constraint is a one-line addition to a list, not a rewrite of a paragraph's syntax to fit a new clause in.
Parameterizing Instructions Without Turning Them Into Templates
Sometimes instructions need small, controlled variation rather than being copy-pasted into near-duplicate constants. A function that returns instructions text, rather than a bare constant, handles this without introducing full templating machinery (Lesson 3 covers templating for cases with many variable slots):
def build_support_instructions(*, max_words: int = 150, allow_code: bool = False) -> str:
lines = [
"You are a billing support assistant for Acme Cloud.",
"",
"Scope:",
"- Billing, invoices, subscription plans only.",
"",
"Constraints:",
f"- Responses under {max_words} words.",
"- Never invent specific dollar amounts.",
]
if allow_code:
lines.append("- You may include short Python examples for API-related billing questions.")
return "\n".join(lines)
instructions = build_support_instructions(max_words=100, allow_code=True)
This keeps a single function as the source of truth while allowing controlled variation for different deployment contexts (an internal tool that wants code examples versus a public chat widget that does not). The key design decision is that variation is expressed as explicit, named parameters with defaults — max_words, allow_code — not as arbitrary string interpolation. A caller reading build_support_instructions(allow_code=True) immediately understands what varies; a caller reading a raw f-string with six interpolated values does not.
Composing Instructions From Shared Fragments
Larger applications often have instructions that share a common foundation — a company-wide tone policy, a shared safety disclaimer, a shared output-format rule — combined with task-specific rules. Rather than duplicating the shared portion into every instructions constant, compose it:
COMPANY_TONE_POLICY = (
"Always respond in a professional, concise tone. "
"Do not use exclamation points or emojis."
)
SAFETY_DISCLAIMER = (
"If the user describes a medical, legal, or financial emergency, "
"advise them to contact a qualified professional immediately."
)
def build_instructions(task_rules: str) -> str:
return "\n\n".join([COMPANY_TONE_POLICY, SAFETY_DISCLAIMER, task_rules])
BILLING_INSTRUCTIONS = build_instructions(
"You are a billing assistant. Only answer billing questions."
)
REFUND_INSTRUCTIONS = build_instructions(
"You are a refund-policy assistant. Explain refund eligibility clearly."
)
When the company tone policy changes — say, legal asks for an added compliance line — it changes in exactly one place (SAFETY_DISCLAIMER or COMPANY_TONE_POLICY), and every assistant built with build_instructions picks up the change automatically the next time it runs. This is the same principle as sharing a base CSS file across pages, or a base class across subclasses: the shared, decided-once part is centralized, and only the genuinely task-specific rules vary per assistant.
Testing Instructions Without Calling the API
Because instructions are now plain Python values, they can be tested like any other data the application produces — without any network call:
def test_billing_instructions_mention_scope():
instructions = build_support_instructions()
assert "billing" in instructions.lower()
assert "invoices" in instructions.lower()
print("PASS: billing instructions mention required scope terms")
def test_build_instructions_includes_shared_policy():
result = build_instructions("Task-specific rule here.")
assert "professional, concise tone" in result
assert "Task-specific rule here." in result
print("PASS: composed instructions include shared and task-specific text")
test_billing_instructions_mention_scope()
test_build_instructions_includes_shared_policy()
These tests do not verify that the model obeys the instructions — that requires evaluation against real outputs, which Lesson 9 covers. What they verify is that the instructions-building code itself is correct: that a required policy line was not accidentally dropped during a refactor, that a new task rule actually gets included in the composed string, and so on. This is a cheap, fast layer of protection that catches a large class of regressions (a broken f-string, a missing list item, an accidental duplicate) before they ever reach a model call.
Common Mistakes
Copy-pasting instructions across files "just this once." The exception always becomes the rule. A second copy of an instructions string is a second thing that must be remembered and kept in sync every time the policy changes; centralize before copying, even under time pressure.
Cramming every possible variation into one giant conditional string. A single instructions-building function with a dozen boolean flags and deeply nested string concatenation becomes as hard to reason about as scattered duplicates. Beyond a small number of variation points, split into genuinely separate instruction sets rather than one function with combinatorial branching.
Treating well-organized instructions as sufficient testing. Structuring instructions cleanly makes them easier to review and less likely to contain accidental bugs, but it says nothing about whether the model's actual responses meet requirements. That verification belongs in evaluation (Lesson 9), not in code organization.
Best Practices
Keep one module (or package) as the home for all instruction text. Reviewers and new contributors should be able to find every system prompt the application sends by looking in one predictable place.
Prefer composition over duplication for shared policy. Extract company-wide tone, safety, or formatting rules into named constants combined via a builder function, so a policy change propagates everywhere automatically.
Write structural unit tests for instruction-building code. Assert that required phrases, scope statements, and composed fragments appear in the final string, catching accidental omissions from refactors without needing a live model call.
Parameterize with named arguments, not raw string interpolation. When an instructions set needs controlled variation, expose it as explicit function parameters with sensible defaults rather than open-ended f-string substitution.