このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
8年前にWorkersを初めてローンチしたとき、それは開発者向けの直接プラットフォームでした。長年にわたり、私たちはエコシステムを拡大・拡張してきました。これにより、プラットフォームはWorkers上で直接構築するだけでなく、そのお客様が多くのマルチテナントアプリケーションを通じて当社にコードを出荷できるようになる可能性もあります。Workers:アプリでは、ユーザーが欲しい結果を記述し、AIが実装を書きます。マルチテナントSaaSでは、すべてのお客様のビジネスロジックが、実行時に、プラットフォームがこれまでに見たことのないTypeScriptを使用します。独自のツールを作成して実行するエージェント。すべてのリポジトリが独自のパイプラインを定義するCI/CD製品。
先月、Dynamic Workersのオープンベータ版を出荷した際、私たちはこれらのプラットフォームに、コンピューティング側でクリーンなプリミティブを提供しました。Workersランタイムに実行時にコードを渡すと、同じマシン上で数ミリ秒で、分離されたサンドボックス化されたWorkerが返されます。Durable Object Facetsは、同じ考え方をストレージにも拡張しました。動的にロードされる各アプリは、オンデマンドで起動される独自のSQLiteデータベースを持つことができ、プラットフォームがスーパーバイザーとして前に位置します。Artifactsは、ソース管理に対しても同様の機能を提供しました。Gitネイティブでバージョン管理されたファイルシステムを、数千万単位で作成できます。エージェントごとに1つ、セッションごとに1つ、テナントごとに1つといった具合にです。そのため、ストレージやソース制御のために動的なデプロイメントが可能です。今後の展開は?
現在は、Dynamic Workflowsで耐久性の高い実行と動的デプロイを橋渡ししています。
Cloudflare Workflowsは、当社の耐久性のある実行エンジンです。これは、run(event, step)関数を、すべてのステップが失敗しても存続し、数時間または数日間スリープでき、外部イベントを待つことができ、アイソレートがリサイクルされると中断した場所から正確に再開されるプログラムに変換します。単一のリクエストを超えて「継続」する必要があるものすべてに最適なプリミティブです。オンボーディングフロー、動画トランスコーディングパイプライン、複数段階の課金、長期実行されるエージェントループ、そして — Workflows V2以降 — 最大50,000の同時インスタンスと、アカウントあたり毎秒300の新規インスタンスに対応し、エージェント時代に向けて再設計されました。
しかし、Workflowsには、常に暗号化された前提があります。ワークフローコードはデプロイメントの一部であるということです。あなたの wrangler.jsoncには、"エンジンが WORKFLOWSを呼び出すと、MyWorkflowというクラスが実行されます。" というブロックがあります。1つのバインディング、1つのクラス。都度、デプロイごとに。
すべてのコードを所有している場合は問題ありません。従来のアプリケーションを実行している場合は問題ありません。
お客様がワークフローを出荷させたいと思った瞬間、機能を停止します。
例えば、AIがすべてのテナントのTypeScriptを書き込むアプリプラットフォームを構築しているとします。例えば、各リポジトリが独自のパイプラインを持つCI/CD製品を実行しているとします。各エージェントが独自の耐久性のあるプランを記述するエージェントSDKを使用しているとします。いずれの場合も、ワークフローはテナントごと、エージェント、リクエストごとに異なります。バインドするクラスはありません。
これはDynamic Workersがコンピューティングのために解決し、Durable Object Facetsがストレージのために解決したものと同じ形の問題です。耐久性のある実行を実現する方法をまだ解決していませんでした。
@cloudflare/dynamic-workflowsは小さなライブラリです。およそ300行のTypeScript。単一のWorker — Worker Loader — が、すべてのcreate()呼び出しを別のテナントのコードにルーティングでき、重要なことに、ワークフローが実際に実行される数秒、数時間、あるいは数日後に、その同じコードにrun(event, step)をディスパッチします。
全体のパターンは次の通りです。Worker Loader:
import {
createDynamicWorkflowEntrypoint,
DynamicWorkflowBinding,
wrapWorkflowBinding,
} from '@cloudflare/dynamic-workflows';
// The library looks this class up on cloudflare:workers exports.
export { DynamicWorkflowBinding };
function loadTenant(env, tenantId) {
return env.LOADER.get(tenantId, async () => ({
compatibilityDate: '2026-01-01',
mainModule: 'index.js',
modules: { 'index.js': await fetchTenantCode(tenantId) },
// The tenant sees this as a normal Workflow binding.
env: { WORKFLOWS: wrapWorkflowBinding({ tenantId }) },
}));
}
// Register this as class_name in wrangler.jsonc.
export const DynamicWorkflow = createDynamicWorkflowEntrypoint<Env>(
async ({ env, metadata }) => {
const stub = loadTenant(env, metadata.tenantId);
return stub.getEntrypoint('TenantWorkflow');
}
);
export default {
fetch(request, env) {
const tenantId = request.headers.get('x-tenant-id');
return loadTenant(env, tenantId).getEntrypoint().fetch(request);
},
};
お使いの wrangler.jsonc に追加します:
"workflows": [
{
"name": "dynamic-workflow",
"binding": "WORKFLOW",
"class_name": "DynamicWorkflow"
}
]
テナントは、プレーンで構文的なWorkflowsコードを記述します。自分たちが派遣されていることに気づいていないのです。
import { WorkflowEntrypoint } from 'cloudflare:workers';
export class TenantWorkflow extends WorkflowEntrypoint {
async run(event, step) {
return step.do('greet', async () => `Hello, ${event.payload.name}!`);
}
}
export default {
async fetch(request, env) {
const instance = await env.WORKFLOWS.create({ params: await request.json() });
return Response.json({ id: await instance.id });
},
};
これで、以上です。テナントは、ごく通常のWorkflowバインディングのように見えるものに対してenv.WORKFLOWS.create(...)を呼び出します。ワークフローID、.status()、.pause()、リトライ、ハイバネーション、耐久性のあるステップ、step.sleep('24時間')、step.waitForEvent()— すべてが上手く機能するのです。
ライブラリが処理するのは1つことだけです。Workflowsエンジンが最終的に起動してrun(event, step)を呼び出すと、最終的に正しいテナントのコード内に行き着くのです。
3つの層があります。上はWorkflowsエンジン(プラットフォーム)、中央はWorker Loader、下はテナントのコード(Dynamic Worker)です。
リクエストがWorker Loaderに届くと、その場で正しい動的コードの実行をルーティングします。残りの実行は、これら3つのレイヤー間の受け渡しで、時間と共に左から右へ、リクエストが入り、エンジンにバウンスし、持続され、後に再びバウンスします。
フローの流れを認証する:
① → ② テナントのコードを入力します。Worker Loaderは、HTTPリクエストを受信し、どのテナントに対するものであるかを判断し、Worker Loaderを介してそのテナントのコードを読み込み、リクエストをそのdefault.fetchに転送します。テナントに手渡されるenvには、WORKFLOWS: wrapWorkflowBinding({ tenantId })が含まれています。テナントに関する限り、それは実際のWorkflowバインディングのように見え、動作します。
③ Worker Loaderまで。テナントがenv.WORKFLOWS.create({ params })を呼び出すと、実際には、Worker LoaderにRemote Procedure Call(RPC)を作成しています。ラップされたバインディングは、ランタイムが読み込み時にテナントのメタデータに特化したWorkerEntrypointサブクラス(DynamicWorkflowBinding)です。そのため、Worker Loaderから{ DynamicWorkflowBinding }をエクスポートする必要があります。ランタイムは、cloudflare:workersのエクスポートでクラスを検索することにより、テナントごとのスタブを構築します。Dynamic Worker境界をまたぐバインディングは、RPCスタブである必要があります。プレーンの{ create, get } オブジェクトは構造化複製されず、未加工のWorkflowバインディングもシリアル化できません。
Worker Loader内部では、ラップされたバインディングがペイロードを透過的に書き換えます。
tenant calls: create({ params: { name: 'Alice' } })
│
▼
engine sees: create({ params: {
__workerLoaderMetadata: { tenantId: 't-42' },
params: { name: 'Alice' }
}})
④ エンジンまで。次にWorker Loaderは、エンベロープを引数として、実際のWORKFLOWSバインディングで.create() を呼び出します。ここから、Workflowsエンジンが引き継ぎます。event.payload(エンベロープを含む)を保持し、実行をスケジュールします。エンジンが後でワークフローを起動するたびに(それが24時間のスリープ後、クラッシュ、またはデプロイの後であろうと)、メタデータはペイロードと共に乗り、実行のルーティングを待ちます。
つまり、メタデータを認証ではなくルーティングヒントとして扱うということです。テナントは、instance.status()を介してそれを読み戻すことができます。秘密情報を入れないでください。
⑤ → ⑥ エンジンが下降します。エンジンがステップを実行する準備ができると、wrangler.jsoncで登録されたクラスの.run(event,step)を呼び出します。これは、createDynamicWorkflowEnpointが提供したものです。そのクラスはエンベロープを展開し、メタデータをloadRunnerコールバックが書いたものに渡し、ラップされていないイベントをコールバックが返すランナーに転送します。
コールバックは興味深いことが起きる場所で、完全にお客様のものになります。R2からテナントの最新ソースを取得します。プラン階層を確認し、地域を選択します。テナントごとのログ記録用にtail Workerを接続します。@cloudflare/worker-bundlerでTypeScriptをオンザフライでバンドルします。一般的なケースでは、Worker Loaderに引き渡すだけです。
const stub = env.LOADER.get(tenantId, () => loadTenantCode(tenantId));
return stub.getEntrypoint('TenantWorkflow');
Worker LoaderはID別にキャッシュするため、多くのステップを何時間も実行するワークフローは、それらの間で同じ動的Workerを再利用します。最終的に分離が追い出されると、次の step.do() は再びコードをプルして、行き続けます。テナントのワークフローは、何が起こったかに気づきません。Dynamic Workerは、数メガバイトのメモリを使用して1桁ミリ秒で起動するため、ディスパッチのオーバーヘッドは本質的に無料です。独自のワークフローコードを持つ100万のテナントを保有でき、それぞれが必要なステップ境界で遅延してスピンアップされ、アイドル状態になっているテナントのいずれもコストはかかりません。
WorkflowEntrypointを自身でサブクラス化する場合 — たとえば、run() の周りにロギングを追加したり、テナントごとの可観測性を確保したり、カスタム状態をスレッド化したりする目的で — ライブラリは、 createDynamicWorkflowEntrypointの基盤となっている、より低レベルのdispatchWorkflowプリミティブを公開しています。
import { dispatchWorkflow } from '@cloudflare/dynamic-workflows';
export class MyDynamicWorkflow extends WorkflowEntrypoint {
async run(event, step) {
return dispatchWorkflow(
{ env: this.env, ctx: this.ctx },
event,
step,
({ metadata, env }) => loadRunnerForTenant(env, metadata),
);
}
}
その他のすべて(ID、一時停止/再開、sendEvent、リトライ)は、そのまま実際のWorkflowsエンジンに渡されます。
ちょっと具体的な説明から考えてみましょう。このライブラリのすべての興味深い行は、アウトバウンド側の.create()のラッパー、またはインバウンド側のWorkflowEntrypointのラッパーのいずれかです。実際の作業(テナントのコードのスピンアップ、サンドボックス化、境界を越えたRPCルーティング、分離のキャッシング、ステップ間のハイバネーション)はすべてDynamic Workersによって行われます。
これが現実のストーリーであり、Workflowsより遥かに大きな規模です
Dynamic Workersは、すべてを組み込むプリミティブです。Durable Object Facetsは、Durable Objectsに適用される同じパターンです。Dynamic Workflowsは、WorkflowEntrypointに適用されるパターンと同じです。それぞれが、これまでの静的バインディングとお客様に手渡できる動的バージョンの間のエンベロープとラップを解除するための接着剤です。
Workflowsにとどまりません。現在Workersで公開されているバインディングはすべて、対応するものです。つまり、各プロデューサーが独自のハンドラー、キャッシュ、データベース、オブジェクトストア、AIバインディング、そしてすべてのテナントが独自のツールを持ち込むMCPサーバーを送信するキューです。現在、Workerに何にバインドしていても、間もなく動的にバインドできるようになります。テナントごと、エージェント、リクエストごとに、アイドル状態のコストはゼロです。
このようなプラットフォームを運営するための単位経済は、率直に言って途方もないものです。かつてはマルチテナント製品を出荷するということは、すべての顧客に独自のコンテナ、独自のデータベース、独自のディスク、独自のスケジューラを提供し、オーケストレーション用接着剤、サービスメッシュ、ヘアプル型請求計算でつなぎ合わせることを意味していました。そうしたアプリケーションの多くは、少なくとも数千人の顧客をサポートしなければなりません。数百万ドルに達することもあります。Dynamic Workersとその上に構成するすべてのものでは、アイドル状態のテナントは約ゼロで、アクティブなテナントは分離レベルのマルチテナンシーを通じて同じハードウェアを共有します。フロアの高さは桁違いに低下します。これまでは、数千人の有料顧客を利用していたプラットフォームが、数千万ドル単位の顧客に合理的にサービスを提供できるようになりました。
コーディングエージェント — OpenCode、Claude Code、Codex、Pi — は、過去1年間で、LLMが時系列なツール呼び出しよりもコードを書く方がはるかに得意であることを証明しました。Cloudflare Agents SDKとProject Thinkは、そのインサイトを耐久性のある実行にまで拡張します。ファイバーやサブエージェントのようなプリミティブを使用することで、エージェントの長期実行プランは、ユーザーが気づくことなく、クラッシュ、ハイバネーション、再デプロイを乗り越えることができます。
Dynamic Workflowsは、そのプランを第一級のCloudflare Workflowにするものです。つまり、エージェントが文字通り記述し、プラットフォームが文字通り実行し、その背後には完全な耐久性メカニズムが備わっています。モデルが分前に書いたrun(event, step)関数は、すべてのstep.do(...)が独立して再試行可能で、すべてのstep.sleep('24時間')が無料でハイバネーションし、すべてのstep.waitForEvent(...)は、人間が次のアクションを承認するのを無期限に待ちます。エージェントはワークフローを書き込み、プラットフォームがそれを実行するプランの内容を事前に知る必要はありません。
ユーザーがロジックを持ち込むSDKとフレームワーク
顧客がrun(event, step)関数を記述するフレームワーク(ワークフロービルダーUI、ビジュアルオートメーションツール、テナントごとの拡張システム、非開発者向けローコードツールなど)を出荷する場合、Dynamic Workflowsは妥協することなく機能させるためのプリミティブとなります。wrapWorkflowBinding({ tenantId })を一度呼び出し、結果をWORKFLOWSとしてコードに渡すと、作成されたすべてのワークフローインスタンスは自動的にタグ付けされ、ルーティングされ、サンドボックスで実行されます。フレームワークはWorker Loaderを所有します。ユーザーがワークフローを所有している一方を気にする必要はありません。
こちらは、当社で最も興奮しているユースケースです。
既存のCI/CDプラットフォームはすべて、その根底において、リポジトリごとの設定ファイルをディスパッチするものです。「これらのステップをこの順序で、これらのシークレットを使って実行し、これらのディレクトリをキャッシュし、これらのアーティファクトをアップロードする。」各リポジトリには独自のパイプラインがあります。各ブランチは独自のバリアントを持つことがあります。各プルリクエストにより、完了まで実行されなければならないパイプラインのインスタンスが生成され、マシンがクラッシュしたり、不安定なステップを切り抜けたり、ログをストリーミングしたり、承認のために一時停止したり、結果を保持する必要があります。
それこそが、耐久性のあるワークフローのまさにその形です。これまで、CIがそのような方法で構築されなかった理由は、ワークフロー自体がリポジトリごとに異なり、ランタイム時にディスパッチされ、プロビジョニングコストがゼロであるようなクラウドプリミティブが存在しなかったためです。これで完了です。
これは、CIパイプラインが、お客様のレポジトリで出荷したコードだけの場合の例です。例えば、.cloudflare/ci.ts の場合です。ワークフロー自体は現実のものであり、以下のrunInSandbox() / summarise() / GitHubバインディングヘルパーはプラットフォームが提供する接着剤で、ディスパッチャで一度だけ出荷するようなものです。
import { WorkflowEntrypoint } from 'cloudflare:workers';
export class CIPipeline extends WorkflowEntrypoint {
async run(event, step) {
const { repo, sha, branch, pr } = event.payload;
// Fork an isolated copy of the repo at this commit. Seconds, not minutes.
const workspace = await step.do('checkout', () =>
this.env.ARTIFACTS.fork(repo, { sha })
);
await step.do('install', () => runInSandbox(workspace, ['pnpm', 'install']));
// Each parallel step is independently retryable.
const [lint, test, build] = await Promise.all([
step.do('lint', () => runInSandbox(workspace, ['pnpm', 'lint'])),
step.do('test', () => runInSandbox(workspace, ['pnpm', 'test'])),
step.do('build', () => runInSandbox(workspace, ['pnpm', 'build'])),
]);
if (pr) {
await step.do('comment', () =>
this.env.GITHUB.commentOnPR(repo, pr, summarise({ lint, test, build }))
);
}
// Workflow hibernates until approval arrives. No VM held open.
if (branch === 'main') {
await step.waitForEvent('approval', { type: 'deploy-approval', timeout: '24 hours' });
await step.do('deploy', () => runInSandbox(workspace, ['pnpm', 'deploy']));
}
}
}
プラットフォームがディストリビューターを所有する。Webフックを取り込み、どのリポジトリから来たかを特定し、そのリポジトリのCIPipelineクラスをDynamic Workerとして読み込み、Dynamic Workflowsに実行を引き渡します。プラットフォームは、パイプラインに何があるのかを把握しません。しかし、その必要はありません。これは、お客様のリポジトリに存在する耐久性のある関数を実行するものです。
それでは、各ステップの実際の内容を整理しましょう。
Artifactsは、Cloudflareのグローバルに分散されたネットワーク上に存在する、Gitネイティブのバージョン管理されたファイルシステムをすべてのリポジトリに提供します。ArtifactFSはツリーを遅延なくハイライトするため、マルチGBのリポジトリでも1桁の秒以内に動作可能です。また、fork()は、gitクローンを課すことなく、各CIを独自に分離したコピーを提供します。
Dynamic Workersは、リポジトリのデータと同じマシン上で、ミリ秒で起動するサンドボックス化された分離で各軽量なステップ(lint、フォーマット、タイプチェック、バンドル)を実行します。VMのプロビジョニング、イメージのプル、コールドスタートは不要です。
動的ワークフローが全体を一体化させます。ステップは再試行可能で耐久性があります。実行は承認を待ちながら、無料でハイバネーションします。デプロイ、立ち退き、クラッシュがあっても状態と進捗はそのままです。
サンドボックスは、Dockerビルドが必要なステップ、Postgresの実行が必要な統合スイート、8コアが必要なRustのコンパイルといった、厄介な部分に対応します。R2へのスナップショットは、数秒のウォームスタートでもあります。
中規模のJSリポジトリにおける従来のCI実行は、次のようなものになります: VM割り当て(15-30秒) → ベースイメージのプル(10秒) → git clone(10秒) → npm ci(30-60秒) → テスト実行(実際の作業) → 環境の破棄。最初のテストが実行される数分間のセレモニーで、仮想マシン全体の料金を支払います。
このスタックの同じパイプラインは、次のようになります: リポジトリのエッジフォーク(秒) → 各ステップが新しいアイソレートまたはスナップショット復元されたサンドボックスをミリ秒で起動 → 実際の作業を実行 → ハイバネートします。コールドスタートは必要ありません。何かを事前にプロビジョニングする必要はありません。何も可視化する必要はありません。リポジトリは移動せず、コンピューティングが来ます。
CIはこれまでに高速化することはなく、また高速化されなかった理由は、これらのプリミティブが同じ場所に存在していないためです。実現可能になりました。
@cloudflare/dynamic-workflows はMITライセンスで、本日よりnpmで利用可能です。
npm install @cloudflare/dynamic-workflows
Workers有料プランのオープンベータ版であるDynamic Workers上で動作します。このリポジトリには、動作例が含まれています。インタラクティブなブラウザのプレイグラウンドで、TenantWorkflowクラスを記述し、Runをクリックすると、ライブストリーミングログと、各step.do()が実行されるにつれて点灯するステップごとのチェックリストで、ステップの実行を確認できます。コミットします。クローン、デプロイし、同僚に見せる。
プラットフォーム、SDK、フレームワーク、またはCI/CD製品であり、自社のプロセスでコードを実行することなく、顧客に独自のワークフローを提供したい場合、これはあなたのために構築されたプリミティブです。耐久性のあるプランを記述するエージェントを構築する場合、これはプランを実際のWorkflowsにするプリミティブです。もしこのすべてを見ているだけで、その上に構築するのも楽しいように思えるなら、あなたが作るものをぜひご覧ください。
Cloudflare Developers Discordで私たちを見つけてください。