[Bug] deep-research workflow aborts entire run (and burns millions of tokens) when any schema-bound subagent fails to emit StructuredOutput

Status Open
Reported on v2.1.162
Maintainer reply None cached
Activity 8 comments · opened Jun 4, 2026

What happened: I ran the deep-research skill/workflow. It consistently fails in the Verify phase and produces no report, after consuming a very large number of tokens (~3.5M across three attempts, with zero usable output).

Root cause (from inspecting the workflow script and subagent transcripts):

  • The workflow's Verify phase fans out claims × 3 votes (~75) concurrent agent(prompt, { schema }) calls.
  • When any single one of those subagents ends its turn with prose instead of calling the StructuredOutput tool (even after the 2 automatic nudges), the harness raises a FATAL error: agent({schema}): subagent completed without calling StructuredOutput (after 2 in-conversation nudges).
  • This error is not catchable at the script level. I added a .catch(() => null) around each verify agent() call to degrade a missing output into an abstention; the run still died with the identical error. So the harness escalates this to a workflow-fatal failure that bypasses the script's own .catch, contradicting the documented behavior that "a thunk that throws resolves to null."
  • With ~75 schema-bound subagents per run, the probability that at least one fails to emit StructuredOutput is very high, making the workflow fail almost every time at scale.

Impact: The expensive Search/Fetch phases run first, then the run dies in Verify before Synthesis — so the customer pays full token cost for no output. Resume/caching also did not work (re-ran all phases from scratch), compounding the cost.

Expected: A single subagent failing to emit structured output should degrade to an abstention/skip, not abort the whole workflow. Schema-enforcement failures must be containable, or high fan-out of schema-bound agents should not be used.

Requests: (1) Fix the non-catchable fatal-error behavior for schema-bound subagents; (2) review/remove the deep-research workflow until fixed; (3) token/credit reimbursement for the wasted spend caused by this defect.

Environment Info

  • Platform: darwin
  • Terminal: ghostty
  • Version: 2.1.162
  • Feedback ID: 1d6d1012-4167-4c98-846c-1e4f6b352191

Errors

[]

View original on GitHub ↗

8 Comments

jackjackliew · 2 months ago

Confirming this still reproduces on v2.1.165 (newer than the v2.1.162 in the report). Ran the deep-research workflow 3× in one session (UI / UX / Security topics), ~840K subagent tokens total across 176 agents, zero usable output.

I also hit a second, distinct failure mode in the same workflow that yields the same "pay full cost, get nothing" outcome but never even reaches Verify:

(A) Fetch-phase subagents' WebFetch returns empty for every source

In 2 of my 3 runs the workflow returned early with "25 sources fetched, all empty/failed" and "30 sources fetched, all empty/failed" → 0 claims extracted → early return before Verify. Key points:

  • These runs had no StructuredOutput failures — the fetch subagents emitted structured output correctly; they returned the script's documented claims: [], sourceQuality: "unreliable" fallback (script line ~149) precisely because WebFetch came back empty.
  • The sources were gold-standard and reachable: w3.org, owasp.org, developer.apple.com, nngroup.com, supabase.com, etc.
  • Main-thread WebFetch on the exact same URLs worked perfectly in the same session (e.g. pulled the full WCAG 2.2 criteria list from w3.org/WAI/standards-guidelines/wcag/new-in-22/). So WebFetch appears non-functional or sandboxed specifically inside workflow subagents, independent of the StructuredOutput bug in this issue.

(B) Verify-phase StructuredOutput abort — confirming the original report

My 3rd run did reach Verify and died exactly as described: 24 claims extracted, all killed on 0-0 votes, with dozens of agent({schema}): subagent completed without calling StructuredOutput (after 2 in-conversation nudges) errors, and the script's per-vote .catch/abstention logic not honored.

So there appear to be two independent "full cost, zero output" paths in this workflow:

  • (A) Fetch subagents: WebFetch returns empty → 0 claims → early return. (new — not in the original report)
  • (B) Verify subagents: one schema-bound agent fails to emit StructuredOutput → non-catchable fatal abort. (this issue)

Workaround that fully replaced the workflow

Skip it entirely: fan out plain general-purpose subagents (prose return, no forced schema), each calling WebSearch + WebFetch directly and returning a cited markdown report. Worked first try, produced three thorough cited references, far fewer tokens.

Env: darwin, Claude Code v2.1.165, terminal: Claude Code CLI.

powell-clark · 2 months ago

Cross-linking from #65731. Same verify-phase failure surface as this issue, with an additional downstream defect worth capturing in case these are merged.

When the schema-bound verify subagents fail (whether via the non-catchable abort described here, or via mid-turn 429 throttling), the harness scores the failed or abstaining verifier as a refutation rather than as "unverified". On my runs this produced a completed run reporting "All 25 claims refuted, research inconclusive" despite search and fetch having succeeded (24 sources, 64 claims extracted).

So beyond "abort and burn tokens for zero output", there is a second outcome: the run finishes and silently reports the opposite of what it found. The fix that contains it is making abstain or no-output mark a claim unverified and kept (flagged), never refuted. Full detail in #65731.

telecomcellfone-lab · 2 months ago

Hit this exact failure in an unrelated ultracode workflow. Same error string (subagent completed without calling StructuredOutput (after 2 in-conversation nudges)), same loss pattern — ~250k tokens of subagent work discarded with no surfaced output.

Adding run ID wf_b6c94aca-b35 as another data point. Confirms your characterization that the failure is uncatchable at script level (I'd have wrapped agent() calls in .catch(() => null) too, expecting the documented thunk-throws-resolves-to-null behavior). Not specific to deep-research — any high-fan-out workflow with schema-bound subagents will eventually hit it. +1 on the requested fix: parse the last assistant message as fallback rather than discarding the entire run.

Cross-link: also reported in #65975 (where github-actions[bot] correctly flagged this issue as a partial duplicate).

Nickcom4 · 2 months ago

Corroborating evidence from a separate reproduction (market-research session):

The adversarial-verify phase spawns N voters × up to 25 claims (default ~75 schema-bound subagents in parallel). Under that concurrency the verify subagents repeatedly "completed without calling StructuredOutput (after 2 in-conversation nudges)", so every claim landed at a 0-0 vote and was killed as a false negative — the run reported "All 25 claims refuted" on good, well-sourced claims. Each failed run burned ~2.7M subagent tokens.

Key diagnostic: the same truncation also appeared intermittently in the search/extract schema phases, but only when multiple fresh runs ran concurrently (e.g. 3 at once). Running one fresh run at a time was reliable. So the root cause is concurrency / rate-limit-driven truncation of schema agents before they emit StructuredOutput — not bad data and not a prompt issue. (Likely the same root as #65731's rate-limiting.)

Two distinct fixes worth separating:

  1. Robustness: cap verify concurrency (or batch with backoff) + retry an agent that finishes without StructuredOutput.
  2. Correctness: an all-abstain (all 0-0) vote set must read as INCONCLUSIVE, not REFUTED. Today an all-abstain set silently flips every claim to "refuted," which is the most damaging symptom — it turns a transient infra hiccup into a confidently-wrong "all claims refuted" report. This is independent of the concurrency fix and worth doing on its own.
Quillrun · 2 months ago

Still reproduces on v2.1.187 (latest) — with a distinct, concrete trigger in the Synthesize phase (the single final agent), complementing the Verify-phase variant in the original report.

Repro: ran the bundled /deep-research skill on an ordinary research question. Scope/Search/Fetch/Verify all completed (~100 subagents). The final synthesize step — one agent({schema}) call against a REPORT_SCHEMA (required: [summary, findings, caveats]) — failed and took down the entire run:

agent({schema}): StructuredOutput retry cap (5) exceeded — 5 failed calls with no valid output

Cost of the failed run: 100 agents, ~2.40M tokens, 530 tool calls, ~16 min → zero output. The Search/Fetch/Verify work was fully complete but discarded with the throw.

Root cause of the 5 schema rejections (read directly from the synthesis agent's transcript): the model emitted findings, caveats, and openQuestions as Claude's XML tool-call syntax embedded inside the summary string value, e.g.

{"summary": "…</parameter>\n<parameter name=\"findings\">[ … ]</parameter>\n<parameter name=\"caveats\">…"}

so the validator received {summary: "<one big string>"} and correctly rejected it: root: must have required property 'findings' (call 1), then …'findings', …'caveats' (calls 2–5). Same structural error on all 5 retries. The model self-diagnosed it as output-size truncation and progressively shortened its output (summary 12.3k → 11.5k → 9.4k → 7.0k chars), which never addressed the structural problem. The schema is not over-constrained (3 required string/array fields, no size limits) — the failure is the model conflating the XML <parameter> encoding with the JSON StructuredOutput schema, then chasing the wrong remedy until the cap.

Why this is the worst case: the throw is agent({schema}) raising a fatal TelemetrySafeError that bypasses parallel()'s documented "a thunk that throws resolves to null" degradation, so a single final-stage formatting failure discards an otherwise-complete, expensive run. (Same zero-output-after-large-spend shape as #69213.)

Suggested fixes (any one defuses the worst case):

  1. On retry-cap exhaustion, return null (so the script's own guard / .catch can salvage) instead of throwing a fatal — matching the documented parallel() semantics.
  2. Persist completed agent() results so a downstream failure degrades to a partial report rather than zeroing the run.
  3. Feed the StructuredOutput retry the actual validator error — the model never learned it was a structural missing-key problem (not size), so all 5 retries repeated it.

Environment: darwin, claude-code 2.1.187.

bilal-m · 2 months ago

Corroborating the Synthesize-phase variant (@Quill-AI-Assistant's comment) on v2.1.187, at larger scale.

Run: deep-research on a product/market strategy brief. Scope → Search → Fetch → Verify all completed — 96 subagents, ~3.0M subagent tokens, ~18 min, 75 claims extracted (50 confirmed / 25 refuted). The final single synthesize agent({schema: REPORT_SCHEMA}) call then died with:

agent({schema}): StructuredOutput retry cap (5) exceeded — 5 failed calls with no valid output

…taking the entire run down. Likely trigger: 50 confirmed claims → a large findings array that Opus malforms 5× in a row (consistent with #70645, and the Scope-phase sibling #72267).

One diagnostic worth adding: the built-in's synthesis step has a salvage path — if (!report) { return <verified claims raw> } — but it only handles the null-return case. The retry-cap condition throws, so it bypasses that salvage entirely, contradicting the documented agent() contract ("returns null … on a terminal API error after retries"). Given the reports above that script-level .catch doesn't contain these schema-abort failures either, the fix likely belongs at the harness level (make agent({schema}) return null on retry-cap-exceeded, per its documented contract) rather than in the workflow script.

Practical mitigation that works today: the verified work is fully recoverable from transcripts even on a hard crash — the StructuredOutput tool-call inputs (the claims bundles + refuted verdicts) persist under …/subagents/workflows/<runId>/agent-*.jsonl. I reconstructed the complete synthesized report by extracting those, so a crashed run needn't be a total loss.

jaebit · 1 month ago

Corroborating the Synthesize-phase variant with a root-cause trace from raw subagent transcripts (agent-<id>.jsonl), plus evidence this is not Opus-specific (subagent model here was claude-sonnet-5 — see #70645).

Setup: Claude Code 2.1.199, built-in /deep-research via Workflow({name:'deep-research', args:...}), subagent model claude-sonnet-5 (confirmed from transcript metadata). Final synthesize agent({schema: REPORT_SCHEMA}) had to emit a ~6–8KB Korean-language (CJK) nested report (summary/findings[]/caveats/openQuestions, all required). I needed 4 workflow invocations in one session to get a usable result; the failures are three distinct manifestations of one underlying defect, so I'll attribute each to its actual invocation rather than blend them.

Invocation 1 — placeholder escape hatch (same as #72675). The synthesize agent was rejected 3× in a row with Output does not match required schema: root: must have required property 'findings', root: must have required property 'caveats', then on the 4th attempt submitted {"summary":"test","findings":[{"claim":"test claim","sources":["https://example.com"]}],"caveats":"test caveat"} — a schema-valid placeholder — which passed and became the workflow's final "successful" output.

The root cause, from the raw transcript: each of the 3 rejected attempts had only one top-level key (summary), whose string value ended:

...얼마나 다르게 나타나는가?"]</openQuestions>
</StructuredOutput>

i.e. the model serialized the entire report as Anthropic's XML tool-call grammar (<findings>, <item>, </openQuestions>, </StructuredOutput>) inside the summary string parameter, instead of emitting findings/caveats/openQuestions as sibling JSON keys. Same failure class as #70645 ("mixing XML-tagged tool-call grammar into what the harness expects as a pure-JSON argument"), here with the whole payload getting XML-wrapped into one field.

Invocation 3 — why retries can't recover, and the uncatchable-throw crash. In a later invocation (after I'd patched a degenerate-output detector into a local copy of the script), two things happened that pin down the mechanism:

  • The first synthesize agent hit the same XML-leak rejection, then tried to debug its own content: it wrote the findings array to a scratchpad file, ran python3 -c "json.load(...)" to confirm the content parses, suspected unescaped & in source URLs, rewrote with &amp;-escaping, and resubmitted — still rejected with the identical error, because the defect is in the tool-call serialization envelope, not the content. The generic must have required property 'findings' message reads as a content problem, so the model burns effort fixing the wrong thing; it can't see its own tool-call serialization. It eventually fell back to the same test-placeholder submission, which passed.
  • My detector caught that placeholder and triggered one reinforced retry. That retry agent was rejected 5× identically and then threw TelemetrySafeError: agent({schema}): StructuredOutput retry cap (5) exceeded — a genuine throw, not a null resolution.

On the crash itself. The invocation where the retry threw had the synthesize call not wrapped in try/catch, and it died at status: failed after 101/102 agents had already completed (~3.3M subagent tokens) — the same "pay full cost, get nothing" outcome as your Verify-phase report, just triggered in Synthesize. I then added a try { } catch { return null } around that call as a mitigation, but I can't claim from direct observation that it contains the throw: my next run happened to succeed at synthesis on its own, so the catch path never actually fired. So all I can say firmly is (a) the unwrapped call escalated a schema-retry-cap throw to a full-workflow abort, and (b) whether script-level try/catch reliably contains it — the obvious workaround — is unverified here and worth the maintainers confirming, especially against your Verify-phase case where the failing call is one of ~75 nested in parallel() rather than a bare awaited call.

Mitigations that worked (in a local copy of the workflow script — the built-in one isn't editable):

  1. Post-hoc degenerate-output detection (placeholder strings, findings.length under a threshold, example.com sources) + one reinforced retry ("a prior attempt returned a placeholder — do the real work"). Recovered a normal 4-key submission in my case, but not guaranteed — the serialization drift looks stochastic, not a deterministic function of the prompt.
  2. try/catch around the schema-bound agent() call, converting the throw to null, then falling back to returning the raw upstream (Verify-stage) results unmerged rather than fabricating or losing the run.

Neither should be necessary at the script level. The core defect is the harness reporting "schema validation failed" without ever surfacing why (serialization envelope vs. content), which is what makes 5 retries burn budget with zero correction signal.

Cross-links: root-cause mechanism = #70645, placeholder-escape-hatch = #72675, docs gap on throw-vs-null = #70140 — these read as one underlying defect surfacing at different workflow stages.

saimageshvar · 1 month ago

Using v2.1.205. Without knowing about this issue, I triggered a /deep-research. Exhaused all my tokens, around 804k tokens in deep-research alone