Cloudflare ハンズオン教科書|2026-08 版
第 10 章

Workflows — 耐久性のある多段処理

リトライも待機も宿題にしない。durable execution を体験する

この章で学ぶこと

Workflows とは — 失敗しても「続きから」再開する処理基盤

外部 API を呼び、結果を待ち、加工して保存する——そんな多段処理を普通の Worker で書くと、途中でネットワークエラーが起きたときの再実行、リトライ間隔の管理、途中結果の保存はすべて自前の宿題になります。Workflows はこの宿題を引き受けるサービスです。処理を step という単位に分割すると、各 step の成功結果が自動的に永続化され、失敗しても最後に成功した step の直後から再開されます。この「状態の保存をコードで書かなくても暗黙に永続化される」プログラミングモデルは一般に Durable Execution(耐久実行) と呼ばれます。

普通の Worker で書いた場合 Workflows の step に分けた場合 API 取得 加工 保存(失敗) 成功 成功 途中結果は 保存されない 最初からやり直す 取得も再実行になる API 取得 加工 保存(失敗) 成功 成功 戻り値を永続化 戻り値を永続化 この step だけ 再試行する
図 10-1 同じ 3 段の処理でも、失敗したときに巻き戻る距離が違います。左は途中結果がメモリ上にしかないので取得からやり直しになり、右は step の戻り値が永続化されているため、失敗した step だけを再試行できます。

Workflows のインスタンスは数分どころか数時間・数週間にわたって実行を継続でき、待機中はコンピュートを消費しません。Workers Free プランでも利用できます。まず用語を整理します。

用語意味
Workflow名前付きの処理定義。1 つの Workers スクリプトに紐づく
instanceWorkflow の個々の実行。1 つの Workflow から実質無制限に作れる
step個別にリトライ可能な最小単位。戻り値(状態)が永続化され、再開時の「続きから」の起点になる

公式ドキュメントが挙げる代表的なユースケースは次のとおりです。

step API — do・sleep・waitForEvent

Workflow の本体は WorkflowEntrypoint を継承したクラスで、run(event, step) メソッドに処理を書きます。event.payload には起動時に渡したパラメータが入り、step オブジェクトが提供する次のメソッドで耐久性のあるステップを定義します。

メソッド役割補足
step.do(name, config?, callback)コードを実行し、戻り値を永続化する戻り値は直列化可能な値でストリーム以外は 1MiB まで(2026-08 時点)。ステップ名がキャッシュキーになる
step.sleep(name, duration)相対時間だけ実行を一時停止する"20 seconds" や "1 hour" 形式。最長 365 日。ステップ数上限にカウントされない
step.sleepUntil(name, timestamp)指定日時まで一時停止するDate または UNIX エポックミリ秒を渡す
step.waitForEvent(name, options)外部イベントが届くまで待機するtype で対応するイベントを識別。デフォルトタイムアウトは 24 時間(1 秒〜365 日で指定可)

step.do が失敗(例外を throw)すると、Workflows が自動的にリトライします。リトライ設定を省略した場合の既定値は次のとおりです。

const defaultConfig: WorkflowStepConfig = {
	retries: {
		limit: 5,             // 最大試行回数
		delay: 10000,         // 試行間の遅延(10 秒)
		backoff: "exponential", // "constant" | "linear" | "exponential"
	},
	timeout: "10 minutes",  // 1 試行あたりのタイムアウト
};

backoff は遅延の増やし方(固定・線形・指数)を決めます。delay には固定値のほか、エラー内容や試行回数から次の遅延を計算する関数も渡せます。逆に「認証エラーなどリトライしても無駄な失敗」は、cloudflare:workflows からインポートする NonRetryableError を throw すると、リトライせず即座に instance を失敗させられます。step の失敗で Workflow 全体を止めたくない場合は、step.do の呼び出しを try...catch で囲めば、失敗を捕捉してクリーンアップ用の step に進む、といった制御もできます。

step.do の既定リトライ(limit: 5・delay: 10 秒・backoff: exponential) 試行 1(失敗) 試行 2(失敗) 試行 3(成功) 次の step へ 10 秒待つ さらに長く 成功 遅延は delay(既定 10 秒)を起点に backoff の指定で伸びる リトライが終わる 2 つの道 — 差は途中の待機の有無 遅延を挟んで再試行 最大 5 回 5 回目も失敗 上限到達で instance = errored NonRetryableError 待機も再試行もはさまず即座に
図 10-2 上段は遅延を挟んで再試行が続く様子です。下段の 2 本は同じ errored に着きますが、上の道だけが途中に「待機」という中継点を持ちます。NonRetryableError はその中継点を持たない直線で、throw した瞬間に instance が確定します。

