Use Message Roles

Su Suriya Ravichandran Updated 10 Nov 2026
11 min read ·Lesson 23 of 200

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

RoleWho it representsWhere it appearsPurpose
userThe user side: a person, or your application acting for themmessagesQuestions, tasks, data, tool results
assistantClaudemessages, and the role of every responseClaude's earlier replies, or synthetic replies you write
systemYou, the application operatorThe top-level system parameter, and (on supported models) messagesInstructions, 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:

  1. Real history: sending Claude's previous replies back, so it knows what it already said (Lesson 24).
  2. 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:

FormHowWhen to use
Top-level system parameterclient.messages.create(system="...", ...)Instructions that apply from the very first turn (Lesson 17)
Mid-conversation system message{"role": "system", "content": "..."} inside messagesInstructions 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 system field.
  • When instructions conflict, later system messages take precedence over earlier ones.
  • A mid-conversation system message takes precedence over the top-level system field for the turns that follow it.

Placement Rules

RuleMeaning
Not firstA system message with content cannot be the first entry in messages. Use the top-level system parameter for that.
After a user turnIt 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 lastIt must be the last entry in messages, or be immediately followed by an assistant turn.
Not inside a tool exchangeIt 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 assistant or system messages 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

  1. Role and content check: every message must use a known role and have non-empty content. In the bad list, 'human' is reported here.
  2. First message: must be user. The bad list starts with a system message.
  3. 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. The bad list's system message breaks both rules: nothing comes before it, and it is followed by a 'human' message rather than an assistant message.
  4. Alternation: system messages are filtered out first, because they are allowed between turns. Then consecutive user/user or assistant/assistant pairs are reported as warnings. The bad list has none.
  5. 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 user for input, assistant for Claude's turns, and system for operator instructions.
  • Put instructions that apply from the start in the top-level system parameter.
  • 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

  1. 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".
  2. Run validate_messages() on a list that contains two consecutive user messages. What does it report?
  3. 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.
  4. Explain why a web chat API should accept only the user's text, not a full messages list, from the browser.
Show answers

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.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Use Message Roles and get answers drawn from it.

Signed-in readers only.