Cloudflare Workflows 按通道迁移:Dynamic Workflows 与 V2 的取舍

Cloudflare Workflows 是否该按内容通道拆成 Dynamic Workflows?本文结合 6 条 AI 发布流程,拆解 Workflows V2 的并发与队列上限、确定性重放和实际迁移成本,并说明为何 publish-{brief_id} 幂等键必须留在 HTTP 调度边界。

Friday, September 4, 2026Omid Saffari
Cloudflare Workflows 按通道迁移:Dynamic Workflows 与 V2 的取舍

每天都有 6 个 Anthropic 侧的发布例程向我的系统发起请求,每一个都会 POST 到同一个静态 PublishWorkflow,实例键为 publish-{brief_id}。5 月 1 日,Cloudflare 发布了 @cloudflare/dynamic-workflows;几天后,又推出了 Workflows V2,其控制平面专门针对 Agent 触发的负载进行了重构。真正值得关注的不是新品发布本身,而是另一件事:我当初为了保证幂等性而选择单一静态工作流,如今不再是沿用平台限制,而成了一个必须重新论证的架构决定。

Cloudflare Workflows 真正让我意外的变化

我原以为这次发布只会宣布“Workflows 可以承载更大规模了”:上限提高、队列加深,再来一次常见的年度升级。但这其实只是公告中较次要的一半。

更重要的变化是,工作流代码如今可以在运行时按租户而不同。@cloudflare/dynamic-workflows 于 5 月 1 日发布,采用 MIT 许可证,并基于 Dynamic Workers 构建。它允许开发者注册一个 WorkflowEntrypoint,在创建实例时再从 Dynamic Worker 解析其执行体。这样一来,步骤图便能随租户变化,不必固化在部署包中。Cloudflare 自己将其概括为“跟随租户的持久执行”。如果你曾尝试用功能开关把客户专属逻辑塞进同一套工作流,就会知道这句话有多准确。

我的 6 个发布例程——news、dev、build、design、marketing、founders、business——本质上就是共享同一个 PublishWorkflow 的 6 个租户。它们不是客户,而是 Anthropic 侧的 AI 例程,每个例程负责一条不同的内容通道。但多租户模型恰好适用:编排主干相同,各通道的具体形态略有差异,最终都调度到同一个 Cloudflare 账户。

几天后 Workflows V2 上线,而那篇公告的叙事重点比数字更值得留意。V2 的架构明确围绕一个前提重做:工作流实例由 Agent 以机器速度 创建,而不是由人点击按钮触发。这正是我的触发模式。发布例程决定上线一篇文章,本质上是一个非人工事件,由此创建一个持久执行实例;V1 的控制平面面向的则是另一种负载形态。

非技术创始人真正需要关注什么

“持久执行”这个词听起来很容易让人望而却步。说白了,它就是一项能熬过系统崩溃的多步骤任务:恢复后从上一个已完成步骤继续,而不是从头重跑。如果 8 个步骤中的第 5 步因为某个 API 宕机而失败,系统只会重试第 5 步,不会重新执行第 1 到第 4 步,也不会让你为这些步骤再付一次钱。

这才是创始人需要在意的部分。按步骤划分的重试边界,同时也是按步骤划分的成本边界。假设流水线在第 3 步和第 6 步分别调用付费 AI 模型,而第 7 步崩溃,你希望重试的是第 7 步,而不是整项任务。对一条付费流水线做非幂等重跑,造成的可不只是数据库里多出一行,而是账单被重复计算。

关于该构建还是暂缓,你可以用一句话告诉承包商:只有当各产品线的逻辑确实出现分化时,才拆分工作流;不能只因为平台现在支持拆分就这样做。 Cloudflare 刚推出了一项强大的新抽象,看到这类能力,人很容易围绕它重新设计系统。但 N 个工作流定义意味着 N 倍的维护工作,只有各通道确实在执行不同任务时,收益才会出现。如果有人说“既然现在能拆,就该把每条产品线都改成独立工作流”,创始人应该追问的是业务流程发生了什么变化,而不是平台增加了什么能力。

我实际运行的架构

这套系统的真实结构很小,完全可以装进脑子里。Anthropic 侧的每个发布例程完成调研并起草 brief 后,都会向网站的 /api/admin/publish 发起 POST。该端点校验 payload、生成实例 ID,然后在名为 PublishWorkflow 的单一 WorkflowEntrypoint 上调用 instances.create

工作流本身由 8 个具备幂等性的 step.do 阶段组成:

  1. validate(校验 brief schema、slug 唯一性)
  2. ground(抓取引用、解析链接)
  3. generate(调用 Claude 生成正文)
  4. clean(校验 markdown directive)
  5. persist(写入 Postgres、执行版本管理)
  6. index(生成 embeddings、刷新搜索)
  7. cover(生成图片并上传至 R2)
  8. publish(切换状态、ping sitemap)
TypeScript
export class PublishWorkflow extends WorkflowEntrypoint<Env, PublishParams> {
  async run(event: WorkflowEvent<PublishParams>, step: WorkflowStep) {
    const brief = await step.do("validate", () => validateBrief(event.payload));
    const grounded = await step.do("ground", () => groundCitations(brief));
    const draft = await step.do("generate", () => generateBody(grounded));
    const cleaned = await step.do("clean", () => validateDirectives(draft));
    const row = await step.do("persist", () => persistArticle(cleaned));
    await step.do("index", () => reindex(row.id));
    await step.do("cover", () => generateCover(row.id));
    await step.do("publish", () => flipStatus(row.id));
  }
}

调度边界的代码如下:

TypeScript
const id = `publish-${brief.brief_id}`;
try {
  await env.PUBLISH.create({ id, params: brief });
} catch (e) {
  if (isDuplicateIdError(e)) return new Response("already queued", { status: 200 });
  throw e;
}

instances.create 遇到重复 ID 时会抛出异常。这一行承担着关键职责:它是阻止 Anthropic 例程重复触发后再次发布同一篇文章、并让我为其中的 AI 调用支付双份费用的唯一屏障。

为什么 6 条通道只用一个定义?因为所有通道的 8 个阶段完全相同,区别仅在编辑配置包,包括语气规范、受众规范、反模式清单和 brief 提示。这些内容作为运行时数据放在调度 envelope 中传递,而不是写进代码。通道名决定把哪套配置交给第 3 步(generate);其他步骤都不关心通道。

真正的压力点——也是唯一值得认真讨论 Dynamic Workflows 的理由——在于其中一条通道开始需要不同的 步骤图,而不只是不同的配置包。重调研通道希望在生成前多做一轮事实依据补充,并可能在生成后增加事实核查步骤。这是结构差异,不是数据差异。现在它被写成 ground 内部的条件分支,确实能运行;但如果再让 3 条通道加入各自的特殊分支,这类代码很快就会腐化。

迁移到 Cloudflare Dynamic Workflows 的真实成本

这正是 createDynamicWorkflowEntrypoint 带来的能力。系统会在运行时从 Dynamic Worker 加载各通道的工作流代码,因此当天没有触发的通道几乎不产生保留成本。你不再需要承担“所有通道始终打进同一个部署包”的维护负担,也可以独立演进各通道代码,无需重新部署共享主干。

架构图:/publish HTTP 边界持有 publish-{brief_id} 幂等键,作为分流至各通道工作流之前的闸门
幂等键属于调度边界,绝不能放进通道代码。

公告没有着重展开的,是这项能力的真实代价。你原本拥有一个经过类型检查的 WorkflowEntrypoint,步骤输入契约一旦漂移,编译器就会提醒;迁移后则变成 N 个运行时加载的定义,编译器无法提供同样的保障。跨通道步骤契约漂移会从构建期错误变成运行时故障。假如你有 6 条通道,其中 2 条的 persist 步骤因为有人只重构了一半通道、忘了其余部分,而开始期待略有不同的数据行结构,那么问题不会在 CI 运行时暴露,而要等到真实文章尝试发布时才出现。

