Abstract Syntax Trees (ASTs) を使用してWorkflowsコードを視覚的な図表に変換する方法
2026-03-27 André Venceslau Mia Malden 6分で読める
Cloudflare Workflowsは、ステップの連鎖、失敗時の再試行、長時間実行されるプロセス全体での状態の永続化を可能にする耐久実行エンジンです。開発者はWorkflowsを使用して、バックグラウンドエージェントの実行、データパイプラインの管理、人間参加型の承認システムの構築などを行います。
先月、Cloudflareにデプロイされたすべてのワークフローが、ダッシュボードで完全な視覚的図表を持つようになったことを発表しました。アプリケーションを視覚化できることが、これまで以上に重要になっているため、この機能を構築しました。コーディングエージェントが、あなたが読むかもしれないし読まないかもしれないコードを書いています。しかし、構築されるものの形状は依然として重要です:ステップがどのように接続されるか、どこで分岐するか、実際に何が起こっているかです。
視覚的ワークフロービルダーの図表を見たことがある場合、それらは通常、宣言的なもの(JSON設定、YAML、ドラッグアンドドロップ)から作業しています。しかし、Cloudflare Workflowsは単なるコードです。Promise、Promise.all、ループ、条件分岐を含むことができ、関数やクラス内にネストすることもできます。この動的実行モデルにより、図表のレンダリングは少し複雑になります。
私たちはAbstract Syntax Trees (ASTs)を使用してグラフを静的に導出し、Promiseとawaitの関係を追跡して、何が並列で実行され、何がブロックされ、どのように部品が接続されるかを理解します。
これらの図表をどのように構築したかを学ぶために読み続けるか、最初のワークフローをデプロイして図表を自分で確認してください。
Cloudflare Workflowsコードから生成された図表の例は次のとおりです:
動的ワークフロー実行
一般的に、ワークフローエンジンは動的または順次(静的)実行順序のいずれかに従って実行できます。順次実行はより直感的な解決策のように思えるかもしれません:ワークフロートリガー → ステップA → ステップB → ステップC、ここでステップBはエンジンがステップAを完了した直後に実行を開始し、以下同様です。
Cloudflare Workflowsは動的実行モデルに従います。ワークフローは単なるコードであるため、ランタイムがそれらに遭遇するとステップが実行されます。ランタイムがステップを発見すると、そのステップはワークフローエンジンに渡され、エンジンがその実行を管理します。ステップは、awaitされない限り本質的に順次ではありません — エンジンはすべてのawaitされていないステップを並列で実行します。この方法で、追加のラッパーやディレクティブなしに、ワークフローコードをフロー制御として書くことができます。
ハンドオフの仕組みは次のとおりです:
- そのインスタンスの「スーパーバイザー」Durable Objectであるエンジンが起動します。エンジンは実際のワークフロー実行のロジックを担当します。
- エンジンは動的ディスパッチを介してユーザーワーカーをトリガーし、Workersランタイムに制御を渡します。
- ランタイムが
step.doに遭遇すると、実行をエンジンに戻します。
- エンジンはステップを実行し、結果を永続化し(該当する場合はエラーをスローし)、ユーザーWorkerを再度トリガーします。
このアーキテクチャでは、エンジンは実行しているステップの順序を本質的に「知らない」のですが、図表にとってはステップの順序が重要な情報になります。ここでの課題は、大部分のワークフローを診断的に有用なグラフに正確に変換することにあります;図表がベータ版の間、これらの表現を継続的に反復し改善していきます。
コードの解析
実行時ではなくデプロイ時にスクリプトを取得することで、ワークフロー全体を解析して図表を静的に生成できます。
一歩下がって、ワークフローデプロイメントのライフサイクルは次のとおりです:
図表を作成するために、Workers をデプロイする内部設定サービスによってバンドルされた後(ワークフローデプロイメントのステップ2)にスクリプトを取得します。次に、パーサーを使用してワークフローを表すabstract syntax tree (AST)を作成し、内部サービスがすべてのWorkflowEntrypointsとワークフローステップへの呼び出しを含む中間グラフを生成し、トラバースします。APIで最終結果に基づいて図表をレンダリングします。
Workerがデプロイされると、設定サービスは(デフォルトでesbuildを使用して)コードをバンドルし、特に指定されない限りミニファイします。これは別の課題を提示します — TypeScriptのWorkflowsは直感的なパターンに従いますが、ミニファイされたJavaScript (JS)は密度が高く、理解しにくい場合があります。また、バンドラーに応じて、コードをミニファイする方法も異なります。
エージェントが並列で実行されるWorkflowコードの例は次のとおりです:
const summaryPromise = step.do(
`summary agent (loop ${loop})`,
async () => {
return runAgentPrompt(
this.env,
SUMMARY_SYSTEM,
buildReviewPrompt(
'Summarize this text in 5 bullet points.',
draft,
input.context
)
);
}
);
const correctnessPromise = step.do(
`correctness agent (loop ${loop})`,
async () => {
return runAgentPrompt(
this.env,
CORRECTNESS_SYSTEM,
buildReviewPrompt(
'List correctness issues and suggested fixes.',
draft,
input.context
)
);
}
);
const clarityPromise = step.do(
`clarity agent (loop ${loop})`,
async () => {
return runAgentPrompt(
this.env,
CLARITY_SYSTEM,
buildReviewPrompt(
'List clarity issues and suggested fixes.',
draft,
input.context
)
);
}
);
rspackでバンドルすると、ミニファイされたコードのスニペットは次のようになります:
class pe extends e{async run(e,t){de("workflow.run.start",{instanceId:e.instanceId});const r=await t.do("validate payload",async()=>{if(!e.payload.r2Key)throw new Error("r2Key is required");if(!e.payload.telegramChatId)throw new Error("telegramChatId is required");return{r2Key:e.payload.r2Key,telegramChatId:e.payload.telegramChatId,context:e.payload.context?.trim()}})...
または、viteでバンドルすると、ミニファイされたスニペットは次のようになります:
class ht extends pe { async run(e, r) { b("workflow.run.start", { instanceId: e.instanceId }); const s = await r.do("validate payload", async () => { if (!e.payload.r2Key) throw new Error("r2Key is required"); if (!e.payload.telegramChatId) throw new Error("telegramChatId is required"); return { r2Key: e.payload.r2Key, telegramChatId: e.payload.telegramChatId, context: e.payload.context?.trim() }; })...
ミニファイされたコードはかなり複雑になる可能性があり、バンドラーによって、さまざまな方向で複雑になる可能性があります。さまざまな形式のミニファイされたコードを迅速かつ正確に解析する方法が必要でした。
JavaScript Oxidation Compiler (OXC)のoxc-parserがこの仕事に最適だと判断しました。最初にRustを実行するコンテナを使用してこのアイデアをテストしました。すべてのスクリプトIDがCloudflare Queueに送信され、その後メッセージがポップされ、処理のためにコンテナに送信されました。このアプローチが機能することを確認した後、Rustで書かれたWorkerに移行しました。
WorkersはWebAssemblyを介してRustの実行をサポートしており、パッケージは十分小さく、これを簡単にしました。Rust Workerは、最初にミニファイされたJSをASTノードタイプに変換し、次にASTノードタイプをダッシュボードでレンダリングされるワークフローのグラフィカルバージョンに変換する責任があります。
これを行うために、各ワークフローに対して事前定義されたノードタイプのグラフを生成し、一連のノードマッピングを通じてグラフ表現に変換します。
図表のレンダリング
ワークフローの図表バージョンをレンダリングするには、2つの課題がありました:ステップと関数の関係を正しく追跡する方法と、すべての表面積をカバーしながらワークフローノードタイプを可能な限りシンプルに定義する方法です。
ステップと関数の関係が正しく追跡されることを保証するために、関数名とステップ名の両方を収集する必要がありました。前述したように、エンジンはステップに関する情報のみを持っていますが、ステップは関数に依存する場合があり、その逆もあります。たとえば、開発者はステップを関数でラップしたり、関数をステップとして定義したりする場合があります。また、異なるモジュールから来るステップを関数内で呼び出したり、ステップの名前を変更したりすることもできます。
ライブラリはASTを提供することで最初のハードルをクリアしますが、それをどのように解析するかを決定する必要があります。一部のコードパターンには追加の創造性が必要です。たとえば、関数 — WorkflowEntrypoint内には、ステップを直接、間接的に、またはまったく呼び出さない関数が存在する場合があります。
functionAを考えてみてください。これにはconsole.log(await functionB(), await functionC())が含まれており、functionBはstep.do()を呼び出します。その場合、functionAとfunctionBの両方がワークフロー図表に含まれるべきですが、functionCは含まれるべきではありません。
直接および間接のステップ呼び出しを含むすべての関数をキャッチするために、各関数のサブグラフを作成し、それ自体がステップ呼び出しを含むか、または含む可能性のある別の関数を呼び出すかどうかをチェックします。これらのサブグラフは、すべての関連ノードを含む関数ノードによって表されます。関数ノードがグラフのリーフである場合、つまり、その中に直接または間接のワークフローステップがない場合、最終出力からトリミングされます。
ワークフロー図表を推測できる静的ステップのリストや、最大10の異なる方法で定義された変数など、他のパターンもチェックします。スクリプトに複数のワークフローが含まれている場合、関数用に作成されたサブグラフと同様のパターンに従い、1レベル高く抽象化されます。
すべてのASTノードタイプについて、ワークフロー内で使用される可能性のあるすべての方法を考慮する必要がありました:ループ、分岐、promise、並列、await、アロー関数…リストは続きます。これらのパス内でも、数十の可能性があります。
ループの可能な方法のいくつかを考えてみてください:
for (const item of items) {
await step.do(`process ${item}`, async () => item);
}
while (shouldContinue) {
await step.do('poll', async () => getStatus());
}
await Promise.all(
items.map((item) =>
step.do(`map ${item}`, async () => item)
),
);
await items.forEach(async (item) => {
await step.do(`each ${item}`, async () => item);
});
そして、ループを超えて、分岐の処理方法:
switch (action.type) {
case 'create':
await step.do('handle create', async () => {});
break;
default:
await step.do('handle unknown', async () => {});
break;
}
if (status === 'pending') {
await step.do('pending p