流程图上的回退线与运行时动态回退不是两种画法,而是两种不同的执行语义。前者把"退回"建模成 BPMN 的正常业务路径:当前任务正常完成,Flowable 计算条件 Sequence Flow,执行沿连线回到目标节点。后者不依赖两节点之间存在连线,而是通过 Change Activity State API 取消当前活动、调整 Execution 树,再从指定目标节点继续执行。
企业选型可以先记住一句话:固定、可预期、属于业务规则的退回,优先画在流程模型中;目标由运行时数据、历史轨迹或管理员决定的任意回退,使用动态状态迁移。 不要为了"灵活"把所有回退都做成动态跳转,也不要为了"可视化"给每两个审批节点之间都画一条回退线。
本文以截至 2026 年 8 月最新正式版 Flowable 8.0.0 为例,结合 UserTaskActivityBehavior、TakeOutgoingSequenceFlowsOperation、ChangeActivityStateCmd、AbstractDynamicStateManager 和官方 ChangeStateTest 说明两条路线在源码、监听器、历史、网关、多实例和平台产品化方面的真实边界。
一句话结论:流程图回退线是"正常完成当前节点后按 BPMN 再走一遍";动态回退是"取消当前活动并把运行状态搬到目标节点"。业务语义固定时选前者,运行时目标不固定时选后者。
一、核心结论与问题边界
先给答案:固定业务回路画线,任意节点回退动态迁移
很多流程设计器把"退回"做成一个按钮,看起来只是选择目标节点。对引擎而言,按钮背后至少有两种完全不同的实现。
| 判断维度 | 流程图显式回退线 | 运行时动态回退 |
|---|---|---|
| 目标确定时间 | 流程设计时确定 | 流程运行时确定 |
| 引擎语义 | 正常完成当前活动并沿 Sequence Flow 运行 | 取消当前活动并重建目标运行状态 |
| 是否要求模型有连线 | 要 | 不要 |
| 条件表达式、默认流 | 按 BPMN 正常计算 | 不经过中间 Sequence Flow |
| 当前活动事件 | 通常是正常完成 | Flowable 状态迁移会产生取消语义 |
| 典型用途 | 驳回申请人、资料补正、固定复核闭环 | 任意节点退回、管理员纠错、按历史选择目标 |
推荐采用"三层决策"而不是二选一:
- 流程定义层:少量稳定回路直接建模,例如"部门审批驳回到申请人修改"。
- 运行时能力层:提供受约束的动态回退,只允许回到合法历史节点或配置范围。
- 业务审计层:无论使用哪条路线,都记录操作人、源任务、目标节点、原因、审批轮次和策略版本。

