流程图上的回退线与运行时动态回退,应该选择哪一种?

流程图上的回退线与运行时动态回退不是两种画法,而是两种不同的执行语义。前者把"退回"建模成 BPMN 的正常业务路径:当前任务正常完成,Flowable 计算条件 Sequence Flow,执行沿连线回到目标节点。后者不依赖两节点之间存在连线,而是通过 Change Activity State API 取消当前活动、调整 Execution 树,再从指定目标节点继续执行。

企业选型可以先记住一句话:固定、可预期、属于业务规则的退回,优先画在流程模型中;目标由运行时数据、历史轨迹或管理员决定的任意回退,使用动态状态迁移。 不要为了"灵活"把所有回退都做成动态跳转,也不要为了"可视化"给每两个审批节点之间都画一条回退线。

本文以截至 2026 年 8 月最新正式版 Flowable 8.0.0 为例,结合 UserTaskActivityBehaviorTakeOutgoingSequenceFlowsOperationChangeActivityStateCmdAbstractDynamicStateManager 和官方 ChangeStateTest 说明两条路线在源码、监听器、历史、网关、多实例和平台产品化方面的真实边界。

一句话结论:流程图回退线是"正常完成当前节点后按 BPMN 再走一遍";动态回退是"取消当前活动并把运行状态搬到目标节点"。业务语义固定时选前者,运行时目标不固定时选后者。

一、核心结论与问题边界

先给答案:固定业务回路画线,任意节点回退动态迁移

很多流程设计器把"退回"做成一个按钮,看起来只是选择目标节点。对引擎而言,按钮背后至少有两种完全不同的实现。

判断维度 流程图显式回退线 运行时动态回退
目标确定时间 流程设计时确定 流程运行时确定
引擎语义 正常完成当前活动并沿 Sequence Flow 运行 取消当前活动并重建目标运行状态
是否要求模型有连线 不要
条件表达式、默认流 按 BPMN 正常计算 不经过中间 Sequence Flow
当前活动事件 通常是正常完成 Flowable 状态迁移会产生取消语义
典型用途 驳回申请人、资料补正、固定复核闭环 任意节点退回、管理员纠错、按历史选择目标

推荐采用"三层决策"而不是二选一:

  1. 流程定义层:少量稳定回路直接建模,例如"部门审批驳回到申请人修改"。
  2. 运行时能力层:提供受约束的动态回退,只允许回到合法历史节点或配置范围。
  3. 业务审计层:无论使用哪条路线,都记录操作人、源任务、目标节点、原因、审批轮次和策略版本。

图 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 中,正常路径会依次处理:

  1. 清理当前活动关联的边界事件 Execution;
  2. 执行活动 end Execution Listener;
  3. 记录活动结束并派发 ACTIVITY_COMPLETED
  4. 遍历所有 outgoing Sequence Flow;
  5. 通过 ConditionUtil.hasTrueCondition 判断条件,必要时选 default flow;
  6. 把 Execution 的 currentFlowElement 设置为被选中的 Sequence Flow;
  7. 为并行的其他出边创建 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 为例,主要步骤是:

  1. 根据 executionId 或 activityId 解析活动 Execution,并解析目标 FlowElement;
  2. 清理源 Execution 的子 Execution,例如边界定时器和事件订阅;
  3. Change activity to ... 为删除原因调用 deleteExecutionAndRelatedData
  4. 删除不再需要的父作用域,或创建目标所需的嵌套 SubProcess 作用域;
  5. 在目标 FlowElement 上创建新的子 Execution;
  6. 设置流程变量和目标局部变量;
  7. 调用 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();

多实例还要确定是撤销当前实例、结束全部实例,还是保留已完成成员并新开一轮。nrOfInstancesnrOfActiveInstancesnrOfCompletedInstances 和局部变量需要与业务规则一起验证。不能把普通单任务代码直接复用到会签。

五、数据、规则与状态设计

子流程、边界事件和异步作业怎样处理

动态回退跨越嵌入式 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 是否正确恢复。

回退线很多时,模型为什么会失控

业务人员常提出"每个节点都能退回之前任意节点"。如果直接画线,模型节点数量不变,连线数量却会快速增长。更严重的是,这些线会穿越网关、子流程边界和事件作用域,使流程图难以表达真实令牌语义。

可以用三个规则控制模型复杂度:

  1. 只把制度明确、频率高、目标唯一的退回画成 BPMN 路径。
  2. 回退线尽量汇聚到"退回路由网关",不要从每个节点交叉连接所有前序节点。
  3. 任意历史节点选择放到运行时策略层,设计器只展示"允许动态回退"的配置,不生成大量 Sequence Flow。

显式建模的价值是可理解,而不是让所有运行时可能性都出现在一张静态图上。真正的企业流程平台应同时提供模型视图、运行时轨迹视图和操作审计视图。

六、工程实现与系统集成

应该怎样设计统一的回退命令服务

无论底层选哪种方式,前端都不应直接调用 Flowable API。建议用一个领域命令统一承载审批动作:

java 复制代码
public record RollbackCommand(
    String taskId,
    String operatorId,
    String targetActivityId,
    RollbackMode mode,
    String reason,
    long expectedVersion
) {}

命令服务按以下顺序执行:

  1. 查询并锁定当前任务及业务单据,校验 expectedVersion
  2. 解析流程定义、Execution 树、历史轨迹和可回退目标;
  3. 根据策略选择 MODELED_PATHDYNAMIC_CHANGE_STATE
  4. 对并行、多实例、子流程和边界事件执行专门校验;
  5. 写入审批意见、回退原因、源目标节点、审批轮次和操作幂等键;
  6. 调用 taskService.complete 或 Change Activity State API;
  7. 同步业务单据状态,或写 Outbox 事件;
  8. 重新查询运行时状态,验证新待办数量和目标节点。

平台还应把"驳回""退回""撤回""管理员跳转"拆成不同权限和审计类型。底层可能都使用动态状态迁移,但业务语义、允许目标和副作用不能混在一个万能接口中。

一张决策表:企业到底应该选择哪一种

业务场景 推荐方式 原因
审批驳回后固定回申请人修改 显式回退线 属于稳定业务规则,模型可见
资料不全,固定回材料补正节点 显式回退线 可按正常完成语义形成闭环
审批人从已走历史中任选一个节点 动态回退 目标只有运行时才能确定
管理员修复误操作或异常实例 动态回退 属于运维状态修复,不应污染业务模型
并行审批任一分支驳回后全体重做 专门动态策略或重构模型 需要整体处理多个 Execution
会签只退回当前审批人重办 多实例专门策略 不能套用普通串行迁移
跨 Call Activity 子流程退回父流程 父子流程专用 Change State API 涉及独立流程实例和 Super Execution
审计法规要求路径完整可视 优先显式建模 模型和正常运行轨迹更易解释
流程模板频繁变化、节点数量很多 受约束动态回退 避免连线爆炸,但必须加强校验和审计

如果某个动作同时满足"目标固定"和"属于正常业务规则",默认选显式回退线。如果同时满足"目标运行时选择""跨作用域"或"管理员修复",默认选动态回退。两者都复杂时,先重新审视流程边界,而不是立即增加更多跳转代码。

图 4:先判断业务语义是否固定,再判断目标、Execution 数量和作用域是否动态。

七、安全、性能与治理要求

低代码流程平台应该怎样把两种能力产品化

低代码流程设计器可以在建模阶段提供两类配置:一种是"固定驳回路径",自动生成可读的网关和 Sequence Flow;另一种是"动态回退策略",配置允许返回的节点类型、历史范围、是否跨子流程、并行分支处理方式和意见必填规则。

运行界面不应只显示节点名称。操作人至少要看到目标节点所属审批轮次、原办理人、流程作用域、当前并行分支和回退后的影响范围。管理员还应看到 Execution 树和将被取消的任务、Timer Job、Event Subscription。

云程低代码开发平台可以在 Flowable 之上把设计态回路、运行时迁移、审批意见、数据权限、业务状态同步和审计日志组合成一个完整能力。平台价值不在于包装一个 moveActivityIdTo 按钮,而在于限制危险目标、处理复杂 Execution 结构并给出可解释的结果。

八、案例实践与迁移路线

上线前必须覆盖哪些测试

  1. 固定驳回线的通过、驳回、补正后再次提交路径均可结束。
  2. 显式回退经过排他网关时,条件互斥且存在默认策略。
  3. 动态回退后源任务历史原因、目标新任务和审批轮次正确。
  4. 源节点正常完成监听器不会被误当成动态取消监听器。
  5. 目标节点开始监听器、候选人和表单权限重新计算。
  6. 两名管理员并发回退同一任务时,只有一个请求成功。
  7. 并行分支单分支保留、全部撤销和整体重做三种策略分别验证。
  8. 多实例会签校验计数变量、局部变量和已完成人员处理。
  9. 嵌入式 SubProcess 与 Call Activity 分别验证作用域和父子实例。
  10. 边界 Timer、Message、Signal 和异步 Job 不残留、不重复。
  11. 回退到 Service Task、网关、结束事件等危险目标时被策略拒绝。
  12. Flowable 升级后回归官方 Change State 用例和平台自定义复杂场景。

测试断言不能只看"出现了一个目标待办"。还要检查活动 Execution 数量、任务数量、历史 delete reason、业务单据状态、定时器、事件订阅、审批意见和并发幂等结果。

九、平台落地、测试与选型

标准答案:流程图回退线与动态回退怎样选

流程图上的回退线适合表达稳定、可预期、属于业务流程本身的回路。当前用户任务通过 taskService.complete 正常完成,Flowable 8.0.0 随后由 TakeOutgoingSequenceFlowsOperation 执行 end listener、派发完成事件、计算条件和默认 Sequence Flow,再沿模型回到目标节点。它的优势是业务可视、语义完整、路径容易测试。

运行时动态回退适合目标节点由历史轨迹、用户选择或管理员修复决定的场景。ChangeActivityStateBuilderChangeActivityStateCmd 进入 AbstractDynamicStateManager,取消源活动、清理 Execution 及关联资源、重建目标作用域,然后从目标节点继续。它不会逐条经过源目标之间的 Sequence Flow,因此不能假设中间网关、服务任务和正常完成监听器已经执行。

企业最佳实践不是只选一种,而是建立边界:固定闭环显式建模,任意回退受控迁移;串行任务可使用 Execution 级动态回退,并行、多实例、子流程和边界事件必须采用专门策略;所有回退都进入统一命令服务,记录源目标、原因、审批轮次、影响范围和幂等信息。

如果只记住三句话:

  • 业务规则固定,画线;运行目标动态,迁移。
  • 动态回退是取消并重建状态,不是补走一条隐藏连线。
  • 并行、多实例和子流程的回退先处理 Execution 树,再考虑界面按钮。
相关推荐
大龄码农有梦想6 小时前
开源流程引擎 Camunda 如何实现任意节点跳转?
工作流引擎·流程引擎·camunda·oa·bpn·流程跳转·会签
愚农搬码3 天前
在开源流程引擎 Flowable 里如何实现任意节点跳转?
工作流引擎
Behavior8 天前
一条指令,让 Claude Code 每天定时帮你追热点:动态工作流Workflows从 0 到 1 全流程
claude·workflow·工作流引擎
每天都是不一样的太阳11 天前
别让 AI Agent 先画靶再射箭:一套「结论忠于数据」的证据链工作流
agent·工作流引擎
怕浪猫13 天前
Agent 编排 Agent:DeepSeek Harness 的子代理与工作流系统有多强
agent·工作流引擎·deepseek
腾讯云开发者13 天前
一位教授与WorkBuddy的几个月,看看擦出了什么样的火花
工作流引擎
愚农搬码15 天前
Flowable会签实现:多实例任务的底层原理
工作流引擎
玹外之音24 天前
别做"AI 翻译"了,做"AI 本地化":跨境电商 Listing 系统的工程实践
aigc·工作流引擎·aiops
梦想很大很大24 天前
Workrun 进度更新:从理念到解决具体问题
agent·workflow·工作流引擎