Dynamic workflows
Let Claude write a JavaScript orchestration script that runs dozens of subagents in the background, then save it as a reusable command.
A dynamic workflow is a short JavaScript program that coordinates a fleet of subagents. You describe the job, Claude writes the script, and a separate runtime executes it in the background. Your conversation stays free while the agents work, and you get one result at the end rather than a long transcript.
I reach for a workflow when a job is too wide for one context window: auditing every route in an API, converting a few hundred files, or researching a question where I want the sources checked against each other before I trust the answer.
Note: Workflows are available on every paid plan, through the Anthropic API, and on Amazon Bedrock, Google Cloud's Agent Platform and Microsoft Foundry. Pro users switch them on with the Dynamic workflows row in
/config.
Workflows compared with the other options
Claude Code has several ways to split work up. The real difference is where the plan lives.
| Approach | Who drives the next step | Where partial results sit | Typical scale | If interrupted |
|---|---|---|---|---|
| Subagents | Claude, one turn at a time | Claude's context | A few per turn | The turn restarts |
| Skills | Claude, guided by instructions | Claude's context | Same as subagents | The turn restarts |
| Agent teams | A lead agent | A shared task list | A handful of long-lived peers | Teammates carry on |
| Workflows | The script itself | Variables in the script | Dozens to hundreds of agents | Resumable within the session |
Because the loop, the branching and the intermediate data all live in code, Claude only ever sees the final answer. That also makes it practical to bake in quality patterns, such as having one set of agents try to disprove another set's findings, or drafting a plan from several angles and comparing them.
Try the built-in workflow first
Claude Code ships with one bundled workflow, /deep-research. It needs the WebSearch tool to be available.
- Type
/deep-researchfollowed by your question, for example/deep-research How do Postgres and SQLite differ in handling concurrent writers? - Approve the launch when asked. What the prompt looks like depends on your permission mode (see Approving a run).
- Run
/workflowsto watch it. If several runs exist you get a list; pick one and press Enter. A one-line summary also sits in the task panel under the input box. Press the down arrow to focus it and Enter to expand. - Read the report when it lands in your session. Every claim cites its source. Claims that failed cross-checking have already been removed, and claims the verifiers could not check (after a rate limit, say) are marked unverified rather than treated as false.
/deep-research only runs when you call it. Any workflow you save yourself shows up in / autocomplete next to it.
Watching a run
/workflows lists running and finished runs. With only one run in the session it jumps straight into that run. From the list, select a run and press x to stop it without opening it.
Inside the progress view you see each phase and how many agents it holds. The keys:
| Key | What it does |
|---|---|
↑ ↓ | Move between phases or agents |
Enter or → | Open the selected phase, then an agent; in agent detail, toggle expanded view |
Esc or ← | Go back one level (on v2.1.203 to v2.1.205 only Esc works for this) |
j k | Scroll a long agent detail |
f | Cycle a status filter over the agents in the phase |
p | Pause or resume the run |
x | Stop the selected agent, or the whole run when the run is focused |
r | Restart the selected running agent |
s | Save the run's script as a command |
Agent detail shows the prompt the agent received, its recent tool calls with their state, its result and its own task list if it keeps one. Expanding it shows the full prompt and result, plus each call's input and the start of its output.
Getting Claude to write one
There are two routes.
Ask in the prompt
Put the word ultracode in your message, or just say "use a workflow" or "run a workflow". Either counts as opting in for that one task, and the session's effort level is untouched.
ultracode: find every place we build SQL with string concatenation in services/ and check each for injection risk
The keyword is highlighted as you type. It only changes how Claude organises the work: every tool call the agents make still goes through your normal permission rules and sandboxing.
If you typed it by accident, press Option+W (macOS) or Alt+W (Windows, Linux) to cancel the highlight for this prompt, or backspace straight after the word. To stop it triggering altogether, switch off Ultracode keyword trigger in /config.
The keyword only counts when a human typed it: at the interactive prompt, in an IDE panel, from a Remote Control client, or in an Agent SDK app that marks the input's origin as { kind: "human" }. It is ignored in a -p prompt, an SDK message not marked as human, a scheduled task prompt, and anything relayed in from a webhook or pull request comment. Versions before v2.1.210 did honour it from those routes.
Turn on ultracode for the session
/effort ultracode tells Claude to plan a workflow for every substantial task, at whatever effort level the session is already on. Launching with claude --effort ultracode does the same and also sets effort to xhigh (v2.1.203 or later). In the /effort slider, press Tab to flip the Ultracode toggle and Enter to apply.
Be aware of the cost. One request can become a chain of workflows (understand, change, verify), so everything takes longer and uses more tokens. On a subscription that eats into your session and weekly limits faster.
While ultracode is on, three guard rails are relaxed because you have already opted in to big runs:
- The
Large workflowwarning is not shown. - The concurrent subagent limit is not applied to subagents spawned through the Agent tool.
- Auto mode does not ask you to approve the first workflow launch.
It lasts for the session. Set the ultracode setting in a settings file to have it on by default, and run /effort ultracode off to switch it off.
Approving a run
In the CLI, the launch prompt lists the planned phases and offers:
- Yes, run it
- Yes, and don't ask again for
<name>in<path>: only offered for bundled, saved or plugin workflows run by name, not for one-off scripts - View raw script
- No
Ctrl+G opens the script in your editor, and Tab on Yes or No lets you attach a comment to your answer.
| Mode | When you are asked |
|---|---|
| Auto | Only the first launch ever; any Yes is stored in user settings. Never with ultracode on |
| Manual and accept edits | Every run, unless you chose "don't ask again" for that workflow in this project |
| Bypass permissions | Never; it starts straight away |
claude -p and the Agent SDK | Never shown |
In -p and SDK runs, the Workflow tool call goes through ordinary permission evaluation instead, so deny rules, ask rules and dontAsk mode all apply. To let it through, use any of: an allow rule of Workflow (all workflows) or Workflow(<name>) (one saved workflow); auto mode's classifier; bypass mode; a PreToolUse hook that allows it; or a --permission-prompt-tool / canUseTool callback in your host.
The Desktop app shows an approval card with the name, phases and a token warning, and Once, Always and Deny buttons. Progress appears in the Background tasks side pane.
The agents a workflow spawns follow your permission rules. On a long run, add the tools they will need to your allow list first so you are not woken by prompts.
Saving a workflow as a command
When a run did what you wanted and you will need it again, open /workflows, select the run and press s. Tab switches between two locations:
.claude/workflows/in the project, committed and shared with the team~/.claude/workflows/in your home directory, personal and available everywhere (orworkflows/underCLAUDE_CONFIG_DIRif you set that)
Press Enter and the workflow becomes /<name> in future sessions.
A few details worth knowing:
- Claude Code refuses to write through symlinks. In the project location it checks
.claude,.claude/workflowsand the file itself; in the personal location only the file, so a dotfiles-managed~/.claudestill works. Before v2.1.216 it followed links. - In a monorepo, a project save goes to the nearest existing
.claude/workflows/between your working directory and the repo root, or the root if none exists. Workflows load from every such directory on that path, and the closest one wins on a name clash. - A project workflow beats a personal one with the same name.
Passing input
Saved workflows receive input through args, which the script sees as a global. Claude passes structured data, so a request like this hands the script a real array:
Run /check-licences on packages/api, packages/web and packages/cli
If no input is given, args is undefined.
Shipping a workflow in a plugin
Put scripts in a workflows/ folder at the root of a plugin, or point the workflows field in the manifest elsewhere. Plugin workflows are namespaced: a plugin named ops-kit with a script whose meta.name is dep-audit runs as /ops-kit:dep-audit.
Prompts that suit a workflow
You never have to write the script. These are the shapes that work well, phrased the way I would ask:
- Same check across many files: "use a workflow to check every Terraform module under infra/ for public S3 buckets, and have a second agent challenge each finding"
- Loop until green: "use a workflow to run the lint step and fix what it reports, stopping when it passes or two rounds make no progress"
- Parallel migration: "use a workflow to convert each class component in src/legacy/ to hooks, each file in its own isolated copy"
- Per-file review plus a summary: "use a workflow to review each file in this branch's diff, then merge the findings into one ranked list"
- Search until nothing new turns up: "use a workflow to hunt for dead feature flags, repeating until a round finds nothing new"
Anatomy of a saved script
A saved file starts with a meta export and then plain JavaScript with top-level await:
export const meta = {
name: 'flag-sweep',
description: 'Find feature flags that are always on or never read',
}
phase('Discover')
const list = await agent('List every feature flag key defined in config/flags.ts.', {
schema: {
type: 'object',
required: ['keys'],
properties: { keys: { type: 'array', items: { type: 'string' } } },
},
})
phase('Check')
const verdicts = await pipeline(list.keys, key =>
agent(`Search the codebase for reads of the flag "${key}" and say whether it is dead.`, { label: key }),
)
log(`Checked ${verdicts.length} flags`)
return verdicts.filter(Boolean)
The building blocks:
| Function | Purpose |
|---|---|
agent(prompt, options) | Start one subagent. Resolves to null if stopped or after an unrecoverable API error |
pipeline(items, fn) | Run one task per item; keeps null entries, hence the filter(Boolean) |
parallel(tasks) | Run several tasks at once and wait for all |
phase(title) | Group the following agents under a heading in the progress view |
log(message) | Show a line above the phases |
args | The input passed at invocation |
Passing schema makes the agent return JSON of that shape. Claude Code checks the schema first and fails the call if it is self-contradictory (for example a required key that additionalProperties: false forbids). If output still fails validation after five tries, the call errors with the last failure; change the count with MAX_STRUCTURED_OUTPUT_RETRIES.
In auto mode, the prompt a script passes to agent() is not treated as something you asked for when the classifier judges that agent's actions, since it was computed by the script.
Editing rules
Edit the .js file directly or ask Claude to. Run the /workflow-authoring skill first (v2.1.248 or later) so Claude has the script reference loaded, and run /reload-skills afterwards to pick up the change.
- Keep
export const metaas the first statement and make it a plain literal withnameanddescription. Variables, calls or spreads in it remove the command from autocomplete. - If you list
phasesinmeta, each title must match aphase()call exactly. Unlisted titles get their own group. Date.now(),Math.random()and a barenew Date()throw inside the script, so a relaunch makes the same calls. Pass any timestamp in throughargs.import()is not allowed; the run fails before starting. Put anything that needs a library inside an agent's task.- Syntax errors are reported when you run it.
What happens under the hood
The script runs in an isolated environment, not in your conversation. Each run's script is written to a file under your session's folder in ~/.claude/projects/, and Claude is given the path, so you can read it, diff it against a previous run, or edit it and ask Claude to relaunch. Claude can only launch a script the session can already read, so use /add-dir or a Read allow rule for scripts kept elsewhere.
Caching across the fan-out
Agents with the same model, effort, agent type, tools, output schema and working directory share a prompt prefix, so later ones can read the first one's prompt cache. Claude Code holds the matching siblings until the first response starts, then releases them together. That hold is capped by CLAUDE_CODE_WORKFLOW_PREFIX_STAGGER_MS (default 5000; 0 disables it).
Workflow agents sit outside the main conversation's cache TTL, so their cache lasts five minutes by default, even on a subscription. Set subagentPromptCacheTtl to 1h for an hour, at the higher one-hour write price.
Limits
| Limit | Value |
|---|---|
| User input during a run | None. Runs only pause for agent permission prompts or a usage-limit wait. Split into separate workflows if you need sign-off between stages |
| Filesystem or shell access from the script | None; agents do that work |
| Concurrent agents | 16 by default, fewer on machines or containers with fewer CPUs. CLAUDE_CODE_WORKFLOW_MAX_CONCURRENT_AGENTS accepts 1 to 256 (v2.1.269 or later) |
Items per parallel() or pipeline() | 4,096; longer lists are rejected with an error |
| Agents per run | 1,000 |
Managing runs
Pausing, stopping and resuming
Press p in /workflows to pause and resume. For a stopped run, ask Claude to relaunch it with the same script. If agents from the stopped run are still exiting, the relaunch waits until they have gone.
On relaunch, agents are replayed in start order:
- Completed agents return their saved result, until the first agent whose prompt has changed (because you edited the script or an earlier result differed). That one and everything after it run again.
- Running at the time you stopped agents start over. Stopping the whole run does not mark anything failed.
- Failed agents run again, along with every agent that started after them, completed or not. Stopping a single agent with
xcounts as a failure.
So if A, B, C and D started in that order and B failed, a relaunch reuses A and reruns B, C and D.
Resuming works within the same session. If you background the session, the run continues there. If you exit while agent view is on, choose Move to background and exit to keep it going; Exit and stop tasks stops it. Saved results stay under the session folder, so after claude --resume you can ask Claude to relaunch and completed agents are reused. A brand new session cannot see the earlier run and starts fresh.
Cloud sessions also store run results with the conversation history, so they survive the VM being reclaimed. Anywhere, if Claude tries to relaunch a run whose results cannot be found, you get a nothing to resume error; ask for a new run instead.
Stopping a run leaves it in the task panel while any agent processes are still alive. Stopping again re-signals them.
Hitting a usage limit
From v2.1.271, when an agent hits your claude.ai usage limit the run pauses: affected agents wait, no new ones start, and the run picks up shortly after the reset. The progress line and /workflows header show the reset time. This only happens when all of these are true; otherwise the agent fails:
- The session is interactive and signed in with a claude.ai subscription (not
-p, the SDK, a background session, Remote Control or an agent team teammate). autoContinueAtUsageLimitis on. Switching it off mid-wait fails the waiting agents.- The reset is within 24 hours.
- The run has not already waited twice.
Keeping cost under control
A workflow can burn far more tokens than doing the job by hand in conversation, and it all counts towards your limits. My habit is to try it on one directory before the whole repo. /workflows shows token usage per agent and you can stop at any point, usually keeping finished work.
The task panel shows a Large workflow warning once a run schedules more than 25 agents or projects more than 1.5 million tokens. It is advisory only. Choosing your own size guideline replaces the 25 with that guideline's count, and ultracode hides the warning.
Agent models are chosen the same way as for subagents; a model named in the script counts as the per-call choice, otherwise the session model is used. Check /model before a big run and ask Claude to use a smaller model for easy stages. If your organisation's availableModels list blocks a requested model, a substitute is used and /workflows shows a warning naming both.
Size guideline
The size guideline (v2.1.202 or later) tells Claude roughly how many agents to plan for. It is advice, so a prompt asking for a different scale still wins.
| Value | Target |
|---|---|
unrestricted | No target |
small | Under 5 agents |
medium | Under 10 agents |
large | Under 50 agents |
The default is medium, or small on a Pro plan from v2.1.271. Versions before v2.1.219 default to unrestricted. Change it in /config under Dynamic workflow size, with /config workflowSizeGuideline=small, or by setting workflowSizeGuideline in a settings file (v2.1.219 or later), which overrides /config and hides that row. It applies from your next prompt.
Switching workflows off
Workflows work in the CLI, Desktop, IDE extensions, claude -p and the Agent SDK, and the same switches apply everywhere:
- Toggle Dynamic workflows off in
/config. - Add
"disableWorkflows": trueto~/.claude/settings.json. - Set
CLAUDE_CODE_DISABLE_WORKFLOWS=1before starting.
Admins can set disableWorkflows in managed settings or use the toggle on the Claude Code admin settings page. When disabled, /workflows, workflow commands and /workflow-authoring disappear, the ultracode keyword does nothing and the Ultracode toggle leaves /effort. Runs already in progress carry on. There is no managed setting that blocks ultracode on its own; an effort cap lowers its effort level but does not turn it off.