Claude Code Workflowでサブエージェントを決定論的に束ねる

Claude Code Workflowでサブエージェントを決定論的に束ねる | mohablog

Claude Code 2.1.147 から、サブエージェントを JavaScript のスクリプトで束ねる仕組みが入りました。公式ドキュメントには載っていない機能で、手がかりは changelog と同梱コードだけ。現行の 2.1.220(2026年7月25日)で実際に動かして分かったことを、スクリプトの書き方に寄せて残します。

目次

モデルにファンアウトを任せると再現しない

サブエージェント自体は前からある機能です。困るのは、それを「いつ・何体・どの順で」呼ぶかをモデルが会話ごとに決めてしまう点。ここを固定するのが Workflow です。

サブエージェントは独自のコンテキストで動く

サブエージェントは、メインの会話とは別のコンテキストで作業する仕組み。公式ドキュメント “Create custom subagents” はこう書いています。

“Each subagent runs in its own context window with a custom system prompt, specific tool access, and independent permissions.”

独自のコンテキストとツール権限を持ち、Haiku のような速いモデルへ振ることもできる。この基本は カスタムサブエージェントの作り方 で触れたとおりです。

再現しないのは制御フローの側

ふつうにサブエージェントを投げると、何体を並列で走らせるか、どこでまとめるかはモデル任せ。同じ指示でも、3ファイルのうち1つを飛ばしたり、待ち合わせの順が変わったり。ファンアウトの形が毎回ぶれます。

Workflow はこの制御フローをコードに固定します。ループ・分岐・並列の形をスクリプトに書き、モデルは各 agent() 呼び出しの中身だけを担当する。要は、段取りはコード、考える所だけモデル。

Workflowスクリプトのプリミティブ

Workflow は export const meta = {...} で始まる JavaScript ファイル。/workflows から起動すると、ランタイムがバックグラウンドでスクリプトを走らせます。

最小構成はmetaとagent

export const meta = {
  name: 'summarize-modules',
  description: 'Summarize each source module',
  phases: [{ title: 'Summarize' }],
}

phase('Summarize')
const files = ['auth.ts', 'billing.ts', 'search.ts']
const summaries = await parallel(
  files.map(f => () => agent(`Summarize ${f} in 3 bullets.`))
)
return summaries

起動すると、進捗ツリーにこう出ます。

Summarize ▸ 3 agents
  ✓ agent#1  auth.ts
  ✓ agent#2  billing.ts
  ✓ agent#3  search.ts
→ 3 件の要約を返却

meta は変数も関数呼び出しも挟めない純粋なリテラル。namedescription が必須で、description は起動前の確認ダイアログにそのまま表示されます。

agentは1体、parallelとpipelineは複数

agent(prompt) がサブエージェント1体。返り値はそのエージェントの最終テキストです。schema を渡すと構造化出力を強制でき、検証済みのオブジェクトが返る。複数を回すのが parallel()pipeline()

log・phase・args・budget

補助のプリミティブもあります。

  • phase(title) – 進捗表示のグループを切る。以降の agent() がそのグループにまとまる
  • log(message) – 進捗ツリーの上にナレーター行を出す
  • args – 起動時に渡した JSON 入力がそのまま入るグローバル
  • budget – トークン予算。budget.remaining() でループの深さを調整する

pipelineとparallelの使い分け

どちらも複数エージェントを回しますが、待ち合わせ(バリア)の有無で挙動が分かれます。ここを取り違えると、待たなくていい所で待つ遅いワークフローになります。

parallelは全員を待つバリア

parallel(thunks) は渡した全タスクを同時に走らせ、全部終わるまで次に進みません。5体走らせて1体が遅ければ、残り4体が終わっていても待たされる。全員の結果を揃えてから次に渡したいときの形です。

const all = await parallel(
  DIMENSIONS.map(d => () => agent(d.prompt, { schema: FINDINGS }))
)
const deduped = dedupeByFileAndLine(all.flatMap(r => r.findings))

DIMENSIONS の結果が揃った後で deduped を計算する。重複排除は全件が要るので、ここはバリアが正しい。

pipelineはアイテムごとに流す

pipeline(items, stage1, stage2, ...) は各アイテムを独立してステージへ流します。ステージ間にバリアが無い。アイテムAが stage3 にいる間、アイテムBはまだ stage1、が許されます。全体の所要時間は「一番遅い1本の鎖」であって「各ステージの最遅の合計」ではない。

バリアが本当に要る場面だけparallel

やりがちなのが、ステージ間に毎回 parallel() を挟む書き方。

