Workflow tool: args object silently arrives as a JSON string inside the script, causing silent array/string type confusion

Open 💬 1 comment Opened Jul 3, 2026 by allangreatsoftwares

Summary

Calling the Workflow tool with args set to a JSON object (passed as a real JSON value in the tool call, not a manually-escaped string) resulted in the script's top-level args binding being a raw JSON string instead of the parsed object. Combined with a mistake in my own script (see below), this turned a should-have-been-harmless bug into a silent runaway that spawned roughly 300 agents (expected: ~15) before I noticed the anomaly and stopped the run with TaskStop.

What I did

Tool call (paraphrased, real payload had ~15 small objects with 3 string fields each):

Workflow({
  script: "<script below>",
  args: { "pages": [ {"id":"1","editorUrl":"https://example.com/1","publishedUrl":"https://example.com/pub/1"}, /* ...15 total... */ ] }
})

Script (relevant part):

function chunk(arr, n) {
  const out = []
  for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n))
  return out
}

const pages = args // BUG IN MY CODE: should have been `args.pages`

const groups = chunk(pages, 3)
for (const group of groups) {
  const results = await parallel(group.map(p => () => agent(`... ${p.id} ...`)))
}

Expected behavior

My script has a real bug: const pages = args should have been const pages = args.pages. But given the shape of the mistake, I'd expect it to fail safely and loudly: if args is the object {pages: [...]}, then pages.length is undefined, the for loop condition (0 < undefined) is false immediately, chunk() returns [], and zero agents run. A confusing but harmless no-op — or ideally a thrown TypeError if the harness validates shapes.

Actual behavior

Instead, ~300 agents were spawned, each invoked with p.id === undefined (confirmed via the workflow's journal.jsonl: agent results literally contain strings like "id":"undefined" and note text such as "the page id and editor URL arrived as the literal string 'undefined'"). Several of these degenerate agents, following their (garbled) instructions, went ahead and opened real browser tabs via a connected browser-automation MCP tool at nonsense .../undefined URLs in my actual browser session.

This is only explainable if the top-level args binding inside the script was actually the JSON-encoded string '{"pages":[...]}', not the parsed object:

  • pages = argspages is a string (the whole JSON text, ~2000+ characters).
  • pages.length → the string's character length (large), so the for loop in chunk() does run.
  • arr.slice(i, i+n) on a string returns a 3-character substring, so groups becomes hundreds of 3-character string chunks instead of 5 groups of page-objects.
  • group.map(p => ...) then iterates over individual characters of each 3-char substring (string indexing behaves like array indexing in JS), so p ends up being single-character strings, and p.id is undefined on every single one — matching exactly what the agents reported.

No exception was ever thrown anywhere in this chain, because every operation involved (.length, .slice(), bracket-indexing, .map via Array.from-like iteration) is valid on both strings and arrays — the type confusion is completely silent. The only externally visible symptom was the workflow's live agent counter climbing far past what the script's logic implied (confirmed by the user watching /workflows), which is how this was caught before it ran away further.

I re-ran the same task afterward with the array embedded as a literal directly in the script (not passed through args at all) and it behaved correctly (3 agents spawned for the first batch of 3 items, as expected) — reinforcing that the args transport, not the rest of the script, is where the value's type changed from object to string.

Impact

  • Silent, expensive runaway agent spawning with no error surfaced in the tool result, the background-task notification, or anywhere else — only visible via the live progress UI's agent count looking abnormally high.
  • Side effects leak outside the workflow sandbox: each garbage agent had general tool access (including a browser-automation MCP server) and used it, polluting the user's real browser session with stray tabs.
  • The docs for the Workflow tool already warn about a different, adjacent failure mode ("Pass arrays/objects as actual JSON values in the tool call, NOT as a JSON-encoded string... a stringified list reaches the script as one string"), which suggests this class of bug (object arrives as string) is a known risk — but the mitigation described there assumes the caller is the one who mistakenly stringifies. In this case I passed a genuine JSON object as the args parameter value, and it still arrived as a string inside the script.

Suggested fix

  1. Guarantee args inside the script is exactly the native JS value passed at the call site — never silently transported as a JSON string when the caller passed a real (non-string) JSON value.
  2. Failing that, have the harness detect "top-level args is a string that itself parses as JSON" and either auto-parse or throw a clear, loud error rather than letting it flow into user code untyped.
  3. Consider making chunk/array-utility guidance in the tool description include a defensive tip: if (!Array.isArray(x)) throw new Error(...) before treating anything from args as an array, since string/array confusion is otherwise silent in JS.

Environment

  • Claude Code CLI, background session, Workflow tool (inline script + args).
  • Model: claude-sonnet-5.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