step を設計するときの判断基準は「一部が失敗したとき、このコード全体を再実行してよいか?」です。外部 API 呼び出し・DB クエリ・ストレージ読み取りはそれぞれ別の step に分けると、後続の失敗時に取得済みデータを再利用でき、無駄な API 呼び出しを避けられます。詳しくは公式の Rules of Workflows にまとまっています。

instance の起動・ライフサイクル・観測

Workflow は次の方法で起動(instance を作成)できます。

  1. Worker のバインディング — Wrangler 設定の workflows でバインドし、env.MY_WORKFLOW.create() を呼ぶ。id(省略時は自動生成)と params(JSON 直列化可能な任意のデータ)を渡せる
  2. wrangler CLI — npx wrangler workflows trigger <名前> '<JSON>'
  3. REST API — HTTP 経由でプログラマブルに作成
  4. cron スケジュール — Workflow 定義に schedules(cron 式の配列)を追加すると、定期的に instance が自動作成される
// Worker から起動して ID と状態を返す
let instance = await env.MY_WORKFLOW.create({
	id: crypto.randomUUID(),
	params: { hello: "world" },
});
return Response.json({
	id: instance.id,
	details: await instance.status(),
});

instance.status() が返す status フィールドで、instance が今どの状態かを確認できます。

status意味
queued起動待ち(同時実行数の上限に達している場合など)
running実行中
waiting休止中。sleep・リトライ待ち・イベント待ちのいずれか。同時実行数の上限にカウントされない
paused / waitingForPauseユーザー操作による一時停止(完了 / 移行中)
complete正常終了。run の戻り値が output に入る
erroredリトライ上限到達や未捕捉例外による失敗
terminatedユーザーが明示的に停止した
create() で作成 満枠なら queued に並び直す queued 起動待ち 同時実行の上限待ち running 実行中 waiting 休止中 実行開始 待機に入る 空きがあれば sleep・リトライ待ち・ イベント待ちのいずれか 待機中は同時実行数に 数えられず CPU も使わない run() が正常終了 リトライ上限・未捕捉例外 complete errored pause() / resume() / terminate() / restart() はユーザー操作でいつでも割り込める
図 10-3 waiting は sleep・リトライ待ち・イベント待ちの共通の受け皿で、ここに滞在している間は同時実行数にも CPU 課金にもカウントされません(ただし instance の状態はストレージとして課金対象のまま残ります)。破線に注目してください。復帰先は必ずしも running ではなく、同時実行が満枠なら queued に並び直します。

instance には pause() / resume() / terminate() / restart() という操作も用意されています。同じことは wrangler workflows instances pause|resume|terminate|restart でも行えます。観測系のコマンドは次の 2 つが主役です。