// アンチパターン: 中間の変換のためにバリアを入れる
const reviews = await parallel(DIMS.map(d => () => agent(d.prompt)))
const flat = reviews.flat()                 // 集約はしていない
const verified = await parallel(flat.map(f => () => agent(verify(f))))

この flat() はアイテムをまたいだ集約をしていない。ならステージの中に畳み込めます。

// OK: バリアなしで流す
const verified = await pipeline(
  DIMS,
  d => agent(d.prompt, { schema: FINDINGS }),
  review => parallel(review.findings.map(f => () => agent(verify(f))))
)

進捗を見ると、ステージがまたがって走っているのが分かります。

Review  ▸ bugs   ✓
Verify  ▸ bugs   ✓ 2件検証
Review  ▸ perf   ✓   ← bugs の検証中に perf のレビューが動く
parallel()pipeline()
ステージ間の待ち合わせあり(バリア)なし
次ステージの開始全アイテム完了後そのアイテムが終わり次第
所要時間各ステージの最遅の和一番遅い1本の鎖
向く用途全結果を集約・重複排除してから次へ独立した多段処理を流す
失敗時その枠が nullそのアイテムが以降のステージを飛ばす

バリアが正しいのは、次のステージが「全アイテムの結果」を要るとき。全件からの重複排除、合計0件なら以降を丸ごと飛ばす早期終了、他の結果と突き合わせる比較。それ以外は pipeline で待ち時間を削れます。

決定論はどこまで保証されるか

決定論的と言っても、エージェントの返答が毎回同じになるわけではありません。決まっているのは制御フローの形。ループ・分岐・ファンアウトの構造がコードに書かれているので、同じスクリプトと同じ入力なら段取りは再現します。

再現性を守るため、ランタイムは Date.now()Math.random()・引数なしの new Date() を封じています。静的な正規表現チェックと実行時スタブの二段構え。時刻や乱数でフローが枝分かれすると再開時に形が変わるためで、タイムスタンプは args で外から渡す設計になっています。

中断して再開する

Workflow は同じセッション内なら途中から再開できます。完了済みの agent() 呼び出しは、(prompt, opts) が変わっていなければキャッシュ結果を即座に返す。編集した最初の呼び出しと、それ以降だけがライブで走ります。スクリプトを手直ししながら回すとき、前半の重い調査を作り直さずに済む。

ただしセッションをまたぐと再開はできません。Claude Code を閉じれば次回はまっさらから。長時間の探索を挟むなら、途中結果をファイルへ落とす前提で組みます。

組み込みワークフローと進捗の追い方

自作しなくても同梱されている

自作は必須ではありません。組み込みのワークフローが最初から同梱されています。

  • autopilot / bugfix / bughunt / bughunt-lite / dashboard
  • docs / investigate / deep-research / plan-hunter / review-branch

review-branch は現在のブランチの差分をレビューし、deep-research は複数の調査エージェントをファンアウトさせて1つの回答へまとめます。まずは組み込みの review-branch を1回流すと、pipeline と parallel の動きが実際に見える。

/workflowsで進捗を追う

実行中の様子は /workflows ビューで見られます。2.1.202(2026年7月6日)で付いたのが、このエージェント詳細ビューの f キーによるステータスフィルタ。同じ更新で workflow.run_idworkflow.name が OpenTelemetry 属性に加わり、OpenTelemetry 設定を入れていればどのワークフローがトークンを食ったかを実行単位で追えます。

サイズの上限は/configで決める

2.1.219(2026年7月24日)で workflowSizeGuideline という設定キーが入りました。動的ワークフローのサイズ目安は既定で「中」(エージェント15体未満を狙う)。/config から上限を変えたり無制限にできます。並行実行そのものは既定で最大20エージェント(CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS で変更可)、超えた分はキューで順番待ちになります。

まとめ

Workflow は、サブエージェントの段取りをコードに落とす仕組みです。Agent Teams がエージェント同士を会話させる方向なのに対し、Workflow は制御フローを1人の書き手が固定します。

  • 制御フローはコードで決定論的。考える部分だけ agent() がモデルに任せる
  • parallel() はバリアあり、pipeline() はバリアなし。集約が要るときだけ parallel
  • 再現性のため Date.now()Math.random() は封じられている
  • 同一セッション内なら (prompt, opts) 一致で再開時にキャッシュが効く
  • 組み込み10種類と自作の両対応。進捗は /workflows、上限は workflowSizeGuideline

公式ドキュメントにはまだ無い機能。現行 2.1.220 で挙動を確かめながら、組み込みの review-branch から入ると形がつかめます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次