跳到正文
Cloudflare Blog· Dan Lapid·· 2026-05-01AI 评分64

Introducing Dynamic Workflows:可随租户流转的持久执行功能

Introducing Dynamic Workflows: durable execution that follows the tenant

AI 导读

Dynamic Workflows 是一个函数库,支持将持久执行实时路由至租户提供的代码。该库基于 Dynamic Workers 构建,使平台能够以近乎零闲置成本为数百万个独特工作流提供服务。它允许平台根据租户需求动态分配执行资源,实现了工作流与特定租户代码的即时绑定,大幅降低了多租户场景下的运营开销和资源浪费。

正文 · AI 翻译

八年前我们首次推出 Workers 时,它是一个直接面向开发者的平台。多年来,我们不断扩展和壮大这个生态系统,使得平台不仅可以直接在 Workers 上构建,还能让它们的客户通过众多多租户应用将代码交付给我们。如今我们在 Workers 上看到:用户描述需求、AI 编写实现代码的应用;每个客户的业务逻辑在运行时表现为平台从未见过的 TypeScript 代码的多租户 SaaS;编写并运行自身工具的智能体;每个仓库定义自身管道的 CI/CD 产品。

上个月,当我们推出 Dynamic Workers 公开测试版时,我们为这些平台提供了计算端一个简洁的基元:在运行时将一些代码交给 Workers 运行时,在个位数毫秒内,在同一台机器上获得一个隔离的、沙盒化的 Worker。Durable Object Facets 将同样的理念扩展到存储——每个动态加载的应用都可以拥有自己的 SQLite 数据库,按需启动,平台作为监管者位于前端。Artifacts 对源代码管理做了同样的事:一个原生 Git、带版本控制的文件系统,可以创建数千万个,每个智能体一个、每个会话一个、每个租户一个。至此,我们有了存储和源代码管理的动态部署。下一步是什么?

今天,我们通过 Dynamic Workflows 将持久化执行与动态部署连接起来。

持久化执行与动态执行之间的差距

Cloudflare Workflows 是我们的持久化执行引擎。它将一个 run(event, step) 函数转化为一个程序,其中每一步都能在故障中幸存,可以休眠数小时或数天,可以等待外部事件,并在隔离区被回收时从上次中断的地方精确恢复。它是任何需要“持续运行”超越单个请求的场景的正确基元:入职流程、视频转码管道、多阶段计费、长时间运行的智能体循环,以及——自 Workflows V2 起——每个账户最多 50,000 个并发实例和每秒 300 个新实例,为智能体时代重新设计。

但 Workflows 一直内置了一个假设:工作流代码是你部署的一部分。你的 `wrangler.jsonc` 中有一个代码块写着"当引擎调用 WORKFLOWS 时,运行名为 MyWorkflow 的类"。每次部署,一个绑定对应一个类。

如果你拥有全部代码,这没问题。如果你运行的是传统应用,这也没问题。

但当你希望让客户部署他们自己的工作流时,这套机制就行不通了。

假设你正在构建一个应用平台,AI 为每个租户编写 TypeScript 代码。假设你正在运行一个 CI/CD 产品,每个仓库都有自己的流水线。假设你正在使用一个智能体 SDK,每个智能体都编写自己持久化的执行计划。在上述每一种场景中,每个租户、每个智能体、每个请求的工作流都各不相同。不存在一个单一的类可供绑定。

这与 Dynamic Workers 解决计算问题、Durable Object Facets 解决存储问题时所面临的挑战是同一类问题。我们只是尚未为持久化执行解决这个问题。

动态工作流

`@cloudflare/dynamic-workflows` 是一个小型库,大约 300 行 TypeScript 代码。它允许单个 Worker(即 Worker 加载器)将每次 `create()` 调用路由到不同租户的代码,并且关键的是,当工作流实际执行时(无论是几秒、几小时还是几天后),Workflows 引擎能够将 `run(event, step)` 调度回同一段代码。

整个模式如下。一个 Worker 加载器:

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 hours')`、`step.waitForEvent()`——一切功能都一如既往地正常工作。

这个库只处理一件事:确保当 Workflows 引擎最终唤醒并调用 `run(event, step)` 时,它最终会进入正确的租户代码中。

工作原理

分为三层:最上层是 Workflows 引擎(平台),中间是你的 Worker 加载器,最下层是你的租户代码(一个 Dynamic Worker)。

当请求到达 Worker Loader 时,它会即时将执行路由到正确的动态代码。后续的执行过程是这三个层级之间的交接,按时间从左到右进行:请求进入,向上弹至引擎,被持久化,之后再向下弹回。

流程如下:

① → ② 进入租户代码。Worker Loader 收到一个 HTTP 请求,判断该请求属于哪个租户,通过 Worker Loader 加载该租户的代码,并将请求转发至其 `default.fetch`。它传递给租户的 `env` 中包含 `WORKFLOWS: wrapWorkflowBinding({ tenantId })`。对租户而言,这看起来和用起来就像一个真正的 Workflow 绑定。

③ 向上到 Worker Loader。当租户调用 `env.WORKFLOWS.create({ params })` 时,实际上是在向 Worker Loader 发起一个远程过程调用(RPC)——这个被包装的绑定是一个 `WorkerEntrypoint` 子类(`DynamicWorkflowBinding`),运行时在加载时已用租户的元数据对其进行了特化。这就是为什么你必须从你的 Worker Loader 中导出 `{ DynamicWorkflowBinding }`:运行时通过在 `cloudflare:workers` 导出中查找该类来构建每个租户的存根。跨越动态 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` 中的类(即 `createDynamicWorkflowEntrypoint` 返回给你的那个类)上调用 `.run(event, step)`。该类会解包封装,将元数据交给你编写的 `loadRunner` 回调函数,并将解包后的事件转发给该回调函数返回的任何运行器。

这个回调函数是所有有趣事情发生的地方,而且完全由你掌控。从 R2 获取租户的最新源代码。检查他们的套餐层级并选择一个区域。附加一个用于按租户记录日志的尾部 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()` 会再次拉取代码并继续运行——租户的工作流对此毫无感知。一个动态 Worker 在个位数毫秒内启动,仅使用几兆字节的内存,因此调度开销几乎可以忽略不计。你可以拥有百万级租户,每个租户都有自己独特的工作流代码,每个租户都在需要时按步骤边界懒加载启动,并且在空闲时不产生任何成本。

逃生舱口

