说明:本文由 Codex 根据作者提供的主题、对话素材与 Cloudflare 官方文档辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。
Cloudflare 这里有三个很容易混在一起的概念:Workflows、Dynamic Workers 与 Dynamic Workflows。它们并不是三个同级、可互相替换的产品。
- Workflows:把部署时已经确定的业务代码,作为可持久化、可重试、可等待的多步骤流程运行。
- Dynamic Workers:由一个已部署的 Worker 在运行时加载另一段代码,并在受控沙箱中执行;它本身不提供跨调用的工作流持久化。
- Dynamic Workflows:用
@cloudflare/dynamic-workflows把两者组合起来,让“运行时加载的、按租户或按任务变化的代码”也获得 Workflows 的持久步骤、等待、重试和恢复能力。
因此,面对“订单、审批、ETL 该怎么可靠地跑”的问题,通常从 Workflows 开始;面对“用户或 AI 生成了不同的自动化代码,且每段代码都要可靠地跑几天”的问题,才需要 Dynamic Workflows。本文按 2026-08-07 的官方文档整理,价格、限额和预览功能会变化,应以文末链接为准。
一张表先分清边界#
| 能力 | Workflows | Dynamic Workers | Dynamic Workflows |
|---|---|---|---|
| 代码何时确定 | 部署 Worker 时 | 请求/任务运行时 | 运行时 |
| 主要目标 | 可靠编排多步骤业务过程 | 安全执行动态、AI 生成或用户上传的代码 | 可靠执行动态代码定义的多步骤过程 |
| 跨 sleep / 故障后的状态 | 内建持久化 | 不保证,不能依赖同一 isolate | 由 Workflows 持久化 |
| 典型入口 | WorkflowEntrypoint.run() | env.LOADER.load() / get() | Dynamic Worker 中的 env.WORKFLOWS.create() |
| 典型使用者 | 应用后端、数据管道、AI 流程 | 沙箱、代码模式、预览、可编程平台 | 多租户自动化平台、可恢复的 AI Agent |
| 首选风险控制 | 幂等、细粒度步骤、重试与补偿 | 最小权限 binding、网络出口控制、资源上限 | 同时具备左两列控制,并增加代码版本与租户路由控制 |
还要避免另一个名称相近的误解:Dynamic Workers / Dynamic Workflows 不是 Workers for Platforms 的 Dynamic Dispatch。后者面向已部署 Worker 的多租户路由;前者是运行时加载代码。官方限制页也明确指出,Workflows 不能部署在 Workers for Platforms namespace 中。
1. Cloudflare Workflows:持久化执行,而不是“后台开个异步函数”#
Workflows 是 Workers 平台上的 Durable Execution(持久化执行) 模型。一个 Workflow 实例由多个步骤组成:成功步骤的结果由平台保存;后续步骤失败、Worker 重启或流程休眠后,平台会从已成功的步骤继续,而不是把整个流程从头再跑。
这特别适合以下过程:上传文件后进行 AI 处理并等待人工审批、定时发送试用到期提醒、批量同步第三方 API、等待支付 webhook、或者具有补偿动作的 Saga 流程。官方将其用于 AI 应用、数据管道、用户生命周期与 human-in-the-loop 审批等场景。Workflows 概览
运行时模型#
HTTP / Queue / Cron / CLI / REST API
│
▼
Workflow binding.create(params)
│
▼
独立 Workflow instance(ID、输入、状态、步骤历史)
│
▼
WorkflowEntrypoint.run(event, step)
├─ step.do() 可持久化、可重试的业务步骤
├─ step.sleep() 定时暂停
├─ step.sleepUntil() 等到绝对时间
└─ step.waitForEvent() 等 webhook / 人工输入几个核心对象如下:
WorkflowEntrypoint:代码中的流程类,继承它并实现run(event, step)。- Workflow instance:一次独立运行;有唯一 ID、输入事件、状态、步骤结果与保留期。
WorkflowEvent:包含只读payload、创建时间、实例 ID、Workflow 名;若由 Workflow 自身的 cron schedule 创建,还会带schedule元数据。WorkflowStep:持久化边界。step.do()成功后的可序列化返回值会被保存并在恢复时复用;run()的最终返回值可从 Dashboard、Workers API 或 REST API 读取。Workers API
“持久化”不等于分布式事务,也不自动提供 exactly-once 外部副作用。一个步骤可能因网络中断而重试;如果第三方已成功扣款、但响应在传回前丢失,下一次尝试仍可能再发起调用。因此幂等键、先查后写和补偿逻辑仍是业务代码的职责。
2. Workflows 的核心 API 与最小实现#
定义与配置#
下面是一个“订单等待付款确认”的最小骨架。示例刻意把数据库读取、外部调用和等待拆成不同步骤:这样每一步拥有独立的持久化、超时与重试边界。
import {
WorkflowEntrypoint,
type WorkflowEvent,
type WorkflowStep,
} from "cloudflare:workers";
type OrderParams = { orderId: string };
export class OrderWorkflow extends WorkflowEntrypoint<Env, OrderParams> {
async run(event: WorkflowEvent<OrderParams>, step: WorkflowStep) {
const order = await step.do("load order", async () => {
// 返回值必须可序列化;大对象建议写入 R2/D1 后返回引用。
return loadOrder(event.payload.orderId);
});
await step.sleep("payment grace period", "1 hour");
const approval = await step.waitForEvent<{ paid: boolean }>(
"wait for payment webhook",
{ type: "payment-confirmed", timeout: "24 hours" },
);
if (!approval.payload?.paid) {
return { orderId: order.id, status: "unpaid" };
}
return await step.do(
"fulfill order",
{
retries: { limit: 5, delay: "10 seconds", backoff: "exponential" },
timeout: "10 minutes",
},
async () => fulfillOrder(order),
);
}
}上例之所以会等待,是代码显式调用了 step.waitForEvent();它不是 Workflows 默认要求前端逐步点击“下一步”。常规流程由 run() 中已部署的 await step.do()、条件、循环和 try...catch 自动推进,step.sleep() / step.sleepUntil() 到时也会自动恢复。只有付款 webhook、人工审批或用户决定这类业务关卡,才由前端或外部系统调用受认证的后端 API,再由后端以 instance.sendEvent({ type, payload }) 放行。pause() / resume() 用于运维式暂停和恢复,不能传递“同意/拒绝”等业务数据;详见 Workflows:实例、步骤、事件与恢复 API 实战。
在 wrangler.jsonc 中声明 Workflow binding:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "orders",
"main": "src/index.ts",
"compatibility_date": "2026-08-07",
"observability": { "enabled": true },
"workflows": [
{
"name": "order-workflow",
"binding": "ORDER_WORKFLOW",
"class_name": "OrderWorkflow"
}
]
}class_name 必须匹配导出的类名,binding 则是普通 Worker 中通过 env.ORDER_WORKFLOW 访问的变量名。若希望直接按计划触发,可以在同一项上添加 schedules: ["0 * * * *"];每次匹配 cron 时,平台会创建实例,无需另写顶层 scheduled handler。构建第一个 Workflow 触发 Workflows
步骤、等待、事件和补偿#
| API | 作用 | 实践要点 |
|---|---|---|
step.do(name, [config], callback) | 执行业务步骤并持久化返回结果 | 可定义 retries、退避方式和每次尝试的 timeout;适合一次外部 API 调用、一次数据库查询或一次存储读写。 |
step.sleep(name, duration) | 按相对时间暂停 | 支持毫秒或 "1 hour"、"7 days" 一类时长。 |
step.sleepUntil(name, date) | 暂停到绝对时间 | 适合“下周一 09:00 UTC 再执行”。 |
step.waitForEvent(name, { type, timeout }) | 等待 webhook、人工批准或外部系统回调 | type 要与发送方一致;超时会抛错,允许超时时继续时要 try/catch。 |
instance.sendEvent({ type, payload }) | 向指定实例投递事件 | 事件即使在流程抵达等待点之前送达,也会被缓冲到匹配的等待步骤。 |
| rollback handler | 为 step.do() 注册反向补偿操作 | 适合库存预留、扣款、发票等 Saga;它不是数据库全局事务。 |
step.do() 当前默认配置为 retries.limit: 5(总尝试次数)、10 秒初始延迟、指数退避和 10 分钟单次尝试超时;策略可改为 constant、linear 或 exponential。对验证失败、无效凭证这类不应重试的错误,可使用 NonRetryableError 结束重试。睡眠与重试 事件与参数
外部 webhook 的接收端一般只负责认证、定位 instance 并投递事件:
const instance = await env.ORDER_WORKFLOW.get(instanceId);
await instance.sendEvent({
type: "payment-confirmed",
payload: { paid: true },
});也可以从 HTTP handler、Queue consumer、Cron handler、Durable Object、Wrangler CLI、Dashboard 或 REST API 创建实例。父 Workflow 可以创建子 Workflow,但子流程创建成功后会独立运行,父流程不会自动等待它结束。触发 Workflows
实例生命周期#
通过 binding 的 create() 创建实例,通过 get(id) 获取实例,再调用 status()、pause()、resume()、terminate() 或 restart() 管理它。常见状态包括 queued、running、waiting、paused、complete、errored 与 terminated。如果终止时希望触发已注册的补偿处理器,可使用 terminate({ rollback: true })。触发与实例管理
3. Workflows 的正确使用方式:六条比 API 更重要的规则#
- 步骤要小且有业务边界。 不要把“读数据库 → 调 API → 写数据库 → 发邮件”塞进一个
step.do();独立调用应拆步骤,才能独立重试、超时和观察。 - 步骤要幂等。 对扣款、发信、创建资源等副作用,使用调用方提供的 idempotency key,或在步骤中“先查是否已完成,再执行”。网络失败并不表示第三方未提交。
- 跨步骤状态只能来自输入或步骤返回值。 休眠后内存会丢失,不能依赖模块变量、修改过的数组或被改写的
event.payload。将需要的状态作为step.do()的返回值保存。 - 步骤名和流程分支应可确定。 步骤名相当于状态缓存键;不要在名字中使用
Date.now()、随机数,也不要在步骤外用随机数或当前时间决定走哪条路径。若在循环中建步骤,循环数据必须来自稳定的前置步骤输出。 - 把副作用放入步骤内。 Workflow 在恢复时可能重新执行步骤外的普通代码;步骤外的日志、创建实例或网络请求可能重复。
- 大数据放外部存储。 非流式步骤结果和事件 payload 都有 1 MiB 限制。用 R2/D1/KV 保存大数据,只在步骤结果中返回对象 key、主键或 checksum;JavaScript Workflow 若确实要保存较大的二进制结果,可返回新的、未锁定的
ReadableStream<Uint8Array>。
这些规则决定 Workflow 是否真的可靠,尤其是第 2 条。官方规则文档明确说明:流程会 hibernate,非步骤内存状态会失效,步骤名必须稳定,外部调用应具备幂等性。Rules of Workflows
4. Dynamic Workers:运行时加载代码的沙箱原语#
Dynamic Workers 让一个“宿主 Worker(Worker Loader)”在运行时提供 modules 形式的代码,创建并调用另一个 Worker。它适合 AI Code Mode、用户上传的小应用、预览环境、一次性自动化和自定义工具等场景。Cloudflare 将它定位为容器的轻量替代方案:主系统决定动态代码拥有的 binding、网络访问和资源上限。Dynamic Workers 概览
在宿主 Worker 的配置中,首先声明 worker_loaders binding:
{
"worker_loaders": [{ "binding": "LOADER" }]
}随后使用两种加载方式:
env.LOADER.load(code):每次创建新的 Dynamic Worker,适合一次性、每次代码都不同的执行。env.LOADER.get(id, callback):以 ID 尝试复用已加载的代码,适合同一应用或同一租户的多次请求。它是缓存,不是“固定进程”:相同 ID 后续仍可能由新 isolate 执行,callback也可能再次被调用。
get() 的 ID 必须对应不可变的代码与配置。同一 ID 的 callback 应始终返回相同内容;只要代码、依赖、权限或配置变化,就换一个版本 ID,例如 tenant-42:workflow:v17 或内容 hash。反过来,若每次都用 load() 或随机 ID,会牺牲复用效果并增加按唯一 Dynamic Worker 计费的数量。Dynamic Workers 入门 Worker Loader API
最小权限比“能执行不可信代码”更重要#
Dynamic Worker 的隔离不是把安全设计自动完成。特别需要注意:如果没有显式设置,globalOutbound 会继承宿主 Worker 的网络能力,通常意味着可访问公共互联网。对于用户或 AI 生成的代码,推荐默认禁止网络,再以很小的受控能力集逐项开放。以下片段位于宿主的 fetch(request, env, ctx) 中:
const worker = env.LOADER.get(`tenant:${tenantId}:v${version}`, async () => ({
compatibilityDate: "2026-08-07",
mainModule: "index.js",
modules: { "index.js": await loadBundledCode(tenantId, version) },
// 默认切断 fetch() / connect();动态代码只能使用明确授予的能力。
globalOutbound: null,
env: {
// 由宿主实现的 RPC capability;密钥保留在宿主的 env / props 中。
CRM: ctx.exports.TenantCrm({ props: { tenantId } }),
},
}));
return worker.getEntrypoint().fetch(request);这里 CRM 应是宿主 Worker 导出的 WorkerEntrypoint/Service Binding。动态代码只能调用允许的方法,不能读取 props 中的 token;宿主可以在方法中做鉴权、租户隔离、参数校验、审计和凭证注入。若确实要允许 HTTP,则将 globalOutbound 指向宿主实现的网关,对域名 allowlist、请求体、速率与凭证进行控制,而不是直接放开网络。Dynamic Worker bindings 出口控制
还应配合以下约束:
limits: { cpuMs, subRequests }可按动态 Worker 或getEntrypoint()调用设置,达到任一上限会抛出异常。- 通过
tails附加 Tail Worker,捕获动态代码的日志、异常与请求元数据;日志应带 worker / tenant ID,且不能记录密钥或原始敏感 payload。 - Dynamic Worker 直接接收的是 JavaScript / CommonJS / Python 模块。TypeScript 和 npm 依赖要先转译、解析并 bundle,例如使用
@cloudflare/worker-bundler;官方也提示 Python 启动通常比 JavaScript 慢。 - 如果动态代码需要持久状态,但不需要流程编排,可以考虑 Durable Object Facets:宿主 DO 充当 supervisor,动态 class 作为 facet,二者拥有隔离的 SQLite 存储。
自定义资源上限 可观测性 Durable Object Facets
5. Dynamic Workflows:让动态代码也获得耐久步骤#
Dynamic Workflow 不是另一个独立的编排服务,而是 Dynamic Workers + Workflows 的适配层。@cloudflare/dynamic-workflows 负责把“某个 Workflow 实例属于哪个动态代码版本”的路由信息持久化,并在 sleep、等待事件或 isolate 回收后重新载入正确的 Dynamic Worker。
flowchart LR R["请求:tenantId + codeVersion"] --> L["Worker Loader:已部署的宿主代码"] L --> D["Dynamic Worker:按租户和版本加载"] L --> B["wrapWorkflowBinding:写入路由 metadata"] B --> D D --> I["WORKFLOWS.create():创建实例"] I --> E["Workflows Engine:持久步骤、重试、sleep、事件"] E --> X["DynamicWorkflow 入口"] X --> L L --> D
对应关系可以拆成三部分:
- Worker Loader 是部署的固定代码。它认证请求、确定租户与代码版本、从存储取 bundle,并授予受控 bindings。
- Dynamic Worker 是按租户或任务加载的代码。它可以像普通 Workflow 一样写
step.do()、step.sleep()和step.waitForEvent()。 DynamicWorkflow入口类 由库创建,供 Workflows Engine 调用。引擎恢复某实例时,库读取实例 metadata,要求宿主重新加载相应 Dynamic Worker,并取回其中的 Workflow class 继续执行。
官方流程的关键点是 wrapWorkflowBinding 与 createDynamicWorkflowEntrypoint:前者把 metadata 附着到动态代码创建的每个实例上,后者在恢复时用 metadata 路由回对应的动态 Worker。Dynamic Workflows 官方指南
配置与实现骨架#
宿主 Worker 同时要有 Worker Loader 和一个常规 Workflow binding:
{
"worker_loaders": [{ "binding": "LOADER" }],
"workflows": [
{
"name": "dynamic-workflow",
"binding": "WORKFLOWS",
"class_name": "DynamicWorkflow"
}
]
}宿主端的简化代码如下。这里把 codeVersion 放进 metadata;这比仅保存 tenantId 更稳妥,因为恢复一个数天前的实例时,应载入创建时的代码版本而不是租户的“最新代码”。这是根据 Loader 的“同一 ID 必须映射相同 WorkerCode”和 Workflows 恢复时按 metadata 重载代码这两条规则得出的工程实践。
import {
createDynamicWorkflowEntrypoint,
DynamicWorkflowBinding,
wrapWorkflowBinding,
type WorkflowRunner,
} from "@cloudflare/dynamic-workflows";
// 必须 re-export;运行时据此创建供动态代码使用的包裹 binding。
export { DynamicWorkflowBinding };
function loadTenantWorkflow(env: Env, tenantId: string, codeVersion: string) {
const workerId = `tenant:${tenantId}:workflow:${codeVersion}`;
return env.LOADER.get(workerId, async () => ({
compatibilityDate: "2026-08-07",
mainModule: "index.js",
modules: { "index.js": await fetchBundle(tenantId, codeVersion) },
globalOutbound: null,
env: {
WORKFLOWS: wrapWorkflowBinding({ tenantId, codeVersion }),
// 这里只传入经过能力收敛的 Service Bindings。
},
}));
}
export const DynamicWorkflow = createDynamicWorkflowEntrypoint<Env>(
async ({ env, metadata }) => {
const { tenantId, codeVersion } = metadata as {
tenantId: string;
codeVersion: string;
};
return loadTenantWorkflow(env, tenantId, codeVersion)
.getEntrypoint("TenantWorkflow") as unknown as WorkflowRunner;
},
);动态代码自身几乎就是一个普通的 Workflow:
import { WorkflowEntrypoint } from "cloudflare:workers";
export class TenantWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const result = await step.do("apply tenant action", async () => {
return this.env.CRM.upsert(event.payload);
});
await step.sleep("wait for review", "1 day");
const approval = await step.waitForEvent("wait for approval", {
type: "approval",
timeout: "7 days",
});
return { result, approval };
}
}
export default {
async fetch(request, env) {
const instance = await env.WORKFLOWS.create({
params: await request.json(),
});
return Response.json({ id: await instance.id });
},
};与普通 Workflows 相同,Dynamic Workflows 的 .status()、暂停、恢复、重试、休眠与等待事件语义保持不变;库增加的是“从持久化实例回到正确动态代码”的路由。不要把 secret 放进 wrapWorkflowBinding 的 metadata:metadata 会随实例事件持久化,动态代码可经由状态接口读到它。metadata 仅放 tenant ID、代码版本、路由键等非敏感信息。Dynamic Workflows 安全说明
6. 场景与选型#
| 需求 | 首选能力 | 原因 |
|---|---|---|
| 固定的订单、审批、邮件、ETL 流程 | Workflows | 代码由团队部署,可靠性需求高,模型最简单。 |
| 一段一次性 AI 生成的分析/工具代码 | Dynamic Workers 的 load() | 不需要跨步骤持久化,强调隔离与快速执行。 |
| 用户自定义自动化、每租户不同的审批链或 webhook 链 | Dynamic Workflows | 动态逻辑需要在失败、等待和多天后继续。 |
| 实时房间、单资源串行修改、WebSocket 协调 | Durable Objects | 核心是按对象 ID 的有状态协调,而非长链路编排。 |
| 高吞吐消息削峰、至少一次投递 | Queues,必要时再触发 Workflow | Queue 解决消息传递;Workflow 解决步骤状态与等待。 |
| 重 CPU、长时间纯计算 | 容器或外部计算服务 | Workflow 的 wall-clock 可以很长,但每步骤仍受 Workers 活跃 CPU 上限约束。 |
Dynamic Workflows 最有价值的三类场景是:多租户 SaaS 让客户定义 onboarding / billing retry / approval 自动化;AI Agent 在运行时生成长期计划并等待人工确认;平台让客户提交数据转换、定时任务或 webhook 链,同时由平台保留重试与进度。Dynamic Workflows 使用场景
反过来,以下情况通常不值得上 Dynamic Workflows:流程逻辑固定、只有一个快速步骤、对同步 P99 延迟极敏感、没有能力治理需求,或者无法给用户生成代码建立鉴权、审计、网络控制与版本管理。它会带来代码加载、隔离、版本兼容、日志归属和成本统计的额外复杂度。
7. 限额、成本与可观测性#
下面的数字是本文日期的快照,不是容量或报价承诺:
| Workflows 项目 | Free | Paid |
|---|---|---|
| 每步骤活跃 CPU | 10 ms | 默认 30 s,最高可配到 5 min |
| 每步骤 wall-clock | 不限(仍受 CPU 约束) | 不限(仍受 CPU 约束) |
| 非流式步骤结果 / event payload | 1 MiB | 1 MiB |
| 每实例持久化状态 | 100 MB | 1 GB |
| 最大 sleep / event timeout | 365 天 | 365 天 |
| 每 Workflow 最大步骤数 | 1,024 | 默认 10,000,最高 25,000 |
| 每账户并发运行实例 | 100 | 50,000 |
| 已完成实例状态保留 | 3 天 | 30 天 |
正在 sleep、等待事件或等待重试的实例处于 waiting,不占运行并发;恢复时若无空位仍会排队。对于大数据,R2 中保存对象并在 Workflow 中传递 key,通常比把结果塞进步骤状态更可靠、更便宜。Workflows limits
Workflows 目前按 Workers CPU、实例请求、持久化存储和执行步骤四个维度计费;等待 I/O、sleep 或空闲不产生 CPU 时间。官方计费页在本文整理时注明,步骤和存储计费将从 2026-08-10 开始。Dynamic Workers 则仅面向 Workers Paid,计费还包括每日创建的唯一 Dynamic Worker 数量;使用稳定版本 ID 的 get() 有助于避免反复创建同一代码版本。部署前应按最新价格页重新计算。Workflows pricing Dynamic Workers pricing
运行中至少应记录这些维度:Workflow 名称、实例 ID、步骤名与重试次数、租户 ID、动态代码版本、外部请求的 idempotency key、资源消耗和补偿结果。Workflows 可从 Dashboard、Workers API、REST API、Wrangler 与指标系统观察;Dynamic Workers 的日志要通过 Tail Worker 收集,因为它们不会自动进入宿主 Worker 的 Logs。
8. 上线前检查清单#
- 每个
step.do()都有明确的业务边界、超时和重试策略。 - 所有可重复外部副作用都有幂等键、去重记录或补偿处理。
- 跨步骤数据来自
event.payload或步骤返回值;大数据只传引用。 - 步骤名、循环顺序和分支条件在恢复时可确定。
- Workflow 实例 ID、保留期、并发、步骤数和存储量符合当前计划。
- 动态代码有不可变的版本 ID / hash;恢复中的实例不会悄悄切到“最新版本”。
- Dynamic Worker 默认无网络;所有网络和业务能力经宿主 gateway / Service Binding 收敛。
- 动态代码永远拿不到原始密钥;metadata、日志、错误信息都已排除敏感数据。
- 为每个租户、代码版本和实例建立可检索日志、告警、成本与审计链路。
- 在本地和测试环境验证重试、sleep、事件提前到达、超时、代码版本升级和补偿路径。
结论#
Workflows 解决的是“已知业务流程如何在故障与长等待中可靠继续”;Dynamic Workers 解决的是“未知或按需代码如何被隔离并受控执行”;Dynamic Workflows 则解决“动态代码如何也拥有可靠的长期执行能力”。绝大多数后端流程应先选择普通 Workflows。只有当流程定义本身属于租户、用户或 Agent,并且必须运行时加载时,再引入 Dynamic Workers 和 @cloudflare/dynamic-workflows 这层组合。
继续深入#
- Workflows:实例、步骤、事件与恢复 API 实战:按创建实例、等待 webhook、查询状态、重启和补偿的完整调用链学习 API。
- Dynamic Workflows:多租户动态代码如何跨休眠恢复:聚焦版本 metadata、
wrapWorkflowBinding()与恢复时重新加载租户代码。