图 1:一条是 BPMN 正常路径,另一条是 Execution 状态迁移,不能只看界面上的"退回"按钮。
两种"回退"到底分别是什么
1. 流程图上的回退线
流程图回退线是从当前活动或网关指向上游活动的普通 BPMN Sequence Flow。它不是 Flowable 特有元素,也不是引擎内置的"驳回线"。通常由审批动作变量和排他网关共同决定是否走回退路径。
例如"经理审批"完成时写入 approvalAction=REJECT,排他网关选择返回"申请人修改"的 Sequence Flow。对引擎而言,这次经理任务仍然是正常完成,只是下一步走向了上游节点。
2. 运行时动态回退
运行时动态回退是应用在流程实例运行期间直接改变活动状态。Flowable 8.0.0 的公开入口是 RuntimeService.createChangeActivityStateBuilder(),常用方法包括:
moveExecutionToActivityId(executionId, activityId):精确移动某一个 Execution;moveActivityIdTo(currentActivityId, newActivityId):按活动 ID 解析当前活动并迁移;moveExecutionsToSingleActivityId(executionIds, activityId):把多个并发 Execution 汇聚到一个目标;moveSingleExecutionToActivityIds(executionId, activityIds):把一个 Execution 拆到多个目标;moveActivityIdToParentActivityId:从 Call Activity 子流程实例回到父流程节点。
它更接近流程实例的"状态手术",而不是一条隐藏的 Sequence Flow。目标节点可以与当前节点没有任何直接连线,中间网关、服务任务和条件表达式也不会被逐一执行。
二、关键概念与能力差异
显式回退线在 Flowable 8 中怎样运行
假设流程为"申请人填写 -> 经理审批 -> 结果网关",审批通过走归档,审批驳回走回申请人填写。模型可以使用如下条件流:
xml
<sequenceFlow id="rejectToDraft"
sourceRef="approvalResultGateway"
targetRef="draftTask">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[
${approvalAction == 'REJECT'}
]]></conditionExpression>
</sequenceFlow>
业务代码完成当前任务时提交动作变量:
java
Map<String, Object> variables = new HashMap<>();
variables.put("approvalAction", "REJECT");
variables.put("approvalRound", currentRound + 1);
taskService.complete(taskId, variables);
这段代码没有调用任何"回退 API"。任务完成后,UserTaskActivityBehavior.trigger 检查该 Execution 下的用户任务已经删除,然后调用 leave(execution)。随后引擎进入正常的 Agenda 操作,离开当前 FlowNode 并计算出边连线。
在 Flowable 8.0.0 的 TakeOutgoingSequenceFlowsOperation 中,正常路径会依次处理:
- 清理当前活动关联的边界事件 Execution;
- 执行活动 end Execution Listener;
- 记录活动结束并派发
ACTIVITY_COMPLETED; - 遍历所有 outgoing Sequence Flow;
- 通过
ConditionUtil.hasTrueCondition判断条件,必要时选 default flow; - 把 Execution 的
currentFlowElement设置为被选中的 Sequence Flow; - 为并行的其他出边创建 Execution,并通过 Agenda 继续运行。
因此,回退线走的是完整 BPMN 生命周期。条件、默认流、异步离开、作用域销毁和正常完成事件都属于模型语义的一部分。

图 2:显式回退线先正常完成当前任务,再计算条件流并沿 BPMN 回到上游。
为什么固定业务回路更适合画在流程图上
如果"驳回后必须回申请人修改"是制度的一部分,把它显式建模有四个明显优点。
第一,业务人员能够从 BPMN 图直接看见闭环。模型本身就是流程合同,流程审计、测试设计和需求评审不必依赖一段隐藏在后端服务里的跳转代码。
第二,正常完成语义更清晰。审批人已经作出"不同意"的决定,当前审批任务可以正常完成,后续由条件流决定进入补正环节。它与"管理员发现流程跑错了,需要修复实例"不是同一类动作。
第三,路径上的 BPMN 能力能够复用。排他网关条件、默认流、Sequence Flow 监听逻辑、异步离开和后续服务任务会按模型执行,不需要业务代码重新补齐。
第四,测试更容易穷举。固定回路只有少量明确路径,可以直接编写"通过、驳回、补正后再次提交"的流程测试。
但回退线不是越多越好。一个流程有 12 个审批节点,如果每个节点都能回到任意前序节点,最多会形成大量交叉连线。模型会从业务地图变成线路网,设计器难读,流程验证、版本升级和网关令牌分析也更困难。
三、模型架构与运行机制
动态回退应该怎样调用 Flowable API
对于"从当前任务退回到实际走过的任意前序审批节点",目标只有在运行时读取历史轨迹后才能确定。此时更适合使用 Execution 级迁移:
java
Task currentTask = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(currentTask.getProcessInstanceId())
.processVariable("rollbackReason", reason)
.processVariable("rollbackRound", rollbackRound + 1)
.moveExecutionToActivityId(
currentTask.getExecutionId(),
targetActivityId
)
.changeState();
在普通串行用户任务中,moveActivityIdTo(currentActivityId, targetActivityId) 也能工作。不过企业平台最好优先使用当前任务绑定的 executionId,因为并行、多实例或重复活动场景下,仅用 activityId 可能无法准确表达"移动哪一个令牌"。
调用前至少应完成以下校验:
- 当前任务仍然存在,且操作人有权执行回退;
executionId、任务 ID、流程实例 ID 相互匹配;- 目标
activityId存在于当前流程定义或合法父子流程范围; - 目标属于可回退节点集合,而不是任意 Service Task、网关或结束事件;
- 并行、多实例、子流程和边界事件使用专门策略;
- 同一任务的操作版本号或业务幂等键未变化。
Flowable 8 动态回退源码真正做了什么
ChangeActivityStateBuilderImpl.changeState() 最终调用 RuntimeService.changeActivityState。引擎创建 ChangeActivityStateCmd,在同一个 CommandContext 中完成校验与状态修改。
ChangeActivityStateCmd.execute 的职责很克制:校验至少提供了一项 move 或 enable 配置,按活动 ID 迁移时要求提供流程实例 ID,然后取得 DynamicStateManager,调用 moveExecutionState。
核心逻辑位于 AbstractDynamicStateManager.doMoveExecutionState。以普通用户任务从 managerReview 回到 draftTask 为例,主要步骤是:
- 根据 executionId 或 activityId 解析活动 Execution,并解析目标 FlowElement;
- 清理源 Execution 的子 Execution,例如边界定时器和事件订阅;
- 以
Change activity to ...为删除原因调用deleteExecutionAndRelatedData; - 删除不再需要的父作用域,或创建目标所需的嵌套 SubProcess 作用域;
- 在目标 FlowElement 上创建新的子 Execution;
- 设置流程变量和目标局部变量;
- 调用
planContinueProcessOperation,从目标节点开始执行。
Flowable 8.0.0 的 ExecutionEntityManagerImpl.deleteExecutionAndRelatedData 会记录源活动结束原因;当 cancel=true 时,还会派发活动取消事件。官方 ChangeStateTest 也明确断言状态迁移先产生 ACTIVITY_CANCELLED,再对目标作用域产生 ACTIVITY_STARTED。
这说明动态回退并不是"把下一条线改成向后"。它取消源活动、整理 Execution 树、重建目标运行环境,然后从目标节点开始。源节点与目标节点之间的 Sequence Flow 没有被逐条经过。
图 3:Change State 直接重建目标运行状态,中间 Sequence Flow 和网关不会补走。
四、核心场景与处理策略
两条路线在监听器、历史和业务副作用上有什么不同
这是企业实施中最容易踩坑的部分。两种路线最后都能看到"目标节点出现了新待办",但过程副作用并不相同。
| 行为 | 显式回退线 | 动态回退 |
|---|---|---|
| 当前任务 | 通过 taskService.complete 正常完成 |
状态迁移清理并取消当前活动 |
| 源活动 end listener | 按正常离开链路执行 | 不应假设按正常完成链路执行 |
| 引擎活动事件 | ACTIVITY_COMPLETED |
源活动通常为 ACTIVITY_CANCELLED |
| Sequence Flow 条件 | 正常判断 | 源到目标之间不判断 |
| 中间服务任务 | 按模型执行 | 不执行 |
| 目标节点 | 沿连线到达并启动 | 直接创建 Execution 后启动 |
因此,不要把关键业务副作用只放在"任务正常完成监听器"里。例如审批通过才应该扣减额度,而动态回退取消任务时不能误触发;又如回退时必须解锁业务单据,也不能仅等待某条 BPMN 回退线执行。
更稳妥的做法是把副作用分成三类:正常完成动作、回退补偿动作和目标节点进入动作。由平台的审批命令服务显式编排,并用 Outbox 或同一业务事务保证流程状态与业务单据状态一致。
并行网关和多实例下为什么不能只看一条回退线
串行流程中的回退容易产生"移动一个任务即可"的错觉。进入并行网关或多实例会签后,流程实例中同时存在多个活动 Execution,回退策略必须回答其他令牌怎么办。
1. 显式回退线的风险
如果一个并行分支直接画线回到 Fork 之前,另一个分支已经在 Join 等待,重新经过 Fork 可能再创建一组并行令牌,产生重复任务或难以收敛的 Join。模型回路必须与分支的 Fork、Join 范围一起设计,不能只连接两个用户任务。
2. 动态回退的风险
如果只移动发起驳回的一个 Execution,其他分支可能继续运行或停在 Join;如果业务要求"任一分支驳回,整组审批重做",就要先收拢相关 Execution,再统一迁移:
java
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(processInstanceId)
.moveExecutionsToSingleActivityId(
activeBranchExecutionIds,
targetActivityId
)
.changeState();
多实例还要确定是撤销当前实例、结束全部实例,还是保留已完成成员并新开一轮。nrOfInstances、nrOfActiveInstances、nrOfCompletedInstances 和局部变量需要与业务规则一起验证。不能把普通单任务代码直接复用到会签。
五、数据、规则与状态设计
子流程、边界事件和异步作业怎样处理
动态回退跨越嵌入式 SubProcess 时,AbstractDynamicStateManager 会删除不再需要的父作用域,并在进入目标时重建缺失的嵌套作用域。跨 Call Activity 的独立子流程实例则不能使用普通 moveActivityIdTo,需要使用父子流程专用 API,并覆盖子流程实例的全部活动 Execution。
边界定时器、消息订阅、信号订阅和异步作业也不是附属数据。显式回退线正常离开活动时,TakeOutgoingSequenceFlowsOperation.cleanupExecutions 会清理活动关联的边界 Execution;动态迁移则在删除源 Execution 和创建目标 Execution 时重建目标所需资源。
需要重点测试:
- 从带定时器边界事件的节点回退,旧 Timer Job 是否删除;
- 再次进入目标节点后,新 Timer Job 是否只创建一份;
- 从消息捕获事件回退,Event Subscription 是否解除;
- 目标为异步 Service Task 时,Job 是否正确创建且不会重复;
- Call Activity 子流程是否完整终止,父流程 Super Execution 是否正确恢复。
回退线很多时,模型为什么会失控
业务人员常提出"每个节点都能退回之前任意节点"。如果直接画线,模型节点数量不变,连线数量却会快速增长。更严重的是,这些线会穿越网关、子流程边界和事件作用域,使流程图难以表达真实令牌语义。
可以用三个规则控制模型复杂度:
- 只把制度明确、频率高、目标唯一的退回画成 BPMN 路径。
- 回退线尽量汇聚到"退回路由网关",不要从每个节点交叉连接所有前序节点。
- 任意历史节点选择放到运行时策略层,设计器只展示"允许动态回退"的配置,不生成大量 Sequence Flow。
显式建模的价值是可理解,而不是让所有运行时可能性都出现在一张静态图上。真正的企业流程平台应同时提供模型视图、运行时轨迹视图和操作审计视图。
六、工程实现与系统集成
应该怎样设计统一的回退命令服务
无论底层选哪种方式,前端都不应直接调用 Flowable API。建议用一个领域命令统一承载审批动作:
java
public record RollbackCommand(
String taskId,
String operatorId,
String targetActivityId,
RollbackMode mode,
String reason,
long expectedVersion
) {}
命令服务按以下顺序执行:
- 查询并锁定当前任务及业务单据,校验
expectedVersion; - 解析流程定义、Execution 树、历史轨迹和可回退目标;
- 根据策略选择
MODELED_PATH或DYNAMIC_CHANGE_STATE; - 对并行、多实例、子流程和边界事件执行专门校验;
- 写入审批意见、回退原因、源目标节点、审批轮次和操作幂等键;
- 调用
taskService.complete或 Change Activity State API; - 同步业务单据状态,或写 Outbox 事件;
- 重新查询运行时状态,验证新待办数量和目标节点。
平台还应把"驳回""退回""撤回""管理员跳转"拆成不同权限和审计类型。底层可能都使用动态状态迁移,但业务语义、允许目标和副作用不能混在一个万能接口中。
一张决策表:企业到底应该选择哪一种
| 业务场景 | 推荐方式 | 原因 |
|---|---|---|
| 审批驳回后固定回申请人修改 | 显式回退线 | 属于稳定业务规则,模型可见 |
| 资料不全,固定回材料补正节点 | 显式回退线 | 可按正常完成语义形成闭环 |
| 审批人从已走历史中任选一个节点 | 动态回退 | 目标只有运行时才能确定 |
| 管理员修复误操作或异常实例 | 动态回退 | 属于运维状态修复,不应污染业务模型 |
| 并行审批任一分支驳回后全体重做 | 专门动态策略或重构模型 | 需要整体处理多个 Execution |
| 会签只退回当前审批人重办 | 多实例专门策略 | 不能套用普通串行迁移 |
| 跨 Call Activity 子流程退回父流程 | 父子流程专用 Change State API | 涉及独立流程实例和 Super Execution |
| 审计法规要求路径完整可视 | 优先显式建模 | 模型和正常运行轨迹更易解释 |
| 流程模板频繁变化、节点数量很多 | 受约束动态回退 | 避免连线爆炸,但必须加强校验和审计 |
如果某个动作同时满足"目标固定"和"属于正常业务规则",默认选显式回退线。如果同时满足"目标运行时选择""跨作用域"或"管理员修复",默认选动态回退。两者都复杂时,先重新审视流程边界,而不是立即增加更多跳转代码。

