Workflow subagent killed by maxTurns is misreported as 'completed without calling StructuredOutput'

Status Open
Reported on v2.1.218
Maintainer reply None cached
Activity 1 comment · opened Jul 24, 2026

Environment

  • Claude Code: 2.1.218
  • Platform: Linux (WSL2), x86_64
  • Feature: Workflow tool (workflow-subagent runner) with a custom agent type via agent(..., { 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

  1. Define a custom agent in ~/.claude/agents/ with maxTurns: 30 in the frontmatter.
  2. 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).
  3. The agent works normally, then the transcript ends abruptly immediately after a successful tool result — the runner never issues the next request.
  4. The workflow fails with the message above; agents_error stays 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 assistant event; intermediate chunks carry stop_reason: null and tiny output_tokens, which look exactly like empty/aborted responses in a post-mortem until you dedupe by message.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:

  1. 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.
  2. 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.
  3. A counter in the usage summary (agents_max_turns: 1 or similar) — agents_error: 0 alongside 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 maxTurns anywhere in the failure surface; the transcript simply stops after the last in-budget tool result.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