Flowable 会签的技术基础是 BPMN 多实例活动:引擎进入节点时解析人员集合,为集合中的每个元素创建一个活动实例,并用多实例根执行统一维护总数、活动数和完成数。并行会签一次创建多个子执行,顺序会签始终只保持一个活动实例;每完成一个任务,引擎都会更新计数、判断 completionCondition,满足条件后结束整个多实例活动。
但"多实例任务"不等于"完整会签"。Flowable 只负责重复执行、运行时任务和结束条件,赞成票、反对票、弃权、权重、动态加签、审批意见和责任证据仍应由审批领域服务定义。理解这一边界,才能避免把 nrOfCompletedInstances 错当成赞成票数,也能解释为什么并发会签最容易在计票、监听器和动态参与者上出错。
一、核心结论与问题边界
一句话理解 Flowable 多实例任务
多实例活动可以理解为工作流引擎里的 for each:对人员集合中的每个元素执行一次 User Task。它可以并行执行,也可以顺序执行;可以等待全部实例完成,也可以用 completionCondition 提前结束。
| 概念 | Flowable 负责什么 | 会签平台还要负责什么 |
|---|---|---|
| 人员集合 | 读取 collection 并逐项绑定 | 组织解析、去重、回避和人员快照 |
| 执行方式 | 并行或顺序创建活动实例 | 会签类型、轮次和前后加签语义 |
| 完成判断 | 计算实例完成数并执行表达式 | 赞成、反对、弃权、权重和一票否决 |
| 运行时任务 | 创建、完成和删除 User Task | 意见、签名、附件、责任链和通知 |
| 历史 | 保存任务与活动历史 | 可解释的审批记录和合规证据 |
因此,多实例是会签的执行容器,不是会签的业务规则引擎。
源码里的核心设计:外层行为包装内层任务
Flowable 当前源码中的 MultiInstanceActivityBehavior 是一个抽象行为,直接子类是 ParallelMultiInstanceBehavior 和 SequentialMultiInstanceBehavior。它并不重新实现 User Task,而是包装原活动的 ActivityBehavior:外层负责循环、执行树、计数和结束条件,内层仍按普通用户任务逻辑创建待办、等待用户完成。
这个设计带来两个重要结论:
- 多实例特性可以附加到 User Task、Service Task、SubProcess、Call Activity 等活动上;
- 每个实例仍是一次真实活动执行,不是一张任务表里简单复制几行数据。
官方文档也明确说明,Gateway 和 Event 不能直接变成多实例。原因是多实例针对"可执行活动"进行重复,而网关负责路由、事件负责捕获或抛出流程事件。
一张图看懂多实例执行树

图 1:多实例根执行保存整体计数,子执行保存 loopCounter 和当前集合元素;并行模式下多个子任务同时存在。
流程令牌到达会签节点后,会形成一个多实例根执行。nrOfInstances、nrOfActiveInstances 和 nrOfCompletedInstances 属于整体循环语义;每个子执行拥有自己的 loopCounter,以及由 elementVariable 指定的当前人员,例如 assignee。
并行三人会签通常会看到一个多实例根执行、三个子执行和三个运行时任务。任务完成后,运行时任务记录消失,活动及任务历史进入历史数据;只有整个多实例满足结束条件,令牌才会离开会签节点。
二、关键概念与能力差异
BPMN 配置怎样变成运行时实例
多实例 User Task 的核心配置可以归纳为下面几项:
| 配置 | 典型值 | 运行时含义 |
|---|---|---|
| isSequential | false 或 true | false 并行,true 顺序 |
| collection | assigneeList | 参与者集合或返回集合的表达式 |
| elementVariable | assignee | 当前集合元素的局部变量名 |
| assignee | ${assignee} | 用当前元素分配每个任务 |
| indexVariable | loopCounter | 当前实例下标,可自定义变量名 |
| completionCondition | 布尔表达式 | 每个实例结束后判断是否结束整体活动 |
进入活动时,引擎解析 loopCardinality 或 collection。官方文档说明,实例数量在进入活动时计算一次;如果 collection 是 assigneeList,初始集合有三人,nrOfInstances 就从三开始。后续修改普通流程变量里的列表,并不会自动生成第四个执行实例。
这也是"修改人员列表"和"动态加签"不能混为一谈的原因。真正的运行中加签需要调用受支持的多实例运行时 API,或通过审批领域服务建立新的会签轮次。
并行会签和顺序会签的底层差异
并行多实例会先创建全部子执行,再逐个调度原活动行为。Flowable 源码特别把"创建全部执行"和"执行原行为"分成两个循环,避免无等待活动执行过快时对子执行进行裁剪,造成集合尚未完整创建就被提前结束。
顺序多实例只创建一个子执行。当前实例完成后,引擎增加 loopCounter 和 nrOfCompletedInstances,清理上一实例的局部变量,再让同一个执行进入下一轮。对普通 User Task 来说,表现为上一人的任务完成后才创建下一人的任务。
| 对比项 | 并行多实例 | 顺序多实例 |
|---|---|---|
| 初始子执行 | 按集合大小创建多个 | 只创建一个 |
| 活动任务数 | 多个任务可同时存在 | 同一时刻通常只有一个 |
| nrOfActiveInstances | 随完成逐步减少 | 通常保持为 1 |
| 办理顺序 | 无天然先后 | 按集合迭代顺序 |
| 主要风险 | 并发更新、提前结束、剩余任务清理 | 人员顺序、跳过规则、单点阻塞 |
如果业务要求"同一批人全部可见、同时表态",使用并行;如果要求"部门经理处理后才到分管领导",使用顺序。不要用任务创建时间碰巧先后来模拟顺序规则。
三、模型架构与运行机制
四个变量分别代表什么
Flowable 多实例最常见的四个变量是:
- nrOfInstances:进入多实例时确定的实例总数;
- nrOfActiveInstances:当前仍处于活动状态的实例数;
- nrOfCompletedInstances:已经完成的实例数;
- loopCounter:当前子实例在集合中的下标,默认从 0 开始。
前三个变量描述整个多实例,loopCounter 描述单个实例。集合元素变量也应放在子执行局部作用域,否则并行实例会互相覆盖 assignee,最终可能出现多张任务指向同一个人。
最容易犯的错误是把 nrOfCompletedInstances 理解成"同意人数"。它只表示有多少实例结束。某个用户点击驳回并完成任务,完成数同样会增加。审批结果必须另存为结构化决定,再由会签策略统计 approvedCount、rejectedCount 和 abstainedCount。
一个任务完成后,引擎内部发生什么

