BUILD · PHASE 12 · LESSON 78

Dynamic Workflows — the Script Decides

The plan moves out of Claude's window and into a JavaScript file that a runtime

Type
Build
Prerequisites
Phase 11 (Lessons 71–76), Phase 04 Lesson 30 (Compaction), Phase 06 (Subagents)
Time
~45 minutes

Learning Objectives

  • Run a bundled workflow and read its progress view without steering it
  • Find the script a run generated, and read its meta block, agent() calls, and phases
  • Ask Claude to write a workflow for a task of your own, and approve it deliberately
  • Save a run as a reusable /<name> command, and pass it input through args
  • Say what a workflow cannot do that a conversation can

The Problem

You have 400 files to migrate. Claude spawns five subagents, each migrates a file, and each returns a summary into your conversation. Then it spawns five more. By file twenty, your window holds twenty summaries and three stack traces, and one of them was long enough to trigger a compaction.

The plan — which files, in what order, what to do when one fails — lived in Claude's window. Compaction summarized the window. The plan is now a summary of a plan.

That is not a bug. It is what subagents are: the results come back into the window, and the window is the one budget that fills (Lesson 08). Five delegated tasks per turn is the scale that fits.

The other shape is to stop asking Claude to hold the loop. Write the loop down.


The Concept

A dynamic workflow is a JavaScript script that orchestrates subagents. Claude writes the script for the task you describe. A runtime executes it in the background, in an isolated environment, while your session stays responsive.

The intermediate results stay in script variables. That is the part that matters. The script holds the list of files, the results so far, the branching, and the retries. Your conversation holds the final answer.

flowchart TB
    subgraph C["Conversation (Claude's window)"]
      P["your request"] --> O["orchestration, turn by turn"]
      O --> R["every result lands here"]
      R --> O
      R --> F1["final answer"]
    end
    subgraph W["Workflow run (script's world)"]
      S["the script"] --> A1["agent()"]
      S --> A2["agent()"]
      S --> A3["agent()"]
      A1 --> V["script variables"]
      A2 --> V
      A3 --> V
      V --> F2["the script's return value"]
    end
    C -. "moves the plan into code" .-> W
Mermaid source — renders as a diagram where mermaid.js is available

The trade, in one table

SubagentsSkillsAgent teamsWorkflows
What it isA worker Claude spawnsInstructions Claude followsA lead supervising peer sessionsA script the runtime executes
Who decides what runs nextClaude, turn by turnClaude, following the promptThe lead, turn by turnThe script
Where intermediate results liveClaude's windowClaude's windowA shared task listScript variables
What's repeatableThe worker definitionThe instructionsThe team definitionThe orchestration itself
ScaleA few delegated tasks per turnSame as subagentsA handful of long-running peersDozens to hundreds per run
InterruptionRestarts the turnRestarts the turnTeammates keep runningResumable in the same session

Read the second row twice. Subagents, skills, and teams all leave the decision with Claude — it looks at the last result and picks the next move. A workflow takes that decision away from the model and puts it in if statements.

The fourth row is the payoff: a workflow you saved runs the same orchestration next time. That is the difference between "Claude is good at migrations" and "we have a migration."

The reframe: with subagents Claude is the orchestrator and the plan lives in

its window; with a workflow the script is the orchestrator and the plan lives in

code — decide which of those two you want before you decide how many agents.


Build It

1. Run the bundled one first

/deep-research ships with Claude Code. Give it a question with a real answer.

/deep-research What changed in the Node.js permission model between v20 and v22?

Approve the launch when asked. Then open the run:

/workflows

The view shows each phase and its agent count. ↑/↓ moves, Enter drills in, Esc backs out, p pauses and resumes, x stops an agent or the whole run, and s saves.

✅ You pass when you can name the phases the run used, without having typed a single prompt into it after the first one.

2. Read the script it wrote

Every run writes its script to a file under your session's directory in ~/.claude/projects/. Claude receives the path when the run starts, so you can just ask for it.

