[Bug] deep-research workflow aborts entire run (and burns millions of tokens) when any schema-bound subagent fails to emit StructuredOutput
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
[]
8 Comments
Confirming this still reproduces on v2.1.165 (newer than the v2.1.162 in the report). Ran the
deep-researchworkflow 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'
WebFetchreturns empty for every sourceIn 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:StructuredOutputfailures — the fetch subagents emitted structured output correctly; they returned the script's documentedclaims: [], sourceQuality: "unreliable"fallback (script line ~149) precisely becauseWebFetchcame back empty.w3.org,owasp.org,developer.apple.com,nngroup.com,supabase.com, etc.WebFetchon the exact same URLs worked perfectly in the same session (e.g. pulled the full WCAG 2.2 criteria list fromw3.org/WAI/standards-guidelines/wcag/new-in-22/). SoWebFetchappears 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:
WebFetchreturns empty → 0 claims → early return. (new — not in the original report)Workaround that fully replaced the workflow
Skip it entirely: fan out plain
general-purposesubagents (prose return, no forced schema), each callingWebSearch+WebFetchdirectly 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.
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.
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-b35as another data point. Confirms your characterization that the failure is uncatchable at script level (I'd have wrappedagent()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).
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-0vote 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:
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.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-researchskill on an ordinary research question. Scope/Search/Fetch/Verify all completed (~100 subagents). The finalsynthesizestep — oneagent({schema})call against aREPORT_SCHEMA(required: [summary, findings, caveats]) — failed and took down the entire run: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, andopenQuestionsas Claude's XML tool-call syntax embedded inside thesummarystring value, e.g.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 fatalTelemetrySafeErrorthat bypassesparallel()'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):
null(so the script's own guard /.catchcan salvage) instead of throwing a fatal — matching the documentedparallel()semantics.agent()results so a downstream failure degrades to a partial report rather than zeroing the run.Environment: darwin, claude-code 2.1.187.
Corroborating the Synthesize-phase variant (@Quill-AI-Assistant's comment) on v2.1.187, at larger scale.
Run:
deep-researchon 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 singlesynthesizeagent({schema: REPORT_SCHEMA})call then died with:…taking the entire run down. Likely trigger: 50 confirmed claims → a large
findingsarray 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 documentedagent()contract ("returns null … on a terminal API error after retries"). Given the reports above that script-level.catchdoesn't contain these schema-abort failures either, the fix likely belongs at the harness level (makeagent({schema})returnnullon 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
StructuredOutputtool-call inputs (theclaimsbundles +refutedverdicts) 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.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 wasclaude-sonnet-5— see #70645).Setup: Claude Code 2.1.199, built-in
/deep-researchviaWorkflow({name:'deep-research', args:...}), subagent modelclaude-sonnet-5(confirmed from transcript metadata). Finalsynthesizeagent({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
synthesizeagent was rejected 3× in a row withOutput 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:i.e. the model serialized the entire report as Anthropic's XML tool-call grammar (
<findings>,<item>,</openQuestions>,</StructuredOutput>) inside thesummarystring parameter, instead of emittingfindings/caveats/openQuestionsas 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:
synthesizeagent hit the same XML-leak rejection, then tried to debug its own content: it wrote the findings array to a scratchpad file, ranpython3 -c "json.load(...)"to confirm the content parses, suspected unescaped&in source URLs, rewrote with&-escaping, and resubmitted — still rejected with the identical error, because the defect is in the tool-call serialization envelope, not the content. The genericmust 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 sametest-placeholder submission, which passed.TelemetrySafeError: agent({schema}): StructuredOutput retry cap (5) exceeded— a genuine throw, not anullresolution.On the crash itself. The invocation where the retry threw had the
synthesizecall not wrapped intry/catch, and it died atstatus: failedafter 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 atry { } 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-leveltry/catchreliably 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 inparallel()rather than a bare awaited call.Mitigations that worked (in a local copy of the workflow script — the built-in one isn't editable):
findings.lengthunder a threshold,example.comsources) + 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.try/catcharound the schema-boundagent()call, converting the throw tonull, 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.
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