System prompts and roles
Set behavior once, separately from the conversation.
Create a free account Try part of it here
Why it matters
Every AI feature in a real product has a system prompt. The support chatbot has one. So do the copywriting helper and the tool that tags customer reviews. The system prompt is where you, the developer, set the model's behavior once. After that, each request only has to carry the job at hand. Getting this split right keeps prompts short and keeps thousands of replies consistent. It also means a change to the brand voice is one edit, not fifty.
What you'll be able to do
- Decide whether an instruction belongs in the system prompt or in a user message
- Write a system prompt that sets the audience, facts, rules and output style for an app
- Explain what the user and assistant roles hold, and the rules for ordering messages
- Reuse one system prompt across many requests in code
What it covers
- Two places for instructions
- What goes in a system prompt
- System or user? A rule of thumb
- Worked example: one system prompt, many requests
- The roles and their rules
- What a system prompt can't do
Two places for instructions
Picture hiring a freelance copywriter. On day one you hand them an onboarding brief. It says who the brand is, who the customers are, the house style, and what never to say. After that, you send job tickets, such as "write a description for the new fleece." You give the brief once. You send many tickets.
A request to a model works the same way. It has two places to put text:
system: the brief. These are standing instructions from you, the developer. They apply to the whole conversation. The text you put here is the system prompt.messages: the conversation itself, as a list. Each message has a role, a label that says who it's from. A"user"message holds what the person (or your code) is asking right now. An"assistant"message holds something the model said earlier.
In a live app, the people using it usually never see the system prompt. They type into a box. Your code puts what they typed in a user message, under a system prompt you wrote.
Show the answer
In a user message
What the person types right now is the request at hand, so it goes in a user message. The system prompt holds your standing instructions. Assistant messages hold what the model said earlier.
What goes in a system prompt
A good brief tells a new copywriter what they need on every job. A useful system prompt does the same. It usually covers:
- The job and the audience. "You are a copywriter" isn't enough. Say what the product is, who reads the output, and what it's for.
- Background facts needed on every request: brand voice, product lines, policies.
- Rules, with reasons. For example: "Never mention delivery dates, because they vary by region."
- The default output style: length, format, tone.
- What to do with odd requests: off-topic questions, missing information.
Here's one for an outdoor clothing brand:
You write product descriptions for Trailhead Outfitters, an outdoor clothing brand. Readers are hikers and campers comparing gear on our website, often on their phones. Voice: plain, warm and specific. Prefer concrete details (weight, materials, uses) over general praise. Rules: - Never promise delivery dates; they vary by region. - Don't compare our products with other brands by name. - If the product notes are missing something you'd need, leave it out instead of guessing. Format: one paragraph of 60 to 90 words, with no headings or bullet points.
A one-line role such as "You are a helpful marketing assistant" does little on its own. The details that follow it do the work. A job title tells the freelancer very little. The brief tells them how to do the job.
Keep the user message the same and change only the system prompt. Make the voice playful, or delete the format rule, and compare the replies. If a reply comes back cut off, raise max_tokens, the cap on how long a reply can be.
System or user? A rule of thumb
Ask one question: will this be true for every request? If yes, it goes in the system prompt. If it changes from one request to the next, it goes in a user message.
Put it in system | Put it in a user message |
|---|---|
| What's true for every request: voice, audience, rules, format | This request's task: "write a description for this product" |
| Facts the model always needs: brand, policies | This request's material: the product notes, the review, the question |
| How to handle unusual requests | Anything that changes from one request to the next |
The split pays off in code. You write the system prompt once, as a constant: a variable you set once and never change. Every request reuses it word for word. So every reply works from the same rules. When the voice guidelines change, you edit one string.
There's a second reason. Models are trained to treat the system prompt as the developer's instructions. They keep following it throughout a conversation. That makes it the natural home for rules that shouldn't drift.
Worked example: one system prompt, many requests
Write descriptions for two products with the same system prompt. Only the user message changes. Look for SYSTEM near the top: it's written once and used in every call.
import requests SYSTEM = """You write product descriptions for Trailhead Outfitters, an outdoor clothing brand. Readers are hikers comparing gear on their phones. Voice: plain, warm and specific. Never promise delivery dates. Format: one paragraph of 60 to 90 words.""" def describe(product, notes): response = requests.post( "https://api.anthropic.com/v1/messages", headers={"x-api-key": "added-by-the-academy-server", "anthropic-version": "2026-10-21"}, json={ "model": "claude-opus-5-5", "max_tokens": 1000, "system": SYSTEM, "messages": [ {"role": "user", "content": f"Write a description for the {product}.\n<notes>\n{notes}\n</notes>"}, ], }, timeout=60, ) response.raise_for_status() text = "" for block in response.json()["content"]: if block["type"] == "text": text += block["text"] return text products = { "Ridgeline Fleece": "280 g recycled polyester, two zip pockets, packs into its own pocket", "Summit Rain Shell": "3-layer waterproof fabric, pit zips, hood fits over a climbing helmet", } for product in products: print(product) print(describe(product, products[product])) print()
SYSTEM is defined once and sent with every request. The user message carries only what's specific to this product: its name and its notes. The notes sit inside <notes> tags, which mark where the material starts and ends. With the simulated reply, you can see the system prompt arriving on each call. With a real model, both descriptions follow the same voice, rules and length, because they share one brief.
Show the answer
The list of allowed tags and what each one means
The tag definitions apply to every review, so they're standing instructions. The review itself, and details about it, change on every request. Those go in the user message.
The roles and their rules
The messages list reads like the script of a play: each line says who's speaking. It follows four rules:
- The first message must have the role
"user". - Turns alternate between
"user"and"assistant". If you send two user messages in a row, the API combines them into one turn. - An
"assistant"message holds something the model said in an earlier turn. You add one when you continue a conversation. The Conversations and memory lesson covers that in depth. - The last message is normally a user message: the thing you want answered now.
Break the first rule and the API refuses the request. It sends back the status code 400, which means "bad request." It also sends an error message that says what's wrong. Run this to see it:
import requests response = requests.post( "https://api.anthropic.com/v1/messages", headers={"x-api-key": "added-by-the-academy-server", "anthropic-version": "2026-10-21"}, json={ "model": "claude-opus-5-5", "max_tokens": 1000, "messages": [ {"role": "assistant", "content": "Hi! How can I help?"}, {"role": "user", "content": "Write a tagline for a rain jacket."}, ], }, timeout=60, ) print(response.status_code) print(response.json()["error"]["message"])
This cell has no raise_for_status() on purpose. That way you can read the error instead of stopping on it.
What about a greeting your app shows on screen before the user types? That's display text. It doesn't belong in messages as the first turn.
Watch out: Older tutorials end the list with a half-written assistant message, such as
{"role": "assistant", "content": "{"}, to force the reply to start a certain way. That trick is called prefilling. Current Claude models reject it with a 400 error. Ask for the format in your instructions instead. The Structured output lesson shows how to guarantee JSON.
Go deeper How prefilling used to work
A model writes its reply one token at a time. (A token is a word or a piece of a word.) Each new token continues the text that came before it. Prefilling used that. If the list ended with an assistant message holding {, older models carried on from that brace. So the reply began as JSON. Current Claude models refuse a request that ends this way, so the trick no longer works.
messages = [{"role": "user", "content": "Hi"}, {"role": "assistant", "content": "Hello"}, {"role": "user", "content": "Bye"}]
print(messages[-1]["role"])Show the answer
user
A request ends on the user's turn. The model then writes the next assistant turn.
Show the answer
Combines them into one user turn and replies once
Two user messages in a row are allowed. The API combines them into one turn. The 400 error comes from breaking a different rule: starting the list with an assistant message.
What a system prompt can't do
A system prompt is like a staff handbook. It steers behavior strongly, but it's still a set of instructions, not a lock. Three limits matter:
- It isn't enforcement. The model follows its system prompt the vast majority of the time, not every time. A user can also try to talk it out of a rule. So rules that must hold, such as a character limit or a banned word, should also be checked in code. You did this in the Phase 0 capstone, the ad-copy reviewer.
- It isn't secret. Users can often get a model to repeat or paraphrase its instructions. Never put API keys, passwords or private customer data in a system prompt.
- It isn't the place for per-request details. If each request's material goes in the system prompt, you must rebuild the system prompt on every call. You also lose the clean split between the brief and the job ticket.
Show the answer
Keep the rule in the system prompt and also check each reply in code before showing it
The system prompt makes a broken rule rare. A check in code makes it impossible to ship. Rules that must hold need both.
Practice what you just read
- 2 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
You're building a support bot. Where should "Customers on the Pro plan get free returns" go, and where should "My order #4471 hasn't arrived" go?