数据截至 (上游 commit cf96caaf13f0)
Workflow 状态生命周期
这章讲:一个 workflow 实例有哪些 status、它们怎么流转、为什么必须调用
acknowledgeStartOfProcessing,以及四个收尾/续命动作各自的语义。读完你会明白:workflow 的成败不是由「处理函数有没有抛异常」决定的。
1. 八种 status
一个实例的 status 取值在 SDK 里有完整文档(listInstances 的 JSDoc,packages/sdk/src/bot/workflow-proxy/types.ts:24-35):
| status | 含义 |
|---|---|
pending | 已创建,但还没开始干活 |
in_progress | 正在运行(由 acknowledge 置入) |
completed | 成功收尾 |
failed | 出错收尾 |
cancelled | 通过 API 取消 |
timedout | 因超时收尾 |
listening | 在等一个事件以继续 |
paused | 通过 API 暂停 |
注:具体的 status 联合类型定义在生成的
@botpress/client包里(SDK 用client.Workflow['status'],见types.ts:36),源码里看不到该枚举的字面定义文件,但上面这份语义是 SDK 自带的权威 JSDoc。
2. 状态怎么流转
怎么读这张图:实例总是从 pending 出生;你的处理函数通过动作把它推向终态。方向从上往下。
createWorkflow
│
▼
┌──────────┐
│ pending │ ← 刚创建,还没人干活
└────┬─────┘
acknowledge │
StartOfProcessing
▼
┌────────────┐
│ in_progress│ ← 干活中
└────┬───────┘
┌───────────┼─────────────┬───────────────┐
▼ ▼ ▼ ▼
setCompleted setFailed cancel (后端超时)
│ │ │ │
▼ ▼ ▼ ▼
completed failed cancelled timedout
两个关键事实:
pending → in_progress必须你来推(acknowledgeStartOfProcessing)。后端不会因为「事件投递成功」就自动认为你开始干了。- 终态由动作决定,不是由处理函数正常返回决定。处理函数跑完了但没调
setCompleted,实例可能还停在in_progress/pending——dispatcher 会专门警告这种情况(见 §02 章)。
3. 为什么必须 acknowledge:看门狗 + 重试
这是整个 workflow 模型最容易踩的坑,也是它最妙的设计。acknowledgeStartOfProcessing 的 JSDoc 把规则写死了(packages/sdk/src/bot/workflow-proxy/types.ts:69-81):
「This method should be called in every workflow handler as soon as the workflow starts doing work. ... Should a workflow not be acknowledged in a timely fashion, it will be retriggered 3 times before being marked as failed.」
白话拆解:
- 后端把 workflow 当看门狗任务:投了事件给你,期待你很快把它从
pending推到in_progress(= ack)。 - 你没及时 ack → 后端认为这次唤醒「卡死了」→ 重投;累计 3 次都没 ack → 直接判
failed。 - 所以 ack 不是「可选的礼貌」,而是**告诉看门狗「我活着、 我接手了」**的心跳。
它的真实实现很克制——只在「有事件、当前是 pending、且本进程还没 ack 过」时才真去打后端,否则是 no-op:
// packages/sdk/src/bot/workflow-proxy/proxy.ts:93-110(真实源码,节选)
async acknowledgeStartOfProcessing() {
if (!props.event || props.workflow.status !== 'pending' || isAcknowledged) {
return { workflow: wrapWorkflowInstance(props) } // no-op
}
const { workflow } = await props.client.updateWorkflow({
id: props.workflow.id,
status: 'in_progress',
eventId: props.event.id,
})
isAcknowledged = true
// ...
}
这段在干嘛:把 status 置 in_progress,并带上 eventId(让后端知道是哪次唤醒 ack 的);isAcknowledged 闭包变量保证一次 handler 内重复调用是幂等的。
4. 四个收尾/续命动作
都在 wrapWorkflowInstance 里(packages/sdk/src/bot/workflow-proxy/proxy.ts:112-136),实现高度一致:都是「打 updateWorkflow 改 status,再回调 onWorkflowUpdate,返回新包装的实例」。
| 动作 | status 结果 | 额外参数 | 用途 |
|---|---|---|---|
setCompleted({ output? }) | completed | 可带类型化 output | 成功收尾 |
setFailed({ failureReason }) | failed | 必填 failureReason | 出错收尾 |
cancel() | cancelled | 无 | 丢弃所有 output 并取消 |
update({ timeoutAt? / tags? / output? / userId? }) | 不变 | 至少一项 | 续命(改 timeoutAt)/ 改 tags / 写中途 output |
续命的真实用法(枚举没跑完就把超时推后 5 分钟,等下次 continued):
// plugins/file-synchronizer/src/hooks/workflow-continued/build-queue.ts(真实源码,节选)
if (enumerationState !== undefined) {
const timeIn5Minutes = new Date(Date.now() + 300_000).toISOString()
await props.workflow.update({ timeoutAt: timeIn5Minutes })
await props.states.workflow.buildQueueRuntimeState.set(props.workflow.id, { ...workflowState, enumerationState })
return
}
这段在干嘛:活没干完 → 用 update 把 timeoutAt 往后推、把当前进度存进 workflow-scoped state(以 workflow.id 为 key)、然后直接 return。后端到点会再发一个 continued 事件,下次接着跑。重点看:进度不在内存,而在 state,因为下一次是全新的 handler 调用。