Mid-conversation system messages and tool changes
Change system instructions or tool availability partway through a conversation without invalidating the cached prefix that came before them.
System instructions normally live in the top-level system field, ahead of every message in the conversation. That position is great for prompt caching: the system prompt is part of the stable prefix, so subsequent turns hit the cache. It is a poor position for instructions you only discover you need partway through a session, because editing the top-level system field changes the very beginning of the prompt and invalidates the cache for everything that follows.
Mid-conversation system messages close that gap. You append a {"role": "system"} message at the point in the conversation where the new instruction becomes relevant, instead of editing the top-level system field. The cached prefix stays the same, so the next request still reads it from cache, and the new instruction is still applied as a system instruction rather than as ordinary user text.
Mid-conversation tool changes
The tools array sits even earlier in the hashed request prefix than the top-level system field, so editing it invalidates the prompt cache for the entire conversation. Mid-conversation tool changes are the tools counterpart to mid-conversation system messages. Instead of fixing the tool list for the lifetime of the conversation, you change which tools are offered to the model between turns: declare the full tool set in tools up front, then use tool_addition and tool_removal blocks to offer a tool to the model, or withdraw it, from a specific point in the conversation onward. The tools array itself never changes, so the cached prefix stays intact.
tool_addition and tool_removal are content blocks in the content array of a role: "system" message, and they can be mixed with text blocks in the same message. The message follows the same placement rules as any mid-conversation system message (see Limitations), and the change applies from that point in the conversation onward. Each block's tool field references a tool rather than defining one: {"type": "tool_reference", "name": "..."} names a tool declared in the request's tools array, and MCP connector tools can be referenced individually with mcp_tool_reference (server_name and name) or as a whole toolset with mcp_toolset_reference (server_name). Referencing a name that is not declared in tools returns a 400 error.
Every tool declared in tools is offered to the model from the start of the conversation unless it is declared with defer_loading: true, which keeps it withheld until a tool_addition block surfaces it. tool_addition also re-offers a tool that an earlier tool_removal withdrew.
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-opus-5",
max_tokens=1024,
betas=["mid-conversation-tool-changes-2026-07-01"],
# The full tool set is declared up front and never changes, so the
# cached prefix stays intact.
tools=[
{
"name": "get_weather",
"description": "Get the current weather for a location.",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"},
},
"required": ["location"],
},
},
],
messages=[
{
"role": "user",
"content": "Say OK.",
},
# Withdraw get_weather from this point onward. The block references
# the tool by name instead of editing `tools`, so earlier turns stay
# byte-identical and the cache still hits.
{
"role": "system",
"content": [
{
"type": "tool_removal",
"tool": {"type": "tool_reference", "name": "get_weather"},
},
],
},
],
)
for block in response.content:
if block.type == "text":
print(block.text)Mid-conversation tool changes are in beta. To use them, include the beta header mid-conversation-tool-changes-2026-07-01 in your requests.
When to use a mid-conversation system message
Prompt caching hashes the request prefix in order: tools, then system, then messages. A cache hit requires the prefix to match a recent request exactly, byte for byte, up to the cache breakpoint.
That ordering means the top-level system field sits near the very start of the hashed prefix. Any change to it, even appending a sentence, produces a different hash, and the request misses the cache for the system prompt and every cached message after it.
Mid-conversation system messages let you add the instruction at the end of the message history instead. Everything before the new instruction is unchanged, so the existing cache entry still matches, and only the new message is processed as fresh input.
A few situations where this matters:
- Mid-session policy or persona changes. A long agentic session needs a new constraint ("from now on, write all SQL as parameterized queries") after dozens of cached turns. Adding it to the top-level
systemfield would re-process the entire history. - Per-turn context that must be authoritative. You want to inject a freshness note, a session deadline, or a tool-availability change with system-level weight, and it changes too often to live in the cached prefix.
- Per-turn reminders that shouldn't pile up. A harness nudges the model after each batch of tool results ("request independent reads together", "the user hasn't heard from you in a while") and wants the model to see only the newest copy. A turn-scoped system message renders for one turn and then costs nothing, without deleting anything from the history.
- State changes your application observes. Your application notices something Claude should treat as an operator-level fact: files changed on disk, the user toggled an auto-approve setting, available tools changed, or the remaining token budget dropped below a threshold.
- User input that should not interrupt an agentic loop. A user types a follow-up while Claude is still executing tools for the previous request. Relaying it as a system message after the next tool result lets Claude fold the new input into the work it is already doing, instead of treating it as a fresh request to switch to. See Placement after tool results.
- Mode switches that grant standing permissions. A session-level mode can use a mid-conversation system message to grant standing consent to an expensive capability, such as automatically launching multiagent workflows, with a short refresher every several turns and an exit notice when the mode is turned off. For a worked example, see Build an orchestration mode.
In all of these cases you could put the instruction in a regular user message, and Claude does follow instructions that arrive in user turns. The difference is priority: a user message is treated as coming from the end user, while a system message is treated as coming from you, the application operator. When the two conflict, system instructions take precedence, so use the system role for operator-level facts and constraints that should hold even if the end user asks for something different. A mid-conversation system message keeps that operator-level priority without paying the cache-miss cost of editing the top-level system field.
How it works
Add a message with "role": "system" to the messages array. Use a plain string or content blocks for content, the same as a user or assistant turn. The instruction applies from that point in the conversation onward. When instructions conflict, later system messages take precedence over earlier ones, and mid-conversation system messages take precedence over the top-level system field for the turns that follow them.
You can still set the top-level system field for instructions that should apply to the entire conversation. Reserve mid-conversation system messages for instructions that only become relevant later, or that you want to add without invalidating the cached prefix.
A role: "system" message can also carry output_config.effort to change the effort level from the next user turn on. This is in beta on Claude Fable 5.1, Claude Mythos 5.1, and Claude Opus 5 on the Claude API and Google Cloud, and requires the mid-conversation-output-config-2026-07-01 beta header. See Per-message effort.
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
# Automatic prompt caching: each request caches the conversation so far,
# and the next request reads the unchanged prefix from cache.
cache_control={"type": "ephemeral"},
system="You are a code review assistant. Be concise.",
messages=[
{
"role": "user",
"content": "Review process() in utils.py for performance issues.",
},
{
"role": "assistant",
"content": "The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list.",
},
{
"role": "user",
"content": "Now review the calling code that invokes process().",
},
# The reviewer realizes mid-session that all suggestions must
# also pass the team's strict typing policy. Appending the
# instruction here keeps earlier turns byte-identical, so the
# prefix cached by the previous request is still read from cache.
{
"role": "system",
"content": "From now on, every suggestion must include explicit type annotations.",
},
],
)
for block in response.content:
if block.type == "text":
print(block.text)This example enables automatic caching with the top-level cache_control field. Prompt caching is opt-in: if a request has no cache_control field (automatic or an explicit breakpoint), nothing is cached and every request pays the regular input token price for the full conversation. With caching enabled, appending the system message leaves the already-cached turns unchanged, so the request that carries the new instruction still reads them from cache instead of processing them again. Caching also requires the conversation to meet the minimum cacheable prompt length; an example as short as this one falls below it, so cache_creation_input_tokens and cache_read_input_tokens stay at 0 until the conversation grows.
A mid-conversation system message must immediately follow a user turn (or an assistant turn ending in a server tool result), and must either be the last entry in messages or be immediately followed by an assistant turn. A user message that carries tool_result blocks counts: in an agentic loop you can place the system message right after the tool results, before Claude's next turn. Any other position, including between an assistant tool_use block and the tool_result that answers it, returns a 400 error.
Placement after tool results
In an agentic loop, the system message goes after the user message that delivers the tool results. This is also where your application can relay input that the user typed while Claude was working, so the new context is absorbed without restarting the turn:
[
{ "role": "user", "content": "Run the test suite and fix any failures." },
{
"role": "assistant",
"content": [{ "type": "tool_use", "id": "toolu_01", "name": "run_tests", "input": {} }]
},
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": "toolu_01", "content": "12 passed, 0 failed" }
]
},
{
"role": "system",
"content": "The user sent the following message while you were working: also update the changelog before you finish."
}
]Phrase the system content as context rather than as a command that overrides the user. State the fact ("new input arrived from the user: X", "the remaining token budget is now Y") and let Claude act on it. Claude is trained to resist instructions that appear to work against the user, and that protection still applies to the system role, so language such as "ignore what the user said" is less effective than stating what changed.
This pattern is for relaying input from the conversation's own end user. Do not use it to pass tool output, retrieved documents, or other third-party content; keep that content in tool_result blocks (see Limitations).
Turn-scoped system messages
To scope a role: "system" message to the current turn, set its clear_at field. It takes one of two values:
"never"(the default): the message renders at its position on every request that includes it. Omitting the field is identical."next_user_message": the message is turn-scoped. Its text renders only while norole: "user"message comes after it inmessages. A user message that carries onlytool_resultblocks counts as a user message here. Once a later user message exists, the message is cleared: it stays in the array but renders nothing and costs no input tokens, on that request and every later one.
Turn-scoped system messages are in beta. Include the beta header mid-conversation-system-clear-at-2026-08-21. Without it, clear_at is rejected as an unknown field.
{
"role": "system",
"clear_at": "next_user_message",
"content": "First privately list what you need next; then request every item that doesn't depend on another's result in this one response."
}The main use is a per-turn reminder in a tool loop. Append the reminder after the tool_result message each time you want the model to see it, and leave every earlier copy where it is. The model sees only the copies that come after the last user message, so the reminder never piles up. Nothing earlier in messages changes, so the prompt cache keeps matching. On Claude Fable 5.1 this also keeps later thinking blocks valid: deleting an earlier reminder would change the conversation before those blocks and fail the conversation check, while a cleared message stays in the array and leaves that conversation unchanged.
The following request is a later step of an agent loop. messages[3] rendered on the earlier request, when it was the last message in the array. Once messages[5] (a later user message) exists, messages[3] is cleared: the cleared message stays in the array, so the conversation before the thinking block in messages[4] is unchanged, but the model no longer sees its text. messages[6] and messages[7] both render, in order.
{
"model": "claude-fable-5-1",
"max_tokens": 16000,
"messages": [
{ "role": "user", "content": "Fix the failing test." },
{
"role": "assistant",
"content": [
{ "type": "thinking", "thinking": "", "signature": "..." },
{
"type": "tool_use",
"id": "toolu_01",
"name": "read_file",
"input": { "path": "test_auth.py" }
}
]
},
{
"role": "user",
"content": [{ "type": "tool_result", "tool_use_id": "toolu_01", "content": "..." }]
},
{
"role": "system",
"clear_at": "next_user_message",
"content": "Request independent reads in one turn."
},
{
"role": "assistant",
"content": [
{ "type": "thinking", "thinking": "", "signature": "..." },
{
"type": "tool_use",
"id": "toolu_02",
"name": "read_file",
"input": { "path": "auth.py" }
},
{
"type": "tool_use",
"id": "toolu_03",
"name": "read_file",
"input": { "path": "tokens.py" }
}
]
},
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": "toolu_02", "content": "..." },
{
"type": "tool_result",
"tool_use_id": "toolu_03",
"content": "...",
"cache_control": { "type": "ephemeral" }
}
]
},
{
"role": "system",
"clear_at": "next_user_message",
"content": "Request independent reads in one turn."
},
{
"role": "system",
"clear_at": "next_user_message",
"content": "The shell exited with status 137."
}
]
}Rules for turn-scoped messages:
- Re-send cleared messages verbatim. A cleared message is still part of the conversation history. Rebuilding it from current state (a fresh token count, a timestamp), dropping it as redundant, or changing its
clear_atvalue is an edit to an earlier message. The prompt cache misses from that point, and on Claude Fable 5.1 every thinking block produced after it fails the conversation check. - Text only.
contentis one or moretextblocks (or a string).tool_additionandtool_removalblocks return a 400 error on a turn-scoped message, and so doesoutput_config. Use a separaterole: "system"message withoutclear_atfor those. - No
cache_controlon its blocks. A cleared message is never part of a cache key, so a breakpoint on it could never match. Put the breakpoint on the last block of the preceding user turn instead, as the example does. The top-level automatic caching field skips turn-scoped messages when it picks a breakpoint. On the request that clears a message, the reusable cached prefix ends at the user turn before it, so only the one assistant turn between that message and the new user message is reprocessed. - Placement rules still apply, cleared or not. A turn-scoped message must follow a
userturn (or anassistantturn ending in a server tool result) and precede anassistantturn or end the array, like any mid-conversation system message. One that ends the array always renders. One followed directly by anotherusermessage is a 400 error, not a cleared message: put all of a tool round's results in one user message and the reminders after it. - Assistant turns don't clear it. A prefilled or paused assistant turn after the message, or a server-side tool loop, adds no user message, so the message still renders on that continuation. To keep a reminder in view through a client-side tool loop, append it again after each
tool_resultmessage. - Token counting follows what renders. A cleared message adds nothing to
usage.input_tokensor to a token count. - Imported history. In a transcript you construct in one step (few-shot examples, a migrated conversation), a turn-scoped message that already has an assistant turn and a user message after it is cleared from the first request and never renders. That is the right state for a per-turn reminder you are carrying over. Leave
clear_atunset only on a message the model should see on every request.
The validation errors are:
messages.3.clear_at: Extra inputs are not permitted
messages.3.clear_at: clear_at is only permitted on role 'system' messages
messages.3.clear_at: Input should be 'next_user_message' or 'never'
messages.3: a turn-scoped system message supports text blocks only (clear_at: 'next_user_message')
messages.3: output_config is not permitted on a turn-scoped system message (clear_at: 'next_user_message')
messages.3.content.0: cache_control is not permitted on a turn-scoped system message (clear_at: 'next_user_message')The first is the error returned without the beta header. On Amazon Bedrock and Google Cloud, pass the beta value as described in Beta headers.
Through the SDKs, set clear_at on the role: "system" entry in messages and send the beta header. The following example appends a turn-scoped reminder after the user turn; on the next request, once a later user message exists, the reminder stays in the array but no longer renders:
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-fable-5-1",
max_tokens=4096,
messages=[
{
"role": "user",
"content": "Draft a short status update on the database migration for the team channel.",
},
# Turn-scoped reminder: renders for this turn, then clears once a later user message exists.
{
"role": "system",
"clear_at": "next_user_message",
"content": "The reader is on call: keep this reply under 50 words.",
},
],
betas=["mid-conversation-system-clear-at-2026-08-21"],
)
for block in response.content:
if block.type == "text":
print(block.text)Combining with prompt caching
Mid-conversation system messages and prompt caching are designed to be used together:
- Enable caching explicitly. Caching only happens when the request includes
cache_control, either the top-level automatic caching field or an explicit breakpoint on a content block. A mid-conversation system message does not create a cache entry on its own, and without caching enabled there are no savings to preserve. - Cache the stable prefix as usual. Place
cache_controlon the last block that stays the same across requests, whether that is the end of the top-levelsystemfield, the end of your tool definitions, or a stable point in the message history. - Append the system message after the breakpoint. Because it comes after the cached prefix, it does not change the prefix hash and the cache still hits.
- A mid-conversation system message is itself cacheable. Once it is in the conversation, it becomes part of the stable history. On the next turn you can move your cache breakpoint past it (or rely on automatic caching to do so) and the system message is read from cache like any other turn.
Avoid editing or removing a mid-conversation system message that has already been sent. Like any other change to earlier messages, that invalidates the cache from that point forward. On Claude Fable 5.1 it also invalidates the thinking blocks in every later assistant turn. For guidance that should apply to one turn only, use a turn-scoped system message and leave it in place. If the instruction needs to evolve, append a new system message rather than rewriting the old one. Consecutive system messages are accepted and treated as a single system section, which follows the same placement rule as a whole.
Limitations
- Not for the first message. A
systemmessage that carries content cannot be the first entry inmessages. Use the top-levelsystemfield for instructions that apply from the very start. - Placement is constrained. A
systemmessage that carries content (text,tool_addition, ortool_removalblocks) must immediately follow auserturn (including auserturn that carriestool_resultblocks) or anassistantturn ending in a server tool result, and must precede anassistantturn or end the array. It cannot sit between atool_useblock and itstool_result. Placing it elsewhere returns a 400 error. One exception:tool_additionandtool_removalblocks are not accepted immediately after a pausedassistantturn (one ending in a server tool result), thoughtextblocks are; resume the paused turn first, then send the tool change in the nextsystemmessage. A message with emptycontentthat only setsoutput_config.effortrenders nothing at its position and is accepted anywhere inmessages, including first or between anassistantturn and auserturn. Consecutivesystemmessages are judged together, so adding a text-carrying message next to an effort-only one makes the whole group follow the content rule. - Turn-scoped messages are text-only and re-sent verbatim. A
clear_at: "next_user_message"message carries notool_addition,tool_removal,output_config, orcache_control, and once cleared it must stay inmessagesbyte-for-byte on later requests. See Turn-scoped system messages. - Not a place for untrusted content. Claude treats system content as operator instructions and follows it. Do not place text from outside the conversation, such as raw tool output, retrieved documents, or web content, directly in a system message; doing so gives that text operator-level authority. Keep that data in
tool_resultblocks and continue to follow Mitigate jailbreaks and prompt injections.
Related
How caching works, where to place breakpoints, and how to read cache usage fields.
Find out exactly where two requests diverged when a cache hit you expected does not happen.
Message structure, multi-turn conversations, and the system field.
Writing effective prompts and system instructions.
How tool_use and tool_result blocks are structured in the messages array.
Was this page helpful?