Reusable App Instructions

Ma Mahalakshmi V Updated 19 Sep 2026
8 min read ·Lesson 146 of 224

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.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Reusable App Instructions and get answers drawn from it.

Signed-in readers only.