这次迁移不必全有或全无。把它说成二选一,往往就是过度设计的起点。我的计划是:

  1. 将共享的 8 阶段主干继续保留为静态 WorkflowEntrypoint。这是热路径,6 条通道中有 5 条会原样使用。
  2. 把重调研通道构建成拥有独立步骤图的 Dynamic Workflow,加入额外的依据补充与事实核查步骤。
  3. /publish 边界按通道调度,只需根据 brief.lane 做一行 switch,选择在哪个 binding 上调用 create

我的判断规则带有一个明确数字:只有当某条通道的步骤图与主干相差超过一个阶段时,才将它迁移到独立的 Dynamic Workflow。 低于这个阈值,在现有步骤中增加条件分支,比维护第二个工作流定义更省成本。超过阈值后,步骤内分支会开始掩盖通道究竟在做什么,独立定义才值得。

一条通道超过阈值,5 条仍低于阈值。两句话就能讲完这份迁移计划。如果承包商拿着同一份 brief,建议我第一天就把全部 6 条通道拆成 Dynamic Workflows,我不会批准。构建成本真实存在,而收益恰好只集中在一条通道上。

Cloudflare Workflows V2 为机器速度触发带来了什么

V2 最醒目的数字是:每个工作流可同时运行 50,000 个实例、排队 2,000,000 个实例;V1 的排队上限是 1,000,000 个。这些数字并非虚荣指标,但也远远超出我的实际规模。6 个例程每天各触发一两次,我的负载大约比新上限低 9 个数量级。

对我的技术栈而言,重要的不是这些数字,而是 V2 的控制平面把 Agent 触发的实例创建设为首要场景。V1 针对的是人工触发模式:用户点击按钮、实例启动,系统处理有突发但仍有边界的负载。V2 假设触发者是进程而非人,速率取决于上游 Agent 的吞吐量,而不是用户界面的点击次数。

确定性重放是 V2 最值得用通俗语言再讲一次的特性。每个步骤都彼此隔离、可以重放、具备幂等性;发生重试时,工作流会从最后一个成功步骤继续。这 恰好 就是我在调度边界用 publish-{brief_id} 实例 ID 手动守住的属性。V1 已经提供按步骤重试的保证;V2 进一步强化了重放语义,让我一直保护的结构性约束也得到下层平台的保障。

本周最实际的动作是:热路径什么也不改。V2 需要通过重新部署来选择启用,而对一条运行良好的幂等流水线来说,最糟糕的做法就是为了追逐已有的语义,匆忙迁移到新的控制平面。我会在把重调研通道构建为 Dynamic Workflow 时将它迁至 V2,因为让新代码直接采用新模型成本很低。其余 5 条静态通道保持原样,直到我有一个不是“平台发布了新版本”的真正改动理由。

我绝不会放弃的那条约束

publish-{brief_id} 作为实例 ID 承担着系统稳定性的重任。一旦去掉它,Anthropic 例程在自身一侧重试时,流水线就会再次触发,不仅重复发布同一篇文章,还会让工作流内部每个付费 API 调用都计费两次。

Dynamic Workflows 本身不会直接破坏这条约束,instances.create 的 ID 契约没有改变。真正可能威胁它的,是草率的按通道重构:如果通道代码开始在本地重新派生 ID,例如某条通道为了所谓的“更安全”而把时间戳放进 ID,那么每次重试都会生成唯一 ID,去重机制也就悄无声息地失效了。

无论版本如何升级,下面这条规则都必须保留。在允许任何其他人改动代码前,我甚至愿意把它写在墙上:

这类约束不会出现在发布公告或迁移指南里,因为只有真正经历过它所防止的故障,你才会意识到它的存在。与之对应的成本卡点——即使去重失效,也不能让一次工作流运行演变成失控账单——属于另一层,应设在模型调用层,而不是实例层。两层防护存在的理由相同:当流水线的每一步都在花钱时,结构正确性就是最便宜的保险。

