Claude Code Workflows multi-agent orchestration AI coding

Claude Code Workflow Scripts: Syntax Reference and Practical Examples (2026)

The Prompt Shelf ·

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 gallery
  • description: shown in the permission dialog before running
  • phases: optional array; entries must have title matching phase() 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:

OptionTypeDefaultPurpose
labelstringprompt excerptDisplay name in the progress tree
phasestringcurrent phaseGroups agent under a phase box
schemaobjectnoneForces structured output; agent() returns the validated object
modelstringsession modelOverride model for this agent only
effortstringsession effort'low' / 'medium' / 'high' / 'max'
isolation'worktree'noneGives agent a fresh git worktree; use when agents write files in parallel
agentTypestringdefault agentUse 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() or pipeline() 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 via args instead.
  • Filesystem access or Node.js APIs

Available:

  • Standard JS: JSON, Math (except Math.random()), Array, Object, Set, Map, etc.
  • Workflow functions: agent(), parallel(), pipeline(), phase(), log()
  • Globals: args (the value passed as Workflow.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.

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules