Workflow({name: "<x>"}) silently resolves to the built-in workflow even when a same-named local .claude/workflows/<x>.js exists — no error, no indication
Summary
The Workflow tool's own description states a name resolves to "a predefined workflow (built-in or from .claude/workflows/)", which reads as if a local project/user file can define or override a workflow of that name. In practice, when a built-in workflow and a local .claude/workflows/<name>.js share the same name, Workflow({name: "<name>", ...}) always runs the built-in and silently ignores the local file — no error, no warning, no indication in the tool output that the local file was skipped.
Repro
- Found a bug in the built-in
deep-researchworkflow (StructuredOutput corruption in the Scope phase — same bug class as #79019 / #75086). - Wrote a corrected version of the entire script (same
meta.name: 'deep-research', one schema trimmed) to~/.claude/workflows/deep-research.js. - Called
Workflow({name: "deep-research", args: "..."}). - Confirmed via two independent signals that the built-in ran, not the local file:
- Added a unique marker string to the local file's
meta.description. The tool result'sSummary:field (which echoesmeta.description) never showed the marker. - Grepped the per-run script that gets persisted to the session's transcript dir (
workflows/scripts/deep-research-<runId>.js) — it contained the ORIGINAL (unfixed) schema, not the edited one.
- Confirmed the local file itself is valid and independently loadable:
Workflow({scriptPath: "/home/<user>/.claude/workflows/deep-research.js", args: "..."})(explicit path, noname) correctly ran the fixed version — same marker technique, this time the marker did show up.
So the local file is syntactically fine and independently invocable — it's specifically the name → resolution step that never considers it when a built-in of the same name exists.
Expected
One of:
.claude/workflows/<name>.jsshould take precedence over (or at least be selectable instead of) a built-in of the same name, matching what the tool description implies, or- If built-ins are intentionally not overridable by design, the tool description should say so explicitly, and ideally
Workflow({name: ...})should warn (e.g. "a local file with this name exists but was not used because<name>is a reserved built-in") instead of silently discarding it.
Right now the failure mode is silent: a user (or an agent acting on their behalf) can reasonably believe they've patched a broken built-in workflow, verify the file is syntactically correct, and have every subsequent Workflow({name: ...}) call keep silently running the old broken version with zero signal that the fix isn't being used.
Side effect worth flagging
Skills that hardcode Invoke: Workflow({ name: "<x>", ... }) in their own instructions (e.g. the built-in deep-research skill) inherit this problem: even a perfectly-written local override can never be reached through the normal skill-triggered flow, only by manually calling Workflow({scriptPath: ...}) directly and bypassing the skill entirely.
Environment
- Claude Code CLI v2.1.215, Linux.
- Workflow tool,
namevsscriptPathresolution.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