[Bug] Workflow tool delivers JSON args as string instead of parsed object
Bug Description
Title: Workflow tool — object/array passed as args arrives in the script as a JSON string, contradicting the documented "verbatim" contract
Summary: When the Workflow tool is invoked with args set to a JSON object or array, the workflow script's args global is a JSON-encoded string, not the parsed value. The tool description says args is delivered "verbatim" and that passing "arrays/objects as actual JSON values" lets the script call args.map/args.filter — but those fail because args is a string, so args.foo is undefined and args.map throws.
Minimal repro (zero agents): Workflow call with args: {"probe":"hello","n":42} and script:
export const meta = { name: 'args-diag', description: 'x' }
return { argsType: typeof args, probe: (args && typeof args === 'object') ? args.probe : '(not object)' }
- Observed: {"argsType":"string","probe":"(not object)"} — args is the literal string "{\"probe\":\"hello\",\"n\":42}".
- Expected (per docs): argsType: "object", args.probe === "hello".
Impact: Scripts written to the documented pattern silently get undefined. In my case, a fan-out workflow interpolated undefined file paths into subagent prompts (${args.contextPath} → "undefined"), degrading context-passing — the agents recovered, but it's a silent footgun.
Workaround: const a = typeof args === 'string' ? JSON.parse(args) : args;
Suggested fix: Either deliver args already-parsed (match the docs), or correct the tool description to state args arrives as a JSON string requiring JSON.parse.
Env: Claude Code · model claude-opus-4-8[1m].
Environment Info
- Platform: darwin
- Terminal: WezTerm
- Version: 2.1.195
- Feedback ID: 3a21a48d-ae1c-44cd-990d-33f4b6e41cff
Errors
[]Showing cached comments. Read the full discussion on GitHub ↗
8 Comments
Corroborating data point + transcript-level evidence on where the string originates (v2.1.198, macOS arm64)
Hit this twice on 2026-07-02 (runs
wf_8ab7885a-e52,wf_989f008d-b64). Both crashed in <10 ms withError: undefined is not an object (evaluating 'args.<key>.map')at workflow.js — the signature ofargsbeing a string primitive in the sandbox ("...".<key>→undefined→.mapread throws).One finding that may sharpen the fix: I checked the session transcript JSONL (the API-level record of the assistant's tool_use blocks). In both of my failures,
input.argswas already a JSON-encoded string in the model's emitted tool call — while nested object params of other tools in the same session were recorded as real objects. So at least in my case this is model-side stringification that the tool schema then accepts: the bundled schema isargs: z.unknown().optional(), which serializes to an unconstrained JSON Schema, validates a string, and the runtime injects it verbatim. The tool description's "NOT as a JSON-encoded string" warning is evidently not sufficient guidance for the model.Suggestions, in preference order:
typeof args === 'string'and itJSON.parses to an object/array while the script text referencesargs.<prop>/args.map/args.filter, fail fast with "args arrived as a JSON-encoded string — pass a real object, or JSON.parse it in the script" instead of the opaque TypeError. Cheap, zero behavior change for legit string args.args(e.g. union of object/array/scalar with adescriptionthat strings must not contain JSON) so the model gets structural guidance rather than prose-only.Repro/diagnostic script and full forensic detail available; also note for anyone debugging: workflow.js stack line numbers are offset by the stripped
export const metablock length, and run metadata under~/.claude/projects/<project>/<session>/workflows/persists the receivedargsverbatim (it shows as a JSON string there too).Adding an object-shaped datapoint, since the tool-description warning only describes the array symptom (
args.filterthrowing):When the stringified payload is an object, nothing throws —
args.scratchon a string returnsundefinedsilently, JS template interpolation stringifies it, and every downstream agent prompt materialized the literal token (CWD: undefined,Write undefined/gather-repo.md). In our case an unvalidatedmkdir -p "$CWD"then created realundefined/directories in two repos, agents improvised around the broken paths, and the 18-agent workflow finished green ("0 errors"). Detection was purely incidental (git statusnoise days later).So the failure severity depends on payload shape: arrays fail loud, objects corrupt silently. Two asks beyond auto-parsing:
argsarrives as a string that parses cleanly as JSON, auto-parse (or hard-error) — never deliver the raw string.undefined//: undefined/[object Object]would catch this whole class regardless of which variable leaked.Workaround we now bake into every script:
if (typeof args === 'string') { try { args = JSON.parse(args) } catch {} }followed by a required-field throw.Confirming this in a different environment, in case it helps triage rule out platform-specificity.
claude-fable-5.Same symptom as the report:
argspassed as a clean JSON object arrives in the script as a JSON-encoded string. Minimal zero-agent probe:Invoked with
args = {"worktree": "/x", "brief": "hello", "n": 42}yields:Reproduced across three invocation shapes (named workflow +
args, inlinescript+args, and the probe above) in two independent sessions, so it is not tied to a single call path. The extra confusion in practice is that the recovery hint echoesargsback as a string, which reads like caller error.Workaround we adopted, for anyone landing here: a tolerance line at the top of every workflow script —
const A = typeof args === 'string' ? JSON.parse(args) : (args ?? {}).Still present in 2.1.209 (macOS, model
claude-opus-4-8) — the newest confirmation in this thread is 2.1.202, so no fix has landed as of the current release.Corroborates @grimreaper0's transcript finding on the mechanism: in all four of my
Workflowtool calls this session,input.argswas already a JSON string in the model's emittedtool_useblock — the runtime then delivers it verbatim, as documented. So the defect is the untypedargs: z.unknown()schema silently accepting a stringified payload, not the runtime mangling an object.One diagnostic note for anyone landing here: a zero-agent probe that checks
typeof argsinside the script cannot distinguish model-side stringification from runtime mangling — both look identical from in there. Only the session transcript settles it:I misattributed this to the runtime until I checked. Since the recovery hint also echoes
argsback as a string, the failure reads as caller error in both directions — which is plausibly why it keeps getting re-reported (#77529).Failure was loud in my case (object payload;
args.<key>.lengththrew at <10 ms, 0 agents spawned, nothing written), so nothing to add beyond @chrisb4096-alt's silent-corruption report on the object-shaped path.Supporting the fix ordering already proposed above: an actionable runtime error (or a schema tight enough to steer the model) would be worth more than a defensive auto-parse, precisely because the current symptom is indistinguishable from caller error at the point where you actually hit it.
First Windows data point (2.1.202, Windows 11, claude-fable-5) + transcript confirmation of model-side stringification
Hit this today on Windows 11 (10.0.26200), Claude Code 2.1.202 desktop app, model
claude-fable-5. Every prior report in the thread is macOS or Linux, so this confirms the bug is platform-independent, as expected if the mechanism is model-side.Same signature as the OP: a workflow invoked with an object
argspayload ({"tasks": [...]}) died in 22 ms with 0 agents spawned:Following @szhygulin's diagnostic note, I checked the session transcript JSONL rather than probing from inside the script. Both
Workflowtool_useblocks in the session (initial launch and the resume) recordedinput.argsas a JSON string in the model's emitted tool call:So that's now three models (claude-fable-5 here, plus the opus/fable reports above) and three platforms all showing the same thing: the untyped
argsschema accepts the model's stringified payload and the runtime delivers it verbatim. The prose warning in the tool description ("NOT as a JSON-encoded string") is evidently not steering emission.Two small corroborations of points raised above:
argsback as a string did initially read as caller error on our side too, exactly as predicted in this thread; it cost a diagnosis cycle before the transcript check settled it.const A = typeof args === 'string' ? JSON.parse(args) : argsas the script's first line) plus a resume against the same run ID recovered cleanly; the payload content was intact, only the type was wrong.+1 to the fix ordering already proposed: an actionable runtime error or a tightened schema over a silent auto-parse, given the object-shaped silent-corruption report above.
🤖 Generated with Claude Code
Corroborating data point: both variants (stringified AND undefined), plus fan-out blast-radius severity
Hit this twice in real usage (2026-07-16, 2026-07-17), tracked internally as BluMintInc/agora#43644:
Workflowcall passedargsas a proper JSON object (~80-element array field + several string fields). The script'sconst { skillFiles } = argsdestructured toundefined—argswas the JSON-encoded string, not the parsed object. Failure:Error: undefined is not an object (evaluating 'skillFiles.map')atworkflow.js:203:51. Impact: a ~300-agent planned fan-out was lost after only 1 cheap canary agent had run — the entire run crashed before any real work started.argspassed as a proper top-level object, not stringified): the script's globalargswasundefinedentirely, despite the documented contract ("the value passed as Workflow'sargsinput, verbatim (undefined if not provided)") — it WAS provided. Failure:Error: undefined is not an object (evaluating 'args.prs.map'),agent_count: 0,duration_ms: 49. This matches what looks like the same failure class reported in #75319 (bug 2) and possibly #71762's regression theory (both open) — flagging the connection here since #72248 is the hub other reports get deduped to.Notable: in both cases, the failure notification's own recovery/resume snippet suggested re-invoking with
argsre-encoded as a JSON string — i.e. the tool's own recovery UX pointed at the exact anti-pattern its tool description warns against, which reads as caller error and cost a diagnosis cycle, same as reported above by @szhygulin and @agudmund.+1 to the fix-ordering already proposed (actionable runtime error / schema tightening over silent auto-parse). In the meantime we adopted the same workaround pattern documented above (
typeof args === 'string' ? JSON.parse(args) : args, generalized to also fail loud rather than silently proceed when the result still isn't a usable object) as mandatory boilerplate across every one of our workflow scripts, since the object-shaped silent-corruption failure mode (@chrisb4096-alt's report) is the worse of the two outcomes.**Still reproduces on 2.1.212 (Linux) — newest confirmation in this thread, and a top-level array payload shape**
Following up on my 2026-07-18 comment (tracked internally as BluMintInc/agora#43644): the defect recurred on 2026-07-20 at Claude Code 2.1.212 (Linux, headless). Every prior confirmation here tops out at 2.1.209, so this is the newest data point that no fix has landed.
New payload shape worth recording: the workflow was invoked with
argsas a genuine top-level JSON array (a flat list of URL strings), passed directly in the tool call — not stringified, not wrapped in an object. The script beganconst items = args; items.map(...)and died immediately:agent_count: 0,duration_ms: 23— same signature as the object-shaped reports (a string primitive has no.map), consistent with the transcript-level mechanism others have pinned here (model emitsinput.argsas a JSON string; the untyped schema accepts it; the runtime delivers verbatim). The auto-generated recovery hint again echoedargsback re-encoded as a JSON string, reading as caller error as noted upthread.+1 to the existing fix ordering (actionable runtime error / schema tightening over silent auto-parse). No new mechanism ask — just a newest-version + array-shape corroboration.
Still reproduces on 2.1.217 (macOS desktop app,
claude-fable-5) — and viascriptPathinvocation, not just inlinescriptNewest confirmation in this thread was 2.1.212; the defect is still present at 2.1.217 (Claude Code desktop app,
CLAUDE_CODE_ENTRYPOINT=claude-desktop, darwin arm64).Zero-agent probe (2026-07-24):
Invoked with
argsas an actual JSON object in the tool call:{"fixtureDir": "/tmp/fixtures", "n": 42}.script(runwf_85f9ec69-4c7):{"got":"{\"fixtureDir\": \"/tmp/fixtures\", \"n\": 42}","typeofArgs":"string"}scriptPathpointing at the persisted script file (runwf_a4e7093a-b89): identical result —typeof args === "string".All prior repros in this thread appear to use inline
script; this confirms thescriptPathpath is equally affected — consistent with the model-side/tool-input-side stringification mechanism @grimreaper0 and @szhygulin identified, since the delivery path after the tool call is the same.One more silent-corruption blast-radius datapoint matching @chrisb4096-alt's object-shaped mode: on 2026-07-23 an object payload (
{fixtureDir, sha}) arrived stringified,args.fixtureDirinterpolated asundefinedinto every subagent prompt (git -C undefined show undefined:...), the agents improvised in the wrong repo, and a measurement pilot's token benchmark came out inflated ~20× — while the workflow finished green. Objects fail silently; the corruption only surfaces downstream.Workaround we now put at the top of every script until this is fixed: