工作流中的子流程节点能否驳回到父流程?

工作流中的子流程节点可以驳回到父流程,但必须先判断它是哪一种子流程。嵌入式 SubProcess 与父流程共享同一个流程实例,只是增加了一层 Execution 作用域,可以使用 Flowable 的普通状态变更 API,把子流程内的执行实例移动到外层节点;Call Activity 调用的是独立流程定义,会创建新的子流程实例,必须以子流程实例 ID 为入口,使用 moveActivityIdToParentActivityId,由引擎终止子流程实例并把父流程的 Super Execution 移到目标节点。

真正的边界不是"能不能跳",而是子流程里是否还有并行分支、多实例、边界事件和异步作业。Flowable 8.0.0 源码会校验 Call Activity 子流程实例中的所有活动 Execution 是否都进入迁移池,只迁移一个分支而遗漏其他分支会直接抛出异常,避免删除半个子流程后留下孤儿状态。

一句话结论:嵌入式子流程回到外层节点,用同一流程实例内的普通 Change State;Call Activity 子流程回到父流程,用 moveActivityIdToParentActivityId,并确保子流程实例的全部活动执行都被覆盖。

一、核心结论与问题边界

先给答案:能驳回,但两类子流程走两套路线

BPMN 图上,嵌入式子流程和 Call Activity 都像一个带"+"号的圆角矩形,但运行时完全不同。

对比项 嵌入式 SubProcess Call Activity
流程定义 定义在父流程 BPMN 内 引用外部流程定义
流程实例 与父流程共用同一个 processInstanceId 新建独立子流程实例
运行时关系 父子 Execution 作用域 child process instance + superExecution
返回外层 API moveExecutionToActivityId / moveActivityIdTo moveActivityIdToParentActivityId
子流程结束 删除嵌入作用域 终止整个子流程实例
主要风险 漏删嵌套作用域、边界资源 漏掉子流程活动 Execution、输出变量丢失

图 1:两种子流程的图形相似,但一个是同实例内跨作用域,另一个是跨流程实例迁移。

Flowable 官方文档也明确区分两者:嵌入式 SubProcess 完全定义在父流程中,并创建事件作用域;Call Activity 引用外部流程定义,父流程的 Super Execution 在调用活动处等待,直到被调用流程结束。

先弄清"父流程"到底指什么

业务人员说"退回父流程",通常可能包含三种不同目标:

  • 退回子流程之前的父流程审批节点;
  • 退出子流程,直接进入父流程后续节点;
  • 退回父流程中的某个并行分支或会签节点。

第一种会在重新完成父节点后再次进入子流程,形成新一轮子流程;第二种等于跳过子流程剩余步骤;第三种还涉及父流程并行令牌的收拢。三者的历史、变量和业务含义都不同,不能共用一个没有上下文的"驳回到父流程"按钮。

还要区分"正常结束子流程"和"异常迁移到父流程"。正常结束会沿 BPMN 顺序流离开 Call Activity,并按正常完成语义处理输出参数、监听器和后续节点;状态迁移则是直接终止当前运行状态并激活指定父节点,不会自动补跑中间路径。

二、关键概念与能力差异

嵌入式 SubProcess 怎样驳回到外层节点

嵌入式 SubProcess 与父流程处在同一个流程实例中。Flowable 官方文档说明,顺序流不能跨越 SubProcess 边界;因此在 BPMN 模型上不能直接画一条从子流程内部任务连到外层节点的线,但运行时可以通过 Change State 执行受控迁移。

以子流程内任务 subReview 驳回到外层 applyTask 为例:

java 复制代码
runtimeService.createChangeActivityStateBuilder()
    .processInstanceId(processInstanceId)
    .moveActivityIdTo("subReview", "applyTask")
    .changeState();

如果活动 ID 可能因为并行、循环或多实例同时出现多次,优先使用任务的精确 executionId

java 复制代码
runtimeService.createChangeActivityStateBuilder()
    .moveExecutionToActivityId(task.getExecutionId(), "applyTask")
    .changeState();

Flowable 8.0.0 的 ChangeStateTest.testSetCurrentActivityOutOfSubProcess 就覆盖了这一场景:测试把 subTask 移动到外层 taskBefore,随后断言子任务和 SubProcess 都产生取消事件,外层任务重新启动,运行时 Execution 数量恢复到外层结构。

图 2:嵌入式子流程没有第二个流程实例,引擎删除子流程作用域后继续使用外层 Execution。

嵌入式子流程的源码如何折叠作用域

普通 moveActivityIdTo 最终进入 AbstractDynamicStateManager.doMoveExecutionState。引擎解析当前活动与目标活动后,会判断目标是否仍处于当前 SubProcess 容器内。

如果目标已经在子流程外,deleteParentExecutions 会沿 Execution 父链向上处理:

  1. 找到当前 Execution 的 SubProcess 父作用域;
  2. 判断目标 FlowElement 是否仍是该 SubProcess 的后代;
  3. 如果不是,删除该 SubProcess 下的子 Execution 和关联运行时数据;
  4. 继续向外寻找能够承载目标节点的父 Execution;
  5. 在目标活动上创建新的任务、作业或事件订阅。

源码中的 isSubProcessAncestorOfAnyNewFlowElements 与父 Execution 删除逻辑,共同决定需要折叠多少层嵌套作用域。这也是嵌套 SubProcess 可以一次跨出多层的原因,但每多一层都会增加变量、边界事件和多实例根 Execution 的处理复杂度。

Flowable 测试还验证了带边界定时器的子流程迁移:从 subTask 移到外层 taskBefore 后,原边界 Timer Job 被取消;重新进入子流程时,引擎再按模型创建新的定时器。由此可见,驳回不是只更新活动 ID,而是一整套作用域销毁与目标行为重建。

三、模型架构与运行机制

Call Activity 为什么必须使用专门的父流程 API

Call Activity 到达时,Flowable 会启动一个新的子流程实例。父流程在 Call Activity 对应的 Super Execution 上等待,子流程任务的 processInstanceId 与父流程不同。此时普通 moveActivityIdTo 只能在子流程自己的 BPMN 模型中解析目标,找不到父流程节点。

正确入口是:

java 复制代码
runtimeService.createChangeActivityStateBuilder()
    .processInstanceId(childProcessInstanceId)
    .moveActivityIdToParentActivityId(
        "childReviewTask",
        "parentApplyTask"
    )
    .changeState();

这里有三个容易写错的参数:

  • processInstanceId 必须是被调用的子流程实例 ID,不是父流程实例 ID;
  • currentActivityId 属于子流程定义;
  • newActivityId 属于父流程定义。

Flowable 8.0.0 官方测试 ChangeStateForCallActivityTest.testSetCurrentActivityInParentProcess 使用的就是这条调用链。测试先从父流程查询出 Call Activity 创建的子流程实例,再把子流程 theTask 移到父流程 secondTask;执行后,子流程实例消失,父流程只保留一个活动 Execution,并在目标节点生成新任务。

Flowable 8.0.0 从 Builder 到父流程目标的源码链

moveActivityIdToParentActivityId 并不是普通跳转方法的别名。Flowable 8.0.0 源码会在 ChangeActivityStateBuilderImpl 中创建 MoveActivityIdContainer,并显式设置 moveToParentProcess=true

随后 AbstractDynamicStateManager 执行以下步骤:

  1. 按子流程实例 ID 和当前活动 ID解析活动 Execution;
  2. 通过子流程实例的 getSuperExecution() 找到父流程在 Call Activity 处等待的 Execution;
  3. 使用父流程的 processDefinitionId 加载父流程 BPMN 模型;
  4. 在父模型中解析目标 newActivityId
  5. 调用 safeDeleteSubProcessInstance 校验并删除子流程实例;
  6. 将待迁移对象切换为父流程的 Super Execution;
  7. 删除 Call Activity 下的旧子结构并激活父流程目标节点。

图 3:真正被移动到父节点的是 Super Execution,子流程实例在迁移前会被完整校验并终止。

ChangeActivityStateCmd 负责在 CommandContext 内校验 Builder 参数,再调用 DynamicStateManager.moveExecutionState。因此整个状态变更处在同一引擎命令事务中:目标解析失败、子流程 Execution 不完整或父流程不存在时,事务会回滚,不会留下"子流程删了但父任务没创建"的半成品。

四、核心场景与处理策略

为什么源码要求子流程的全部 Execution 都必须纳入迁移

ChangeActivityStateBuilder 的 Javadoc 对 moveActivityIdToParentActivityId 有一句非常关键的说明:子流程实例会被终止,因此所有子流程实例 Execution 都需要被移动。

源码中的 safeDeleteSubProcessInstance 会读取该子流程实例下的全部子 Execution,再逐个检查它们是否位于迁移池中:

  • 普通活动 Execution 未被包含,抛出 Following execution of sub process instance is not moved
  • 边界事件 Execution 如果没有绑定到已迁移的父活动 Execution,抛出 Unbound boundary event execution prevents...
  • 全部通过后,才调用 deleteProcessInstance 删除子流程实例。

这项校验尤其影响并行网关、多实例和事件子流程。如果子流程同时停在 legalTaskfinanceTask,只传 legalTask 无法代表整个子流程。引擎拒绝迁移,是为了防止财务分支仍在运行、子流程实例却已经被父流程回退删除。

图 4:只有迁移池覆盖全部活动 Execution,子流程实例才允许被安全删除。

并行、多实例和多层 Call Activity 应该怎样处理

1. 子流程内有并行分支

公开接口 moveActivityIdToParentActivityId 以活动 ID 为源。如果并行分支都停在同一个活动 ID,它会解析出该活动的多个 Execution;如果不同分支停在不同活动 ID,单次公开 API 不能自然表达"多个不同源活动合并到父流程一个节点"。

ChangeActivityStateBuilderImpl 内部存在接收多个活动 ID 的父流程迁移方法,但它不属于公开 ChangeActivityStateBuilder 接口。业务代码不应直接依赖内部实现类,否则小版本升级也可能破坏兼容性。更稳妥的选择是:限制跨父流程驳回只在单一活动状态使用,或在工作流平台层实现经过目标版本回归测试的专用 Command。

2. Call Activity 本身是多实例

多实例 Call Activity 会创建多个子流程实例,每个实例都有自己的 Super Execution。只从其中一个子流程实例驳回,只会处理该实例对应的父执行分支。若业务语义是"任一子流程驳回,全部实例作废",必须先收集同一多实例体下的所有子流程实例和父执行,再整体取消或收拢,不能循环调用 API 并让每个子实例都创建一个父任务。

3. 子流程又调用下一层子流程

moveActivityIdToParentActivityId 的"Parent"是直接父流程实例,不是任意祖先。如果孙流程要直接回到根流程,建议逐层设计清晰的退出协议,或者由平台解析完整 Super Execution 链后执行一次经过严格校验的专用状态变更。直接在业务服务里连续调用两次,容易在第一次提交后产生中间待办和监听器副作用。

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

目标选在 Call Activity 前后,业务含义完全不同

父流程目标位置 运行结果 典型用途 风险
Call Activity 之前 完成父节点后可能再次创建新子流程 退回申请人重新修改 重复创建子流程轮次
Call Activity 本身 重新进入调用活动并启动子流程 整轮子流程重做 版本选择和变量输入需重算
Call Activity 之后 跳过子流程剩余步骤 管理员修复、强制放行 输出变量和完成监听器可能未执行
父流程并行域内 恢复到某条父分支 复杂联合审批 需要同步其他父分支令牌

如果目标在 Call Activity 之前,旧子流程实例已经终止;父节点再次通过后启动的是新子流程实例,不应复用旧任务 ID。审批意见需要按 roundNo 记录新旧轮次。如果目标在 Call Activity 之后,则必须明确哪些子流程结果仍有效,哪些变量由平台补齐。

变量、监听器和历史为什么容易不一致

Call Activity 正常完成时,Flowable 可以通过 flowable:out 把子流程变量复制回父流程。异常迁移到父流程不是正常完成路径,不应假设所有输出映射、结束监听器和业务回调都会按原顺序执行。

建议把数据分成三类:

  • 审批意见、材料版本和操作原因写入业务数据库,不能依赖临时流程变量;
  • 父流程继续执行必需的变量,在状态变更前由同一业务事务或可靠 Outbox 显式准备;
  • 子流程局部临时变量随子流程终止,不再向父流程传播。

历史记录也不应被物理删除。旧子任务应保留"因驳回父流程而取消"的删除原因,新父任务使用新的 taskId,并通过 operationIdroundNosourceTaskIdtargetActivityId 关联。这样审计人员才能看清一次跨流程实例的状态变更。

六、工程实现与系统集成

为什么不建议先删除子流程,再单独启动父任务

