Workflows:实例、步骤、事件与恢复 API 实战

This article is extracted from the chat log with AI. Please identify it with caution.

说明:本文由 Codex 根据作者提供的主题、对话素材与 Cloudflare 官方文档辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。

Workflows 最容易被误用成“把异步函数放到后台”。它真正提供的是可恢复的步骤边界step.do() 成功后的可序列化结果会被保存;睡眠、等待事件、Worker 重启或暂时失败后,运行时从已完成步骤继续。本文用“订单等待付款后履约”走完创建、等待、投递事件、查询与补偿的完整闭环。

先看一条实例的时间线#

sequenceDiagram
  participant A as API Worker
  participant B as Workflow binding
  participant W as OrderWorkflow
  participant P as Payment webhook

  A->>B: create({ id, params })
  B->>W: run(event, step)
  W->>W: step.do("reserve stock")
  W->>W: step.waitForEvent("payment")
  P->>B: get(instanceId).sendEvent(...)
  B->>W: 恢复匹配的 waitForEvent
  W->>W: step.do("fulfill order")
  W-->>B: return result
  A->>B: get(instanceId).status()

其中 WorkflowEntrypoint 是流程定义;Workflow binding 用于创建/管理实例;WorkflowInstance 是某一次运行;WorkflowStep 则决定哪些工作可以恢复。

1. 先配置 binding,再写 Workflow class#

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "orders",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-07",
  "workflows": [
    {
      "name": "order-workflow",
      "binding": "ORDER_WORKFLOW",
      "class_name": "OrderWorkflow"
    }
  ]
}

修改 Wrangler binding 后应重新运行 wrangler types,让 env.ORDER_WORKFLOW 获得准确类型。绑定 Workflows 需要足够新的 compatibility date;当前文档要求不早于 2024-10-22。

2. 一个可以恢复的订单流程#

下面的例子刻意把“预留库存”“等待付款”“履约”拆开。不要把它们藏在同一个步骤里,否则重试粒度、超时和补偿边界都会变差。

import {
  WorkflowEntrypoint,
  type WorkflowEvent,
  type WorkflowStep,
} from "cloudflare:workers";
import { NonRetryableError } from "cloudflare:workflows";

type OrderParams = {
  orderId: string;
  customerId: string;
};

export class OrderWorkflow extends WorkflowEntrypoint<Env, OrderParams> {
  async run(event: WorkflowEvent<OrderParams>, step: WorkflowStep) {
    const reservation = await step.do(
      "reserve stock",
      {
        retries: {
          limit: 3,
          delay: "10 seconds",
          backoff: "exponential",
        },
        timeout: "30 seconds",
      },
      async () => {
        const response = await this.env.ORDER_SERVICE.reserve({
          orderId: event.payload.orderId,
          customerId: event.payload.customerId,
          idempotencyKey: event.instanceId + ":reserve",
        });

        if (response.kind === "invalid") {
          throw new NonRetryableError("invalid order");
        }

        return { reservationId: response.reservationId };
      },
      {
        rollback: async ({ output }) => {
          if (output) {
            await this.env.ORDER_SERVICE.release(output.reservationId);
          }
        },
      },
    );

    const payment = await step.waitForEvent<{ paid: boolean }>(
      "wait for payment",
      { type: "payment-confirmed", timeout: "24 hours" },
    );

    if (!payment.payload?.paid) {
      return { orderId: event.payload.orderId, status: "unpaid" };
    }

    const fulfillment = await step.do("fulfill order", async () => {
      return this.env.ORDER_SERVICE.fulfill({
        orderId: event.payload.orderId,
        reservationId: reservation.reservationId,
        idempotencyKey: event.instanceId + ":fulfill",
      });
    });

    return {
      orderId: event.payload.orderId,
      status: "fulfilled",
      fulfillmentId: fulfillment.id,
    };
  }
}

这里的 ORDER_SERVICE 可以是 Service Binding,也可以换成经认证的外部 API client。关键不在实现细节,而在于:每个外部副作用都有稳定 idempotency key;成功预留库存后注册 rollback;付款事件恢复等待步骤;最终返回一个小而可查询的结果。

3. API 速查:实例外部管理与实例内部执行#

接口何时使用
Workflow bindingcreate(options)创建一个实例;可提供业务 ID 与 params。保留期内重复 ID 会报错。
Workflow bindingcreateBatch(options[])一次创建多条实例,适合批量任务;单批最多 100 个。
Workflow bindingget(id)取得既有实例,用于管理、状态查询或投递事件。
WorkflowInstanceidstatus()保存实例标识并查询 queued/running/waiting/complete 等状态。
WorkflowInstancepause()resume()运维暂停/恢复实例,例如维护窗口或人工介入;它们不携带业务审批结果。
WorkflowInstancerestart()terminate()重新运行或停止;terminate({ rollback: true }) 可触发已注册补偿。
WorkflowInstancesendEvent({ type, payload })唤醒匹配的 waitForEvent();事件可先到,平台会缓冲。
WorkflowStepdo()可持久化、可重试业务步骤。
WorkflowStepsleep()sleepUntil()相对或绝对时间等待;不计入最大步骤数。
WorkflowStepwaitForEvent()等待 webhook、用户点击、人工审批等外部信号。

step.do(name, config, callback, rollbackOptions) 中的 name 是恢复状态的键,不是日志标签。名称和控制流必须稳定:不要把随机数、当前时间或可变数组顺序放进 step name 或决定分支的步骤外代码。

4. 谁来驱动“下一步”:代码自动推进,人工关卡显式建模#

常规 Workflow 不需要前端逐节点点击“下一步”。 run() 中的 JavaScript 控制流决定执行顺序:一个 await step.do() 成功后,代码自然继续到下一行;条件、循环、try…catch 与并行 Promise.all() 也都由已部署的 Workflow 代码决定。每个 step.do() 可以按该步骤的依赖特性配置重试、退避和单次尝试的超时;没有显式配置时,平台采用默认策略。

想要的推进方式应使用的 API是否需要前端动作
上一步成功后继续后续的 await step.do() / 普通 JS 控制流
到指定时间再继续step.sleep()step.sleepUntil()
暂时失败后重试step.do(name, { retries, timeout }, callback)
等待付款 webhook、审批或用户决定step.waitForEvent() + instance.sendEvent()仅此类场景需要外部信号
暂停或恢复整条实例pause() / resume()可以由运维系统触发,但不适合传递审批数据

前端“同意/拒绝”按钮只是你应用的一个事件入口,而不是 Workflows 自带的通用“下一步”按钮。代码必须先明确声明这个门槛;之后前端请求自己的受认证 API,由 API 校验操作者、订单/租户归属和实例 ID,再调用 binding 投递事件:

type ApprovalPayload = { approved: boolean };

// Workflow 内:只有写了 waitForEvent,才会在这里等待人或外部系统。
const approval = await step.waitForEvent<ApprovalPayload>(
  "wait for fulfillment approval",
  { type: "fulfillment-approval", timeout: "48 hours" },
);

if (approval.payload?.approved) {
  await step.do("fulfill approved order", async () => {
    // 执行履约;仍应使用幂等键。
  });
}

// 普通 Worker API:前端按钮调用这里,而不是直接获得 Workflow binding。
export async function submitApproval(request: Request, env: Env) {
  const { instanceId, approved } = await validateApprovalRequest(request);
  await requireAuthorizedApprover(request, instanceId);

  const instance = await env.ORDER_WORKFLOW.get(instanceId);
  await instance.sendEvent({
    type: "fulfillment-approval",
    payload: { approved },
  });

  return Response.json({ accepted: true }, { status: 202 });
}

type 必须匹配,且只可使用字母、数字、-_。事件可以在实例到达 waitForEvent() 前发送,平台会先缓冲,再在匹配的等待步骤交付。等待超时默认会使实例失败;若业务需要“未审批即自动拒绝/升级通知”,应在 try…catch 中明确写出该兜底分支。

以当前官方默认值为例,未传 WorkflowStepConfigstep.do() 使用 retries.limit: 5(总尝试次数,不是“首次之外再重试 5 次”)、10 秒初始延迟、指数退避和每次 attempt 10 分钟超时。对支付、库存、第三方 API 等关键边界,应把这些值视为业务配置,逐步骤显式声明,而不是只依赖默认值。

5. 创建实例、投递 webhook、查询状态#

普通 API 请求只负责校验输入、生成稳定业务 ID 和创建实例:

export async function createOrderWorkflow(request, env) {
  const input = await validateCreateOrderRequest(request);
  const instance = await env.ORDER_WORKFLOW.create({
    id: "order-" + input.orderId,
    params: {
      orderId: input.orderId,
      customerId: input.customerId,
    },
  });

  return Response.json({
    instanceId: instance.id,
    status: await instance.status(),
  });
}

付款 webhook 不应自己执行履约;它应在认证签名、查到对应 instance ID 后投递一个事件:

export async function receivePaymentWebhook(request, env) {
  const payment = await verifyPaymentProviderSignature(request);
  const instance = await env.ORDER_WORKFLOW.get("order-" + payment.orderId);

  await instance.sendEvent({
    type: "payment-confirmed",
    payload: { paid: payment.status === "paid" },
  });

  return new Response(null, { status: 202 });
}

TypeScript 泛型只能改善开发期提示,不会验证外部 JSON。创建实例和接收 webhook 时,仍要运行时校验字段、签名、租户归属和事件类型。

6. 恢复语义:四条必须写进设计的规则#

  1. 只依赖输入和步骤返回值。 sleep 或故障后,模块变量、class 内存、步骤外修改的数组都不可靠;跨步骤状态来自 event.payload 或前一步返回值。
  2. 外部副作用一定幂等。 网络超时不代表第三方没成功。扣款、发信、创建资源、预留库存都应带幂等键,或先查后写。
  3. 把步骤做小。 “读 DB → 调支付 → 发邮件”应是多个步骤,分别配置 retry/timeout;不要为了少写几行合并错误边界。
  4. 只保存可序列化、小尺寸的结果。 普通步骤返回值与 event payload 有 1 MiB 限制。大数据写 R2/D1,步骤中只传 object key、主键或 checksum;JavaScript Workflow 如需持久化大二进制,可返回新的、未锁定的 ReadableStream

当前默认 step.do() 策略是 5 次总尝试、10 秒初始延迟、指数退避、每次尝试 10 分钟超时;把这些视作显式业务选择,而不是隐藏默认。验证错误、权限错误等不应重试的情况用 NonRetryableError 结束重试。

7. 何时转向 Dynamic Workflows#

如果流程代码由团队部署且在部署时已知,普通 Workflows 是最简单、最可靠的选择。只有“每租户/每任务的流程代码在运行时才确定”,并且仍要跨 sleep、事件和故障恢复时,才需要 Dynamic Workflows。它额外解决的是:恢复一个旧实例时,如何重新加载创建它时的那一版动态代码。

下一篇:Dynamic Workflows:多租户动态代码如何跨休眠恢复

参考资料#

本文共 2798 字,创建于 Aug 7, 2026

相关标签: Cloud, DevOps, TypeScript, ByAI