说明:本文由 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 binding | create(options) | 创建一个实例;可提供业务 ID 与 params。保留期内重复 ID 会报错。 |
Workflow binding | createBatch(options[]) | 一次创建多条实例,适合批量任务;单批最多 100 个。 |
Workflow binding | get(id) | 取得既有实例,用于管理、状态查询或投递事件。 |
WorkflowInstance | id、status() | 保存实例标识并查询 queued/running/waiting/complete 等状态。 |
WorkflowInstance | pause()、resume() | 运维暂停/恢复实例,例如维护窗口或人工介入;它们不携带业务审批结果。 |
WorkflowInstance | restart()、terminate() | 重新运行或停止;terminate({ rollback: true }) 可触发已注册补偿。 |
WorkflowInstance | sendEvent({ type, payload }) | 唤醒匹配的 waitForEvent();事件可先到,平台会缓冲。 |
WorkflowStep | do() | 可持久化、可重试业务步骤。 |
WorkflowStep | sleep()、sleepUntil() | 相对或绝对时间等待;不计入最大步骤数。 |
WorkflowStep | waitForEvent() | 等待 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 中明确写出该兜底分支。
以当前官方默认值为例,未传 WorkflowStepConfig 的 step.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. 恢复语义:四条必须写进设计的规则#
- 只依赖输入和步骤返回值。 sleep 或故障后,模块变量、class 内存、步骤外修改的数组都不可靠;跨步骤状态来自
event.payload或前一步返回值。 - 外部副作用一定幂等。 网络超时不代表第三方没成功。扣款、发信、创建资源、预留库存都应带幂等键,或先查后写。
- 把步骤做小。 “读 DB → 调支付 → 发邮件”应是多个步骤,分别配置 retry/timeout;不要为了少写几行合并错误边界。
- 只保存可序列化、小尺寸的结果。 普通步骤返回值与 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:多租户动态代码如何跨休眠恢复。