How to Write AI Agent System Prompts That Don't Fail in Production

Alejandro Rioja
Alejandro Rioja
13 min read
TL;DR

A production system prompt has five layers: identity (who the agent is and what it can't do), context (what it knows about the environment), task (what success looks like step by step), output format (the most underrated layer), and edge cases (what to do when inputs go wrong). Most agent prompts fail because they skip layers 4 and 5. Write the output format before you write anything else — it forces you to be precise about what you actually want.

Free newsletter

Every Wednesday. 28,400+ operators. Zero fluff.

Table of contents

Open Table of contents

The system prompt problem nobody admits

Most agent system prompts are written in about 20 minutes, tested against two or three examples, and then never touched again. The model ships. For a while, it works. Then something changes — the inputs get messier, the model gets updated, a new edge case appears — and the agent starts producing garbage. Quietly. At scale.

The problem isn’t that the original prompt was bad. It’s that most prompts are written to demonstrate the happy path. They’re designed for the input you had in mind when you built the agent, not for the full distribution of inputs the agent will actually see.

Production system prompts are different from demo prompts. They need to handle inputs you didn’t design for, fail gracefully when something goes wrong, and produce consistent output even when the model’s behavior shifts slightly across versions.

The five layers of a production system prompt

I think about every system prompt I write in five layers. They don’t have to appear in this order — but they all have to be present.

Layer 1: Identity

Identity tells the model who it is and what its operating constraints are. Not a roleplay character — a functional definition of what this agent does and doesn’t do.

A strong identity layer answers three questions:

  • What is this agent responsible for?
  • What is it explicitly NOT responsible for (and should escalate or refuse)?
  • What standards does it hold itself to?

Weak identity layer:

code
You are a helpful customer service agent for a pickleball facility.

Stronger identity layer:

code
You are the booking assistant for Pickleland, a pickleball facility in
Pflugerville, TX. Your job is to answer questions about court availability,
membership options, and upcoming events.

You do NOT handle billing disputes, refund requests, or complaints about
staff — route those to the human operations team via the escalation path
defined below.

You respond in a friendly but efficient tone. You never fabricate
availability or pricing. When you don't know something, you say so
and offer to take a message for the operations team.

The explicit NOT scope is the part most operators skip. Without it, the model will try to be helpful outside its lane — and that’s where things go wrong.

Layer 2: Context

Context is what the agent knows about its environment that isn’t in the user’s message. This includes:

  • The current date and time (inject this dynamically — never trust the model’s internal sense of time)
  • Relevant state from external systems (upcoming events, inventory, user account details)
  • Business rules that aren’t obvious from the task description

Most agents I review are context-starved. The operator assumes the model “knows” things it doesn’t — current pricing, the names of specific staff members, which features are actually active in the system. Don’t assume. Inject it.

For time-sensitive agents, I inject a context block at the top of every prompt:

code
Current date/time: {{now_utc}}
Facility status: {{facility_open_today ? "Open" : "Closed today"}}
Next available court slots: {{next_slots | json}}
Active promotions: {{active_promos | join(", ") || "None"}}

The model can’t hallucinate information that’s already been injected correctly. The context layer is your first line of defense against confabulation.

Layer 3: Task

The task layer describes what the agent does, step by step. Not “help customers” — the actual decision flow.

The trap here is writing a task layer that’s too abstract. “Answer the customer’s question” is not a task layer. A real task layer looks like this:

code
When a customer message arrives:

1. Classify the intent: availability inquiry, event question, membership
   question, complaint, or other.

2. For availability inquiries: look up the court schedule for the
   requested date/time. If slots are available, quote them with prices.
   If not, offer the nearest available slot.

3. For event questions: pull from the events list in context. Include
   date, time, cost, and registration link.

4. For membership questions: use the membership tier table in context.
   Do not invent pricing. If asked about a tier not listed, say we'll
   have someone follow up.

5. For complaints: acknowledge the issue, apologize briefly without
   making any promises, and tell the customer that a team member will
   reach out within 24 hours. Log the complaint via the escalation tool.

6. For anything that doesn't fit: ask one clarifying question.
   Don't attempt to answer until you understand the intent.

Notice what this does: it gives the model a flowchart, not a directive. Flowcharts are more robust than directives because they reduce the model’s need to infer what you want in ambiguous cases.

Layer 4: Output format

This is the most underrated layer, and the one most responsible for silent failures.

If you don’t specify output format precisely, the model will produce output that looks right to a human reader but is inconsistent enough to break downstream parsing. I’ve had agents that worked perfectly for weeks and then started adding an extra newline before the JSON that broke my extraction logic.

Write the output format before you write anything else. If you can’t describe exactly what you want the output to look like, you don’t understand the task well enough to automate it yet.

For structured output (JSON, tool calls, specific fields), specify the exact schema:

code
Output a single JSON object with these exact fields:
{
  "intent": "availability" | "event" | "membership" | "complaint" | "other",
  "reply": string,           // the message to send to the customer
  "escalate": boolean,       // true only for complaints and billing issues
  "escalation_note": string  // required if escalate is true, else empty string
}

Do not include any text outside the JSON object.
Do not add markdown code fences around the JSON.

For prose output, specify structure, length, and tone constraints:

code
Respond in 1–3 sentences. Match the customer's tone — casual if they're
casual, professional if they're professional. Never use bullet points.
Never start with "Certainly" or "Of course."

The last line is not a formatting request — it’s a failure mode prevention. I put explicit anti-patterns in my output format layers because I know what the model will default to when left to its own devices.

Layer 5: Edge cases

Most system prompts handle the happy path. Edge cases are where agents fail in ways that erode trust slowly and then catastrophically.

The edge case layer answers: what does the agent do when the input is:

  • Ambiguous or incomplete
  • In the wrong language
  • Hostile or abusive
  • Trying to get the agent to do something outside its lane
  • Clearly wrong (a date that doesn’t exist, a court number that isn’t in the system)

For each edge case, give the model an explicit response path:

code
If the customer's message is in a language other than English:
Reply in their language using your best translation of the standard
response. Do not apologize for language limitations.

If the customer asks for something outside your scope (refunds, staff
complaints, account changes): acknowledge the request, explain you can't
handle it directly, and trigger the escalation tool with a summary.

If the customer is hostile or uses abusive language: respond once with
a calm, brief de-escalation message. If the next message is still
hostile, trigger escalation with the transcript and stop responding.

If input data is missing or malformed (e.g., a date isn't in context):
ask one clarifying question. Don't guess.

These rules feel obvious when you read them. They are not obvious to a model that hasn’t been told them explicitly. The model’s default behavior in ambiguous situations is to try to be helpful — which often means making something up. The edge case layer is how you override that default.

The identity trap: why vague personas fail

The most common system prompt mistake I see in production is the vague persona:

code
You are a helpful, friendly, professional AI assistant who is eager to help.

This tells the model nothing useful. “Helpful” and “friendly” are the model’s defaults. “Eager to help” is almost harmful — it’s the exact disposition that makes models hallucinate when they don’t know the answer.

A functional identity is specific about constraints, not personality. “You work at a pickleball facility in Pflugerville, TX, you have access to these specific data sources, and you escalate these specific categories of requests to humans” — that’s identity. “You are friendly and helpful” is not.

When I audit agent prompts that are producing bad outputs, the diagnosis is almost always in the identity layer: the agent has been given a personality but not a function. It knows how to sound like it’s doing its job. It doesn’t know what its job actually is.

Output format is the leverage point

If I had to pick one layer to spend the most time on, it’s output format. Here’s why.

Every downstream action your agent takes — writing to a database, sending a message, calling a tool, updating a record — depends on parsing the model’s output. If the output is inconsistent, the downstream action fails. The failure usually doesn’t look like an error — it looks like data that’s slightly wrong, a field that’s missing, a message that got sent twice.

When I’m building a new agent, I write the output format first and work backwards. “What exactly do I need the model to return for the next step to work?” That question drives every other prompt decision. The task layer is about getting the model to the right answer. The output format layer is about getting that answer in a form I can actually use.

For high-stakes agents (anything that writes to a CRM, sends external messages, or triggers financial transactions), I use Claude’s structured output with a defined JSON schema. The model is forced to call a tool with a validated schema — no parsing logic, no regex, no hoping the JSON is well-formed. The schema is the contract.

For lower-stakes agents, a precisely specified text format is usually enough — as long as I’m explicit about every possible variation and test it against adversarial inputs before shipping.

”You are not allowed to…” — the anti-pattern

A note on negation in system prompts. Telling the model what it’s NOT allowed to do is useful in the identity layer (to establish scope), but it’s a poor substitute for telling it what TO do.

“Do not make up pricing” is weaker than “Only quote pricing from the price table in the context block. If a price you need isn’t there, say ‘I don’t have that pricing on hand, but a team member can confirm.’”

The first version relies on the model’s compliance. The second version gives it an explicit behavior. Under distribution shift — a new model version, an unusual input, a slightly different phrasing — the explicit behavior holds better than the prohibition.

I still use prohibitions. I use them in combination with explicit alternatives. Never prohibition alone.

How I maintain system prompts over time

A production system prompt is a living document. Here’s the maintenance cycle I use:

Weekly spot-check. I review five to ten random outputs from each high-stakes agent against the expected output. I’m looking for drift — not catastrophic failure, but subtle changes in format, tone, or scope coverage that suggest the model is interpreting the prompt differently than I intended.

Post-model-update review. Every time the underlying model version changes, I run the agent against the full golden set from my eval framework. Model updates are the number one cause of prompt drift in production. Even a “minor” update can change how the model interprets edge cases.

Edge-case log. I maintain a running log of inputs the agent handled poorly. Every entry becomes a candidate for a new edge case rule in the prompt. When three or more entries share a pattern, I add an explicit rule.

Prompt versioning. Every significant prompt change gets a version comment at the top of the system prompt file:

code
# system-prompt.txt
# v1.4 — 2026-06-15: added hostile-input escalation rule
# v1.3 — 2026-05-20: tightened output format after JSON drift
# v1.2 — 2026-04-10: added multilingual edge case
# v1.1 — 2026-03-01: initial production version

This isn’t just housekeeping. When an agent starts failing, the change log is the first place I look. Most production regressions trace to a prompt change that seemed safe.

A real example: the Pickleland event promoter

Here’s a condensed version of the system prompt for the agent that writes Facebook event promo posts for Pickleland. I use this to illustrate all five layers in practice.

code
## IDENTITY
You are the event promoter for Pickleland, a pickleball facility in
Pflugerville, TX. You write Facebook posts that promote upcoming events
to local pickleball groups. You do not respond to customer questions,
handle bookings, or represent the facility in any way other than
promoting events.

## CONTEXT
Current date: {{now_date}}
Upcoming events (next 7 days):
{{events | json}}

Target Facebook groups:
{{groups | json}}

Facility tone: enthusiastic but not salesy. First-person plural ("we").
Never use exclamation marks in the opening sentence.

## TASK
For each event in the events list:
1. Match it to the appropriate groups from the groups list based on
   skill level and format (recreational vs competitive).
2. Write one post per matched group. Each post should be 80–120 words.
3. Include: event name, date and time, cost (if any), and a call to
   action with the registration link.
4. Vary the opening sentence across posts for the same event so they
   don't read as duplicates if someone sees them in multiple groups.

## OUTPUT FORMAT
Return a JSON array. Each element:
{
  "event_id": string,
  "group_id": string,
  "post_text": string,    // 80–120 words, no HTML
  "review_flag": boolean  // true if you're uncertain about tone or
                          // accuracy — these get human review first
}

No text outside the JSON array.

## EDGE CASES
If an event has no matching groups: include it with group_id "unmatched"
and set review_flag to true.

If pricing information is missing from the event data: do not invent
a price. Write "free to attend" only if the event explicitly says free.
Otherwise omit pricing and set review_flag to true.

If fewer than two events are in context: return an empty array.
Do not ask for more events — just return [].

This prompt has been in production since early 2026. I’ve updated it three times: once to add the duplicate-opening-sentence rule (posts were too repetitive), once to add the missing-price edge case (an agent once wrote “only $0!” for a free event — technically correct, weirdly phrased), and once to tighten the word count after posts started running long.

Three updates in six months for a non-trivial prompt is reasonable. The key is that each update addresses a specific observed failure, not a hypothetical one.

FAQ

Should my system prompt be in the user turn or the system turn?

Always use the system turn for instructions and identity. The user turn should contain the current input only. Mixing instructions into the user turn creates ambiguity about what’s instruction and what’s content — models handle it, but less reliably than a clean separation.

How long should a production system prompt be?

Long enough to cover all five layers. Short enough that you can read it in two minutes and spot drift. For most of my agents, that’s 200–600 words. If you’re much longer than that, you’re either over-specifying (trust the model for low-stakes details) or your task is too complex to be one agent.

When should I break a complex task into multiple agents instead of one long prompt?

When the task has two or more distinct modes that require different context, different output formats, or different error handling. The Pickleland booking pipeline splits into an event-triggered confirmation agent and a scheduled reporting agent because they need completely different prompts — combining them would make both worse. See event-triggered vs scheduled agents for the pattern.

What’s the most common reason a prompt that worked in testing fails in production?

The test inputs weren’t representative of the production distribution. Most prompt writers test on clean, well-formed inputs that match what they had in mind when writing the task layer. Production inputs are messier — shorter, misspelled, out of scope, in unexpected languages. Build a test set from real production traffic, not imagined inputs.

How do I know when to update the prompt vs update the code?

If the agent is producing the wrong output format, update the prompt. If the agent is producing the right output but the downstream system can’t use it, update the code. If the agent is producing confidently wrong facts, check the context layer first — the model usually isn’t making things up, it’s filling in gaps you left empty.

Keep reading

Related posts

Keep reading

Get the AI playbook in your inbox

Every Wednesday. 28,400+ operators. Zero fluff.

↵ to see all results esc esc to close