ローカル開発では npx wrangler dev がエミュレートされた Workflows 実行環境を提供し、各コマンドに --local フラグを付けるとローカルの instance を操作できます(--local は Wrangler 4.79.0 以降)。さらに wrangler dev 実行中にターミナルで e を押すと、ブラウザベースの Local Explorer(http://localhost:8787/cdn-cgi/explorer、Wrangler 4.82.1 以降)が開き、instance の step 履歴表示・イベント送信・一時停止などを GUI で行えます。

ハンズオン 10-1|多段ステップの Workflow をローカルで動かす

データ取得 → 20 秒待機 → 加工、という 3 段の Workflow を作り、ローカルで起動して status の変化を観察します。所要時間は 15 分程度です。

Workflows は Workers Free プランで動作します(ステップ数などの上限は Paid より小さめです。2026-08 時点で Free は 1 instance あたり 1,024 step、Paid は既定 10,000 step)。

  1. Worker プロジェクトを作成します。質問には「Hello World example」「Worker only」「TypeScript」、デプロイは「No」を選びます。
    npm create cloudflare@latest -- my-workflow
    cd my-workflow
  2. src/workflow.ts を新規作成し、Workflow 本体を定義します。
    import { WorkflowEntrypoint, WorkflowStep } from "cloudflare:workers";
    import type { WorkflowEvent } from "cloudflare:workers";
    
    type Params = { name?: string };
    type IPResponse = { result: { ipv4_cidrs: string[] } };
    
    export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
    	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
    		const data = await step.do("fetch data", async () => {
    			const response = await fetch(
    				"https://api.cloudflare.com/client/v4/ips",
    			);
    			return await response.json<IPResponse>();
    		});
    
    		await step.sleep("pause", "20 seconds");
    
    		const result = await step.do(
    			"process data",
    			{ retries: { limit: 3, delay: "5 seconds", backoff: "linear" } },
    			async () => {
    				return {
    					name: event.payload.name ?? "World",
    					ipCount: data.result.ipv4_cidrs.length,
    				};
    			},
    		);
    		return result;
    	}
    }
  3. wrangler.jsonc に workflows 設定を追加します。class_name はエクスポートしたクラス名、binding はコードから参照する変数名です。
    {
    	"$schema": "node_modules/wrangler/config-schema.json",
    	"name": "my-workflow",
    	"main": "src/index.ts",
    	"compatibility_date": "2026-08-10",
    	"observability": { "enabled": true },
    	"workflows": [
    		{
    			"name": "my-workflow",
    			"binding": "MY_WORKFLOW",
    			"class_name": "MyWorkflow"
    		}
    	]
    }
  4. src/index.ts を、instance の作成と status 照会を行う fetch ハンドラに置き換えます。
    export { MyWorkflow } from "./workflow";
    
    export default {
    	async fetch(request: Request, env: Env): Promise<Response> {
    		const url = new URL(request.url);
    		const instanceId = url.searchParams.get("instanceId");
    		if (instanceId) {
    			const instance = await env.MY_WORKFLOW.get(instanceId);
    			return Response.json(await instance.status());
    		}
    		const instance = await env.MY_WORKFLOW.create();
    		return Response.json({ instanceId: instance.id });
    	},
    } satisfies ExportedHandler<Env>;
  5. バインディングの型を生成してから、ローカル開発サーバーを起動します。
    npx wrangler types
    npx wrangler dev
  6. 別ターミナルから instance を作成し、返ってきた ID で status を数回確認します。
    curl http://localhost:8787
    # {"instanceId":"abc-123-def"}
    
    curl "http://localhost:8787?instanceId=abc-123-def"
    # {"status":"running", ...}

sleep の 20 秒が明けると status が complete になり、output に process data step の戻り値(name と ipCount)が入っていれば成功です。

ハンズオン 10-2|意図的な失敗で自動リトライを観察する

最初の 2 回はわざと失敗し 3 回目で成功する step を追加して、Workflows が自動リトライする様子を wrangler workflows instances describe で観測します。所要時間は 10 分程度です。

  1. src/workflow.ts の step.sleep の直後(process data の前)に、失敗する step を追加します。コールバックの第 1 引数 ctx(WorkflowStepContext)の attempt には現在の試行回数(1 始まり)が入ります。
    const flaky = await step.do(
    	"flaky step",
    	{ retries: { limit: 3, delay: "3 seconds", backoff: "constant" } },
    	async (ctx) => {
    		if (ctx.attempt < 3) {
    			throw new Error(`attempt ${ctx.attempt} failed on purpose`);
    		}
    		return { succeededOnAttempt: ctx.attempt };
    	},
    );
  2. wrangler dev が再読み込みされたら、curl http://localhost:8787 で新しい instance を作成します。
  3. 20 秒の sleep が明けた頃に、別ターミナルから最新 instance の詳細を表示します。
    npx wrangler workflows instances describe my-workflow latest --local

出力には step ごとの成否・リトライ・エラーが並びます。flaky step の欄で「1・2 回目の試行が attempt N failed on purpose というエラーで失敗し、約 3 秒間隔をおいた 3 回目で成功している」こと、そして先行する fetch data は再実行されていないことを確認してください。これが「最後に成功した step から再開する」durable execution の動きです。ちなみにこのエラーが NonRetryableError だった場合はリトライされず、instance は即座に errored になります。

ハンズオン 10-3|waitForEvent で承認イベントを待つ

Workflow を外部イベント(人間の承認)が届くまで待機させ、CLI からイベントを送って再開させます。所要時間は 10 分程度です。

  1. src/workflow.ts の return result; の直前に、承認待ちの step を追加します。
    let approved = false;
    try {
    	const approval = await step.waitForEvent(
    		"wait for approval",
    		{ type: "approval", timeout: "15 minutes" },
    	);
    	// sendEvent で送られた payload が approval.payload に入る
    	approved = (approval.payload as { approved?: boolean }).approved === true;
    } catch (e) {
    	// タイムアウト: 承認が来なかった場合もここで拾って続行できる
    	approved = false;
    }
  2. curl http://localhost:8787 で新しい instance を作成し、20 秒の sleep とリトライが終わった頃に status を確認します。イベント待ちに入った instance は waiting になります。
  3. 別ターミナルから、waitForEvent の type と一致する --type approval でイベントを送信します(<INSTANCE_ID> は手順 2 の ID に置き換えます。latest でも指定できます)。
    npx wrangler workflows instances send-event my-workflow <INSTANCE_ID> \
      --type approval --payload '{"approved":true}' --local
  4. 再度 status を確認し、complete になっていれば成功です。