Show me the script for that run.

You are looking for four things:

export const meta = {
  name: 'audit-routes',
  description: 'Audit every route handler for missing auth checks',
}

const found = await agent('List every .ts file under src/routes/.', {
  schema: { type: 'object', required: ['files'], properties: { files: { type: 'array', items: { type: 'string' } } } },
})

const audits = await pipeline(found.files, file =>
  agent(`Audit ${file} for missing authentication checks.`, { label: file }),
)

return audits.filter(Boolean)

agent() spawns one subagent. pipeline() runs one per item in a list. parallel() runs a set of tasks at once. phase() groups the agents that follow under a title. log() prints above the phases. args is the input a saved workflow was called with.

Three rules the runtime enforces, and each one will bite you once: no import() — the body is plain JavaScript; no direct filesystem or shell access — agents do the work and the script coordinates; and Date.now(), Math.random(), and a no-argument new Date() throw, so that a relaunched run repeats the same calls. Pass a timestamp in through args instead of reading the clock.

✅ You pass when you can point at the line that decided how many agents would run, and say what would change it.

3. Ask for one of your own

The keyword opts in for a single prompt:

ultracode: audit every API endpoint under src/routes/ for missing auth checks

Asking in your own words — "use a workflow", "run a workflow" — is treated the same. For a session-wide setting, /effort ultracode makes Claude plan a workflow for each substantive task, which uses more tokens and takes longer per request. Turn it off with /effort ultracode off when you go back to routine work.

The keyword only opts in from a prompt you type: the interactive prompt, an IDE panel, a Remote Control client, or an SDK app that stamps the input as human. It does not start a workflow from -p, from a scheduled task prompt, or from a webhook or pull-request comment relayed into the conversation. Before v2.1.210 it did, which is why old notes may say otherwise.

✅ You pass when a workflow starts from a keyworded prompt you typed, and you can name one route that would not have triggered it.

4. Approve it deliberately

The per-run prompt shows the planned phases and four options:

ChoiceWhat it does
Yes, run itStarts the run
Yes, and don't ask again for <name> in <path>Offered for bundled, saved, and plugin workflows — not for a one-off script Claude just wrote
View raw scriptRead it before deciding. Ctrl+G opens it in your editor
NoCancels

Which prompt you see depends on your permission mode: auto prompts once and records consent; manual and accept edits prompt every run unless you chose "don't ask again"; bypass permissions never prompts; and claude -p and the Agent SDK never prompt — the Workflow call goes through the ordinary permission evaluation instead, so an allow rule of Workflow or Workflow(<name>) is how you let it start there.

✅ You pass when you have read a generated script before running it and found something in it you would have changed.

5. Save the one worth repeating

/workflows → select the run → s. The save dialog toggles between two places:

  • .claude/workflows/ — this project, shared with everyone who clones it
  • ~/.claude/workflows/ — every project, visible only to you

It runs as /<name> from then on. A saved workflow can read the args global, so the same script takes a different list each time:

Run /triage-issues on issues 1024, 1025, and 1030

Claude passes that as structured data — an array or an object, not a string you have to parse.

✅ You pass when the saved name appears in / autocomplete and a second run with different args produces different work.

6. Bound the next run before it starts

A workflow spawns many agents, so one run can use meaningfully more tokens than doing the same task in conversation. Two settings bound it:

  • Size guideline — small (under 5 agents), medium (under 10), large (under 50), or unrestricted. Default is medium, or small on Pro accounts at recent versions. Set workflowSizeGuideline in a settings file, or the Dynamic workflow size row in /config.
  • A large-run warning — above 25 agents, or a projected 1.5 million tokens, the run's progress line grows a Large workflow warning. It is advisory: it does not pause anything.

✅ You pass when you can state your session's current guideline and where you found it, and you have run once on a small slice before committing to the whole repo.

Workflows are young and version-gated. Verified against

the workflows page on 2026-10-15.

