Claude Code Workflows are deterministic orchestration scripts that fan out to multiple subagents in parallel, collect structured output, and synthesize results. This guide covers the full script syntax with working examples.
If you’ve used Claude Code’s /workflow command or seen references to Workflow() in settings, this is what powers them: plain JavaScript files that describe multi-agent jobs.
Script Structure
Every workflow script must start with a meta block, followed by the body using async/await:
export const meta = {
name: 'find-security-issues',
description: 'Find security vulnerabilities across the codebase',
phases: [
{ title: 'Scan', detail: 'parallel scan across dimensions' },
{ title: 'Verify', detail: 'adversarial check each finding' },
],
}
// Body runs in an async context — use await directly
phase('Scan')
const results = await agent('Scan for SQL injection and XSS vulnerabilities', {
schema: FINDINGS_SCHEMA,
})
Requirements for meta:
- Must be a pure object literal — no variables, function calls, or template strings
name: used in the workflow gallerydescription: shown in the permission dialog before runningphases: optional array; entries must havetitlematchingphase()calls
Core Functions
agent()
Spawns a subagent and returns its output.
// Returns a string (the subagent's final text output)
const analysis = await agent('Analyze the authentication module for timing attacks')
// Returns a validated object when schema is provided
const findings = await agent('Find bugs in src/api/', {
schema: {
type: 'object',
properties: {
bugs: {
type: 'array',
items: {
type: 'object',
properties: {
file: { type: 'string' },
line: { type: 'number' },
description: { type: 'string' },
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
},
required: ['file', 'line', 'description', 'severity'],
},
},
},
required: ['bugs'],
},
})
// findings.bugs is a typed array — no parsing needed
Options:
| Option | Type | Default | Purpose |
|---|---|---|---|
label | string | prompt excerpt | Display name in the progress tree |
phase | string | current phase | Groups agent under a phase box |
schema | object | none | Forces structured output; agent() returns the validated object |
model | string | session model | Override model for this agent only |
effort | string | session effort | 'low' / 'medium' / 'high' / 'max' |
isolation | 'worktree' | none | Gives agent a fresh git worktree; use when agents write files in parallel |
agentType | string | default agent | Use a registered custom agent type |
Return values:
- Without
schema: string (the agent’s final message) - With
schema: validated object matching the schema - On error or skip:
null— always filter before using results
const result = await agent('...', { schema: MY_SCHEMA })
if (!result) return // agent was skipped or errored
parallel()
Runs all thunks concurrently and waits for all to finish (barrier).
const results = await parallel([
() => agent('Review for security issues', { schema: FINDINGS }),
() => agent('Review for performance issues', { schema: FINDINGS }),
() => agent('Review for accessibility issues', { schema: FINDINGS }),
])
// results is an array; some items may be null if agents failed
const valid = results.filter(Boolean)
Use parallel() when you need all results before proceeding — for example, to deduplicate findings across dimensions before starting verification.
When NOT to use parallel(): if stage N doesn’t need all of stage N-1’s results, use pipeline() instead. parallel() creates a barrier: every item must finish before any item can continue to the next stage.
pipeline()
Runs items through stages without a barrier between stages. Item A can be in stage 3 while item B is still in stage 1.
const DIMENSIONS = [
{ key: 'security', prompt: 'Find SQL injection and XSS vulnerabilities' },
{ key: 'performance', prompt: 'Find N+1 queries and slow loops' },
{ key: 'types', prompt: 'Find unsafe type casts and any annotations' },
]
const verified = await pipeline(
DIMENSIONS,
// Stage 1: review
d => agent(d.prompt, { label: `review:${d.key}`, phase: 'Review', schema: FINDINGS }),
// Stage 2: verify — runs as soon as stage 1 finishes for each item
review => parallel(
review.findings.map(f => () =>
agent(`Verify this finding is real: ${f.description} in ${f.file}:${f.line}`, {
label: `verify:${f.file}`,
phase: 'Verify',
schema: VERDICT,
})
)
),
)
Stage callback signature: (previousResult, originalItem, index) => Promise<any>
Use originalItem and index in later stages to access the original data without threading it through stage 1’s return value.
phase()
Sets the current phase label. Subsequent agent() calls are grouped under this label in the progress tree.
phase('Research')
const data = await agent('...')
phase('Analyze')
const analysis = await agent('...')
phase('Report')
const report = await agent('...')
log()
Emits a status message visible to the user as a narrator line above the progress tree.
log(`Found ${bugs.length} potential issues — starting verification`)
Schema Output
When you pass a schema option to agent(), the subagent is forced to call a StructuredOutput tool and the result is validated before being returned to your script. You never need to parse JSON from the agent’s text output.
Define schemas as constants:
const BUG_SCHEMA = {
type: 'object',
properties: {
bugs: {
type: 'array',
items: {
type: 'object',
properties: {
file: { type: 'string' },
line: { type: 'number' },
summary: { type: 'string' },
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
fix: { type: 'string' },
},
required: ['file', 'line', 'summary', 'severity'],
},
},
},
required: ['bugs'],
}
const bugs = await agent('Find bugs in this repository', { schema: BUG_SCHEMA })
// bugs.bugs is typed — iterate directly
for (const bug of bugs.bugs) {
log(`${bug.severity}: ${bug.summary} at ${bug.file}:${bug.line}`)
}
Tip: if the agent’s output doesn’t match the schema, it retries automatically. You don’t need to handle parse errors.
Concurrency and Scale
- Concurrent agents per workflow:
min(16, cpu_cores - 2)— excess calls queue automatically - Total agent limit: 1000 per workflow run
- Max items per
parallel()orpipeline()call: 4096
You can safely pass 100+ items to pipeline() — only ~10 run at any moment, but all complete.
Practical Patterns
Pattern 1: Adversarial Verification
Have skeptical agents try to refute each finding. A finding survives only if a majority don’t refute it.
export const meta = {
name: 'verified-code-review',
description: 'Find bugs, then adversarially verify each one',
phases: [{ title: 'Find' }, { title: 'Verify' }],
}
const FINDINGS = {
type: 'object',
properties: {
findings: { type: 'array', items: { type: 'object', properties: {
file: { type: 'string' },
line: { type: 'number' },
description: { type: 'string' },
}, required: ['file', 'line', 'description'] } },
},
required: ['findings'],
}
const VERDICT = {
type: 'object',
properties: {
refuted: { type: 'boolean' },
reason: { type: 'string' },
},
required: ['refuted', 'reason'],
}
phase('Find')
const found = await agent('Find bugs in src/', { schema: FINDINGS })
log(`Found ${found.findings.length} potential issues`)
phase('Verify')
const verified = await parallel(
found.findings.map(f => async () => {
const votes = await parallel(
[1, 2, 3].map(() => () =>
agent(
`Try to REFUTE this bug report. Default refuted=true if uncertain.\n\nBug: ${f.description}\nFile: ${f.file}:${f.line}`,
{ schema: VERDICT, effort: 'low' }
)
)
)
const refuted = votes.filter(Boolean).filter(v => v.refuted).length
return refuted < 2 ? f : null // survive if <2 of 3 refute it
})
)
const confirmed = verified.filter(Boolean)
log(`Confirmed: ${confirmed.length} real bugs`)
return { confirmed }
Pattern 2: Loop Until Dry
Keep finding until N consecutive rounds return nothing new.
export const meta = {
name: 'exhaustive-bug-hunt',
description: 'Find bugs until no new ones appear for 2 rounds',
}
const seen = new Set()
const bugs = []
let dry = 0
while (dry < 2) {
const result = await agent('Find bugs not already known: ' + [...seen].join(', '), {
schema: { type: 'object', properties: { bugs: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, desc: { type: 'string' } }, required: ['id', 'desc'] } } }, required: ['bugs'] },
})
const fresh = result.bugs.filter(b => !seen.has(b.id))
if (fresh.length === 0) {
dry++
log(`Round dry (${dry}/2)`)
} else {
dry = 0
fresh.forEach(b => {
seen.add(b.id)
bugs.push(b)
})
log(`Found ${fresh.length} new bugs (total: ${bugs.length})`)
}
}
return { bugs }
Pattern 3: Token Budget Scaling
Scale depth to match a user-specified +500k token budget directive.
export const meta = {
name: 'budget-scaled-review',
description: 'Review depth scales with the token budget',
}
// budget.total is null if no +Nk directive was given
const FINDERS = budget.total
? Math.floor(budget.remaining() / 80_000) // scale finder count
: 3 // default
log(`Running ${FINDERS} finder agents`)
const results = await parallel(
Array.from({ length: FINDERS }, (_, i) => () =>
agent(`Find bugs using angle ${i + 1} of ${FINDERS}`, { schema: FINDINGS })
)
)
return { findings: results.filter(Boolean).flatMap(r => r.findings) }
Pattern 4: Worktree Isolation for Parallel File Edits
Use isolation: 'worktree' when multiple agents need to edit files simultaneously.
export const meta = {
name: 'parallel-refactor',
description: 'Refactor multiple modules in parallel without conflicts',
}
const MODULES = ['src/auth', 'src/api', 'src/db', 'src/utils']
const results = await parallel(
MODULES.map(mod => () =>
agent(`Refactor ${mod} to use the new error handling pattern`, {
label: `refactor:${mod}`,
isolation: 'worktree', // each agent gets its own git worktree
})
)
)
log(`Completed ${results.filter(Boolean).length}/${MODULES.length} modules`)
Without isolation: 'worktree', agents editing the same files simultaneously will conflict. The worktree is automatically cleaned up if the agent makes no changes.
Resuming Workflows
Every Workflow() call returns a runId. If a long-running workflow crashes, you can resume from where it stopped:
Workflow({ scriptPath: './my-workflow.js', resumeFromRunId: 'wf_abc123' })
- Completed
agent()calls with unchanged prompts return cached results instantly - Only changed or new calls re-run
- The cached result pool is per-session
Restrictions
Scripts are plain JavaScript, not TypeScript — no type annotations, interfaces, or generics.
Not available in scripts:
Date.now()/new Date()/Math.random()— these would break resume. Pass timestamps viaargsinstead.- Filesystem access or Node.js APIs
Available:
- Standard JS:
JSON,Math(exceptMath.random()),Array,Object,Set,Map, etc. - Workflow functions:
agent(),parallel(),pipeline(),phase(),log() - Globals:
args(the value passed asWorkflow.args),budget(token budget tracker)
Passing Arguments
Use args to parameterize a workflow — pass a research question, file list, or config object:
// Script body
const targetFiles = args.files || ['src/**/*.ts']
const severity = args.minSeverity || 'medium'
const result = await agent(
`Find ${severity}+ severity issues in: ${targetFiles.join(', ')}`,
{ schema: FINDINGS }
)
Pass args as actual JSON values, not stringified:
Workflow({ script: '...', args: { files: ['src/auth.ts'], minSeverity: 'high' } })
FAQ
Can I call a workflow from another workflow?
Yes, via workflow(). Pass a saved name or { scriptPath }. The child shares concurrency cap, agent counter, and token budget. Nesting is one level only.
Can I use TypeScript syntax in scripts? No — scripts are plain JS. Type annotations, interfaces, and generics cause parse errors. Use JSDoc comments for type hints if needed.
How do I debug a failing agent?
Add log() calls before and after suspect agents. Agents that return null were either skipped by the user, hit a terminal API error, or (with schema) couldn’t produce valid output after retries.
What happens when parallel() has a failing thunk?
It resolves to null in the results array. The call itself never rejects. Filter with .filter(Boolean) before using results.
Is there a way to share state between pipeline stages?
Use the return value of each stage — it becomes the input to the next. If you need the original item in stage 2, access it via originalItem (the second parameter of the stage callback). Don’t use external variables for this.
Can workflow scripts import modules? No. Scripts are self-contained. Utility logic should be inlined as JavaScript functions within the script body.
For more on multi-agent patterns and real-world orchestration examples, browse our gallery of CLAUDE.md and AGENTS.md files — many include references to workflow scripts teams use in practice.