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.
>This is one agentic fact. Claude Certified Architect Foundations: Crash Course boils all five exam domains to a weekend and decodes the trick-question craft, with one realistic mock.

Claude Certified Architect Foundations: Crash Course
Pass the CCA-F Exam in a Weekend. The 5 Domains Boiled Down, the Judgment Patterns Decoded, and One Realistic Mock.
Summary:
- Every Claude Messages API response returns exactly one
stop_reasontelling you why it stopped.- There are seven values, and each maps to a different branch in your agent loop.
- The three people forget are
pause_turn,refusal, andmodel_context_window_exceeded.- 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_reason | Why it fired | What you do |
|---|---|---|
end_turn | Claude finished naturally. | Use the response. |
max_tokens | The response hit your max_tokens limit. | Raise the limit or continue the response. |
stop_sequence | Claude emitted one of your stop_sequences. | Read stop_sequence to see which fired. |
tool_use | Claude is calling a tool. | Run the tool, return the result. |
pause_turn | A server-tool loop hit its iteration limit. | Send the assistant content back to continue. |
refusal | Claude declined to respond. | Read stop_details, retry on a fallback model. |
model_context_window_exceeded | The response filled the model’s context window. | Treat as truncated; reduce the input. |
Source: Claude API docs, handling stop reasons.

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_reasonistool_use→ run the tool and return a message of onlytool_resultblocks. - If
stop_reasonisrefusal→ readstop_detailsand retry on a different fallback model. - If
stop_reasonispause_turn→ append the assistant content and continue the loop. - If
stop_reasonismodel_context_window_exceeded→ reduce the input; do not just raisemax_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_turncontinues,refusalswitches models,model_context_window_exceededreduces input. None of those is “raise a number and retry.” - A
tool_resultcarries results only. The moment you tuck a note in beside it, you have invented a failure the docs already warned you about.
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.