Worker のコードからは instance.sendEvent({ type: "approval", payload }) で同じことができ、Stripe などの Webhook を受けて Workflow を進める構成が組めます。なお、instance が作成済みであれば waitForEvent に到達する前に届いたイベントもバッファされ、対応する step に到達した時点で消費されます。

step が先に到達した場合 — 待つのは instance instance waitForEvent 到達 待機に入る waiting のまま待つ 即再開 続きの step へ event sendEvent が到着 届いた時点で消費 イベントが先に届いた場合 — 待つのは event instance 別の step を実行中 そのまま waitForEvent 到達 即消費 待たずに続行 event sendEvent が先着 保持へ バッファに保持 到達と同時に渡す
図 10-4 どちらの順でも結果は同じですが、2 段を見比べると「待つ側」が入れ替わっているのが分かります。上段は instance 側の帯が waiting になり、下段は instance が止まらず event 側の帯がバッファになります。バッファが効くのは instance が作成済みのときだけで、待つ側が instance の場合はタイムアウト(既定 24 時間・1 秒〜365 日)を過ぎると例外が throw されます。

つまずきポイント

ステップ名に日時や乱数を入れるとキャッシュが効かない — step の名前は永続化された結果を引き当てる「キャッシュキー」です。step.do(`sync at ${Date.now()}`, ...) のような非決定的な名前を付けると、再開のたびに別ステップと見なされて再実行されます。名前は決定的にし、動的な値は戻り値やログに入れます。

step の外に置いた変数はハイバネーションで消える — 長い sleep やイベント待ちの間、エンジンは休止してメモリ上の状態を失うことがあります。トップレベルの配列に push して状態を溜めるコードは、再開後に空へ戻ります。状態は必ず step.do の戻り値として組み立て、副作用(別 Workflow の作成や Math.random() など)も step の中で行います。step の外の console.log はエンジン再起動時に複数回出力されることがあります。

waitForEvent の type 制約とタイムアウトの挙動 — type に使えるのは英数字と - _ のみで、. を含めると workflow.invalid_event_type エラーになります。またタイムアウト時は例外が throw され、そのままだと instance が失敗します。イベントが来なくても処理を続けたい場合は try...catch で囲んでください。

create の ID 重複エラー — 完了済み instance の状態は保持期間(2026-08 時点で Free 3 日 / Paid 30 日)の間残っており、その間に同じ ID で create を呼ぶと例外になります。同じ ID で再実行したい場合は既存 instance の restart() を使うか、ID を変えます。ちなみに createBatch(最大 100 件を一括作成)は冪等で、重複 ID はスキップされます。

理解度チェック

Q1. リトライ設定を省略した step.do は、失敗時にどう振る舞う?

既定値では 10 秒の遅延から始まる exponential バックオフで自動リトライされ、試行回数は合計最大 5 回(retries.limit: 5)です。1 試行あたりのタイムアウトは 10 分です。retries と timeout を渡せば step ごとに上書きできます。

Q2. step.sleep で 30 日眠っている instance は、同時実行数の上限やステップ数上限、CPU 課金にカウントされる?

いずれもカウントされません。sleep・リトライ待ち・イベント待ちの instance は waiting 状態となり同時実行数の上限から除外され、step.sleep はステップ数上限に数えられず、待機中は CPU 時間も消費しません。

Q3. 認証エラーのような「リトライしても無駄な失敗」を検出したとき、即座に instance を失敗させるには?

cloudflare:workflows からインポートした NonRetryableError を step 内で throw します。step のリトライは行われず、エラーはトップレベルへ伝播して instance は errored になります(try...catch で捕捉すれば続行も可能です)。

Q4. step.waitForEvent のデフォルトタイムアウトと、タイムアウトしたときの挙動は?

デフォルトは 24 時間で、1 秒〜365 日の範囲で変更できます。タイムアウトすると例外が throw され、捕捉しなければ instance は失敗します。イベントが来なくても続行したい場合は try...catch で囲みます。

まとめと次章へ

参考リンク:

多段処理の耐久性を手に入れたところで、次章は Workers AI です。エッジで動く AI 推論を Worker から呼び出します。この章で作った「AI パイプライン + 承認待ち」のような構成は、Workflows と Workers AI を組み合わせる典型例になります。