Workflow subagent killed by maxTurns is misreported as 'completed without calling StructuredOutput'
Environment
- Claude Code: 2.1.218
- Platform: Linux (WSL2), x86_64
- Feature: Workflow tool (
workflow-subagentrunner) with a custom agent type viaagent(..., { agentType }) - Subagent models observed: claude-sonnet-5, claude-opus-4-8
Summary
When a Workflow-tool subagent (dispatched with agentType pointing at a custom agent whose frontmatter sets maxTurns) exhausts its turn budget, the failure surfaces as:
agent({schema}): subagent completed without calling StructuredOutput (after in-conversation nudge)
with agents_error: 0 in the usage summary. Nothing in the error, the result payload, or the counters mentions the turn cap. The message describes model non-compliance ("completed without calling StructuredOutput"), which sends diagnosis in exactly the wrong direction — the agent didn't decline to produce structured output; the runner stopped granting turns.
Reproduction
- Define a custom agent in
~/.claude/agents/withmaxTurns: 30in the frontmatter. - Run a Workflow script that dispatches it via
agent(prompt, { agentType: '<name>', schema })on a task that needs more than 30 turns (e.g. a long sequential implementation checklist with verification commands). - The agent works normally, then the transcript ends abruptly immediately after a successful tool result — the runner never issues the next request.
- The workflow fails with the message above;
agents_errorstays 0; the agent's return value is null.
Observed across four consecutive runs of the same workflow: each terminated at exactly 30 unique API message IDs (the configured cap), at four different task positions. Agents of other types in the same session (default workflow-subagent, no maxTurns pin) completed fine at 32–53 turns, ruling out a global cap.
Diagnosis cost
The misattribution is expensive in practice. In our incident, the same failure produced three successive wrong root-cause hypotheses (context exhaustion, then an empty-response/stream anomaly, then a suspected hard harness cap) before transcript archaeology — counting unique message.ids per agent and correlating with agent-definition frontmatter — revealed the configured maxTurns as the cause. Total cost: four failed workflow runs and several hours; the actual fix was a one-line frontmatter change.
Two secondary factors compounded the misread:
- Per-content-block transcript events: the subagent JSONL records each content block (thinking/text/tool_use) of one API message as a separate
assistantevent; intermediate chunks carrystop_reason: nulland tinyoutput_tokens, which look exactly like empty/aborted responses in a post-mortem until you dedupe bymessage.id. - The "(after in-conversation nudge)" wording implies the model was given a recovery chance and declined — but a turn-capped agent may have no budget left for the nudge response to matter.
Expected
When an agent ends because it hit maxTurns:
- The failure reason should say so explicitly, e.g.
agent exhausted maxTurns (30) before returning a result, ideally naming the agent definition the cap came from. - The condition should be machine-distinguishable in the result payload (e.g.
endReason: "max_turns"), so orchestration scripts can retry/split/resume instead of treating it as model non-compliance. - A counter in the usage summary (
agents_max_turns: 1or similar) —agents_error: 0alongside a turn-cap kill reads as "nothing went wrong".
Actual
- Error:
subagent completed without calling StructuredOutput (after in-conversation nudge) agents_error: 0,agents_empty_result: 1- No mention of
maxTurnsanywhere in the failure surface; the transcript simply stops after the last in-budget tool result.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