Free playbooks in your inbox
Hands-on tutorials for people who want to build with AI.

The 7 Claude API stop_reason Values, and How to Handle Each

The Claude API stop_reason values tell you why a response stopped. All seven, the handler each one needs, and the three people forget in an agent loop.

From the youcanbuildthings catalog ▸ Build-tested 7 min read

Summary:

  1. Every Claude Messages API response returns exactly one stop_reason telling you why it stopped.
  2. There are seven values, and each maps to a different branch in your agent loop.
  3. The three people forget are pause_turn, refusal, and model_context_window_exceeded.
  4. You get the full handler table, the loop code, and the two false facts that break agents.

The Claude API stop_reason values tell you why a response stopped, and each of the seven maps to a different move in your agent loop. Read the wrong one, or forget that three of them exist, and your loop hangs, truncates silently, or treats a refusal as a finished answer. This is the spine of an agentic system, and on the Claude Certified Architect exam it is Domain 1, Agentic Architecture, worth 27% of your score, the heaviest slice on the test.

What are the seven stop_reason values?

Every Messages API response includes a stop_reason field, and it takes one of seven values. Here they are with the move each one needs, pulled from the docs:

stop_reasonWhy it firedWhat you do
end_turnClaude finished naturally.Use the response.
max_tokensThe response hit your max_tokens limit.Raise the limit or continue the response.
stop_sequenceClaude emitted one of your stop_sequences.Read stop_sequence to see which fired.
tool_useClaude is calling a tool.Run the tool, return the result.
pause_turnA server-tool loop hit its iteration limit.Send the assistant content back to continue.
refusalClaude declined to respond.Read stop_details, retry on a fallback model.
model_context_window_exceededThe response filled the model’s context window.Treat as truncated; reduce the input.

Source: Claude API docs, handling stop reasons.

Radial map of the seven Claude API stop_reason values around a central hub reading read stop_reason not the sentence, each spoke pairing a value with its move, with pause_turn refusal and model_context_window_exceeded called out as the three everyone forgets

Read the hub of that map first: you decide why the turn ended by reading stop_reason, not by reading the sentence.

Why read the field instead of the text?

You read the field because the control signal is programmatic, and the prose is not. An agent is a loop. You send a message with tools available, Claude responds with a stop_reason, and you branch on it. If the reason is tool_use, you run the tool and hand the result back. You repeat until a turn returns end_turn, and then you are done.

messages = [{"role": "user", "content": "What's the weather in SF?"}]

while True:
    resp = client.messages.create(model=MODEL, max_tokens=1024,
                                   tools=tools, messages=messages)
    if resp.stop_reason == "tool_use":
        messages.append({"role": "assistant", "content": resp.content})
        results = [run_tool(b) for b in resp.content if b.type == "tool_use"]
        # the continuation message is NOTHING but tool_result blocks
        messages.append({"role": "user", "content": results})
        continue
    break   # end_turn, max_tokens, refusal, etc. — exit and inspect stop_reason

Any handler that decides an agent is done by scanning the text for “finished” or “complete” is broken. The model can say “all done” mid-tool-call, and it can finish a turn without saying anything of the sort. You read stop_reason, not the sentence. One more rule hides in that loop: the message you send back after a tool_use must contain nothing but the tool_result blocks. Slip a chatty note in beside them and you can end the turn early or error out on a server tool.

The three everyone forgets

The three that break real agents are pause_turn, refusal, and model_context_window_exceeded, because their handlers are not intuitive and a careless list leaves them out. Branch on them explicitly:

if resp.stop_reason == "refusal":
    # NOT: resend to the same model with a firmer prompt
    resp = client.messages.create(model=FALLBACK_MODEL, ...)   # different model
elif resp.stop_reason == "pause_turn":
    messages.append({"role": "assistant", "content": resp.content})
    continue   # server-tool loop paused; append and keep going

Look at what each branch refuses to do. On a refusal you switch models, because re-prompting the same one more sternly earns the same refusal. On a pause_turn you append and continue, because the partial turn is not the final answer. And model_context_window_exceeded is the one people collapse into max_tokens. They are different: max_tokens means you hit the cap you set, so raise a number; model_context_window_exceeded means you filled the model’s window, so you cannot just raise a number, you have to reduce the input you send.

Two flat-out false statements show up as bait, and both are worth memorizing as false. “A tool_result block can carry an explanatory note” is wrong; results only. “On a refusal, retry the same model with a firmer prompt” is wrong; you switch models. Knowing a claim is false is as useful as knowing the right handler.

Prove you have all seven

Recall feels solid until you write it down and find a hole. Grade yourself in code instead of trusting your memory. Fill in the set you can name, then let the script name what you dropped:

# self-quiz: fill these in from memory, then run this to grade yourself
CANON = {
    "end_turn": "use the response",
    "max_tokens": "raise your limit or continue the response",
    "model_context_window_exceeded": "reduce the input, don't just raise a number",
    "stop_sequence": "read which sequence fired",
    "tool_use": "return tool_result blocks, nothing else",
    "pause_turn": "append the content and loop again",
    "refusal": "switch to a fallback model",
}
my_recall = {"end_turn", "max_tokens", "stop_sequence", "tool_use"}  # <- your set

missing = set(CANON) - my_recall
print("all seven present" if not missing
      else f"you forgot: {sorted(missing)}")

With the four-value set above, you get the exact three a fake list strips out:

you forgot: ['model_context_window_exceeded', 'pause_turn', 'refusal']

Widen my_recall until it prints all seven present. That is the whole drill.

What should you actually do?

  • If you are deciding “is the agent done” → branch on stop_reason == "end_turn", never on the model’s text.
  • If stop_reason is tool_use → run the tool and return a message of only tool_result blocks.
  • If stop_reason is refusal → read stop_details and retry on a different fallback model.
  • If stop_reason is pause_turn → append the assistant content and continue the loop.
  • If stop_reason is model_context_window_exceeded → reduce the input; do not just raise max_tokens.

The bottom line

  • Seven values, one per response, each a branch. end_turn, max_tokens, stop_sequence, tool_use, pause_turn, refusal, model_context_window_exceeded.
  • Read the field, not the prose. An agent that decides “done” by parsing text is a bug waiting for the model to phrase something inconveniently.
  • The forgotten three are where agents break. pause_turn continues, refusal switches models, model_context_window_exceeded reduces input. None of those is “raise a number and retry.”
  • A tool_result carries results only. The moment you tuck a note in beside it, you have invented a failure the docs already warned you about.
Why trust this? Every youcanbuildthings guide is pulled from a build-tested book: code that ran in production before it was written down.

Frequently Asked Questions

What are the stop_reason values in the Claude API?+

There are seven: end_turn, max_tokens, stop_sequence, tool_use, pause_turn, refusal, and model_context_window_exceeded. Each Messages API response returns exactly one, telling you why generation stopped.

How should an agent decide it is finished?+

On stop_reason equal to end_turn, not by scanning the model's text for a word like 'finished' or 'complete'. The control signal is the stop_reason field, not the prose.

What do you do on a refusal stop_reason?+

Read stop_details, which is null for every other reason, and retry on a genuinely different fallback model. Re-sending the same request to the same model with a firmer prompt just earns the same refusal.