TL;DR
Claude Code dynamic workflows let you orchestrate tens to hundreds of parallel subagents from a JavaScript script. I’ve been building custom workflow scripts for three weeks: a code audit that checks 200+ files in under 4 minutes, a migration pipeline that ports modules in parallel with dual reviewers, and a research harness that cross-checks sources against each other before reporting. This tutorial walks you through building four real workflow scripts, from the simplest (12 lines) to a production adversarial review pipeline. You’ll see the actual output, the real token costs, and the patterns that separate a useful workflow from an expensive waste of context.
Why I Started Writing Workflow Scripts
I’d been using Claude Code’s subagents for months, spawning a handful per task, watching them fan out, collecting results. That worked fine for 3-5 file operations. Then I tried to audit an entire Express API for missing auth checks: 47 route files, each needing independent analysis, each finding needing verification by a second agent that tried to prove it wrong.
Subagents couldn’t hold that. Claude’s context filled up managing the orchestration, results got lost mid-conversation, and I couldn’t rerun the same audit the next week without re-explaining the whole setup.
Dynamic workflows solved every one of those problems. The orchestration lives in a script, not in Claude’s head, so the plan survives a thousand agents. I can save it and rerun it tomorrow with one command.
The feature shipped on May 28, 2026 and is available on Pro (toggle it on in /config), Max, Team, and Enterprise plans. It works with the Anthropic API, Amazon Bedrock, Vertex AI, and Microsoft Foundry.
Workflows vs Subagents vs Agent Teams
Before writing scripts, you need to know when a workflow is the right tool. Here’s the decision matrix:
| Subagents | Agent Teams | Dynamic Workflows | |
|---|---|---|---|
| Who decides what runs next | Claude, turn by turn | Lead agent, turn by turn | The script |
| Where results live | Claude’s context window | Shared task list | Script variables |
| Scale | A few tasks per turn | A handful of long-running peers | Dozens to hundreds per run |
| Rerunnable | No (conversational) | Team definition is reusable | The entire orchestration |
| Resumable | Restarts the turn | Teammates keep running | Cached agent results |
If you need 2-5 parallel workers and the orchestration fits in conversation, subagents are the right call. Agent teams make sense for long-running peers that coordinate over hours. Workflows earn their keep when the task has 10+ independent subtasks, you want repeatable orchestration, or you need quality patterns like adversarial verification where agents check each other’s work.
Getting Started: Requirements and First Run
You need Claude Code v2.1.154 or later. Check your version:
claude --version
On Pro plans, enable workflows in /config (look for the “Dynamic workflows” toggle). Max, Team, and Enterprise have them on by default.
Your first workflow: /deep-research
The fastest way to see a workflow in action is the bundled /deep-research command:
/deep-research What changed in the Node.js permission model between v20 and v22?
Claude Code asks for confirmation, then fans out web searches across several angles, fetches sources, cross-checks claims against each other, and returns a cited report. Run /workflows while it’s going to watch the progress:
/workflows
You’ll see phases with agent counts, token totals, and elapsed time. Arrow keys navigate; Enter drills into a phase to see individual agents and their findings. Press p to pause, x to stop, or s to save the script.
That bundled workflow is a good demo, but the real value is writing your own. Let me show you four scripts, from simple to production-grade.
Script 1: The 12-Line Code Audit
Start with the simplest useful workflow: scan every route file for a specific pattern and collect the results.
Tell Claude:
ultracode: check every file under src/routes/ for API endpoints that don't call requireAuth() before handling the request
Claude writes something close to this:
export const meta = {
name: 'auth-audit',
description: 'Find API endpoints missing auth checks',
phases: [
{ title: 'Scan', detail: 'Check each route file for missing requireAuth()' }
]
}
phase('Scan')
const files = await agent('List all .ts files under src/routes/', {
schema: { type: 'object', properties: { files: { type: 'array', items: { type: 'string' } } }, required: ['files'] }
})
const results = await parallel(files.files.map(f => () =>
agent(`Read ${f} and check if every exported route handler calls requireAuth() before processing. Report any endpoint that skips it.`, {
label: `scan:${f}`,
schema: {
type: 'object',
properties: {
file: { type: 'string' },
missingAuth: { type: 'array', items: { type: 'string' } }
},
required: ['file', 'missingAuth']
}
})
))
const issues = results.filter(Boolean).filter(r => r.missingAuth.length > 0)
log(`Found ${issues.length} files with missing auth checks`)
return { issues }
Key patterns in this script:
- Every script starts with
export const meta, a pure literal metadata block.nameanddescriptionare required. - The
schemaoption forces the agent to return structured data via aStructuredOutputtool call. No parsing needed; you get a validated JavaScript object. parallel()runs all file scans concurrently, up to 16 at a time. Excess tasks queue automatically.- Agents that error out resolve to
null, so alwaysfilter(Boolean)before processing results.
I ran this on a 47-file Express API. Results: 6 endpoints without auth checks, 4 minutes wall time, roughly 180K output tokens. Running the same audit by hand in conversation took 25 minutes and missed 2 endpoints because Claude’s context dropped earlier findings.
Script 2: A Migration Pipeline With Dual Reviewers
The pipeline() function is the default for multi-stage work. Unlike parallel() between stages (which waits for ALL stage-1 results before starting stage 2), pipeline() lets each item flow through all stages independently. Item A can be in stage 3 while item B is still in stage 1.
Here’s a workflow that migrates React class components to function components with hooks, then has a second agent verify each migration:
export const meta = {
name: 'react-migration',
description: 'Migrate class components to hooks with verification',
phases: [
{ title: 'Migrate', detail: 'Convert class components to function components' },
{ title: 'Verify', detail: 'Independent review of each migration' }
]
}
phase('Migrate')
const discovery = await agent(
'Find all React class components in src/components/. Return the file path and component name for each.',
{ schema: {
type: 'object',
properties: { components: { type: 'array', items: {
type: 'object',
properties: { file: { type: 'string' }, name: { type: 'string' } },
required: ['file', 'name']
}}},
required: ['components']
}}
)
const migrated = await pipeline(
discovery.components,
(comp) => agent(
`Migrate the class component "${comp.name}" in ${comp.file} to a function component using hooks. ` +
`Preserve all existing behavior. Write the changes to disk.`,
{ label: `migrate:${comp.name}`, phase: 'Migrate', isolation: 'worktree' }
),
(migrateResult, comp) => agent(
`Review the migration of "${comp.name}" in ${comp.file}. ` +
`Check: 1) all lifecycle methods converted to useEffect correctly, ` +
`2) state management uses useState/useReducer, ` +
`3) refs converted to useRef, 4) no behavioral changes. ` +
`If anything is wrong, fix it and write the corrected version.`,
{ label: `verify:${comp.name}`, phase: 'Verify', isolation: 'worktree' }
)
)
const completed = migrated.filter(Boolean)
log(`Migrated ${completed.length} of ${discovery.components.length} components`)
return { completed: completed.length, total: discovery.components.length }
Two things to notice.
The isolation: 'worktree' option gives each agent its own git worktree. This is expensive (~200-500ms setup per agent) but necessary when agents mutate files in parallel. Without it, two agents editing the same directory would corrupt each other’s changes.
Second, pipeline() stage callbacks pass both the previous stage’s result and the original item into each stage. You don’t need to thread component metadata through the migration stage’s return value; comp is available directly in the verify stage via the third argument pattern (migrateResult, comp, index).
When to use parallel() barriers vs pipeline()
Most people reach for parallel() between stages because it looks cleaner. Resist that instinct. A barrier is correct only when stage N needs cross-item context from ALL of stage N-1:
- Deduplication across the full result set before expensive downstream work
- Early-exit if zero items found (“0 bugs → skip verification entirely”)
- Stage N’s prompt references “the other findings” for comparison
If you’re just mapping or filtering between stages, do it inside a pipeline() callback. The latency difference is real: if your slowest item takes 3x the fastest, a barrier wastes 2/3 of the fast items’ idle time.
Script 3: Adversarial Security Review
This is where workflows earn their cost. Instead of one agent reviewing code and trusting its judgment, you send multiple independent agents to review the same code, then have separate agents try to refute each finding. Only findings that survive the adversarial pass make the final report.
export const meta = {
name: 'security-review',
description: 'Multi-angle security audit with adversarial verification',
phases: [
{ title: 'Review', detail: 'Independent security reviews from 3 angles' },
{ title: 'Verify', detail: 'Adversarial refutation of each finding' },
{ title: 'Synthesize', detail: 'Compile confirmed findings' }
]
}
const DIMENSIONS = [
{ key: 'injection', prompt: 'Audit src/ for injection vulnerabilities: SQL injection, command injection, LDAP injection, XSS. Check every user input path.' },
{ key: 'authz', prompt: 'Audit src/ for authorization flaws: broken access control, privilege escalation, IDOR, missing permission checks on sensitive operations.' },
{ key: 'secrets', prompt: 'Audit src/ for secret management issues: hardcoded credentials, API keys in source, secrets in logs, insecure token storage.' }
]
const FINDING_SCHEMA = {
type: 'object',
properties: {
findings: { type: 'array', items: {
type: 'object',
properties: {
file: { type: 'string' },
line: { type: 'number' },
severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },
title: { type: 'string' },
description: { type: 'string' }
},
required: ['file', 'line', 'severity', 'title', 'description']
}}
},
required: ['findings']
}
const VERDICT_SCHEMA = {
type: 'object',
properties: {
refuted: { type: 'boolean' },
reason: { type: 'string' }
},
required: ['refuted', 'reason']
}
phase('Review')
const reviews = await parallel(DIMENSIONS.map(d => () =>
agent(d.prompt, { label: `review:${d.key}`, phase: 'Review', schema: FINDING_SCHEMA })
))
const allFindings = reviews.filter(Boolean).flatMap(r => r.findings)
log(`Found ${allFindings.length} potential issues across ${DIMENSIONS.length} dimensions`)
if (allFindings.length === 0) {
return { confirmed: [], message: 'No security issues found' }
}
phase('Verify')
const verified = await parallel(allFindings.map(f => () =>
parallel([
() => agent(
`Try to REFUTE this security finding. Assume it is wrong and look for evidence that disproves it. ` +
`Finding: "${f.title}" in ${f.file}:${f.line} — ${f.description}. ` +
`Default to refuted=true if the evidence is ambiguous.`,
{ label: `skeptic1:${f.file}`, phase: 'Verify', schema: VERDICT_SCHEMA }
),
() => agent(
`Independently verify: is this a real vulnerability or a false positive? ` +
`Finding: "${f.title}" in ${f.file}:${f.line} — ${f.description}. ` +
`Check if the input is actually user-controlled, if sanitization exists upstream, ` +
`and if the attack vector is reachable in production.`,
{ label: `skeptic2:${f.file}`, phase: 'Verify', schema: VERDICT_SCHEMA }
)
]).then(votes => {
const valid = votes.filter(Boolean)
const survives = valid.filter(v => !v.refuted).length >= 1
return survives ? f : null
})
))
phase('Synthesize')
const confirmed = verified.filter(Boolean)
log(`${confirmed.length} of ${allFindings.length} findings confirmed after adversarial review`)
return {
confirmed,
dismissed: allFindings.length - confirmed.length,
dimensions: DIMENSIONS.map(d => d.key)
}
The key pattern here is nested parallel(): each finding gets two independent skeptics that run concurrently. A finding survives only if at least one skeptic can’t refute it. This catches the plausible-but-wrong findings that a single-pass review would let through.
I ran this on a 15K-line Express + Prisma API. The review phase found 23 potential issues. After adversarial verification, 9 survived. When I checked manually, 8 of those 9 were genuine. The single-pass alternative (one agent doing the whole review) found 14 issues, 6 of which were false positives.
Script 4: Budget-Aware Research With Loop-Until-Dry
Sometimes you don’t know how many agents you’ll need. A research task might need 5 rounds of searching or 15, depending on how many unique sources exist. The budget global lets you scale dynamically to the user’s token target (set via “+500k” directives in the prompt).
export const meta = {
name: 'research-sweep',
description: 'Multi-angle research with dedup and budget scaling',
phases: [
{ title: 'Search', detail: 'Fan-out web searches from different angles' },
{ title: 'Verify', detail: 'Cross-check claims against sources' },
{ title: 'Report', detail: 'Synthesize verified findings' }
]
}
const CLAIM_SCHEMA = {
type: 'object',
properties: {
claims: { type: 'array', items: {
type: 'object',
properties: {
claim: { type: 'string' },
source: { type: 'string' },
confidence: { type: 'string', enum: ['high', 'medium', 'low'] }
},
required: ['claim', 'source', 'confidence']
}}
},
required: ['claims']
}
const question = args || 'What are the real-world performance impacts of sparse attention in production LLMs?'
const seen = new Set()
let dry = 0
phase('Search')
while (dry < 2) {
if (budget.total && budget.remaining() < 80000) {
log('Budget running low, stopping search rounds')
break
}
const angles = [
`Search for academic papers about: ${question}`,
`Search for blog posts and technical writeups about: ${question}`,
`Search for benchmark results and production reports about: ${question}`
]
const round = await parallel(angles.map((a, i) => () =>
agent(a, { label: `search:round${dry}:angle${i}`, phase: 'Search', schema: CLAIM_SCHEMA })
))
const fresh = round
.filter(Boolean)
.flatMap(r => r.claims)
.filter(c => {
const key = c.claim.toLowerCase().slice(0, 80)
if (seen.has(key)) return false
seen.add(key)
return true
})
if (fresh.length === 0) {
dry++
log(`Dry round ${dry}/2 — no new claims found`)
} else {
dry = 0
log(`Found ${fresh.length} new claims (${seen.size} total)`)
}
}
phase('Verify')
const allClaims = [...seen].map(k => ({ claim: k }))
const verified = await parallel(allClaims.slice(0, 30).map(c => () =>
agent(
`Verify this claim by searching for independent sources: "${c.claim}". ` +
`Return whether it holds up and cite the source.`,
{ label: `verify:${c.claim.slice(0, 30)}`, phase: 'Verify', schema: {
type: 'object',
properties: { verified: { type: 'boolean' }, source: { type: 'string' }, note: { type: 'string' } },
required: ['verified']
}}
)
))
phase('Report')
const confirmedCount = verified.filter(Boolean).filter(v => v.verified).length
log(`${confirmedCount} of ${allClaims.length} claims verified`)
const report = await agent(
`Write a concise research report answering: "${question}". ` +
`Use only these verified claims: ${JSON.stringify(verified.filter(Boolean).filter(v => v.verified))}. ` +
`Cite each claim's source inline.`,
{ label: 'synthesize', phase: 'Report' }
)
return { report, claimsFound: seen.size, claimsVerified: confirmedCount }
Three patterns at work here.
The loop-until-dry strategy keeps searching until 2 consecutive rounds find nothing new. A fixed “search 3 times” misses the tail; this adapts to the density of available information.
The budget guard via budget.remaining() prevents runaway spending. Without the budget.total check, remaining() returns Infinity when no target is set, and the loop would run to the 1,000-agent cap.
Then there’s dedup by content. The seen Set prevents the same claim from consuming verification tokens twice. This is plain JavaScript inside the script, not an agent call. Dedup is too cheap to waste an agent on.
The Cost Reality
Workflows use meaningfully more tokens than conversation. Here’s what my four scripts actually consumed on real codebases:
| Workflow | Agents Spawned | Output Tokens | Wall Time | Equivalent Conversation |
|---|---|---|---|---|
| Auth audit (47 files) | 48 | ~180K | 4 min | ~25 min manual |
| React migration (12 components) | 25 | ~290K | 8 min | ~45 min manual |
| Security review (15K lines) | 52 | ~420K | 11 min | Would miss findings |
| Research sweep | 18-40 | ~350K | 7 min | Depends on depth |
On Max plan ($100/month with 5x usage), these runs are well within budget. On Pro ($20/month), one large security review could eat a meaningful chunk of your daily allocation. Start with the auth audit pattern. It’s cheap and immediately useful.
When NOT to use workflows
Workflows are overkill for:
- Single-file edits, where a regular prompt is better and cheaper
- Questions Claude can answer from context (no agents needed, just ask)
- Tasks with fewer than 5 independent subtasks, where subagents are simpler and sufficient
- Exploratory conversations, because workflows need a defined plan and exploration doesn’t have one yet
The rule of thumb: if you can describe the fan-out pattern in advance (scan N files, check N claims, migrate N modules), it’s a workflow. If you’re still figuring out what to do, stay in conversation.
Saving and Rerunning Workflows
Once a workflow does what you want, save it as a reusable command:
- Run
/workflows - Select the completed run
- Press
s - Choose a save location:
.claude/workflows/in your project (shared with the team)~/.claude/workflows/in your home directory (personal, works everywhere)
The saved workflow runs as /<name> in future sessions. Your auth audit becomes /auth-audit, your security review becomes /security-review. They show up in / autocomplete alongside the bundled commands, right next to your custom hooks.
/auth-audit
To pass input to a saved workflow, include it in your prompt. Claude sends it as structured data via the args global:
Run /security-review on the payments/ directory only
FAQ
How do Claude Code dynamic workflows work?
A dynamic workflow is a JavaScript script that orchestrates subagents. Claude writes the script for your task, and a runtime executes it in the background. The script uses functions like agent(), parallel(), pipeline(), and phase() to coordinate work. Up to 16 agents run concurrently, with a cap of 1,000 agents per run. Results stay in script variables, not Claude’s context window, so the orchestration scales independently of context limits.
What is ultracode in Claude Code?
Ultracode is a setting that combines xhigh reasoning effort with automatic workflow orchestration. Set it with /effort ultracode and Claude plans a workflow for every substantive task in the session. You can also include the keyword “ultracode” in a single prompt for a one-off workflow without changing the session setting. Drop back with /effort high when you’re done with heavy tasks.
How many subagents can run in parallel in Claude Code workflows?
Up to 16 concurrent agents (fewer on machines with limited CPU cores). The total agent count per run is capped at 1,000. You can pass 100+ items to parallel() or pipeline() and they all complete; the runtime queues excess calls and runs them as slots free up.
How much do dynamic workflows cost?
Workflows consume substantially more tokens than a typical conversation because each subagent uses its own token allocation. A simple 48-agent audit uses around 180K output tokens; a complex adversarial review with 50+ agents can use 400K+. Every agent runs on your session’s model unless the script routes specific stages to a cheaper one. On Pro ($20/month), budget carefully. On Max ($100/month with 5x usage), most workflows fit comfortably.
When should I use workflows vs regular Claude Code?
Use workflows when you have 10+ independent subtasks, need repeatable orchestration, or want quality patterns like adversarial verification. Use regular Claude Code for single-file edits, quick questions, exploratory conversations, or tasks with fewer than 5 subtasks. The decision point is whether you can describe the fan-out pattern in advance. If you’re still figuring out the task shape, stay in conversation; once you know the pattern, codify it as a workflow.
Sources
- Claude Code Dynamic Workflows Documentation — official documentation with the full API reference, behavior limits, and permission modes
- Introducing Dynamic Workflows in Claude Code — Anthropic’s announcement post with the Bun migration case study and design rationale
- Claude Code Adds Dynamic Workflows for Parallel Agent Coordination — InfoQ — independent coverage of the feature’s architecture and availability
- Dynamic Workflows in Claude Code — Hacker News discussion — developer reactions and early usage reports
Bottom Line
Dynamic workflows turn Claude Code from a conversational coding assistant into a programmable orchestration engine. If you’ve been following the agentic coding trend, this is where it lands in practice: scripts that manage dozens of agents with deterministic control flow. The scripts I’ve shown here took 15-30 minutes each to build, and the auth audit alone has already paid for itself by catching two endpoints I’d have missed in manual review.
Start with Script 1, the 12-line auth audit pattern. Run it on your own codebase. Once you see the fan-out in /workflows, you’ll immediately think of three more things that pattern could check. That’s the moment workflows become part of your toolkit instead of a feature you read about.
The cost is real. Every agent burns tokens. But the alternative isn’t free either. It’s you, context-switching between files, losing track of what you already checked, missing the finding in file 38 because you got sloppy by file 30. Workflows don’t get sloppy. They just cost money.