[Bug] Workflow tool delivers JSON args as string instead of parsed object

Status Fixed / completed
Reported on v2.1.195
Maintainer reply None cached
Activity 12 comments · opened Jun 29, 2026 · closed Aug 15, 2026

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

[]

View original on GitHub ↗

8 Comments

grimreaper0 · 1 month ago

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 with Error: undefined is not an object (evaluating 'args.<key>.map') at workflow.js — the signature of args being a string primitive in the sandbox ("...".<key>undefined.map read 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.args was 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 is args: 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:

  1. Actionable runtime error: if typeof args === 'string' and it JSON.parses to an object/array while the script text references args.<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.
  2. Schema tightening: constrain args (e.g. union of object/array/scalar with a description that strings must not contain JSON) so the model gets structural guidance rather than prose-only.
  3. Defensive parse (riskier): auto-JSON.parse strings that parse to object/array — but this changes semantics for callers who legitimately pass a JSON-looking string, so 1+2 seem safer.

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 meta block length, and run metadata under ~/.claude/projects/<project>/<session>/workflows/ persists the received args verbatim (it shows as a JSON string there too).

chrisb4096-alt · 1 month ago

Adding an object-shaped datapoint, since the tool-description warning only describes the array symptom (args.filter throwing):

When the stringified payload is an object, nothing throws — args.scratch on a string returns undefined silently, 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 unvalidated mkdir -p "$CWD" then created real undefined/ directories in two repos, agents improvised around the broken paths, and the 18-agent workflow finished green ("0 errors"). Detection was purely incidental (git status noise days later).

So the failure severity depends on payload shape: arrays fail loud, objects corrupt silently. Two asks beyond auto-parsing:

  1. If args arrives as a string that parses cleanly as JSON, auto-parse (or hard-error) — never deliver the raw string.
  2. A cheap pre-dispatch lint of composed agent prompts for 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.

andrebrait · 1 month ago

Confirming this in a different environment, in case it helps triage rule out platform-specificity.

  • Environment: managed remote execution (cloud session), Linux — not local/macOS.
  • Model: claude-fable-5.
  • Client: Claude Code 2.1.202.

Same symptom as the report: args passed as a clean JSON object arrives in the script as a JSON-encoded string. Minimal zero-agent probe:

export const meta = { name: 'args-probe', description: 'echo args' }
return { typeofArgs: typeof args, value: args === undefined ? 'UNDEFINED' : JSON.stringify(args).slice(0, 200) }

Invoked with args = {"worktree": "/x", "brief": "hello", "n": 42} yields:

{ "typeofArgs": "string", "value": "\"{\\\"worktree\\\": \\\"/x\\\", \\\"brief\\\": \\\"hello\\\", \\\"n\\\": 42}\"" }

Reproduced across three invocation shapes (named workflow + args, inline script + 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 echoes args back 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 ?? {}).

szhygulin · 1 month ago

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 Workflow tool calls this session, input.args was already a JSON string in the model's emitted tool_use block — the runtime then delivers it verbatim, as documented. So the defect is the untyped args: 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 args inside the script cannot distinguish model-side stringification from runtime mangling — both look identical from in there. Only the session transcript settles it:

jq '.message.content[]? | select(.type=="tool_use" and .name=="Workflow") | .input.args | type' \
  ~/.claude/projects/<project>/<session>.jsonl

I misattributed this to the runtime until I checked. Since the recovery hint also echoes args back 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>.length threw 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.

agudmund · 1 month ago

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 args payload ({"tasks": [...]}) died in 22 ms with 0 agents spawned:

Error: undefined is not an object (evaluating 'args.tasks.map')
    at <anonymous> (workflow.js:35:25)

Following @szhygulin's diagnostic note, I checked the session transcript JSONL rather than probing from inside the script. Both Workflow tool_use blocks in the session (initial launch and the resume) recorded input.args as a JSON string in the model's emitted tool call:

call 1: keys=['args', 'script']                          args_type=str
call 2: keys=['args', 'resumeFromRunId', 'scriptPath']   args_type=str

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 args schema 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:

  • The recovery hint echoing args back 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.
  • The tolerant-parse workaround (const A = typeof args === 'string' ? JSON.parse(args) : args as 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

oconnorjoseph · 1 month ago

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:

  1. Stringified-object variant (matches this issue exactly): a Workflow call passed args as a proper JSON object (~80-element array field + several string fields). The script's const { skillFiles } = args destructured to undefinedargs was the JSON-encoded string, not the parsed object. Failure: Error: undefined is not an object (evaluating 'skillFiles.map') at workflow.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.
  2. Undefined variant (next day, same repro discipline — args passed as a proper top-level object, not stringified): the script's global args was undefined entirely, despite the documented contract ("the value passed as Workflow's args input, 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 args re-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.

oconnorjoseph · 1 month ago

**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 args as 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 began const items = args; items.map(...) and died immediately:

Error: items.map is not a function. (In 'items.map((url) => ...)', 'items.map' is undefined)
    at <anonymous> (workflow.js:32:12)

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 emits input.args as a JSON string; the untyped schema accepts it; the runtime delivers verbatim). The auto-generated recovery hint again echoed args back 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.

honestbilly · 1 month ago

Still reproduces on 2.1.217 (macOS desktop app, claude-fable-5) — and via scriptPath invocation, not just inline script

Newest 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):

export const meta = { name: 'args-repro', description: 'Return args verbatim to test args plumbing' }
return { got: args, typeofArgs: typeof args }

Invoked with args as an actual JSON object in the tool call: {"fixtureDir": "/tmp/fixtures", "n": 42}.

  • via inline script (run wf_85f9ec69-4c7): {"got":"{\"fixtureDir\": \"/tmp/fixtures\", \"n\": 42}","typeofArgs":"string"}
  • via scriptPath pointing at the persisted script file (run wf_a4e7093a-b89): identical result — typeof args === "string".

All prior repros in this thread appear to use inline script; this confirms the scriptPath path 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.fixtureDir interpolated as undefined into 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:

const a = typeof args === 'string' ? JSON.parse(args) : args

Showing cached comments. Read the full discussion on GitHub ↗