图 4:先判断业务语义是否固定,再判断目标、Execution 数量和作用域是否动态。
七、安全、性能与治理要求
低代码流程平台应该怎样把两种能力产品化
低代码流程设计器可以在建模阶段提供两类配置:一种是"固定驳回路径",自动生成可读的网关和 Sequence Flow;另一种是"动态回退策略",配置允许返回的节点类型、历史范围、是否跨子流程、并行分支处理方式和意见必填规则。
运行界面不应只显示节点名称。操作人至少要看到目标节点所属审批轮次、原办理人、流程作用域、当前并行分支和回退后的影响范围。管理员还应看到 Execution 树和将被取消的任务、Timer Job、Event Subscription。
云程低代码开发平台可以在 Flowable 之上把设计态回路、运行时迁移、审批意见、数据权限、业务状态同步和审计日志组合成一个完整能力。平台价值不在于包装一个 moveActivityIdTo 按钮,而在于限制危险目标、处理复杂 Execution 结构并给出可解释的结果。
八、案例实践与迁移路线
上线前必须覆盖哪些测试
- 固定驳回线的通过、驳回、补正后再次提交路径均可结束。
- 显式回退经过排他网关时,条件互斥且存在默认策略。
- 动态回退后源任务历史原因、目标新任务和审批轮次正确。
- 源节点正常完成监听器不会被误当成动态取消监听器。
- 目标节点开始监听器、候选人和表单权限重新计算。
- 两名管理员并发回退同一任务时,只有一个请求成功。
- 并行分支单分支保留、全部撤销和整体重做三种策略分别验证。
- 多实例会签校验计数变量、局部变量和已完成人员处理。
- 嵌入式 SubProcess 与 Call Activity 分别验证作用域和父子实例。
- 边界 Timer、Message、Signal 和异步 Job 不残留、不重复。
- 回退到 Service Task、网关、结束事件等危险目标时被策略拒绝。
- Flowable 升级后回归官方 Change State 用例和平台自定义复杂场景。
测试断言不能只看"出现了一个目标待办"。还要检查活动 Execution 数量、任务数量、历史 delete reason、业务单据状态、定时器、事件订阅、审批意见和并发幂等结果。
九、平台落地、测试与选型
标准答案:流程图回退线与动态回退怎样选
流程图上的回退线适合表达稳定、可预期、属于业务流程本身的回路。当前用户任务通过 taskService.complete 正常完成,Flowable 8.0.0 随后由 TakeOutgoingSequenceFlowsOperation 执行 end listener、派发完成事件、计算条件和默认 Sequence Flow,再沿模型回到目标节点。它的优势是业务可视、语义完整、路径容易测试。
运行时动态回退适合目标节点由历史轨迹、用户选择或管理员修复决定的场景。ChangeActivityStateBuilder 经 ChangeActivityStateCmd 进入 AbstractDynamicStateManager,取消源活动、清理 Execution 及关联资源、重建目标作用域,然后从目标节点继续。它不会逐条经过源目标之间的 Sequence Flow,因此不能假设中间网关、服务任务和正常完成监听器已经执行。
企业最佳实践不是只选一种,而是建立边界:固定闭环显式建模,任意回退受控迁移;串行任务可使用 Execution 级动态回退,并行、多实例、子流程和边界事件必须采用专门策略;所有回退都进入统一命令服务,记录源目标、原因、审批轮次、影响范围和幂等信息。
如果只记住三句话:
- 业务规则固定,画线;运行目标动态,迁移。
- 动态回退是取消并重建状态,不是补走一条隐藏连线。
- 并行、多实例和子流程的回退先处理 Execution 树,再考虑界面按钮。