CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/API & SDK
API & SDK/2026-05-23Intermediate

Absorbing the Claude API "Tool Result Submitted" Error in a Retry Layer: A Small Conversation-History Repair

How I absorbed the Claude API "Tool result could not be submitted because the previous turn was not a tool use" error inside a small retry layer, with the diagnosis order I followed after it hit a production batch.

claude-api81anthropic-sdk3retry7indie-dev14

Early in May my AdMob review-summary batch went down with Tool result could not be submitted because the previous turn was not a tool use. I was processing roughly 8,000 reviews per day for the wallpaper app portfolio, so a production halt was not gentle. The error message has multiple possible entry points, which makes the first hour of triage harder than it has to be. These are the notes from working through it.

I am Masaki Hirokawa, an indie developer running wallpaper apps since 2014 (over 50 million downloads across the portfolio). I use the Claude API for article generation and AdMob review-response automation, and this story comes straight from that AdMob batch.

The error always means one thing, with three entry points

The error always means the same thing: a tool_result block landed somewhere that wasn't immediately after a matching tool_use. Where it gets confusing is that there are three different paths into that state. Mixing them up means the bug refuses to go away no matter how many times you "fix" it.

  • A. Manual history edits: you trimmed an older turn and lost the tool_use, leaving an orphan tool_result.
  • B. Retry-implementation bug: a transient API error triggered a retry that re-sent the tail of messages starting with the tool_result.
  • C. Concurrent-request ordering: two async calls were in flight and the tool_result got submitted before the tool_use was written back.

I hit B. My retry path naively re-sent only the tail of messages whenever a 429 came back, which is fine until the tail starts with a tool_result.

Triage order

When the error fires, log the entire messages array you were about to send. Dropping a json.dumps() immediately before messages.create() is enough. Then walk the tail in reverse and check just two things:

  1. Every tool_result block must have a tool_use immediately before it in conversation order.
  2. The tool_use_id on the tool_result must match an id on that preceding tool_use.

If both hold, the error cannot fire. If they fail, the failure shape tells you exactly which of A, B, or C you are looking at.

A small repair layer that lives inside retry

What I ended up shipping is a retry layer that re-normalizes the conversation history before each send. The minimal Python version is roughly this much code:

from anthropic import Anthropic, APIError
import time
 
client = Anthropic()
 
def ensure_tool_pairing(messages):
    """Drop tool_result blocks that lost their preceding tool_use."""
    safe = []
    seen_tool_use_ids = set()
    for msg in messages:
        content = msg.get("content", [])
        if not isinstance(content, list):
            safe.append(msg)
            continue
        new_blocks = []
        for b in content:
            t = b.get("type")
            if t == "tool_use":
                seen_tool_use_ids.add(b["id"])
                new_blocks.append(b)
            elif t == "tool_result":
                if b.get("tool_use_id") in seen_tool_use_ids:
                    new_blocks.append(b)
            else:
                new_blocks.append(b)
        if new_blocks:
            safe.append({**msg, "content": new_blocks})
    return safe
 
def call_with_repair(messages, max_retry=3):
    for attempt in range(max_retry):
        try:
            return client.messages.create(
                model="claude-sonnet-4-6",
                max_tokens=2048,
                messages=ensure_tool_pairing(messages),
                tools=[...],
            )
        except APIError as e:
            if "tool_result could not be submitted" in str(e):
                messages = ensure_tool_pairing(messages)
                time.sleep(0.5 * (2 ** attempt))
                continue
            raise
    raise RuntimeError("repair retry exhausted")

The detail that matters is calling ensure_tool_pairing both on the first send and inside the retry. My first implementation only called it inside retry, which missed the case A failures that hit on the very first send.

A note on concurrent requests

A friend of mine ran into case C the harder way. He had two asyncio.gather legs in flight; one finished first and submitted a tool_result before the other leg had written its tool_use back to shared state. The SDK is blameless here - it is a state-management bug that only becomes visible when two coroutines share the same messages array.

My personal recommendation: keep any conversation that uses tools inside a single coroutine. If you want parallelism, run independent conversations with independent messages arrays. Debugging is much faster, and the throughput ends up similar.

What to try first

If you are looking at this error in your own code right now, drop a print statement just before messages.create() that emits each tool_use_id and each tool_result.tool_use_id in order. Five minutes of that output and you will know whether you are in A, B, or C.

Even on the scale of a 50-million-download wallpaper portfolio, API integration bugs tend to be the last category of issues to settle. I hope this is useful if you are looking at the same stack trace.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API & SDK2026-05-26
Stabilizing Claude API Structured Responses in Production — Notes on tool_use, JSON Schema, and Layered Validation
Getting Claude to return JSON takes a few lines. Keeping that JSON usable in production is a different problem. Here is the layered design I landed on after running a wallpaper classification pipeline through Claude API, built around tool_use, JSON Schema, and domain validation.
API & SDK2026-05-02
Calling Claude API from iOS Shortcuts: A Personal Setup for Reshaping Selected Text on the Fly
A personal setup guide for invoking the Claude API directly from iOS Shortcuts. Reshape selected text in seconds with a Cloudflare Workers proxy that keeps your API key off the device.
API & SDK2026-05-01
When Your Claude API Retry Logic Made Rate Limits Worse — The Retry-After Header You Forgot to Read
If 429 errors went up after you added retry logic to your Claude API client, the cause is almost always the same: ignoring the Retry-After header and using exponential backoff without jitter. Here is how to diagnose and fix it.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →