引言
工作流是低代码平台里最接近「业务本质」的部分。表单收集数据,流程决定数据如何流转、由谁处理、何时终止。审批、报销、工单、订单状态机,本质上都是流程。低代码平台如果不内置流程能力,就只能做「静态的表单工具」,价值大打折扣。
但流程引擎的复杂度远高于表单:它要处理并发(多人同时审批)、持久化(流程可能挂起数天)、版本(流程定义改了,运行中的实例怎么办)、超时(三天未审批自动升级)、补偿(某步失败如何回滚)。这些问题在表单引擎里都不存在,在流程引擎里却是日常。
本文按「流程模型 → 节点语义 → 表单绑定 → 审批模式 → 条件分支 → 状态持久化 → 定时超时 → 外部集成 → 版本灰度 → 可观测性」展开,给出流程定义 DSL、状态流转代码与持久化表设计。读完后你应当能判断:一个流程引擎的复杂度分布在哪,以及哪些问题必须在设计阶段就解决。
目录
- 工作流在低代码中的位置
- 流程模型的表达
- 节点类型与执行语义
- 表单与流程的绑定
- 会签、或签与加签
- 条件分支与表达式
- 状态机与持久化
- 定时任务与超时
- 与外部系统集成
- 流程的版本与灰度
- 可观测性与运维
1. 工作流在低代码中的位置
三层分工:
数据层:存什么(数据模型)
界面层:怎么录入/查看(表单/页面)
流程层:谁在什么时候做什么(工作流)
流程层的输入输出:
输入:表单提交的数据 + 触发条件
输出:任务分配 + 状态变更 + 通知
流程层是「粘合剂」:它把静态的数据与界面串成有生命周期的业务对象。
2. 流程模型的表达
流程定义有两种主流表达:完整的 BPMN 与简化的自定义 DSL。
BPMN 2.0:
优点:标准、表达力强、有工具生态
缺点:复杂、学习曲线陡、低代码用户难理解
简化 DSL:
优点:易读易编辑、贴合低代码场景
缺点:表达力有限、无标准
折中:
内部用简化 DSL 建模,可导出 BPMN
或直接采用 BPMN 子集(仅顺序/分支/并行/会签)
{
"id": "leave_flow",
"version": 3,
"start": "submit",
"nodes": {
"submit": { "type": "start", "form": "leave_form", "next": "mgr_approve" },
"mgr_approve": {
"type": "task",
"assignee": { "type": "manager", "of": "{{ initiator }}" },
"next": "days_check"
},
"days_check": {
"type": "gateway",
"branches": [
{ "when": "{{ form.days > 3 }}", "next": "hr_approve" },
{ "when": "else", "next": "end" }
]
},
"hr_approve": {
"type": "task",
"assignee": { "type": "role", "value": "hr" },
"next": "end"
},
"end": { "type": "end" }
}
}
建议采用简化 DSL 的子集:顺序、条件分支、并行、会签四种结构,覆盖 95% 的审批场景,且低代码用户能看懂。
3. 节点类型与执行语义
节点类型决定了引擎的执行逻辑。
| 节点 | 语义 | 是否等待 | 典型场景 |
|---|---|---|---|
| start | 流程入口 | 否 | 提交表单 |
| task | 人工任务 | 是 | 审批、填写 |
| service | 自动任务 | 是(异步) | 调接口、发通知 |
| gateway | 条件分支 | 否 | 金额分流 |
| parallel | 并行分支 | 是 | 多部门会签 |
| timer | 定时等待 | 是 | 超时提醒 |
| end | 流程结束 | 否 | 归档 |
interface FlowNode {
type: 'start' | 'task' | 'service' | 'gateway' | 'parallel' | 'timer' | 'end';
assignee?: AssigneeRule; // task
branches?: Branch[]; // gateway
service?: ServiceRef; // service
duration?: string; // timer(如 PT72H)
next?: string | string[];
}
「是否等待」是核心语义:等待节点会持久化实例并挂起,直到外部事件(用户操作、定时器、回调)唤醒它。
4. 表单与流程的绑定
流程与表单的关系有三种模式。
模式 A:流程内嵌表单
每个节点可指定一个表单,用于该节点的输入
适合:不同节点填不同信息(申请填 A,审批填 B)
模式 B:流程引用表单
流程只引用一个主表单,节点做只读/编辑切换
适合:单一单据的审批
模式 C:表单驱动流程
表单提交即触发流程,流程无独立表单
适合:简单的一次性审批
{
"mgr_approve": {
"type": "task",
"form": {
"ref": "leave_form",
"mode": "readonly",
"editableFields": ["approveComment"]
}
}
}
字段级权限在流程节点上的表达是难点:同一表单在不同节点,不同角色看到的可编辑字段不同。这要求表单渲染器支持「节点上下文」下的字段权限。
5. 会签、或签与加签
多人审批是最容易做错的部分。
会签(all):所有人同意才通过
或签(any):任一人同意即通过
依次(sequential):按顺序逐个审批
加签(add):审批过程中临时增加审批人
{
"hr_approve": {
"type": "task",
"assignee": { "type": "role", "value": "hr" },
"approvalMode": "all",
"strategy": {
"onReject": "toStart",
"onTimeout": "escalate",
"escalateTo": { "type": "role", "value": "hr_lead" }
}
}
}
5.1 会签的判定与短路
会签要处理两个边界:短路(或签中第一个同意即通过,其余任务自动取消)与驳回的传播(一人驳回,其余待办如何处理)。这些必须显式定义,否则会出现「流程卡死」或「重复审批」。
6. 条件分支与表达式
条件分支决定了流程的走向。
表达式来源:
表单字段:{{ form.amount }}
流程变量:{{ variables.level }}
上下文:{{ initiator.dept }}
系统:{{ now }}
求值语义:
按顺序匹配,第一个为 true 的分支胜出
else 分支兜底
function evaluateGateway(node: FlowNode, ctx: FlowContext): string {
for (const branch of node.branches ?? []) {
if (branch.when === 'else') return branch.next;
if (evaluateExpr(branch.when, ctx)) return branch.next;
}
throw new Error(`gateway ${node.id} no branch matched`);
}
表达式引擎与表单引擎共用同一套实现,确保语义一致——这是 Schema 驱动的表单引擎 中「一次描述、多处消费」的延续。
7. 状态机与持久化
流程实例必须持久化,因为它可能挂起数天。
CREATE TABLE wf_instance (
id varchar(36) PRIMARY KEY,
def_id varchar(64) NOT NULL,
def_version int NOT NULL,
tenant_id varchar(36) NOT NULL,
business_key varchar(64), -- 业务单据 id
status varchar(20) NOT NULL, -- running/completed/terminated
current_node varchar(64),
variables jsonb NOT NULL DEFAULT '{}',
started_at timestamptz NOT NULL DEFAULT now(),
ended_at timestamptz
);
CREATE TABLE wf_task (
id varchar(36) PRIMARY KEY,
instance_id varchar(36) NOT NULL REFERENCES wf_instance(id),
node_id varchar(64) NOT NULL,
assignee varchar(64),
status varchar(20) NOT NULL, -- pending/approved/rejected/canceled
created_at timestamptz NOT NULL DEFAULT now(),
acted_at timestamptz,
comment text
);
CREATE INDEX idx_task_assignee ON wf_task(assignee, status);
状态流转:
instance: running → completed / terminated
task: pending → approved / rejected / canceled
7.1 幂等与并发
同一任务被两个请求同时处理(双击、重试)会导致重复流转。用「任务状态乐观锁」:UPDATE wf_task SET status='approved' WHERE id=? AND status='pending',影响行数为 0 说明已被处理。
8. 定时任务与超时
超时是流程的常见需求,实现依赖定时器。
超时场景:
- 节点超时提醒(3 天未审批 → 发提醒)
- 节点超时自动处理(5 天 → 自动通过/升级)
- 流程级 SLA(整体 7 天未完成 → 告警)
实现:
方案 A:数据库轮询(扫 due 时间到期的任务)
方案 B:延迟队列(Redis ZSet / MQ 延迟消息)
方案 C:调度框架(Quartz / Temporal)
-- 方案 A:扫到期任务
SELECT id FROM wf_task
WHERE status = 'pending' AND due_at <= now()
ORDER BY due_at LIMIT 100;
方案 B 更精确但需要额外基础设施;方案 A 简单但有轮询延迟。多数低代码平台用「延迟队列 + 兜底轮询」组合。
9. 与外部系统集成
流程中的自动节点常需调用外部系统。
{
"notify": {
"type": "service",
"service": {
"kind": "http",
"method": "POST",
"url": "/internal/api/notify",
"headers": { "Authorization": "Bearer {{ secrets.notifyToken }}" },
"body": { "to": "{{ task.assignee }}", "title": "待审批" },
"retry": { "max": 3, "backoff": "exponential" },
"timeout": "PT10S"
},
"onError": "continue"
}
}
失败策略:
- 重试:网络抖动,指数退避
- 跳过(continue):非关键步骤
- 挂起(suspend):关键步骤失败则暂停流程等人工处理
- 终止(terminate):不可恢复错误
关键原则:自动节点失败不能让流程凭空消失。要么重试成功,要么进入「异常待处理」状态,绝不能静默丢弃。
10. 流程的版本与灰度
流程定义会变,但运行中的实例不能受影响。
版本策略:
1. 新实例用新版本,旧实例继续用旧版本(推荐)
2. 强制所有实例升级(危险,可能不兼容)
3. 双版本并行(灰度,按比例分流)
实现:
实例持久化时记录 def_version
引擎按实例的 def_version 加载对应定义
function loadDefinition(defId: string, version: number): FlowDefinition {
// 版本化加载:确保运行中实例不受新版本影响
return definitionStore.get(defId, version);
}
这是流程引擎与普通代码发布最大的不同:代码发布后所有请求用新代码,流程发布后旧实例仍跑旧定义。
11. 可观测性与运维
流程是长事务,出问题时排查困难。
必备能力:
- 实例视图:当前节点、停留时长、处理人
- 任务视图:待办、超时、异常
- 流转日志:每次状态变更的时间与操作人
- 死信/异常队列:失败自动节点的重试入口
- 看板:各节点平均耗时、超时率
没有流转日志,用户问「我的单子为什么卡住」时你无法回答。日志是流程引擎的一等公民,不是可选项。
权衡取舍
| 决策点 | 选项 A | 选项 B | 建议 |
|---|---|---|---|
| 模型表达 | 完整 BPMN | 简化 DSL | 简化子集,覆盖 95% |
| 超时实现 | 轮询 | 延迟队列 | 延迟队列 + 轮询兜底 |
| 版本策略 | 强制升级 | 实例隔离 | 实例隔离,安全 |
| 自动节点失败 | 静默跳过 | 挂起待处理 | 挂起,不可静默丢弃 |
| 任务并发 | 无锁 | 乐观锁 | 乐观锁,防重复流转 |
常见坑清单
- 会签驳回不定义传播:一人驳回后其余待办状态不明,流程卡死,需显式定义。
- 或签不短路:多人同意后仍等待其余人,体验差且浪费。
- 自动节点静默失败:外部调用失败后流程凭空消失,必须挂起或重试。
- 无任务乐观锁:双击导致重复审批与重复流转。
- 流程升级影响运行实例:新定义与旧实例不兼容导致报错,需实例级版本隔离。
- 无流转日志:无法回答「为什么卡住」,日志必须一等公民。
- 超时只做轮询:延迟大且扫表压力高,应用延迟队列。
- 表达式引擎与表单不一致:同一表达式两处结果不同,必须共用实现。
- 节点级字段权限未表达:不同节点可编辑字段不同却无法配置。
- 会签任务无取消机制:分支合并后残留待办,需在合并时取消。
小结
低代码与工作流引擎的集成,骨架是「流程模型 → 节点语义 → 表单绑定 → 审批模式 → 条件分支 → 状态持久化 → 定时超时 → 外部集成 → 版本灰度 → 可观测性」。其中三条原则最容易被忽视却最致命:实例级版本隔离、自动节点失败不静默、流转日志一等公民。
流程引擎与表单引擎共享表达式求值、字段权限等基础能力,因此两者应构建在同一套元数据之上,而不是各自为政。这也是低代码平台「统一内核」的价值所在。
流程的落地离不开数据模型的定义,见 数据模型设计器 ;而流程中需要调用外部能力时,如何以插件形式扩展,见 插件机制与扩展体系 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。