AIワークフローの重複実行は、AIの回答精度だけでは防げません。同じWebhookの再送、実行のRetry、タイムアウト後の再送で、メールやレコード作成などの副作用が二度起きることがあります。対策は、イベントを識別するID、処理状態、Action結果を保存し、再受信時に状態を確認することです。
大切なのは「同じTriggerを二度受けない」だけでなく、「同じ業務Actionを二度実行しない」ことです。受信元の重複排除と、下流での二重作成・二重送信は別の境界なので、両方を確認します。完全に一度だけ実行されると仮定せず、再送されても結果が壊れにくい設計を目指します。
目次
まず重複が起きる境界を分ける
表は横にスクロールできます
| 境界 | 例 | 確認すること |
|---|---|---|
| イベント受信 | Webhook送信元が同じEventを再送 | 元システムの安定したEvent IDがあるか |
| Workflow起動 | PollingやTriggerが同じ項目を再検出 | どの範囲で既処理IDを記録するか |
| AI Step再実行 | 前段の失敗後に分類・抽出を再度実行 | 同じ入力・版・出力を追跡できるか |
| 外部Action | CRM作成、メール送信、決済など | 相手側が冪等性キーや照会を支援するか |
| Workflowの自分への再Trigger | 処理結果が再び起動条件になる | ループを識別して止められるか |
同じイベントを識別できても、処理途中でサーバーが停止すると、イベント記録と外部Actionの結果が別々の状態になります。例えば、Action成功後にcompleted保存前で落ちると、再実行側からは未完了に見えます。この区間を設計せずに「IDとStateを入れたから安全」とは言えません。
Stable IDとAction単位のKey
最優先で使うのは、Webhookや元データが提供する不変で一意なEvent IDです。注文番号のような業務IDでも、同じ注文で複数の正当な更新イベントが起きるならEvent IDの代わりにはなりません。日時だけをIDにすると、同時刻の別イベントを区別できない可能性があります。
Workflow内に複数の副作用がある場合は、Event全体のIDにAction名を加え、個別の処理状態を追います。例として、event_42:crm_upsertとevent_42:notify_ownerを別の処理として記録します。どの値が重複判定の正本なのか、イベントの再送期間、記録を保持する期間も仕様として決めます。
event_id = 送信元が付与した安定ID
action_key = event_id + 対象Actionの識別子
correlation_id = 関連する一連のWorkflowを追跡するID
この文字列は説明用の例です。実際のIDは衝突しない範囲とActionの単位に合わせて設計します。個人名、メールアドレス、本文などをIDへ直接埋め込まず、個人を特定しない参照値を使います。
状態遷移を設計する
表は横にスクロールできます
| State | 意味 | 再受信・再実行時の判断 |
|---|---|---|
| received | イベントとActionを受け付けた | 未着手なら処理開始を試みる |
| processing | 担当Workerが処理中 | 同時実行を防ぎ、完了を確認する |
| action_pending | 外部Actionを呼び出す直前 | Actionの状態を確認してから開始 |
| completed | Action結果を確認して記録済み | 同じActionを繰り返さず結果を返す |
| failed_retryable | 再試行が安全と分かっている失敗 | 回数・間隔・期限内で再試行 |
| failed_terminal | 入力や権限など修正が必要な失敗 | Holdして人へ通知 |
| unknown_outcome | 送信後に応答が失われ、結果未確認 | 相手側を照会するまで再送しない |
processingは永遠に残さないように、開始時刻、担当、期限またはLeaseの扱いを決めます。実行が停止したとき、期限切れのStateをどう回収するか、別Workerが処理を引き継いでも二重処理にならないか確認します。状態名は実装に合わせて変えられますが、成功・失敗・結果不明を同じ値へまとめないことが重要です。
受信から完了までの処理
1. Eventを受信し、event_idとpayloadの参照を検証
2. action_keyを作り、永続Storeで原子的に処理権を確保
3. 既にcompletedなら保存済みの結果を返して終了
4. processingなら待機・重複終了・レビューの規則へ進む
5. Actionを実行し、下流IDまたは結果を安全に記録
6. 完了確認後にcompletedへ遷移
7. Retry時は最新Stateと下流の実在状態を再確認
重要なのは、確認してから書き込む二段階が、同時実行時にも一つの実行だけを通すことです。単純に「Storeを読む→空なら書く」だけだと、同時に届いた2つのRunが両方とも空を見て処理を進める競合が起きます。データベースの一意制約、原子的な挿入、ロック、Queue等、利用する環境で保証できる方法を使います。
状態の保存先に実行履歴だけを使えるか、別の永続Storeが必要かは製品と設定に依存します。履歴を消すと重複判定も消える設計なら、保持期間と再送期間を比較してください。状態の保管・削除仕様が分からない場合は、運用前に公式資料と環境を確認します。
失敗時に二重実行を作らない
表は横にスクロールできます
| 失敗地点 | 起きた可能性 | Retry前にすること |
|---|---|---|
| Action呼び出し前 | 副作用はまだ起きていない | 入力と失敗原因を確認し、再試行条件を満たせば処理 |
| Action送信中 | 相手側で処理されたか不明 | unknown_outcomeとして状態照会または手動確認 |
| Action成功後・完了記録前 | 副作用は起きたがStoreは未完了の可能性 | 外部Recordや冪等性Keyで既存結果を確認 |
| Validation / 認証失敗 | 仕様や資格情報の問題 | Retryを繰り返さず設定を修正 |
| Rate limit / 一時障害 | 短時間で復旧する可能性 | 公式の待機・Retry指示、上限、期限を確認 |
下流APIが冪等性Keyを提供する場合は、公式仕様が許す形で同じActionの再試行に同じKeyを使います。Keyの有効期限、同じKeyで異なるpayloadを送った時の挙動、エラー後の結果照会などはAPIごとに違うため、実装前に確認します。機能が存在しない場合、検索してから作成するfind-or-createも候補ですが、検索と作成が原子的でなければ同時実行による競合は残ります。
架空ケース:CRMへ問い合わせを登録する
次の例は設計を説明するための架空ケースです。実サービスでの動作検証ではありません。Webhookがevent_id=evt_42を送るとし、ActionをCRM登録と担当者通知へ分けます。
表は横にスクロールできます
| Action | 処理Key | 成功時に保存する値 | 再実行時 |
|---|---|---|---|
| CRM登録 | evt_42:crm_upsert | CRM record ID、完了時刻 | 既存Recordを照会し再作成を避ける |
| 担当者通知 | evt_42:notify_owner | 通知Message ID、送信状態 | 同一Notificationの扱いを確認 |
CRM登録が成功し、通知だけ失敗した場合、Run全体を最初からやり直すとCRMへ再登録してしまうことがあります。Actionごとに結果を記録すれば、完了済みのCRM処理を飛ばし、失敗した通知だけ再開できます。ただし、通知先が同じMessage IDや冪等Keyを扱うかは別途確認が必要です。
Find-or-CreateとLoopの注意
一意な業務Keyで既存Recordを検索し、あれば更新またはSkip、なければ作成する方法は重複を減らすのに役立ちます。しかし、曖昧な検索Keyで既存データへMergeすると、別顧客のRecordを誤更新する危険があります。また、同時処理が検索と作成の間に割り込むと二重作成が残り得ます。Keyの一意性と競合対策を確認してください。
Workflowの更新が同じWorkflowのTrigger条件に戻る場合は、処理済みMarker、対象Fieldの変更Filter、発生元の識別などを使い、再入を止めます。例えばCRMを更新した結果のWebhookが同じ処理を再起動するなら、更新者や変更Fieldだけでなく、Action単位のEvent IDと処理Stateも確認します。
Retryルールを分ける
Retryは、エラーが起きたら何度でも実行する仕組みではありません。入力が不正、権限不足、存在しないRecordなど、条件を直さなければ成功しないエラーは、RetryではなくHoldや修正担当へ送ります。一時的なネットワーク障害や利用制限は、公式資料にある待機条件、最大試行数、全体期限を確認してから再試行します。
Retry前に、今回の処理が外部へ何をしたかを記録します。成功の応答がなかったことと、処理が起きていないことは同じではありません。下流の結果照会、冪等性Key、または人による確認のどれも用意できない場合は、繰り返し自動再送するよりunknown_outcomeで止める方が安全です。
検証ケースと運用QA
-
同じEventを2回順に送る → 外部Actionは重複せず、保存済み状態が返る
-
同じEventをほぼ同時に送る → 原子的な処理権の確保が機能する
-
Action成功後、completed保存前に止める → 下流を照会し重複Actionを避ける
-
Action送信後にTimeoutさせる → unknown_outcomeへ進み、無条件Retryしない
-
Validation / 認証Error → 指定されたRetryへ流れずHoldする
-
Actionが複数あるEventで一つだけ失敗 → 完了Actionを再実行しない
-
Trigger更新が自分へ戻る → Loopを検出して停止する
各テストでEvent ID、Run ID、Action Key、実行回数、結果Record ID、State遷移を確認します。正常時だけのテストでは、タイムアウト・競合・途中停止が作る重複を見つけられません。検証環境で意図的な失敗を作り、再実行後の副作用数まで照合します。
ログにはEvent ID、Run ID、Action名、State、attempt、時刻、結果参照を残すと調査しやすくなります。payload全文、Secret、個人情報を重複調査のために無制限保存する必要はありません。入力を開ける権限、記録を保持する期間、削除後の再送リスクを、業務上の必要性と合わせて決めます。
次にすること
まずワークフローから外部副作用のあるActionをすべて列挙し、それぞれについて安定ID、状態の保存先、下流照会手段、Retry可否を記入します。次に、正常な再送、同時再送、Action後Timeoutの三ケースを検証環境で試し、下流の作成数とStateを比較します。
既存Workflowに同じEvent IDがない場合は、Triggerデータに安定IDがあるか確認します。なければ、生成方法と衝突・保持期間を設計します。状態Store、原子的な処理権、下流の照会・冪等性のいずれも確保できないActionは、少なくとも自動Retryを止め、担当者確認へ回すことを次の選択肢にします。
AIものさしの判断
重複実行対策はPromptの工夫ではなく、Event ID・Action単位State・副作用の結果照会を組み合わせる設計課題です。特にタイムアウト後は「失敗」と決めつけず、処理済みか分からないStateを扱います。同じEventを何度受けても処理権と結果を一貫して確認し、確認できないActionを安全に止めることが完成条件です。
引用・データ出典
公式情報を編集部が見た内容です。Hands-on未実施の記述は本文かここに分けています。
確認日:2026-10-07
