Skip to main content
Module 5: The Fable 5 Era 3 / 8
Advanced Session 27 Workflow Orchestration Multi-Agent

The Workflow Tool: Scripted Multi-Agent Orchestration

Learn Claude Code's Workflow tool — deterministic JavaScript orchestration of subagents with agent(), pipeline(), parallel(), schemas, and resume.

July 8, 2026 20 min read

What You’ll Learn

The biggest architectural addition of the Fable 5 era is the Workflow tool: Claude Code writes a plain-JavaScript orchestration script, and the harness executes it — coordinating many subagents through loops, conditionals, and fan-out that are code, not model improvisation.

By the end, you’ll know:

  • Why scripted orchestration beats improvised delegation at scale
  • The meta block rules and the primitives: agent(), pipeline(), parallel(), phase(), log(), args
  • How JSON Schema forces validated structured output from subagents
  • The barrier discipline that decides between pipeline() and parallel()
  • The hard limits, the banned nondeterminism, and how resume works

The Problem

You already know two delegation models. In Session 4, the main agent spawns subagents one decision at a time — read a result, think, spawn the next. In Session 9, agents coordinate through messages. Both are improvised: the control flow lives in a model’s working memory.

Improvisation breaks down at scale. Ask an agent to “review all 40 changed files, verify every finding, and don’t stop early,” and you are trusting a language model to execute a 40-iteration loop while juggling results in its context. It may drift, deduplicate inconsistently, or quietly stop at file 28. Models are excellent at judgment — is this a bug? does this evidence hold? — and unreliable at bookkeeping.

Code has the opposite profile. A for loop never gets tired at iteration 28. So the Fable 5 era splits the job:

Improvised delegation              Scripted orchestration
─────────────────────              ──────────────────────
Model decides each step           Script decides each step
Loop = model discipline           Loop = JavaScript
Fan-out = model memory            Fan-out = pipeline(items, ...)
Hard to reproduce                 Deterministic, resumable

How It Works

The Core Model

The main agent writes a plain-JavaScript script (not TypeScript). The harness executes it in the background and returns a task ID; a notification arrives on completion. /workflows shows live progress.

The script body runs in an async context with standard JS built-ins. Two things are deliberately missing:

  1. No filesystem or Node.js APIs in the script itself. Agents have tools; the script does not. Anything that touches the world goes through agent().
  2. No nondeterminism. Date.now(), Math.random(), and argless new Date() all throw. We’ll see why under “Determinism and Resume.”

The meta Block

Every script must begin with:

export const meta = {
  name: 'review-changes',
  description: 'Review changed files, verify findings, summarize',
  phases: [
    { title: 'Collect' },
    { title: 'Review' },
    { title: 'Report' },
  ],
};

The meta block must be a pure literal — no variables, no function calls, no spreads. And the titles in phases must match your phase() calls in the body, so the harness can render accurate progress.

The Primitives

agent(prompt, opts?) spawns a subagent and returns its final text. Options: label, phase, schema, model, effort, isolation: 'worktree', agentType. It returns null if the user skips the agent or it dies on a terminal API error — always handle that. The model (sonnet | opus | haiku | fable) and effort (low to max) overrides let you mix tiers: cheap models at low effort for mechanical stages, top tier at high effort for verification and synthesis (see Session 26).

isolation: 'worktree' gives an agent a fresh git worktree — expensive (roughly 200–500 ms setup plus disk per agent), so use it only when agents mutate files in parallel. Unchanged worktrees are auto-removed.

pipeline(items, stage1, stage2, ...) sends each item through every stage independently, with no barrier between stages. Item A can be in stage 3 while item B is still in stage 1. This is the default for multi-stage work; wall-clock time equals the slowest single-item chain, not the sum of stage-wide waits.

parallel(thunks) runs thunks concurrently with a barrier — it awaits all of them. A thunk that throws resolves to null (the call never rejects), so filter results with .filter(Boolean):

const results = (await parallel([
  () => agent('Check the API layer for the bug'),
  () => agent('Check the data layer for the bug'),
])).filter(Boolean);

phase(title) and log(message) group progress and narrate for the user. args carries the value passed to the Workflow call, so scripts can be parameterized. workflow(nameOrRef, args?) runs another workflow inline — saved-by-name or by script path — but nesting goes one level deep only, and the child shares the parent’s concurrency cap, abort signal, and budget.

Structured Output via Schema

Pass opts.schema (a JSON Schema) and agent() returns a validated object instead of free text: the subagent is forced through a StructuredOutput tool, with retry-on-mismatch at the tool-call layer. This is what makes scripted control flow practical — the script can branch on result.files.length because files is guaranteed to exist and be an array.

Pipeline vs Parallel: Barrier Discipline

The most common orchestration mistake is inserting barriers that aren’t needed. The rule:

A barrier is justified only when stage N needs cross-item context from all of stage N−1.

Legitimate barriers: deduplicating or merging across the full set, early-exit when the total is zero, or “compare this against the other findings” prompts.

The smell test: “I need to flatten/map/filter first” or “the stages are conceptually separate” does not justify a barrier — do the transform inside a pipeline stage. Every unjustified barrier makes the whole batch wait for its slowest member.

Determinism and Resume

Why ban Date.now() and Math.random()? Because every run persists its script, and relaunching with resumeFromRunId replays the longest unchanged prefix of agent() calls from cache — only edited or new calls run live. A journal.jsonl in the transcript directory records each agent’s actual return value. If the script produced different values on replay, the cached prefix would diverge from reality. Determinism is what makes a big workflow cheap to fix: edit the broken stage, resume, and everything before it comes from cache.

Limits

  • Concurrent agents per workflow: min(16, CPU cores − 2); excess calls queue.
  • Lifetime cap: 1,000 agents per workflow — a backstop against runaway loops.
  • A single pipeline() or parallel() call: max 4,096 items, enforced with an explicit error rather than silent truncation.

Saved Workflows and Opting In

Named workflows live in .claude/workflows/ and can be invoked by name. And workflows are opt-in: without an explicit signal — the “ultracode” keyword, “use a workflow” in your prompt, or a skill that instructs it — Claude Code uses individual subagents or works solo.

Hands-On Example

Here is a canonical review-changes workflow, walked through line by line:

export const meta = {
  name: 'review-changes',
  description: 'Review changed files, verify findings, summarize',
  phases: [
    { title: 'Collect' },
    { title: 'Review' },
    { title: 'Report' },
  ],
};

phase('Collect');
const changes = await agent(
  'List the source files changed on this branch relative to main ' +
  '(git diff --name-only). Return only files that exist.',
  {
    label: 'collect-changes',
    phase: 'Collect',
    model: 'haiku',
    effort: 'low',
    schema: {
      type: 'object',
      properties: {
        files: { type: 'array', items: { type: 'string' } },
      },
      required: ['files'],
    },
  }
);

if (!changes || changes.files.length === 0) {
  log('No changed files — nothing to review.');
} else {
  phase('Review');
  log(`Reviewing ${changes.files.length} changed files`);

  const reviews = await pipeline(
    changes.files,
    (file) => agent(
      `Review ${file} for correctness bugs introduced on this branch. ` +
      'For each suspected bug, give the location and a concrete failure scenario.',
      { label: `review:${file}`, phase: 'Review', model: 'sonnet' }
    ),
    (review) => review && agent(
      `A reviewer reported:\n${review}\n\nTry to REFUTE each finding by ` +
      'reading the actual code. Return only findings that survive.',
      { label: 'verify', phase: 'Review', effort: 'high' }
    )
  );

  phase('Report');
  const confirmed = reviews.filter(Boolean);
  const report = await agent(
    'Merge these verified review results into one report, deduplicating ' +
    `overlapping findings:\n${confirmed.join('\n---\n')}`,
    { label: 'summarize', phase: 'Report', model: 'fable', effort: 'high' }
  );
  log(report || 'Summary agent returned nothing.');
}

The meta block is a pure literal, and its three phase titles match the three phase() calls exactly.

Collect uses a schema so the script gets { files: [...] }, not prose to parse. A mechanical listing job gets haiku at low effort. The !changes guard matters: agent() returns null if the agent is skipped or dies.

Review is a two-stage pipeline() with no barrier — the moment one file’s review finishes, its verification starts, even while other files are still being reviewed. The verify stage hands each finding to a skeptic prompted to refute it, at high effort, because judgment is where you spend tokens. The review && guard propagates null through the chain instead of crashing.

Report is the one place a barrier is justified: merging and deduplicating needs all verified results — and awaiting the pipeline provides exactly that barrier. Synthesis gets the top-tier fable model. And since nothing here is nondeterministic, if the summarize prompt needs tweaking later, a resume replays Collect and Review from cache and re-runs only the final agent.

This script uses just one skeptic per finding. Session 28 scales verification up properly.

What Changed

Improvised delegation (s04/s09)Scripted workflows (Fable 5 era)
Control flow in the model’s contextControl flow in deterministic JavaScript
Fan-out limited by model bookkeepingpipeline() over up to 4,096 items
A dropped iteration goes unnoticedLoops cannot skip items
Failed run = start overResume replays cached prefix, re-runs the rest
Free-text results, parsed by hopeSchema-validated structured output
One model tier for everythingPer-agent model and effort overrides

Subagents and agent teams didn’t disappear — for exploratory or small-scale work they remain the right tool. Workflows take over when the shape of the work is known and the scale is large.

Key Insight

Control flow in code, judgment in models. That one sentence is the entire design.

Everything a model does badly at scale — counting, looping, not stopping early, remembering all 40 results — is handled by JavaScript, which does those things perfectly. Everything code cannot do — judge whether a diff is a bug, refute a plausible finding, synthesize a report — is delegated to agent() calls, each with the model tier and effort that stage deserves.

The rules that feel restrictive at first (meta as a pure literal, no Math.random(), no filesystem access) buy the guarantees: reproducible runs, cache-backed resume, and parallelism without chaos.

Next Session

Session 28 covers Orchestration Quality Patterns — the canonical structures built on these primitives: adversarial verify, judge panels, loop-until-dry, multi-modal sweeps, and completeness critics. Answer quality, it turns out, is a structural property of the harness, not just the model.