如果你想自己继承 `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 引擎处理。

动态 Worker 是原语

暂且退一步,从具体细节中抽离出来。这个库中每一行有趣的代码,要么是对出站侧 `.create()` 的封装,要么是对入站侧 `WorkflowEntrypoint` 的封装。实际的工作——启动租户的代码、对其进行沙箱隔离、跨边界路由 RPC、缓存隔离区、在步骤之间休眠——全部由底层的动态 Worker 完成。

这才是真正的故事,而且它的意义远不止于 Workflows 本身。

Dynamic Workers 是一种能够吞纳一切的底层原语。Durable Object Facets 是将同一模式应用于 Durable Objects 的产物。Dynamic Workflows 则是将该模式应用于 WorkflowEntrypoint 的结果。每一个都是在你一直使用的静态绑定与你现在可以交给客户的动态版本之间,那一小层“封装与解包”的粘合剂。

我们的脚步不会止于 Workflows。Workers 当前暴露的每一种绑定都将迎来其动态对应版本——队列(每个生产者自带处理器)、缓存、数据库、对象存储、AI 绑定,以及 MCP 服务器(每个租户自带工具)。无论你今天将什么绑定到 Worker,很快你都能动态地进行绑定:按租户、按智能体、按请求进行分发,且空闲成本为零。

坦率地说,运行这样一个平台的单位经济模型是荒谬的。过去,交付一个多租户产品意味着要给每个客户分配他们自己的容器、自己的数据库、自己的磁盘、自己的调度器,然后用编排胶水、服务网格和令人抓狂的计费算法把它们拼凑在一起。许多这类应用至少需要支持数千个客户,最多则可达数百万。而在 Dynamic Workers 及其之上构建的一切之上,空闲租户的成本几乎为零,活跃租户则通过隔离级别的多租户技术共享同一硬件。成本底线骤降数个数量级。一个过去只能容纳数千付费客户的平台,现在可以合理地服务数千万用户。

这将解锁什么

像工程师一样规划的智能体平台

编程智能体——OpenCode、Claude Code、Codex、Pi——在过去一年中已经证明,大语言模型在编写代码方面远比在按顺序进行工具调用方面出色。Cloudflare Agents SDK 和 Project Think 将这一洞察延伸到了持久化执行领域:借助 fiber 和子智能体等原语,智能体的长期运行计划可以在崩溃、休眠和重新部署后依然存活,而用户对此毫无察觉。

动态工作流(Dynamic Workflows)正是让计划成为一等公民的 Cloudflare Workflow 的组件——智能体实际编写它,平台实际运行它,背后有完整的持久化机制支撑。模型一分钟前编写的 `run(event, step)` 函数,其中每个 `step.do(...)` 都可独立重试,每个 `step.sleep('24 hours')` 免费休眠,每个 `step.waitForEvent(...)` 无限期等待人类批准下一步操作。智能体编写工作流;平台运行工作流;两者都无需事先知道计划是什么。

用户自带逻辑的 SDK 与框架

如果你正在交付一个框架,让客户编写 `run(event, step)` 函数——无论是工作流构建器 UI、可视化自动化工具、按租户划分的扩展系统,还是面向非开发者的低代码工具——那么动态工作流现在就是那个能让你毫无妥协地实现功能的底层原语。你只需调用一次 `wrapWorkflowBinding({ tenantId })`,将结果以 `WORKFLOWS` 的形式交给他们的代码,他们创建的每个工作流实例都会自动打上标签、路由回来,并在其沙箱中执行。框架拥有 Worker Loader;用户拥有工作流;两者互不关心。

原语速度下的 CI/CD

以下这个用例最让我们兴奋。

所有现有的 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']));
    }
  }
}

平台拥有调度器。它接收一个 webhook,判断其来自哪个仓库,将该仓库的 `CIPipeline` 类作为动态 Worker 加载,并将运行任务交给动态工作流。平台不知道流水线里有什么,也不需要知道。它运行的只是一个恰好位于客户仓库中的持久化函数。

现在来看看每个步骤实际做了什么:

  • Artifacts 为每个仓库提供一个基于 Git、带版本控制的文件系统,运行在 Cloudflare 的全球分布式网络上。ArtifactFS 惰性加载文件树,因此即使是数 GB 的仓库也能在个位数秒内准备就绪——而 `fork()` 为每次 CI 运行提供独立的隔离副本,无需承担 git clone 的开销。

  • 动态 Worker 在沙箱隔离环境中运行每个轻量级步骤(lint、格式化、类型检查、打包),启动时间仅需毫秒级,且与仓库数据位于同一台机器上。无需配置虚拟机、拉取镜像或冷启动。

  • 动态工作流将整个运行过程串联起来。步骤支持重试且具有持久性。运行在等待审批期间可免费休眠。状态和进度在部署、驱逐和崩溃后依然保留。

  • 沙箱处理那些繁重的场景——需要 docker build 的步骤、需要运行 Postgres 的集成测试套件、需要 8 核的 Rust 编译。通过快照到 R2,即使是这些场景也能在几秒内热启动。

对于一个中等规模的 JS 仓库,传统的 CI 运行大致如下:分配虚拟机(15-30 秒)→ 拉取基础镜像(10 秒)→ git clone(10 秒)→ npm ci(30-60 秒)→ 运行测试(实际工作)→ 销毁。在第一个测试运行之前,需要花费几分钟的准备工作,而整个过程你都在为整个虚拟机付费。

同一流水线在此技术栈上的运行方式如下:仓库的边缘分支(秒级)→ 每个步骤在毫秒内启动一个全新的隔离沙箱或基于快照恢复的沙箱 → 执行实际任务 → 进入休眠。无需冷启动,无需提前预置任何资源,无需保持任何服务常驻。仓库本身不动——计算资源主动向其靠拢。

CI 从未如此之快,而过去之所以做不到,是因为这些基础组件从未在同一平台中同时存在。现在,它们做到了。

立即体验

@cloudflare/dynamic-workflows 采用 MIT 许可证,现已发布至 npm:

npm install @cloudflare/dynamic-workflows

它运行在 Dynamic Workers 之上,后者已在 Workers 付费计划中开放公测。仓库中包含一个可运行的示例——一个交互式浏览器沙盒,你可以在其中编写 TenantWorkflow 类,点击运行,即可观察各步骤的执行过程,并实时查看流式日志以及随每个 step.do() 提交而逐项亮起的步骤清单。克隆它、部署它、展示给同事看。

如果你是一个平台、SDK、框架或 CI/CD 产品,希望让客户拥有自己的工作流,而无需在自己的进程中运行他们的代码:这就是我们为你构建的基础组件。如果你正在构建能够制定持久计划的 AI 智能体,这就是能让那些计划变成真正 Workflows 的基础组件。如果你只是在一旁观望,觉得在此基础上构建很有趣:我们很期待看到你的作品。

欢迎在 Cloudflare 开发者 Discord 中找到我们。我们整周都在那里。

来源:Cloudflare Blog · blog.cloudflare.com