Workflow Creator
Turn a goal into a runnable workflow file — a JavaScript orchestrator for
Claude Code's Workflow tool.
A workflow fans work out to fresh-context subagents under deterministic
JavaScript control flow: the loops, the conditionals, the fan-out are plain
code, and only the leaf agent() calls spend model tokens. The Workflow tool is
new and undocumented, so this skill carries the format, the judgment calls, and a
tested authoring procedure. Use it to write a new workflow, convert a multi-step
job into one, or fix a broken script.
The deep material lives in two reference files — read them when the step says so:
references/api-reference.md— the complete manual: every global, every option, every cap and constant, what happens at each limit.references/patterns.md— copy-paste orchestration patterns (fan-out, pipeline, loop-until-budget, adversarial verify, judge panel, nested workflow).
Starter files are in assets/templates/. Six complete, runnable example
workflows are in assets/examples/ — assets/examples/README.md maps each one
to a topology and to the model / structured-output techniques it shows. A linter
is in scripts/.
Step 0 — Confirm the Workflow tool is available
A workflow can only run if the Workflow tool is enabled. It is off by default, gated behind an environment variable. You can always write the file, but check this so you can tell the user the truth about running it:
echo "${CLAUDE_CODE_WORKFLOWS:-<not set>}"
If it is not set, the workflow file is still worth writing — but tell the user they must enable the tool before it will run, either of:
# per session
export CLAUDE_CODE_WORKFLOWS=1 && claude
// or persistently, in .claude/settings.local.json
{ "env": { "CLAUDE_CODE_WORKFLOWS": "1" } }
Workflow files live in .claude/workflows/<name>.js (project-local) or
~/.claude/workflows/<name>.js (global). The filename is not the workflow name —
the name inside the meta block is.
Step 1 — Decide whether a workflow is even the right tool
Do not reach for a workflow by default — it is the heaviest option and it is gated for a reason (it can spend a lot of tokens). Pick deliberately:
| The job | Right tool |
|---|---|
| One subagent, one task | The plain Agent tool — no workflow |
| A reusable procedure where Claude picks the steps each run | A Skill |
| Many subagents in a fixed shape (fan-out / pipeline / loop), same every run, worth resuming | A Workflow ✅ |
A workflow earns its cost when all of these are true: the work is parallel or multi-stage; you want the orchestration deterministic and resumable; and isolating each step in its own fresh context window is an advantage. If you are unsure, say so and offer the lighter option instead.
Step 2 — Find the shape of the job
Before writing a line of code, answer these. The answers pick the topology for you.
- What is the unit of work? The thing one subagent does once — review one file, research one question, draft one platform. Name it concretely.
- How many units, and is the count known up front? A known list → map over it. An unknown count (discovery, "find all the bugs") → a loop.
- What is the topology?
- Independent units, one pass each → fan-out.
- Units flow through ordered stages (review → verify) → pipeline.
- Keep going until a target count or a budget runs low → loop.
- Does any later step need all the earlier results at once — to dedup,
merge, count, or early-exit on a zero total? If yes, you need a barrier.
If no, you do not — and you should prefer
pipeline. - Does a step need structured data back (not free text)? Then that
agent()call needs aschema.
Write these five answers down for the user before coding. They are the design.
Step 3 — The decision that matters most: pipeline vs parallel
This is the call people get wrong, so make it explicitly.
pipeline(items, stage1, stage2, …)is the default for multi-stage work. Each item flows through every stage on its own — there is no barrier between stages. Item A can be in stage 3 while item B is still in stage 1. Wall-clock = the slowest single item's whole chain, not the sum of the slowest stage at each step.parallel(thunks)is a barrier. It waits for every task before returning. Reach for it only when a stage genuinely needs the entire previous result set in hand — dedup across all findings, merge, a count-based early-exit. "It is cleaner code" or "the stages feel separate" are not reasons — a pipeline models separate stages fine.
Smell test: if you would write const a = await parallel(...), then a plain
transform (flat/map/filter) with no cross-item dependency, then another
parallel(...) — that middle transform does not need the barrier. Make it a
pipeline stage instead. When in doubt, pipeline.
Step 4 — Write the file
A workflow file has exactly two parts, in this order. The parser is strict.
Part 1 — the meta block (must be the very first statement)
export const meta = {
name: 'review-changes', // required, non-empty
description: 'Review changed files, verify each finding', // required — shown in the permission dialog
whenToUse: 'Before shipping a branch', // optional — shown in the workflow list
phases: [ // optional — one entry per phase() call
{ title: 'Review' },
{ title: 'Verify', model: 'haiku' },
],
}
meta must be a pure literal — no variables, function calls, spreads, or
template strings inside it. Build dynamic values in the body, never in meta.
Use the same phase title strings in meta.phases as in your phase() calls.
Part 2 — the body (async JavaScript)
Everything after meta is the body. It runs inside an async function, so you
await at the top level. These globals are injected — do not import anything:
| Global | Purpose |
|---|---|
agent(prompt, opts?) |
Spawn one fresh-context subagent. Returns its final text, or a validated object if opts.schema is set. Returns null if the user skips it. |
pipeline(items, s1, s2, …) |
Run each item through all stages, no barrier between stages. The default for multi-stage work. |
parallel(thunks) |
Run an array of functions () => Promise concurrently. A barrier. |
phase(title) |
Group the agents that follow under a heading in /workflows. |
log(msg) |
Emit a narrator line above the progress tree. |
console |
A standard-looking console — its output is routed into the workflow log. |
budget |
{ total, spent(), remaining() } — token target for budget-aware loops. |
args |
Whatever was passed as the Workflow tool's args input — but the runtime serializes it to a string before the script sees it (so a JSON object/array arrives as JSON text, not a live value). undefined if nothing was passed. Normalize it before reading fields (see below). |
workflow(name, args?) |
Run another workflow inline. One level of nesting only. |
The body's return value becomes the tool result handed back to Claude.
Normalize
argsbefore reading fields. Empirically (probed 2026-05-29), the runtime serializesargsto a string before the script runs — even when you pass a JSON object or array via the tool'sargsfield, the script'sargsis the JSON text, soargs.minUsers/args.map(...)do not work directly.Workflow({ args: { minUsers: 5 } })gives the script the string'{"minUsers":5}'; nothing passed givesundefined. The one normalizer below covers every case — it parses a JSON string and is still correct if a future runtime ever passes a live object through:// A JSON string parses to an object/array; plain text and `undefined` // fall through unchanged; a live object (future-proofing) passes straight through. const input = typeof args === 'string' ? (() => { try { return JSON.parse(args) } catch { return args } })() : args const threshold = input?.minUsers ?? 20Keep the
typeof === 'string'guard rather than callingJSON.parse(args)unconditionally: a bare-text arg ("build a todo app") isn't valid JSON, and the guard also protects you if the runtime's serialization behavior changes.
Setting a model, and getting structured data back — the two agent() opts you tune most
Model — agent(prompt, { model }). Each agent call runs on its own model.
Accepts 'haiku', 'sonnet', 'opus', 'inherit', or a full model ID; omit it
and the agent inherits the session's model. Drop cheap, high-volume,
mechanical leaf work (per-item summaries, refute-this checks, classification)
to 'haiku'; leave judgement-heavy work on the inherited model. Two cautions:
- There is no validation. A typo (
'hauku') is not rejected — it is passed through and the agent fails later. Spell the alias exactly. modelon ameta.phases[]entry does nothing at runtime — it is a label for the permission dialog only. The model is set solely by themodelopt on theagent()call. For a Haiku phase, setmodelin both places: the phase entry (honest dialog) and everyagent()call in it (actual effect).
Structured output — agent(prompt, { schema }). Without schema you get the
agent's final text as a string. Pass a JSON Schema and the agent is forced
to return a validated object matching it — the runtime builds a hidden
StructuredOutput tool from the schema, AJV-validates the result, and makes the
agent retry on a mismatch. agent() returns the parsed object directly — no
JSON.parse. Use schema for any result a later line of JavaScript reads a
field off of; keep schemas small and required-tight. To pass data between
stages, stringify it into the next prompt (JSON.stringify) — the orchestrator
shares no memory with the subagent, only the prompt text.
For full signatures, every option, and every cap, read
references/api-reference.md now. For ready-made orchestration shapes, read
references/patterns.md and copy the one that fits Step 2's answers. Or start
from a file in assets/templates/, or adapt a full worked example from
assets/examples/ (its README.md says which one fits).
Step 5 — Validate before running
Catch the parser's hard rules before you waste a run. Use the bundled linter:
node ~/.claude/skills/workflow-creator/scripts/validate-workflow.mjs <path-to-file.js>
It flags: missing or non-first meta, a non-literal meta, a missing
name/description, banned non-deterministic calls, and an oversized script.
Fix every error it reports before invoking the workflow.
Step 6 — Run, watch, iterate
Run a named workflow with Workflow({ name: 'review-changes' }), or a file with
Workflow({ scriptPath: '…' }). It runs in the background — the call returns
a run ID immediately and a <task-notification> arrives on completion. Watch it
live with the /workflows command, where you can also skip or retry a single
agent mid-run.
To iterate: edit the saved file, then re-invoke with
Workflow({ scriptPath, resumeFromRunId }). Every agent() call before your
first edit replays instantly from cache; only the changed call and everything
after it re-runs. Same script + same args = a 100% cache hit. Never re-paste the
whole script after the first run — edit the file.
Gotchas — check every one before you hand over the file
These are the mistakes that actually break workflows:
- Determinism bans.
Date.now(),Math.random(), and arglessnew Date()throw inside a workflow — they would break resume. Pass timestamps in viaargsand stamp results after the workflow returns; vary "randomness" by loop index instead.new Date(specificValue)is fine. - No filesystem, no Node APIs in the orchestrator. No
require,fs,process. Any file read/write/Bash work belongs inside anagent()— the subagent has the normal tools; the orchestrator does not. parallel()takes thunks, not promises. It must be[() => agent(...), () => agent(...)], never[agent(...), agent(...)]. Bare calls start immediately and defeat the concurrency limiter.- Always
.filter(Boolean).parallel()andpipeline()putnullin the slot of any item that threw, was skipped, or was dropped by the budget. The result arrays have holes by design. metais a pure literal and the first statement. No dynamic values, no code before it.- Open-ended loops need a hard stop — a counter (
while (found < 10)) or a budget guard (while (budget.total && budget.remaining() > 50_000)). With no budget set,budget.remaining()isInfinity; an unguarded loop sprints into the 1000-agent lifetime cap and throws. isolation: 'worktree'is expensive (~200–500 ms + disk per agent). Use it only when parallel agents mutate files and would otherwise collide.
Worked example — review a branch, verify each finding
The shape: fan out one reviewer per dimension; the moment a dimension's review
returns, fan out a verifier per finding. pipeline, because a finding should
verify as soon as its review is done — no waiting for the slowest dimension.
export const meta = {
name: 'review-branch',
description: 'Review the branch across dimensions, adversarially verify each finding',
phases: [{ title: 'Review' }, { title: 'Verify' }],
}
const FINDINGS = { type: 'object', required: ['findings'], properties: {
findings: { type: 'array', items: { type: 'object', required: ['title', 'file'],
properties: { title: { type: 'string' }, file: { type: 'string' } } } } } }
const VERDICT = { type: 'object', required: ['isReal'], properties: {
isReal: { type: 'boolean' }, reason: { type: 'string' } } }
const DIMENSIONS = [
{ key: 'bugs', prompt: 'Find logic bugs in the changed files on this branch.' },
{ key: 'perf', prompt: 'Find performance regressions in the changed files.' },
{ key: 'tests', prompt: 'Find missing or weak test coverage in the changes.' },
]
const results = await pipeline(
DIMENSIONS,
d => agent(d.prompt, { label: `review:${d.key}`, phase: 'Review', schema: FINDINGS }),
review => parallel((review?.findings ?? []).map(f => () =>
agent(`Adversarially verify this finding. Try to refute it. Finding: ${f.title} (${f.file})`,
{ label: `verify:${f.file}`, phase: 'Verify', schema: VERDICT })
.then(v => ({ ...f, verdict: v }))
))
)
const confirmed = results.flat().filter(Boolean).filter(f => f.verdict?.isReal)
return { confirmedCount: confirmed.length, confirmed }
Trace it: dimension bugs can be verifying its findings while perf is still
being reviewed. No wasted wall-clock. Each agent reasons from a clean context.
The orchestrator JavaScript spends zero model tokens.
When the user wants to learn, not just get a file
If the request is "explain how workflows work" rather than "build me one", walk
them through references/api-reference.md — it is written to be read top to
bottom as the missing manual. Then offer to scaffold their first workflow from a
template so they have something runnable to poke at.