Managed Agentsのworkflow runを追う — 状態遷移・イベント・完了判定
Managed Agentsの動的ワークフローは、エージェントが書いたプログラムをサーバーが1回のworkflow runとして実行します。runの状態、イベント、完了の見分け方、再接続後の復元、上限をまとめます。
Managed Agentsのworkflow runは、エージェントが書いたワークフロー(プログラム)をサーバーがバックグラウンドで実行する単位です。クライアントからrunを始めることも止めることもできません。できるのは、セッションのイベントストリームでrunを追うことです。
この記事は、その追い方をまとめます。runが取る3つの状態、流れてくるイベント、「作業が終わった」と言える条件、切断後の状態復元、上限値の順に見ていきます。この機能はbetaで、リクエストには managed-agents-2026-04-01 のbetaヘッダーが付きます。
workflow runとは何か
workflowは、エージェントが多数のエージェントを走らせて結果をまとめるために書くプログラムです。workflow runはその1回分の実行を指します。dynamic workflowsは、エージェントにワークフローを書かせてrunを始めさせる機能の名前です。
オン・オフはエージェント定義の multiagent ブロックにある workflows 設定で切り替えます。multiagent_20261001 タイプでは subagents と workflows がどちらも既定で有効です。設定の詳細はManaged Agentsでマルチエージェントを編成する方法で扱っています。Claude Code側の同じ発想の機能はダイナミックワークフローとはにあります。
runは3つの層でできています。
run を構成する3つの層
workflow run
サーバーがバックグラウンドで実行する1回分の実行です。1つのセッションに複数のrunが同時に開いていても構いません。
phase
ワークフローが宣言する名前付きの段階です。「契約書を読む」「結果を突き合わせる」のような区切りで、進捗はphaseイベントで追います。
agent thread
phaseの中で動く各エージェントです。1体ごとに専用のセッションスレッドを持ちます。
ワークフローの中では、エージェントの並列実行(fan out)、結果の受け渡し、繰り返しと分岐、エージェント失敗時の処理を書けます。各エージェントの結果はプログラムに戻り、セッションを動かしている主エージェントには戻りません。次にどのエージェントを動かすかはプログラムが決めます。
runが終わると、主エージェントに結果を読む番が回ります。その上で、ユーザーへ答えるか、次のrunを始めます。
runの状態はrunning・idle・endedの3つ
runはrunningかidleで始まり、最後はendedで終わります。
| 状態 | 意味 | 抜け方 |
|---|---|---|
| running | 意味実行中 | 抜け方ワークフロー完了・停止・失敗・寿命切れ・アーカイブでended |
| idle | 意味一時停止中(例: セッションの予算に到達) | 抜け方予算を引き上げるか外すとrunningに戻る |
| ended | 意味終了 | 抜け方workflow_run.status_ended が最後のイベント |
予算到達で止まったrunは、予算を引き上げるか外せば再開します。ただし割り込み(interrupt)でも止まっていた場合は、予算だけでは戻りません。
runがopenなのは、workflow_run.created から workflow_run.status_ended までです。runningかidleかは問いません。
runの寿命は既定で24時間で、エージェントが開始時にもっと短く設定することもあります。クライアントの応答待ちの時間も寿命に数えます。一時停止中も寿命は進むので、止まったままのrunは timeout_error で終わることがあります。
runイベントを読む
runのイベントは、主スレッドのイベントストリームに届きます。セッションのイベント一覧にも含まれます。webhookは発火しません。runのスレッドの状態イベントも同じストリームに流れ、session_thread_id でどのスレッドか分かります。
| イベント | 届くタイミング | 受け取り側でやること |
|---|---|---|
workflow_run.created | 届くタイミングエージェントがrunを始めた | 受け取り側でやることrunをopenとして記録し、name と phases を表示する |
workflow_run.status_running | 届くタイミング実行が始まったとき。予算停止からの再開時にも届く | 受け取り側でやることrunningと表示する |
workflow_run.status_idle | 届くタイミング一時停止。理由は載っていない | 受け取り側でやること再開は予算の調整か割り込み後の操作で行う |
workflow_run.phase_started / phase_ended | 届くタイミングphaseの開始と終了 | 受け取り側でやること進捗を更新する |
workflow_run.status_ended | 届くタイミングrunの終了。workflow_run.* の最後 | 受け取り側でやることresult を読む |
workflow_run.error | 届くタイミングrunのエラー、または拒否された開始 | 受け取り側でやることログに残す。runの終了とは見なさない |
押さえておきたい癖がいくつかあります。
- phaseイベントにはphaseの名前がありません。
workflow_run.createdのphasesからworkflow_run_phase_idで引きます。 - phaseは順番に、1回ずつ動くのが通常です。ただしAPIはこれを保証しません。複数のphaseが同時にopenなこと、
phasesに無いphaseが現れること、宣言されたphaseが一度も始まらないことを、完了するrunでも想定しておきます。 - phaseの終了イベントは、そのphaseの仕事が成功したかを教えません。startとの対応は
phase_started_idで取ります。 workflow_run.errorはstatus_endedの前に届きます。workflow_run_idがnullなら、runは作られていません。
resultの種類
status_ended の result が終わり方を示します。
| result | 意味 |
|---|---|
{"type": "completed"} | 意味ワークフローが最後まで走った。仕事の成否は示さない |
{"type": "stopped"} | 意味エージェントが止めたか、セッションがアーカイブされた。どちらかは区別できない |
error + timeout_error | 意味寿命(既定24時間)に達した |
error + program_error | 意味コードの失敗やルール違反、またはスレッドの失敗をワークフローがrunの終了に回した |
error + thread_limit_error | 意味1つのワークフローが起動できるエージェント数の上限を超えた |
error + unknown_error | 意味サーバーが続行できなかった、またはサーバー側の別の上限を超えた |
ここで誤解しやすいのが completed です。スレッドの作業が失敗していても、スレッドが作れなかった場合でも、runは completed で終わることがあります。completed は「プログラムが終わった」という意味で、「仕事に合格した」という意味ではありません。失敗した作業を探すには、runに属する各スレッドのイベントを読みます。
result.type や error.type が見知らぬ値でも、落とさない作りにしておきます。未知の result.type は「別の終わり方をしたrun」、未知の error.type は単にエラーとして扱います。
例: 300件の契約書を読むrun
契約書レビュー用のエージェントに「300件のうち、支配権変更条項があるのはどれか」と尋ねたとします。公式の流れは次のとおりです。
workflow_run.createdがrun名「Find change-of-control clauses」とphase「Read the contracts」「Reconcile the findings」を運ぶ。続いてstatus_runningが届く。- phaseイベントが段階を区切り、runがスレッドを作るたびに、runの
workflow_run_idつきでsession.thread_createdが届く。 workflow_run.status_endedがresult: {"type": "completed"}で届く。- エージェントが「300件中41件です」と答え、
session.status_idleがend_turnで届く。
「作業が終わった」を見分ける
runが走っている間、セッションは running のままです。スレッドが1つも動いていなくても、そうです。スレッドがクライアントの応答を待っているときは、requires_action つきでidleになります。idleになっただけでは、作業の完了を意味しません。
完了と言えるのは、次の2つが両方成り立つときです。
- 作成を見たrunのすべてに
workflow_run.status_endedが届いている - その後に、
stop_reasonがend_turnのsession.status_idleが届いている(自分のinterruptが原因のidleは数えない)
idle を完了と読むと外す場面
idle だけを見る
session.status_idle が来た時点で完了と見なす。予算で止まったrunや、主エージェントがまだ次のrunを始めようとしている場面を、完了と取り違える。
run の open 数と一緒に見る
openなrunが0件で、end_turn のidleが来て初めて完了と見なす。結果を読んだ主エージェントが新しいrunを始めたら、もう一度同じ判定をする。
idleの理由ごとの注意点も押さえておきます。
- 予算到達: 一時停止中のrunはセッションをrunningに保たないので、セッションはidleになれます(
budget_reached)。runが終わるまで作業は終わっていません。 - 別のrun: 主エージェントは結果を読んだ後に新しいrunを始められます。完了判定はそのたびにやり直します。
- outcomeの評価: アウトカムを定義していても、runがopenな間は評価が始まりません。runningかidleかは関係ありません。
retries_exhausted: 主エージェントのターンがエラーで失敗した場合です。runが終わっていて、主エージェントがまだ結果を読んでいなければ、サーバーが入力なしで新しいターンを始めます。セッションが再びrunningになるので、次のidleを待ちます。idleのまま動かなければ、その手前のsession.errorを読んで原因を直します。
完了判定を書く
公式のサンプルは、workflow_run.created でopenなrunを記録し、status_ended で外し、openが0件のときの end_turn で止まります。同じ考え方を、ストリームでも一覧取得でも使える状態更新関数にすると、再接続時に同じコードを使い回せます。次は、公式のサンプルのロジックに沿って書いた一例です。
from dataclasses import dataclass, field
@dataclass
class RunBook:
open_runs: dict[str, str] = field(default_factory=dict) # run_id -> name
phase_names: dict[tuple[str, str], str] = field(default_factory=dict)
ended: dict[str, str] = field(default_factory=dict) # run_id -> result type
def apply(self, event) -> None:
match event.type:
case "workflow_run.created":
self.open_runs[event.workflow_run_id] = event.name
for phase in event.phases:
self.phase_names[event.workflow_run_id, phase.id] = phase.name
case "workflow_run.status_ended":
self.open_runs.pop(event.workflow_run_id, None)
self.ended[event.workflow_run_id] = event.result.type
def is_done(self, idle_event) -> bool:
# 全 run が ended で、end_turn の idle が来たときだけ完了
return not self.open_runs and idle_event.stop_reason.type == "end_turn"is_done に渡すidleは、自分が送ったinterruptが原因のものを除きます。interruptの後は、次の user.message か user.define_outcome より後に来たidleだけを数えます。
runのスレッドと、runがopenな間にできないこと
runの各エージェントは、サーバーが作るセッションスレッドで動きます。一覧、読み取り、ストリームは通常の子スレッドと同じです。
| 項目 | 内容 |
|---|---|
| 見分け方 | 内容スレッドと session.thread_created にrunの workflow_run_id が付く。それ以外のスレッドでは null |
| どのphaseか | 内容スレッドがどのphaseで動くかを示す項目は無い。同じrunのスレッドが同じ agent_name を持つこともある。見分けは session_thread_id で行う |
| 共有されるもの | 内容サンドボックスのファイル(セッションがマウントするメモリストアのファイルを含む)。会話履歴はスレッドごと |
| 子スレッド上限 | 内容runのスレッドは、セッションの子スレッド数の上限の対象外 |
| 開始できるのは | 内容主スレッドのエージェントだけ。runのスレッドのエージェントはrunを始められず、runは入れ子にならない |
| アーカイブ | 内容サーバーが遅くともrunの終了までに行う。openな間にクライアントがアーカイブすると、400(error.details.error_code: "workflow_run_open")になる |
ワークフローのコードそのものは見えません。見たいときは、runが終わってセッションがidleになった後に、user.message で「始めたときのワークフローをそのまま出力して。新しいrunは始めないで」と頼みます。主エージェントが書いた内容を教えてもらう形です。
runがopenな間に、クライアントの操作がどうなるかも確認しておきます。
| 操作 | openな間の挙動 | 対処 |
|---|---|---|
| セッションのアーカイブ・削除 | openな間の挙動400になることがある(成功することもある)。成功したアーカイブは、openなrunを stopped で終わらせる | 対処主エージェントにrunを止めさせるか、終了を待つ |
| runのスレッド単体のアーカイブ | openな間の挙動400(サーバーが先にアーカイブ済みの場合を除く) | 対処何もしない。サーバーが行う |
セッションの agent の更新 | openな間の挙動一時停止中のrunがあっても400。ほかのフィールド(例: budget)を同じリクエストに混ぜると全体が拒否される | 対処すべてのrunが終わるまで待つか、止めさせる |
| runのスレッドからのツール呼び出し・確認への応答 | openな間の挙動許可される | 対処イベントが届いたらすぐ、そのイベントの id を使って答える |
最後の行は実装上いちばん効きます。runのスレッドが応答を待っていても、セッションは running のままです。idleを待ってから答える作りだと、いつまでも答えられません。
落とし穴
遅れて送ったツール結果が400になる
サーバーがスレッドをアーカイブした後に、そのスレッドの呼び出しへツール結果を送ると、効果がなく400を返すことがあります。400のときはスレッド一覧でそのスレッドを探し、状態が terminated なら遅すぎたということなので、結果を捨てます。結果は1リクエストに1つずつ送ります。1つでも拒否されると、リクエスト全体が拒否されるためです。なお、遅れた確認(confirmation)への応答は200を返しますが、ツールが実行されたことを意味しません。
割り込みと停止
user.interrupt を session_thread_id なし(または主スレッドのID)で送ると、主エージェントのターンが止まります。runは終わりません。runが一時停止するか動き続けるかは、イベントから分からないことがあります。一時停止中も寿命は進むため、timeout_error で終わることがあります。
割り込み後にやることは3つあります。
- 待機中のツール呼び出し: 割り込み後も、runのスレッドのツール呼び出しが応答を待っていることがあります。確認を求める呼び出しはdeny、カスタムツールは
is_error: trueと理由のテキストを付けた結果で取り消します。requires_actionのidleの間はuser.messageが400になるので、先に全部答えます。 - runを止める: 主エージェントにrunを止めるよう
user.messageで頼みます。止まったrunは{"type": "stopped"}で終わります。 - 続ける: 続行を頼む
user.messageを送ります。割り込み後のrunは、このメッセージを待つことがあります。
budget_reached のidleの間は user.message が400になるため、先に予算を引き上げるか外します。予算の側の実装は予算到達・アイドル再開の実装パターンにあります。
再接続したら、イベント一覧からrunの状態を作り直す
ストリームは取りこぼしたイベントを再送しません。新しい接続が届けるのは、接続を開いた後のイベントだけです。切断中に起きたことは、イベントの一覧取得で埋めます。手順はこうなります。
typesフィルターにイベント種別を並べて、セッションのイベントを一覧取得する- レスポンスの
next_pageをpageに渡し、next_pageがnullか欠けるまで繰り返す workflow_run.created/status_running/status_idle/status_endedから各runの状態を作るphase_started/phase_endedから進捗を作る
前節の RunBook.apply に一覧を順に流せば、手順3と4のうちopenなrunの集計までは同じコードで済みます。注意点が2つあります。
- 割り込み後に止まったrunは、一覧の上ではrunningのままに見えることがあります。
- ステータスイベントが1つも無いrunは、まだ実行が始まっていません。
runを一覧する専用のエンドポイントはありません。runの把握は、すべてセッションのイベントから行います。ストリームの実装の基本はSSEイベントストリームの実装を参照してください。
予算と上限
runには専用の価格がありません。runのエージェントが使うトークンは、セッションのほかのトークンと同じく、モデルごとの単価で課金されます。runのモデルリクエストはセッションの予算に数えます。
run 1回分の使用量を知りたいときは、セッションのスレッド一覧から、そのrunの workflow_run_id を持つスレッドの usage を足し合わせます。アーカイブ済み(terminated)のスレッドも一覧に含まれるので、終わったrunも集計できます。usage が null のスレッドは飛ばします。list_cost を足す方法もありますが、合計にセッションの稼働時間分が入らず、各値が個別に丸められています。
予算に達すると、開いているrunがすべて一時停止し、セッションは budget_reached でidleになります(ツール呼び出しも待っていれば requires_action)。各スレッドは開始済みのモデルリクエストを最後まで終えるので、動いているスレッド1つにつき最大1リクエスト分、予算を超えます。
| 上限 | 値 | 上限に達すると |
|---|---|---|
| 1 runで同時に動くスレッド | 値64 | 上限に達すると1つ終わるまで新しいスレッドを作らない。数値は保証されず変わりうる |
| 1つのワークフローがrun全体で起動するエージェント | 値1,000 | 上限に達するとこれ以上は起動せず、thread_limit_error で終わる |
| runの寿命 | 値既定24時間(エージェントが設定した値) | 上限に達するとtimeout_error で終わる |
| セッションで同時にopenなrun | 値既定10 | 上限に達すると新しいrunを拒否。workflow_run.error の error.type が max_workflow_runs_error |
1,000の数え方には注意が要ります。失敗したエージェントをサーバーが新しいスレッドで再実行することがあり、runのスレッド総数が1,000を超える場合があります。idleのrunも、同時にopenなrunの上限に数えます。
runやphaseの name は64文字、description は256文字に切り詰められます。ほかの上限やルールに触れたときの見え方は、気づく時点で変わります。
| 状況 | 見えるもの |
|---|---|
| runの開始時点で、ほかの上限を超えている | 見えるもの開始が拒否される。workflow_run.error が届き、runは作られない |
| 開始後に、ほかの上限を超える | 見えるものworkflow_run.error の後、runが unknown_error で終わることがある |
| 開始後に、ワークフローがルール違反だと分かる(上限以外) | 見えるものworkflow_run.error の後、runが program_error で終わることがある |
1つのセッションが生涯で始められるrunの数に上限はありません。
レート制限
runの仕事は、組織にすでにあるレート制限に数えられます。
- 取得・一覧のリクエスト: セッション、スレッド、イベントの取得や一覧は、Managed Agentsエンドポイントの読み取り上限に数えます。ポーリングではなくイベントストリームで追います。
- スレッドのモデルリクエスト: スレッドが使うモデルのMessages APIのレート制限に、ほかのトラフィックと合わせて数えます。runの分の余裕を残すか、上限の引き上げを申請します。
スレッドのモデルリクエストがレート制限やモデルの過負荷に当たると、そのスレッド自身のストリームに model_rate_limited_error か model_overloaded_error の session.error が出ます。retry_status.type が retrying ならサーバーが再試行中で、スレッドはまだ動いています。exhausted ならそのスレッドは失敗です。ワークフローがその失敗をrunの終了に回していれば、runは原因を示さない program_error で終わります。原因は失敗したスレッドのイベントを読んで探します。
組織全体のセッションが毎分に行える作業量にも上限があります。これに当たったスレッドは止まり、スレッド自身のストリームにレート制限を名指しする session.error が出ます。エージェントに続行を頼む前に、1分待ちます。レート制限の全体像はManaged Agentsのレート制限にあります。
runは同じ仕事のために複数のスレッドを作ることがあります。エージェントが呼ぶツールは、2回呼ばれても安全な作りにしておきます。
まとめ
runを扱う側の要点は3つです。完了はidleではなく「openなrunが0件で end_turn のidle」で判定します。completed は仕事の合否を示さないので、失敗はスレッド側で探します。切断したらストリームを頼らず、イベント一覧からrunの状態を作り直します。
実装前に確かめる4点
- 完了判定は、openなrunの数と
end_turnのidleの両方で行う - runのスレッドのツール呼び出しには、idleを待たずに答える
- 想定外のphaseイベントと未知の
result.typeで落ちない - ツールは2回呼ばれても壊れない作りにする