The runtime caps — concurrency, list length, total agents — get their own

treatment in Lesson 81. Check https://code.claude.com/docs/llms.txt before

trusting a number on this page.


Use It

NeedReach forNote
See a workflow work/deep-researchThe bundled one. Read its script afterwards
Watch or steer a run/workflowsp pause, x stop, r restart an agent, s save
One task as a workflowultracode in a prompt you typeOwn words like "use a workflow" work too
Every substantive task as a workflow/effort ultracodeCosts more per request. Off with /effort ultracode off
Make one permanent/workflows → s.claude/workflows/ or ~/.claude/workflows/
Give a saved workflow inputargs in the scriptPassed as structured data, not a string
Change a saved script/workflow-authoring, then /reload-skillsThe skill loads the script-writing reference
Ship one to a teamA plugin's workflows/ directoryNamespaced as /<plugin>:<name>
Bound the sizeworkflowSizeGuideline setting or /configAdvice to Claude, not a hard cap
Turn it all off/config, disableWorkflows, or CLAUDE_CODE_DISABLE_WORKFLOWS=1A run already in progress keeps going

Exercises

  1. Take a task you have done in conversation and that took more than ten tool calls. Ask for it as a workflow, then read the generated script and count the agent calls before you run it. ✅ You pass when your prediction of the agent count is within one of what the progress view shows, and you can say which line set it.
  1. Run a workflow that fans out over a directory, then stop one agent mid-run with x and let the run finish. ✅ You pass when you can say what the stopped agent's result became, and whether the run ended or carried on.
  1. Save a workflow, then run it twice with different args — one list of three items, one of ten. ✅ You pass when both runs complete from the same script and the second does visibly more work, with no edit to the file between runs.
  1. Explain, in your own words, why Date.now() throws inside a workflow script, and what you would do instead if you genuinely needed the run's timestamp. ✅ You pass when your answer names both the reason (a relaunch must repeat the same calls) and the mechanism (args).

Key Terms

TermWhat people sayWhat it actually means
dynamic workflow"an ultracode thing"A JavaScript script the runtime executes to orchestrate many subagents
agent()"a subagent call"Spawns one subagent. Resolves to its result, or null if it was stopped or hit an unrecoverable error
pipeline()"the loop"One agent per item in a list. Keeps a null in the slot of an item that failed
parallel()"run them all"A set of tasks at once. Waits for all of them
meta"the header"A plain object literal with name and description, as the first statement. Non-literals drop it from autocomplete
args"the parameter"The structured input a saved workflow was invoked with. undefined if omitted
ultracode"a magic word"The opt-in keyword. Works only in a prompt you typed yourself
size guideline"a cap"Advice to Claude about how many agents to aim for. The runtime caps still apply separately
resumable"it remembers"Within one session, a relaunched run replays agents and returns saved results for the ones that completed

What this lesson skipped

The two combinators in depth. parallel() versus pipeline() looks like a one-line difference and is not. Lesson 79 is entirely that.

**What happens to a fan-out's order.** This lesson's example ends in .filter(Boolean) and does not explain why that is a loaded line. Lesson 80 is about the ordering trap hiding in it.

The runtime's numbers. Sixteen concurrent agents, 4,096 items per call, 1,000 agents per run, stall windows and restarts. Lesson 81.

Running agents on a schedule. A workflow is a plan, not a timer. /loop and the cron tools are Lesson 82.

Plugin distribution in full. The workflows directory and its manifest field, and what a plugin's namespace does to names, is Phase 09 Lesson 62.

The Agent SDK's Workflow tool. Same script, different host and permission model. Phase 17.

Check yourself