有些实现会先调用 deleteProcessInstance(childId, reason),再用自定义 Command 修改父流程。这种拆分有三个问题:

  1. 两次调用可能不在同一个事务中,第二步失败后子流程已经丢失;
  2. 父流程的 Super Execution、Call Activity 订阅和实体链接可能没有按引擎规则清理;
  3. 历史删除原因与父任务创建之间缺少统一操作标识。

Flowable 的专用 API 已经把"校验子实例、删除子实例、切换 Super Execution、激活父目标"放在同一个 Dynamic State 命令内。企业二开应围绕公开 API 增加权限、幂等和业务同步,而不是绕过引擎直接改 ACT_RU_EXECUTIONACT_RU_TASK 或手工删除子流程实例。

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

工作流平台如何把跨子流程驳回产品化

低代码流程设计器应在建模阶段识别 SubProcess 与 Call Activity,并给出不同配置项:允许退回的父级范围、是否允许跨多层、子流程并行状态处理方式、父目标位置、变量映射策略和意见失效规则。

云程低代码开发平台可以在发布校验中加入几条硬约束:Call Activity 子流程存在多个活动分支时,禁止使用简单单节点父流程驳回;目标位于 Call Activity 之后但缺少必需父变量时,禁止发布;跨过父流程并行 Fork 时,要求配置整域收拢策略;子流程包含不可逆服务任务时,要求配置补偿或人工处理方案。

操作界面也不应只显示节点名称。管理员至少要看到父流程实例、子流程实例、直接 Super Execution、当前活动 Execution 数量、目标流程定义版本以及即将取消的任务和作业清单,确认后再提交。

八、案例实践与迁移路线

上线前必须覆盖的测试场景

  1. 嵌入式子流程单任务退回外层前置任务,验证 SubProcess 作用域被删除;
  2. 嵌入式子流程带边界定时器,验证原 Timer Job 取消且重入后重新创建;
  3. Call Activity 单任务退回父流程,验证子流程实例消失、父目标只创建一个任务;
  4. 错把父流程实例 ID 传给 Builder,验证请求被拒绝且状态不变;
  5. 子流程存在两个并行活动,遗漏一个 Execution 时验证事务整体回滚;
  6. 子流程活动绑定边界事件,验证未绑定边界 Execution 不会遗留;
  7. 多实例 Call Activity 中一个实例驳回,验证其他实例按产品策略保留或整体取消;
  8. 孙流程退回直接父流程和根流程,分别验证 Super Execution 链;
  9. 目标位于 Call Activity 前后两侧,验证子流程轮次和变量映射;
  10. 状态变更与业务单据同步失败,验证幂等、Outbox 和人工修复通道;
  11. 两名管理员并发执行跨流程驳回,验证乐观锁和重复提交保护;
  12. Flowable 升级后复跑官方测试拓扑及企业自定义并行、多实例用例。

测试断言不能只看"父任务出现了"。还要检查子流程实例数量、Execution 树、活动任务、定时器与事件订阅、历史删除原因、审批轮次、父变量以及外部副作用次数。

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

工作流中的子流程节点能否驳回到父流程?

可以,但必须区分嵌入式 SubProcess 与 Call Activity。嵌入式子流程和父流程属于同一个流程实例,可以使用 moveActivityIdTo 或基于精确 Execution 的 moveExecutionToActivityId,Flowable 会删除不再需要的 SubProcess 父作用域并在外层目标节点重建运行状态。Call Activity 会创建独立子流程实例,应以子流程实例 ID 调用 moveActivityIdToParentActivityId;引擎先通过 Super Execution 找到父流程,再完整删除子流程实例并激活父流程目标。

Call Activity 子流程存在并行、多实例或边界事件时,不能只迁移当前一个任务。Flowable 8.0.0 的 safeDeleteSubProcessInstance 会校验子流程实例的所有活动 Execution 是否都纳入迁移池,遗漏任何普通活动 Execution 或未绑定的边界事件 Execution 都会抛出异常并回滚。企业实现还必须同步处理审批轮次、历史意见、父子变量、外部副作用和父流程并行令牌,不能直接修改运行时数据库,也不能把删除子流程与启动父任务拆成两个独立事务。

相关推荐
愚农搬码2 小时前
流程图上的回退线与运行时动态回退,应该选择哪一种?
工作流引擎
大龄码农有梦想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