图 2:TaskService 完成一个任务后,内层活动离开,外层多实例行为更新计数并判断是否继续或结束。
以并行会签为例,一次任务提交大致经历以下步骤:
- 业务服务校验办理人、任务版本和会签策略;
- 保存本人的结构化决定与审批意见;
- 调用 TaskService.complete 完成当前 User Task;
- User Task 的内层行为请求离开活动;
- ParallelMultiInstanceBehavior 使当前子执行失活并更新完成计数;
- 引擎执行活动结束监听器和变量聚合;
- 引擎计算 completionCondition;
- 条件未满足则继续等待,满足则结束多实例并沿顺序流前进。
Flowable 当前并行实现还会锁定首个父 scope,避免多个子实例同时完成时都认为自己是"最后一个"。当前主分支对并行循环计数采用专门的变量类型,同时保留对旧运行中实例的兼容逻辑。这说明并发正确性属于引擎内部实现细节,应用不应直接修改计数变量或 ACT_RU_EXECUTION。
四、核心场景与处理策略
completionCondition 应该怎样写
completionCondition 会在每个实例结束后求值,结果必须是布尔值。它适合表达"什么时候结束整个多实例",但表达式引用什么数据,决定了会签语义是否正确。
| 会签规则 | 推荐判断 | 不应使用的判断 |
|---|---|---|
| 全员处理 | nrOfCompletedInstances 等于 nrOfInstances | 只判断当前任务已完成 |
| 任一同意 | approvedCount 大于等于 1 | nrOfCompletedInstances 大于等于 1 |
| 60% 同意 | approvedCount 乘 100 大于等于 nrOfInstances 乘 60 | 完成数除以总数大于等于 0.6 |
| 一票否决 | rejectedCount 大于等于 1 | 任意任务完成即否决 |
| 过半且无否决 | approvedCount 过半且 rejectedCount 为 0 | 只看赞成票,不处理否决票 |
计票变量必须并发安全。简单在监听器里读取 approvedCount、加一再写回,两个事务可能读到同一个旧值并覆盖彼此。更可靠的做法是保存一人一票的业务记录,以唯一键防止重复表态,再在同一锁或同一事务边界内计算聚合结果。
提前结束时,剩余任务去了哪里
completionCondition 一旦满足,Flowable 会结束多实例活动并清理仍在运行的子执行。对用户界面而言,其他人的运行时待办会消失;对审计而言,这些任务不能被误写成"已同意"或"正常完成"。
业务系统应为剩余参与者记录明确原因,例如:达到通过比例、触发一票否决、管理员终止或边界事件中断。历史任务的结束时间只能说明任务生命周期结束,不能单独证明办理人作出了什么决定。
提前结束还会影响催办、移动端缓存和待办索引。推荐在事务提交后发布"会签整体结束"事件,由投影服务撤销未办提醒、更新工作台并保留取消原因,而不是只依赖前端下一次刷新发现任务不见了。
五、数据、规则与状态设计
执行表、任务表和历史表怎样分工
多实例运行时通常会涉及 ACT_RU_EXECUTION、ACT_RU_TASK 和运行时变量;完成后,活动与任务轨迹进入历史表。理解表的职责有助于排查问题,但业务代码应优先使用 RuntimeService、TaskService 和 HistoryService 查询,不应直接更新引擎表。
- ACT_RU_EXECUTION 体现流程实例、scope、多实例根和子执行关系;
- ACT_RU_TASK 保存当前仍需人工处理的 User Task;
- 运行时变量保存集合、计数、局部人员变量和业务上下文;
- 历史活动记录可观察每个活动实例的开始、结束和删除原因;
- 历史任务记录可观察办理人、任务生命周期和完成时间。
排查"三人会签为什么只有两张任务"时,应先看人员集合快照,再看执行树和局部 assignee,最后看是否有 completionCondition、边界事件或监听器提前结束。只查任务表容易把执行树问题误判成任务创建问题。
赞成、反对和弃权应该存在哪里
推荐把审批决定存入独立业务记录,而不是只写一个全局流程变量。每条记录至少包含 processInstanceId、taskId、activityId、roundNo、userId、decision、comment、operationId 和完成时间,并对 taskId 或"轮次加人员"建立唯一约束。
这样做有三个好处:
- 审批决定与任务完成解耦,能够解释每一票;
- 并行提交可以通过唯一键和事务锁避免重复计票;
- 动态加签、撤回意见和多轮会签可以保留版本,而不是覆盖一个变量。
流程变量适合保存轻量、需要驱动流程的聚合结果,例如 approvedCount、rejectedCount 和 meetingResult;原始意见、附件、签名和人员快照应由业务库承担。
六、工程实现与系统集成
动态加签为什么比增加一个任务复杂
Flowable RuntimeService 提供 addMultiInstanceExecution 和 deleteMultiInstanceExecution 等公开能力,可以向正在运行的多实例父执行增加或删除实例。但 API 解决的是执行树变更,不会替企业定义会签规则。
运行中加签至少要决定:
- 新人员是否计入 nrOfInstances 和通过比例分母;
- 当前已经满足的比例是否需要重新计算;
- 新任务属于原轮次还是新轮次;
- 被删除实例算弃权、取消还是管理员调整;
- 加签人与被加签人的责任关系怎样记录;
- 重复请求和并发完成同时发生时怎样保证幂等。
因此,加签入口必须由领域命令服务统一封装。不要让前端直接拿 parentExecutionId 调用引擎 API,更不要手工插入任务和执行记录。
监听器为什么经常执行两遍
多实例活动上的 execution listener 与普通活动不同。官方文档说明,进入多实例整体时会触发一次 start,此时 loopCounter 尚未设置;每个实际实例开始时还会再触发 start,此时 loopCounter 有值。end 事件同理:每个实例结束触发一次,整体结束还会再触发一次。
如果监听器不区分"整体事件"和"实例事件",就可能重复发消息、重复写审批记录或重复更新业务状态。判断时应结合 loopCounter、execution 是否为 multi-instance root、事件类型和 operationId,而不是看到 activityId 相同就认为是同一次回调。
Task listener 则面向每张具体用户任务,更适合记录任务创建、指派和完成事件;跨系统通知仍建议通过事务 Outbox 在提交后异步发送,避免监听器远程调用拖长引擎事务。
七、安全、性能与治理要求
边界事件和超时会怎样影响会签
多实例活动仍然是普通 BPMN 活动,因此可以挂载边界事件。官方文档说明,中断型边界事件被捕获时,所有仍活动的实例都会被销毁。例如会签节点设置三天超时,定时器触发后可以统一转入升级审批,而不是让每个人各自进入一条升级分支。
若业务要求"每个人分别超时",边界定时器应放在内部任务语义上并明确是否中断单个实例。两种模型的影响范围完全不同:一个中断整个会签,一个只处理当前参与者。
超时处理也必须保存原因。取消任务、自动同意、自动弃权和转交上级是四种不同的审批语义,不能只用任务已结束来表示。
一套可靠的会签领域架构