如果在 2026 年 5 月从零开始,我会怎么做

如果今天才开始搭建这套系统,而不是接手一年前的架构决定,我会改变 3 件事。

第一,我会从共享静态主干开始,再为任何真正出现分化的通道增加 一个 Dynamic Workflow,而不是直接创建 6 个独立定义。新抽象出现时,人总想把它用到所有地方,但 N 个工作流定义带来的维护成本,会比规模收益更早到来。6 个定义意味着修复一个 bug 要改 6 个地方,升级依赖要处理 6 个地方,步骤契约也可能在 6 个地方发生漂移。“一条主干加一个例外”才是满足需求的最小拆分方案。

第二,我会从第一天起就把幂等键放在 HTTP 边界。等到发生重复发布事故后再补上 publish-{brief_id},代价要高得多:你必须核对重复发布的数据行、退还受影响的费用,还要在系统已经上线后给调度层补监控。相反,在流水线首次上线前加入 try { create({id}) } catch (dup) {} 模式只需十分钟,却能消灭一整类故障。

第三,我会把 Workflows V2 的确定性视为设计的 最低标准,而不是以后再选择启用的功能。即使永远不会碰到 50k 并发上限,也要让每个步骤都能安全重放。安全重放不是扩展性属性,而是正确性属性。不能安全重放的步骤一定会在重试时出错,而任何规模的系统都会发生重试。

最后这一点,是披着技术决策外衣的资深工程判断。陷阱在于把平台新能力当作等待采用的功能,真正的工程纪律则是把它们当作设计约束——无论你最终是否会用到那些容量余量。

所有租户运行相同逻辑时,还需要 Dynamic Workflows 吗?

不需要。如果步骤图完全相同、只有数据不同,就把数据放进调度 payload,继续使用一个静态工作流。只有代码本身因租户而异,也就是步骤图不同而不只是参数变化时,Dynamic Workflows 才能体现价值。

Workflows V2 会破坏现有的 V1 工作流吗?

V2 是经过重构的控制平面,重点支持确定性的 Agent 触发执行。应把迁移视为需要主动选择的操作;移动热路径之前先验证幂等性,也不要仅仅因为新版本出现,就迁移运行正常的代码。

现在实际的并发上限是多少?

每个工作流可同时运行 50,000 个实例、排队 2,000,000 个实例,原来的排队上限是 1,000,000 个。对大多数运营者来说,这远超实际需求;相比新上限,V2 更重要的特性是确定性重放。

@cloudflare/dynamic-workflows 已可用于生产环境,还是仍处于预览阶段?

它于 2026 年 5 月 1 日发布,采用 MIT 许可证,并基于 Dynamic Workers 构建。真正应该作为门槛风险来评估的是库的成熟度和你的幂等契约,而不是许可证。如果调度层足够可靠,那么这套库的就绪速度会快于你的迁移计划。

创始人应该在什么时候花钱请工程师做这次迁移?

当某条产品线的自动化逻辑确实与其他产品线出现分化时——步骤图不同,而不只是参数不同。仅仅为扩展规模而拆分还为时过早;逻辑分化才是真正的触发条件。如果你的团队也需要算清这类迁移账,这正是我通过 DVNC.dev 提供的架构评审服务。

最近更新

2026年9月4日

分类Build

在 Google 中优先显示本站

将 omidsaffari.com 添加为 Google 搜索的优先来源

把 omidsaffari.com 设为优先来源,Google 会在 Top Stories、AI Overviews 和 AI Mode 中为您优先展示。

更多 Build 文章

查看全部 Build 文章
订阅通讯

每周日,一封信。 写运转中的系统,不写热评。

来自一组 AI 项目组合运营的构建日志、生产系统与一线笔记。

每周一期。无垃圾邮件。随时退订。