/config を開いて Dynamic workflow size という行が増えているのに気づいたとき、私が最初にやったのは large を選ぶことでした。せっかく選べるのだから大きいほうがよいだろう、という素朴な発想です。
一週間後、Console の使用量グラフだけが素直に伸びていました。夜間に回している記事の下調べは、small のときと比べて目に見えて良くなったわけではありません。それでも費用は積み上がる。何が増えて何が増えなかったのかを、私は説明できませんでした。
つまみが手元に来た、というのはそういうことです。既定値に守られていた頃は考えなくてよかった判断が、こちらの責任になります。以下は、その判断を感覚から引き剥がして台帳に載せるまでの記録です。
GA で手に渡った二つのつまみ
2026-07-15 に Dynamic Workflows が一般提供となり、CLI・Desktop・VS Code 拡張と、API / Bedrock / Vertex AI / Foundry で使えるようになりました。計画を Claude 自身が立て、一つのセッションで多数の並列サブエージェントを走らせ、報告の前に出力を検証する — この流れ自体はリサーチプレビューの頃と同じです。
変わったのは、こちらが握れる場所が二つ増えたことでした。
つまみ 設定場所 効く方向
Dynamic workflow size /config(small / medium / large)並列サブエージェント数の目安。横方向の広がり
effort effort メニュー(Claude Code では xhigh まで) 一つの判断にかけるトークン量。縦方向の深さ
ultracode 設定を有効にすると effort が xhigh に上がり、ワークフローを使うかどうかの判断まで Claude に委ねられます。便利ですが、この二つを同時に上げると、増えた費用がどちらに起因するのかが分からなくなります。私が一週間を無駄にしたのは、まさにここでした。
なお Dynamic Workflows の対象は Enterprise・Team・Max プランです。Pro で /config を探しても見つからない場合、それは設定の問題ではありません。動かし方そのものはClaude Code の Dynamic Workflow を動かして掴んだ、サブエージェント並列実行の勘所 で整理しています。
大きくして伸びるもの、伸びないもの
一週間分のログを眺めて、ようやく形が見えてきました。並列サブエージェントを増やして効くのは「探索の幅」です。候補を広く集める、複数の観点から独立に検証する、抜けを潰す。こうしたタスクは、走らせる頭数がそのまま質に変わります。
一方、探索の幅がもともと狭いタスクは、頭数を増やしても行き先が同じです。私の記事の下調べは、参照するソースが四つか五つに収まります。ここに large を当てても、同じ結論に到達するサブエージェントが増えるだけでした。
Anthropic は Opus 4.8 について、自ら書いたコードの欠陥を見逃す確率が前世代の約4分の1になったと述べ、75万行規模の移行を11日・テスト合格率99.8%で完走した事例を公表しています。これは large が活きる典型で、変更対象が数万ファイルに散らばり、探索の幅そのものが仕事の本体になっているケースです。私の手元の四つのブログには、その規模の探索面がありません。
つまり判断すべきは「大きいほうが良いか」ではなく、「このタスクの探索面は広いか」でした。そしてそれは、タスクごとに測れば分かります。
テレメトリを手元に引き込む
Claude Code は OpenTelemetry でメトリクスを吐けます。監視基盤を立てる話に聞こえますが、判断材料を集めるだけならコンソール出力で十分です。私は最初 Prometheus を立てようとして、その準備だけで半日を溶かしました。今なら勧めません。
# ~/.claude/settings.json に置く場合は env ブロックへ。
# まずは1セッションだけ試すならシェルで十分です。
export CLAUDE_CODE_ENABLE_TELEMETRY = 1
export OTEL_METRICS_EXPORTER = console
export OTEL_LOGS_EXPORTER = console
export OTEL_METRIC_EXPORT_INTERVAL = 10000 # 既定は60秒。短い実行を取りこぼさない
# 実行のたびにファイルへ落とす(stdout に混ざるので分離しておく)
mkdir -p ~/.wf-ledger/raw
claude -p "$( cat ~/.wf-ledger/task.md)" 2> ~/.wf-ledger/raw/ $( date +%s ) .otel.jsonl
OTEL_METRIC_EXPORT_INTERVAL を既定のままにすると、60秒より短い実行がフラッシュ前に終わってしまい、メトリクスが一件も残りません。最初の三回、私はここで空のファイルを眺めていました。10秒に縮めておくと取りこぼしがなくなります。
拾いたいのは claude_code.token.usage と claude_code.cost.usage の二つです。前者は input / output / cacheRead / cacheCreation の type 属性を持ち、後者は概算の費用を返します。概算である点は覚えておいてください。請求額と一致させる用途ではなく、条件間の比較に使う数字です。
同一タスクを掃き出すスイープハーネス
条件を一つずつ変えて、同じタスクを繰り返します。単純ですが、これをやらない限り「large にしたら良くなった気がする」から抜け出せません。
#!/usr/bin/env bash
# ~/.wf-ledger/sweep.sh — 同一タスクを size × effort で掃き出す
set -euo pipefail
TASK_FILE = " ${1 :? usage : sweep . sh < task . md > [repeats] } "
REPEATS = " ${2 :- 3} "
LEDGER = " $HOME /.wf-ledger"
mkdir -p " $LEDGER /raw"
export CLAUDE_CODE_ENABLE_TELEMETRY = 1
export OTEL_METRICS_EXPORTER = console
export OTEL_METRIC_EXPORT_INTERVAL = 10000
for size in small medium large ; do
for effort in high xhigh ; do
for i in $( seq 1 " $REPEATS " ); do
run_id = "${ size }-${ effort }-${ i }-$( date +%s)"
start = $( date +%s%3N )
# size は設定ファイル側を書き換えてから起動する。
# 対話の /config と同じ値が反映されます。
python3 - " $size " << 'PY'
import json, pathlib, sys
p = pathlib.Path.home() / ".claude" / "settings.json"
cfg = json.loads(p.read_text()) if p.exists() else {}
cfg["dynamicWorkflowSize"] = sys.argv[1]
p.write_text(json.dumps(cfg, indent=2))
PY
claude -p "$( cat " $TASK_FILE ")" \
--output-format json \
> " $LEDGER /raw/${ run_id }.result.json" \
2> " $LEDGER /raw/${ run_id }.otel.jsonl" || echo "run failed: $run_id " >&2
end = $( date +%s%3N )
echo "{ \" run_id \" : \" ${ run_id } \" , \" size \" : \" ${ size } \" , \" effort \" : \" ${ effort } \" , \" elapsed_ms \" :$((end - start))}" \
>> " $LEDGER /runs.jsonl"
echo "done ${ run_id } ($(( (end - start) / 1000 ))s)" >&2
done
done
done
繰り返し回数を3にしているのは、1回だと差が読めないからです。同じ条件でも実行ごとにトークンは揺れます。私の環境では、同一条件3回の出力トークンで最大18%の開きが出ました。1回の結果で判断していた頃、その揺れを条件の差だと思い込んでいたわけです。
設定ファイルのキー名は環境によって差があり得ます。/config で一度手動設定してから ~/.claude/settings.json を開き、実際に書かれたキーを確認してからスクリプトに写すのが確実です。
台帳に畳んで、初めて比べられる
生の OTel 出力はそのままでは読めません。必要な三列 — トークン、概算費用、所要時間 — に畳みます。
#!/usr/bin/env node
// ~/.wf-ledger/ledger.mjs — raw/*.otel.jsonl を条件別の台帳に畳む
import { readFileSync, readdirSync } from "node:fs" ;
import { homedir } from "node:os" ;
import { join } from "node:path" ;
const DIR = join ( homedir (), ".wf-ledger" );
const RAW = join ( DIR , "raw" );
// console エクスポータは1メトリクスを複数行の JSON で吐くため、
// 行単位ではなく { ... } のブロック単位で拾う。
function extractMetrics ( text ) {
const out = [];
const re = /"name" \s * : \s * "(claude_code \. [a-z._] + )" [\s\S] *? "value" \s * : \s * ( [\d.] + )( [\s\S] *? "attributes" \s * : \s * \{ ( [\s\S] *? ) \} ) ? / g ;
let m;
while ((m = re. exec (text)) !== null ) {
out. push ({ name: m[ 1 ], value: Number (m[ 2 ]), attrs: m[ 4 ] ?? "" });
}
return out;
}
const runs = readFileSync ( join ( DIR , "runs.jsonl" ), "utf8" )
. trim (). split ( " \n " ). map (( l ) => JSON . parse (l));
const rows = runs. map (( run ) => {
const file = readdirSync ( RAW ). find (( f ) => f. startsWith (run.run_id) && f. endsWith ( ".otel.jsonl" ));
if ( ! file) return { ... run, tokens: null , cost: null , note: "otel missing" };
const text = readFileSync ( join ( RAW , file), "utf8" );
const metrics = extractMetrics (text);
const tokens = metrics
. filter (( x ) => x.name === "claude_code.token.usage" )
. filter (( x ) => ! /cacheRead/ . test (x.attrs)) // キャッシュ読みは費用が別枠
. reduce (( s , x ) => s + x.value, 0 );
const cost = metrics
. filter (( x ) => x.name === "claude_code.cost.usage" )
. reduce (( s , x ) => s + x.value, 0 );
const result = JSON . parse ( readFileSync ( join ( RAW , `${ run . run_id }.result.json` ), "utf8" ));
return { ... run, tokens, cost: Number (cost. toFixed ( 4 )), passed: gradeOutput (result) };
});
// 合否は自分のタスクの受け入れ条件で判定する。ここは各自の要件に置き換える。
function gradeOutput ( result ) {
const text = typeof result === "string" ? result : (result.result ?? JSON . stringify (result));
const hasAllSections = [ "## 前提" , "## 検証" , "## 結論" ]. every (( h ) => text. includes (h));
const hasCitations = (text. match ( /https ? : \/\/ / g ) ?? []). length >= 3 ;
return hasAllSections && hasCitations;
}
// 条件ごとに畳む
const grouped = {};
for ( const r of rows) {
const key = `${ r . size }/${ r . effort }` ;
(grouped[key] ??= []). push (r);
}
const avg = ( xs ) => xs. reduce (( a , b ) => a + b, 0 ) / xs. length ;
console. table (
Object. entries (grouped). map (([ key , rs ]) => ({
condition: key,
runs: rs. length ,
tokens_avg: Math. round ( avg (rs. map (( r ) => r.tokens ?? 0 ))),
cost_avg: Number ( avg (rs. map (( r ) => r.cost ?? 0 )). toFixed ( 4 )),
elapsed_s: Math. round ( avg (rs. map (( r ) => r.elapsed_ms)) / 1000 ),
pass_rate: `${ Math . round (( rs . filter (( r ) => r . passed ). length / rs . length ) * 100 ) }%` ,
})),
);
肝は gradeOutput です。合否列がないと、この台帳はただの費用表になります。安いほうが良いに決まっているので、判断が「一番安い条件」に落ちてしまう。受け入れ条件を先に決めて、それを機械が判定できる形に落とす。ここに時間をかける価値があります。
私の場合は、下調べの出力に前提・検証・結論の三つの見出しが揃っているか、引用元URLが3件以上あるかを合否としました。粗いですが、粗くても列があれば判断は変わります。
出てきた表と、そこから決めたこと
四つのブログのうち一つ、Dolice Labs の記事下調べタスクで掃き出した結果が下の表です。3回平均・私の環境での値なので、そのまま持ち帰る数字ではありません。手順を持ち帰っていただければ十分です。
条件 出力トークン平均 所要時間平均 合格率
small / high 約 41,000 3分12秒 67%
small / xhigh 約 68,000 5分01秒 100%
medium / high 約 96,000 4分48秒 67%
medium / xhigh 約 158,000 7分33秒 100%
large / xhigh 約 402,000 14分20秒 100%
読み取れたのは、このタスクで合格率を動かしていたのは size ではなく effort だった、ということです。small のまま effort を xhigh に上げるだけで合格率は100%に届き、large / xhigh と比べて出力トークンは約6分の1でした。私が一週間 large を選び続けて買っていたのは、行き先の同じサブエージェントの群れだったわけです。
一方、四つのサイト横断でリンク切れを洗い出すタスクでは逆でした。small では取りこぼしが出て、medium 以上で安定します。探索面が広いタスクだからです。
タスクの性質 size effort 理由
参照ソースが数件・結論が一つ(記事の下調べ) small xhigh 幅より一つの判断の深さが効く
対象が広く散在(横断リンク検査・依存棚卸し) medium〜large high 頭数がそのまま網羅率になる
失敗コストが高い(本番設定の変更提案) medium xhigh 幅で見落としを減らし深さで根拠を固める
反復が多い定型処理 small high そもそもワークフローを使わない選択も含めて検討
私はこの表を ~/.wf-ledger/policy.md に置き、無人実行のプロンプト側から参照させています。判断に迷う場合は、まず small / xhigh から始めることをお勧めします。幅が足りないことは合格率に表れますが、深さの不足は静かに質だけを削るためです。ultracode に判断を委ねるのをやめたわけではありませんが、委ねる前に自分の答えを持っておきたい、という程度の話です。
読み間違えやすいところ
台帳を作ってからも、私は何度か読み違えました。つまずいた順に四つ挙げます。
キャッシュ読みトークンを合算してしまう
claude_code.token.usage の cacheRead は費用の重みが違うため、素朴に足すと size を上げたときの増分が実態より大きく見えます。台帳では type 属性で分けることを推奨します。上のスクリプトで cacheRead を除いているのはこのためです。
概算コストを請求額として扱ってしまう
claude_code.cost.usage はドキュメント上も概算とされており、条件間の比較には使えても、月次の突き合わせには使えません。実額の確認は Console 側の使用量画面が正です。ここを混同すると、本番運用の予算管理そのものがずれます。
所要時間だけで判断してしまう
large は時間もトークンも伸びますが、無人実行では時間は必ずしもコストではありません。私の夜間タスクは誰も待っていないので、14分でも困りません。困るのは費用のほうです。対話中に使う場合は時間が主役になります。同じ表を見ても、実行の文脈で結論が変わる — この点は注意点として覚えておいてください。
合否の閾値を後から緩めてしまう
これが一番厄介です。安い条件が落ちると、つい gradeOutput を甘くしたくなります。回避策は単純で、閾値は掃き出す前に決めて、結果を見てから触らないこと。当たり前のようでいて、自分の財布が絡むと簡単に揺らぎます。私はしきい値を別ファイルに切り出し、スイープ中は読み取り専用にする対処をとりました。
走らせる前に、測る三十分を
つまみが増えることを、私は素直に歓迎しています。ただ、増えたのは選択肢であって、正解ではありません。既定値のときは考えなくてよかったぶん、判断の責任がこちらへ移っただけです。
やることは多くありません。手元の代表的なタスクを一つ選び、OTEL_METRICS_EXPORTER=console を立てて、size と effort を掃き出す。合否列を一つ足す。それだけで、一週間ぼんやり large を選んでいた私よりは確実に賢い選択ができます。三十分で終わります。
設定値や属性名は更新で変わり得るため、Claude Code の監視ドキュメント とワークフローのドキュメント で一次情報を確認してから写してください。書く側の話 — 自分でワークフローを組み立てる方法はDynamic Workflow を自作する にまとめています。
私自身、測ってみるまでは「大きいほうが良い」を疑ってもいませんでした。同じ回り道をされる方が一人でも減れば嬉しく思います。お読みいただきありがとうございました。