图 3:审批领域服务负责人员、投票和审计,Flowable 多实例负责执行树、任务和流程推进。
推荐把会签分成五个职责:
- Participant Resolver:按部门、岗位、逐级主管等规则生成并冻结人员快照;
- Vote Service:保存赞成、反对、弃权、权重和一票否决等结构化决定;
- Counter Policy:根据当前轮次与人员快照计算是否达到结束条件;
- Flowable Adapter:完成任务、增加或删除多实例执行,并隔离版本差异;
- Audit and Projection:生成审批时间线、待办投影、取消原因和通知事件。
Flowable 只接收经过校验的命令。所有 PC、移动端、开放 API 和管理员入口都经过同一个 operationId 幂等检查,避免同一人双击、重试或多端提交产生两次投票。
八、平台落地、测试与选型
低代码平台应该封装哪些配置
云程低代码开发平台可以在 BPMN.js 属性面板中把底层多实例配置包装成业务可理解的会签选项:并行或顺序、人员来源、全员或比例通过、一票否决、超时策略、是否允许加签、人员快照时机和剩余任务处理方式。 
设计器发布时应把这些选项转换为版本化节点策略,并校验 collection、elementVariable、assignee 和 completionCondition 是否一致。运行时仍由统一会签服务完成计票和审计,避免每条流程复制监听器脚本。