Workflow tool: StructuredOutput retry exhaustion silently accepts degenerate placeholder that passes loose schema — no retry/degraded signal exposed

Open 💬 0 comments Opened Jul 12, 2026 by charitarora

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)

  1. A review agent produces a correct, substantive review and calls StructuredOutput. On attempts 1–4 it serializes the required findings array as XML-style tags inside the sibling summary string (...</summary>\n<findings>\n<finding>\n<severity>BLOCK</severity>...) instead of a top-level findings key.
  2. 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.
  3. On attempt 5 the model emits a degenerate stub (file:"a", title:"b", detail:"c" — or x/y/z, a.md/t/d in other instances) which trivially validates against {type:"string"} fields, is accepted (Structured output provided successfully), and is returned as the agent's result.
  4. The workflow script faithfully forwards r.findings to a fixer agent. In two of the five instances the stub's arbitrarily-chosen severity was BLOCK, 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

  1. 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 / degraded field on the agent() 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.
  2. 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. "emit findings as a top-level JSON array, not text inside summary") would likely break the loop at attempt 2.
  3. (Optional, docs) The Workflow/agent docs could recommend minLength/pattern constraints 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

View original on GitHub ↗