Use Message Roles
What a Message Role Is
Every message in the messages list has a role that says who that turn belongs to:
messages = [
{"role": "user", "content": "What is a Python list?"},
{"role": "assistant", "content": "A list is an ordered, mutable collection of items."},
{"role": "user", "content": "How do I add an item to one?"},
]
Claude reads the conversation as a dialogue between participants, and the role tells it how to interpret each message: as input to respond to, as something it said earlier, or as an instruction from the application.
Roles matter because they determine meaning and authority. The same sentence has a different effect depending on whether it appears as user input, as Claude's own earlier reply, or as a system instruction.
The Three Roles
| Role | Who it represents | Where it appears | Purpose |
|---|---|---|---|
user | The user side: a person, or your application acting for them | messages | Questions, tasks, data, tool results |
assistant | Claude | messages, and the role of every response | Claude's earlier replies, or synthetic replies you write |
system | You, the application operator | The top-level system parameter, and (on supported models) messages | Instructions, rules, and context |
user
The user role carries the input Claude should respond to. "User" refers to the side of the conversation, not necessarily a human typing. When your code inserts a document and asks for a summary, that is also a user message.
assistant
The assistant role represents Claude's turns. You use it in two ways:
- Real history: sending Claude's previous replies back, so it knows what it already said (Lesson 24).
- Synthetic turns: replies that your code writes and presents as if Claude had said them. The documentation states explicitly that earlier turns do not need to originate from Claude.
Every response from the API has role == "assistant". It is the next assistant turn.
system
System instructions come from you, the application operator, and carry higher authority than user messages. When user input conflicts with system instructions, the system instructions take precedence.
There are two ways to provide them:
| Form | How | When to use |
|---|---|---|
Top-level system parameter | client.messages.create(system="...", ...) | Instructions that apply from the very first turn (Lesson 17) |
| Mid-conversation system message | {"role": "system", "content": "..."} inside messages | Instructions that become relevant later in a conversation |
Mid-Conversation System Messages
On current models, you can place a message with "role": "system" inside the messages list, after a user turn, to add an instruction partway through a conversation.
According to the documentation, this works on Claude Fable 5.1, Mythos 5.1, Fable 5, Mythos 5, Opus 5.5, Opus 5, Opus 4.8, Sonnet 5.5, and Haiku 5.5. Availability can differ on cloud platforms, so check the documentation for yours.
messages = [
{"role": "user", "content": "Review the function process() in utils.py."},
{"role": "assistant", "content": "process() loops over the list twice; you can merge the loops."},
{"role": "user", "content": "Now review the code that calls process()."},
{"role": "system", "content": "From now on, include explicit type annotations in every suggestion."},
]
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
system="You are a code review assistant. Be concise.",
messages=messages,
)
Why Mid-Conversation System Messages Exist
You could edit the top-level system parameter instead. But prompt caching (Lessons 121–130) reuses the beginning of a request only if it is byte-for-byte identical to an earlier request. Editing the system prompt changes the beginning, so the cached prefix becomes useless. A system message appended later leaves everything before it unchanged, so the cache keeps working, and the new instruction still has system-level authority.
Precedence Rules
- A mid-conversation system message has the same authority as the top-level
systemfield. - When instructions conflict, later system messages take precedence over earlier ones.
- A mid-conversation system message takes precedence over the top-level
systemfield for the turns that follow it.
Placement Rules
| Rule | Meaning |
|---|---|
| Not first | A system message with content cannot be the first entry in messages. Use the top-level system parameter for that. |
| After a user turn | It must come directly after a user turn (or after an assistant turn that ends with a server tool call). A user message containing tool results also counts. |
| Followed by an assistant turn, or last | It must be the last entry in messages, or be immediately followed by an assistant turn. |
| Not inside a tool exchange | It cannot be placed between an assistant tool_use block and the tool_result that answers it. |
System-role messages do not appear in the response. Claude follows them but does not echo them.
Rules for Ordering Roles
1. Start with a User Message
The conversation begins with user input. A system message cannot be the first entry.
2. Alternate User and Assistant
A normal conversation alternates user → assistant → user → … If you send two consecutive messages with the same role, the API combines them into a single turn. Building one message with several content blocks is clearer.
3. End with User Input
On current models, the request must not end with an assistant message. Ending with an assistant message is a technique called prefill, and it returns a 400 error on Claude 4.6 and later models. The last entry is normally a user message, or a system message placed directly after a user message, as shown above.
Using Synthetic Assistant Turns
Because assistant turns do not have to come from Claude, you can write example exchanges that show Claude the exact behavior you want. This is called few-shot prompting with conversation turns:
messages = [
{"role": "user", "content": "Convert to a Python variable name: 'Total Price (USD)'"},
{"role": "assistant", "content": "total_price_usd"},
{"role": "user", "content": "Convert to a Python variable name: 'Number of Items'"},
{"role": "assistant", "content": "number_of_items"},
{"role": "user", "content": "Convert to a Python variable name: 'Customer E-mail Address'"},
]
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=messages,
)
print("".join(block.text for block in message.content if block.type == "text"))
customer_email_address
How it works: the first four messages are examples written by you. They demonstrate the input format, the output format (only the name, nothing else), and the naming style (snake_case, punctuation removed). Claude continues the established pattern for the final user message.
Why this works well: examples communicate style and format more precisely than descriptions. Lesson 35 (Add Examples to Prompts) compares this approach with putting examples directly inside one prompt.
Warning: Synthetic turns are powerful, so only your application should create them. Never let end users submit
assistantorsystemmessages directly. That would let them put words "in Claude's mouth" or issue operator-level instructions.
Practical Example: A Message List Validator
When an application builds message lists dynamically, from stored history, user input, and system instructions, mistakes in role order cause 400 errors. This validator checks the rules from this lesson before sending a request, with clear error messages.
"""validate_roles.py: check a messages list against the role rules before sending it."""
VALID_ROLES = {"user", "assistant", "system"}
def validate_messages(messages: list[dict]) -> list[str]:
"""Return a list of problems; an empty list means the messages look valid."""
problems = []
if not messages:
return ["The messages list is empty."]
for index, message in enumerate(messages):
role = message.get("role")
if role not in VALID_ROLES:
problems.append(f"messages[{index}]: invalid role {role!r}.")
if not message.get("content"):
problems.append(f"messages[{index}]: content is empty.")
if messages[0].get("role") != "user":
problems.append("The first message must have the 'user' role.")
for index, message in enumerate(messages):
if message.get("role") != "system":
continue
previous = messages[index - 1].get("role") if index > 0 else None
following = messages[index + 1].get("role") if index + 1 < len(messages) else None
if previous != "user":
problems.append(f"messages[{index}]: a system message must follow a user message.")
if following not in (None, "assistant"):
problems.append(f"messages[{index}]: a system message must be last or followed by an assistant message.")
conversation_roles = [m.get("role") for m in messages if m.get("role") != "system"]
for index in range(1, len(conversation_roles)):
if conversation_roles[index] == conversation_roles[index - 1]:
problems.append(f"Consecutive '{conversation_roles[index]}' turns: consider merging them.")
if messages[-1].get("role") == "assistant":
problems.append("The last message is from the assistant (prefill is not supported on current models).")
return problems
if __name__ == "__main__":
good = [
{"role": "user", "content": "Hi"},
{"role": "assistant", "content": "Hello!"},
{"role": "user", "content": "Explain tuples."},
{"role": "system", "content": "Answer in under 50 words."},
]
bad = [
{"role": "system", "content": "Be brief."},
{"role": "human", "content": "Hi"},
{"role": "assistant", "content": "The answer is"},
]
for name, messages in [("good", good), ("bad", bad)]:
problems = validate_messages(messages)
print(f"{name}: {'valid' if not problems else 'INVALID'}")
for problem in problems:
print(f" - {problem}")
Output:
good: valid
bad: INVALID
- messages[1]: invalid role 'human'.
- The first message must have the 'user' role.
- messages[0]: a system message must follow a user message.
- messages[0]: a system message must be last or followed by an assistant message.
- The last message is from the assistant (prefill is not supported on current models).
How It Works
- Role and content check: every message must use a known role and have non-empty content. In the
badlist,'human'is reported here. - First message: must be
user. Thebadlist starts with a system message. - System placement: for each system message, the script looks at its neighbors.
messages[index - 1]must be a user message, and the next message must be an assistant message or not exist. Thebadlist's system message breaks both rules: nothing comes before it, and it is followed by a'human'message rather than an assistant message. - Alternation: system messages are filtered out first, because they are allowed between turns. Then consecutive
user/userorassistant/assistantpairs are reported as warnings. Thebadlist has none. - Ending: a final assistant message is reported, because prefill is not supported.
This validator covers the common rules for text conversations. Tool-use conversations add rules of their own, such as the requirement that tool_result blocks immediately follow their tool_use. Those are covered in Lessons 85–88. The API remains the final authority, so still handle BadRequestError.
Common Mistakes
Inventing Role Names
What happens: "human", "bot", "ai", or "model" cause a 400 error.
Fix: use exactly "user", "assistant", or "system".
Putting the System Message First in messages
What happens: a 400 error, because a system message with content cannot be the first entry.
Fix: put initial instructions in the top-level system parameter.
Ending with an Assistant Message
What happens: a 400 error on Claude 4.6 and later models, which do not support prefill.
Fix: end with a user message, or with a system message directly after a user message.
Letting Users Control Roles
What happens: if a web API accepts a full messages list from the browser, a user can add fake assistant or system turns and manipulate Claude's behavior.
Fix: accept only the user's text from clients. Build the message list, including roles, on the server.
Using Mid-Conversation System Messages on Unsupported Models or Platforms
What happens: a 400 error on models or platforms that do not support system-role messages.
Fix: check the documentation's list of supported models and platforms, and fall back to the top-level system parameter where needed.
Best Practices
- Use
userfor input,assistantfor Claude's turns, andsystemfor operator instructions. - Put instructions that apply from the start in the top-level
systemparameter. - Use mid-conversation system messages for instructions that become relevant later, especially when using prompt caching.
- Use synthetic assistant turns to demonstrate formats and styles through examples.
- Build message lists on the server, never from client-supplied roles.
- Validate role order in code that assembles conversations dynamically.
Exercise
- Write a few-shot conversation with three synthetic examples that convert dates like
"3 Nov 2026"into ISO format (2026-11-03), then ask Claude to convert"21 Oct 2026". - Run
validate_messages()on a list that contains two consecutive user messages. What does it report? - Start a short conversation, then add a mid-conversation system message asking Claude to answer only in Spanish from that point on. Send the next user message and observe the change.
- Explain why a web chat API should accept only the user's text, not a full
messageslist, from the browser.
Step 1:
messages = [
{"role": "user", "content": "3 Nov 2026"},
{"role": "assistant", "content": "2026-11-03"},
{"role": "user", "content": "15 Jan 2025"},
{"role": "assistant", "content": "2025-01-15"},
{"role": "user", "content": "1 Jul 2024"},
{"role": "assistant", "content": "2024-07-01"},
{"role": "user", "content": "21 Oct 2026"},
]
Expected answer: 2026-10-21.
Step 2: it reports Consecutive 'user' turns: consider merging them.
Step 3:
messages = [
{"role": "user", "content": "What is a dictionary in Python?"},
{"role": "assistant", "content": "A dictionary stores key-value pairs."},
{"role": "user", "content": "How do I loop over one?"},
{"role": "system", "content": "From now on, answer only in Spanish."},
]
The reply to the last question arrives in Spanish. Because the instruction is a system message, it keeps operator-level priority for the rest of the conversation.
Step 4: if the browser could send the full list, a user could add fake assistant turns, putting words in Claude's mouth, or system turns, issuing instructions with operator authority. Accepting only text and building roles on the server keeps that control with your application.