5 questions. Answer before you reveal — recognising the right option is not the same as being able to produce it.

  1. A workflow fans out over 400 files and each agent returns a long summary. Where does the summary of the 40th file live while the remaining agents are still running?

    1. In a variable inside the workflow script, outside Claude's context window
    2. In Claude's context window, where the next decision can read it
    3. In the agent team's shared task list
    4. Appended to the session transcript, which the script reads back
    Show answer

    A. In a variable inside the workflow script, outside Claude's context window

    The runtime executes the script in an isolated environment and keeps intermediate results in script variables, which is the whole reason a workflow can scale past what one window holds. The first wrong option is the instinct carried over from subagents — there, results do land in the window, and that is exactly the constraint a workflow removes. Agent teams use a shared task list, which is for tasks rather than agent outputs, and the transcript is a record of what was said, not a working store the script reads.

  2. A CI job runs claude -p "ultracode: audit every route handler". The audit runs turn by turn and no workflow starts. Why?

    1. The keyword only opts in from a prompt a human typed; a -p prompt does not trigger it
    2. Workflows are unavailable in non-interactive mode, so Claude Code stripped the keyword
    3. The keyword has to be the first character of the prompt, and a quote preceded it
    4. The workflow needs to be saved as a command before the keyword can launch it
    Show answer

    A. The keyword only opts in from a prompt a human typed; a -p prompt does not trigger it

    The docs list the routes that count as human input — the interactive prompt, an IDE panel, a Remote Control client, an SDK app that stamps the input as human — and -p is explicitly not one of them; neither is a scheduled task prompt or a relayed webhook. The unavailable-in--p option is the tempting one because workflows are indeed young, but the pages state they run in non-interactive mode and in the Agent SDK. The quote has nothing to do with it, and a workflow does not need saving before it can run — saving is for reuse.

  3. Your workflow script needs to read .tool-versions and branch on its contents. What does the script do?

    1. Give an agent the job of reading the file and returning the value, because the script itself has no filesystem access
    2. import { readFileSync } from 'node:fs' inside the script body
    3. Read the value with a top-level await fetch('file://.tool-versions')
    4. Nothing — the script must ask the user for the value, because runs take no input
    Show answer

    A. Give an agent the job of reading the file and returning the value, because the script itself has no filesystem access

    The workflow coordinates; agents read, write, and run commands. The first wrong option is the one most developers reach for, and it fails twice: the runtime rejects a script containing import() before the run even starts, and there is no filesystem for the script to use. Fetching a file:// URL is not part of the documented script surface and runs into the same missing-access wall. The last option confuses the run's inability to accept user input mid-run with the script's ability to receive input — args covers that case.

  4. You want each saved run to write into a new folder named after the time it started, and you want a relaunch to be able to repeat the run. What is the right move?

    1. Pass the timestamp in through args and let the script use it
    2. Call Date.now() at the top of the script and format it
    3. Call new Date() for the folder name and Date.now() for a log line
    4. Add a phase('timestamp') call and read the time from it
    Show answer

    A. Pass the timestamp in through args and let the script use it

    The runtime makes Date.now(), Math.random(), and a no-argument new Date() throw, precisely so a relaunched run repeats the same agent() calls instead of silently diverging. That is why the first two tempting answers are wrong rather than merely inelegant — the calls do not return values, they fail. phase() is real but it groups agents under a title in the progress view; it carries no clock.

  5. You edit a saved workflow's .js file while a session is open, and run /<name> again. Nothing changes. What did you miss?

    1. Run /reload-skills so the session re-reads the workflow directories, then run /<name> again
    2. Delete the run from /workflows first, or the old script is reused
    3. Re-save the workflow from /workflows so the edit is registered
    4. Restart Claude Code; workflow files are read once at launch
    Show answer

    A. Run /reload-skills so the session re-reads the workflow directories, then run /<name> again

    Saved workflows are files, and the session caches the directories it read; /reload-skills re-reads them, and then /<name> runs the edited version. /workflows manages running and completed runs, so deleting one has no bearing on which file a name resolves to — that is the most plausible wrong answer, because the runs list is where you normally touch workflows from the inside. Re-saving writes the run's script back to a location, which is the opposite direction. And a restart would eventually work, which is why it is tempting, but the documented step is the reload.