Workflow tool: StructuredOutput retry exhaustion silently accepts degenerate placeholder that passes loose schema — no retry/degraded signal exposed
Summary
In the Workflow tool (agent() with a schema: option), when the subagent repeatedly fails StructuredOutput schema validation and hits the retry cap, the model's final give-up attempt — a minimal placeholder like {"summary":"test","findings":[{"severity":"BLOCK","file":"a","title":"b","detail":"c"}]} — can pass validation (if the schema has bare type:"string" fields) and is then returned to the workflow script as a valid result, with no signal that it was produced under retry exhaustion. Downstream agents then act on fabricated, content-free data.
Observed 5 times with an identical deterministic signature across two long-running multi-agent build programs (Claude Code 2.1.207, macOS/darwin).
The mechanism (from on-disk journals + agent transcripts)
- A review agent produces a correct, substantive review and calls
StructuredOutput. On attempts 1–4 it serializes the requiredfindingsarray as XML-style tags inside the siblingsummarystring (...</summary>\n<findings>\n<finding>\n<severity>BLOCK</severity>...) instead of a top-levelfindingskey. - Each attempt is rejected:
Output does not match required schema: root: must have required property 'findings'. The validation feedback does not steer the model off the XML-in-string shape across 4 consecutive tries. - On attempt 5 the model emits a degenerate stub (
file:"a", title:"b", detail:"c"— orx/y/z,a.md/t/din other instances) which trivially validates against{type:"string"}fields, is accepted (Structured output provided successfully), and is returned as the agent's result. - The workflow script faithfully forwards
r.findingsto a fixer agent. In two of the five instances the stub's arbitrarily-chosen severity wasBLOCK, so a fixer was dispatched against file"a", title"b"— and in one instance the real review verdict had been PASS, i.e. the stub fabricated a blocking finding that did not exist.
Which runs were harmed was determined solely by which enum value the stub happened to pick (NOTE/MINOR got filtered out downstream; BLOCK reached the fixer) — which made this look intermittent/flaky for weeks when it was fully deterministic.
Reproduction
The schema half reproduces trivially: the observed rejected payloads fail Draft-7 validation with exactly the journal's error, and the observed stubs pass. (Tightening the schema with minLength/pattern rejects every observed stub while passing genuine findings — that's our mitigation, alongside treating review FILES on disk as ground truth and the structured return as an index only.)
The model half (give-up stub emission after repeated validation failure) lives inside the harness retry loop and can't be driven externally; it is evidenced by 5 independent journal instances with the same 4-rejections-then-stub signature.
Asks
- Expose retry-exhaustion state. When a StructuredOutput result is only accepted on the final retry after N validation failures, surface that to the caller (e.g. a
retries/degradedfield on theagent()result, or a configurable "fail the agent instead of accepting the last attempt" mode). Today a validated-but-degenerate stub is indistinguishable from a first-try healthy result. - Improve the validation-retry feedback.
must have required property 'findings'did not stop the model from re-embedding the array as XML inside a string field four times in a row. Naming the expected location/shape in the retry prompt (e.g. "emitfindingsas a top-level JSON array, not text insidesummary") would likely break the loop at attempt 2. - (Optional, docs) The Workflow/agent docs could recommend
minLength/patternconstraints on required string fields for exactly this reason — a schema that can be satisfied by one-character strings turns the retry cap into a silent-garbage path.
Environment
- Claude Code 2.1.207, macOS (darwin 25.5)
- Workflow tool multi-agent scripts (
agent(prompt, {schema})), subagent model: opus