Skip to content

规范 02 · 流程定义配置项完整参考(属性字典) ​

流程定义 = LogicFlow JSON。同一份 JSON 在所有语言实现(Java/Go/Python/Node/PHP/Rust/MoonBit/C#)中 必须产生一致的执行结果。

本文档是流程定义所有配置项的权威参考(事实来源:Java 参考实现的 ModelParser / NodeParser 系列解析器,八语言行为对齐):

  • "引擎"列:✅ = 引擎已实现(解析并影响行为);「前端」= 仅设计器/发起页使用,引擎忽略; 「预留」= 键保留但当前无行为实现
  • 未列出的 properties 键 = 引擎不识别(不参与执行),在任务节点上原样存入节点扩展属性 ext(任务变量可回显)——ext 容器为任务节点专属,其余节点类型的未列出键直接丢弃 (go/python/node 无 ext 装配腿:未列出键不出口,ext 回显以 properties.field 投影为准,见 规范 08 场景 43);已识别键(本表所列,含经父类继承链 或黑名单判据识别的)不得再镜像进 ext(issues/231 · owner 2026-10-15 拍 A:java/csharp 判据收窄向 boot2 血统对齐)
  • 前端设计器属性面板字段与本表的关系见 用户指南 11 · 流程设计器

1. JSON 结构总览 ​

json5
{
  // ── 顶层(引擎解析)──
  "name": "leave",            // 流程编码(唯一,deploy 版本管理键)
  "displayName": "请假审批",   // 流程显示名称
  "type": "approval",         // 流程类型
  "expireTime": "2026-12-31", // 流程期望完成时间
  "relTableName": "biz_leave",// 关联业务表名(bizData 回显)
  "persistMode": "ARCHIVE",   // 业务落表模式(persist)
  "postInterceptors": "...",  // 流程级后置拦截器(逗号分隔)
  "preInterceptors": "...",   // 流程级前置拦截器(逗号分隔)
  "instanceUrl": "...",       // 实例详情 URL(预留)
  "instanceNoClass": "...",   // 业务号生成类(预留)
  // ── LogicFlow 图 ──
  "nodes": [ /* 见 §3 */ ],
  "edges": [ /* 见 §9 */ ]
}

节点(LogicFlow 标准字段):

json5
{
  "id": "apply",          // 节点唯一编码 → 任务名 taskName(全流程唯一)
  "type": "snaker:task",  // 节点类型(接受带/不带前缀两种写法,见 §3)
  "x": 250, "y": 200,     // 画布坐标(设计器布局,引擎不参与执行)
  "text": { "value": "发起申请" },  // 显示名称 displayName(引擎读取位置)
  "properties": { /* 引擎扩展属性,见 §4~§8 */ }
}

兼容:type 接受 snaker:task 与 task 两种写法(设计器导出带前缀,手动编写可省略)。

2. 顶层属性(流程级) ​

字段类型引擎说明
namestring✅流程编码(唯一)——deploy 版本管理键(同 name 从 0 递增);bizData 表名回落
displayNamestring✅流程显示名称
typestring✅流程类型(listByType 分组键)
expireTimestring✅流程期望完成时间
relTableNamestring✅关联业务表名——bizData 回显定位表;缺省回落 name
persistModestring✅业务落表模式:ARCHIVE(缺省,流程结束归档)/ SYNC(发起 INSERT → 节点 UPDATE → 定稿),见 指南 08 persist
preInterceptorsstring✅流程级前置拦截器注册名(逗号分隔多注册名;勿用数组形——引擎侧为单串模型,数组形各栈解析行为不一,参考实现解析报错)
postInterceptorsstring✅流程级后置拦截器注册名(逗号分隔多注册名)——persist 的办理节点权限判定路径,必须配置
instanceUrlstring预留实例详情 URL(前端用)
instanceNoClassstring预留业务号生成类(前端/集成方用)
selectUserOnInitiateint前端发起时选择处理人(0/1)
enableCcActorsint前端发起页抄送开关(0/1)
enableApplyReasonint前端发起页申请理由开关(0/1)
enableAttachmentint前端发起页附件开关(0/1)
enableFieldPermint前端字段权限开关(0/1)

3. 节点公共属性(全部节点通用) ​

字段引擎说明
id✅节点唯一编码 → 任务名 taskName
type✅节点类型(8 种,见下)
text.value✅显示名称 displayName
properties.name前端唯一编码(钉钉模式设计器写入);引擎取节点 id 为节点名,此键不参与执行
preInterceptors✅节点前置拦截器注册名(逗号分隔多注册名)
postInterceptors✅节点后置拦截器注册名(persist 模型级执行后触发)
x / y / layout布局画布坐标/布局(引擎不参与执行)

节点级拦截器的优先序、回落与键形(audit8 R91 立法,2026-10-16 owner 拍丙):

  1. 节点声明优先、缺省回落流程级:节点 properties.preInterceptors/postInterceptors 非空串 ⇒ 只跑节点声明的那几条; 空串或键缺位 ⇒ 回落流程根的同名键。两档不叠加(节点非空时流程级不参与本次挂载)。 参考实现 java NodeModel.java:49-51/:57-59,php NodeModel.php:41-43/:49-51,csharp Models.cs:89-91/:97-99 三家同形。
  2. 声明了就必须被执行或显式暴露:节点声明的键解析不到 ⇒ 必须可诊断报错或留痕(带节点 id 与实得串),不得静默忽略。 本条与 §3 义务 2「未知档不得静默丢节点」同族,力度从节点 type 扩到 interceptors。
  3. 键形统一=Java 全类名字符串(跨栈可移植的硬要求:13 栈一键部署共用同一套库与同一份流程定义)。 规范键取 com.mldong.jeeflow.* 形式的全类名字符串,装配方式各栈自定——java/boot2 走 Class.forName 反射(NodeModel.java:68-69),其余各栈走以同一串为键的注册表(go persist/interceptor.go:324、 node src/persist.ts:402、py jeeflow/persist.py:566、php FlowInterceptorRegistry.php:11-24、 csharp ServiceContext.cs:68-69 现读均已在用该串作注册键)。 ⇒ 本条是把既成惯例写进契约;未实现节点级声明的栈(见 R91 代码腿)补齐时必须沿用同一串,不得另发明短名。

| properties.width / properties.height | 布局 | 画布尺寸——与 x/y 拼成 layout 串(引擎不参与执行) |

节点类型总览:

type(兼容写法)说明必填 properties
snaker:start / start开始-
snaker:end / end结束(全流程只能有一个)-
snaker:task / task任务节点assignee 或 assignmentHandler
snaker:decision / decision条件分支出边 expr
snaker:fork / fork并行分支-
snaker:join / join并行合并-
snaker:custom / custom自定义节点clazz
snaker:subProcess / subProcess子流程form

类型键的三条义务(issues/141 G4 · 2026-09-29 立法;第 1、3 条已按 owner 2026-10-01 裁定改写)

  1. 规范名=设计器实际输出(owner 2026-10-01 拍板:「只能按照设计器做,历史旧账不管」): 子流程类型键立驼峰 snaker:subProcess——mldong-flow-designer-plus 两模式(canvas / dingtalk) 实际输出的就是它(NodeTypeEnum.subProcess = 'subProcess'),vben5-wf 原样透传无改写层, 库里全部存量子流程定义也都是驼峰。原 09-29 立的「小写 snaker:subprocess + 查表前大小写归一」 作废:归一化不做、各写法间历史兼容不做。普查现读(保留作背景;行号已按 2026-10-01 HEAD 复锚): 八栈查表全部大小写敏感、零归一化——subProcess(大写 P)只有 java/c# 认(Configuration.java:52 / ModelParser.cs:134)、subprocess(全小写)只有 php/rust/moon 认(ModelParser.php:40 / parser.rs:178 / parser.mbt:157)、go/python/node 压根没有这一档(类型常量表各 7 档: go model/types.go:40-46、python model.py:37-43、node model.ts:30-36)。 ⚠️ 这三栈也没有兜底臂:未知类型不进任何档,只被 _find_by_type/findNodeByType 静默略过—— 旧文那句「驼峰串在 go/node 落 Custom 兜底」不成立;有 _ => Custom 兜底的是 rust/moon 的旧形状, 已随 issues/142 A 批拆成显式 Unknown(rust parser.rs:179、moon parser.mbt:161)。 补齐方向(owner 2026-10-01 二拍:「可以暂不支持,后续再单独开」)=子流程暂不进契约面: 六栈(php/rust/moon/go/python/node)不补 subprocess 档——java/c# 的执行侧也不是真实现 (java 简化壳、c# 抛 20010002 打断办理,四态见 §7),补档只会造出「看起来支持、其实不执行」; 设计器画出子流程节点时,六栈按下面义务 2 的未知档日志显式暴露(go/python/node 的日志腿已随批二 §3-5 补齐,见下)。 子流程真实现后续单独开案。(本段取代此前「缺档补齐方向=补驼峰档、另轮实施」的记法)

  2. 未知档不得静默丢节点:类型不在表里时,必须记一条可诊断日志(带节点 id 与实得类型串)再决定跳过, 不允许"静默丢节点+连带丢它的出边"。 历史反面样本(09-29 快照,其中两条已被 issues/142 A 批换代,别再当现状读):php 类型表当时根本没有 custom 档(七档无 'custom',也无 CustomNodeParser.php),而它自家共享夹具 jeeflow-php/flows/08-custom-node.json:38 就写着 "type": "snaker:custom" ⇒ 这个节点在 php 栈被无声吞掉; rust/moon 当时用 _ => Custom 兜底"不丢",但那等于把未知塌成记录类(违反 §6.1)。 现状(2026-10-01 复锚):php 已补 custom 档(Parser/CustomNodeParser.php 已存在、 ModelParser.php:44 注册)+未知档 error_log WARNING(ModelParser.php:112);rust/moon 的兜底臂已拆成 显式 Unknown+诊断文案(rust parser.rs:118-127/:179,moon parser.mbt:161+ engine/engine.mbt:203 println(unknown_node_warning(...)));java ModelParser.java:87、 c# ModelParser.cs:135/145 亦已有日志。曾有的唯一欠账=go/python/node 三栈未知档零条日志 (09-29 横扫命中文件数:java 1/php 1/c# 2/rust 7/moon 7/go 0/python 0/node 0) 已由批二 §3-5 补齐(2026-10-01,八栈 8/8):go engine/engine_impl.go:643-646(守卫 isKnownNodeType:610)、 python jeeflow/engine.py:696-700+model.py:55 KNOWN_NODE_TYPES、node src/engine.ts:757-779 +model.ts:50 KnownNodeTypes。三栈都没有独立解析期丢弃臂(go/python 的 parse 不按类型过滤、 node 连解析都没有)⇒ 日志只能落执行腿"没人认领"那一处;挂到解析/读定义出口会跟着每次 发起与办理各打一遍。 ⚠️ 两条不要顺手对齐的口径:① 未知判据必须是具名类型表(∉ 表),不能是"if/elif/switch 走完 没人认领"——snaker:start 在表里却恰无执行分支,用后者会把拓扑病误报成类型病;② 日志文案不是逐字契约面 (门面 msg 才是),各家按本栈设施与形状写:java/php/c# 在解析期 ⇒ 「没有对应解析器」, go/python/node/rust/moon 在执行期或类型表 ⇒ 「不在类型表里」,两要素齐(nodeId= +实得 type= 原文) 即可横扫取证。

    义务 2 的后半:指向被丢弃节点的那条边必须「落穿停住」,严禁打崩办理(issues/143 · 2026-09-30)。 「记日志再跳过」只说了节点怎么办,没说上游那条指向它的边怎么办——而「跳过节点」若让令牌撞上 悬空引用,用户面就是「配错一个类型串 ⇒ 整条流程办不动」,与 §6.2 第 2 条「严禁抛错打断建单」 以及 spec/04「节点属性配错不该把流程炸掉」直接冲突。 统一形状=记一条可诊断日志(带 from/to)+ 令牌停在这条边:实例留 DOING、库里不产生越过它的行。 不选「越过未知节点继续往下跑」——未知档没被解析成任何模型,越过它等于用一条臆造的通路把跑不通的 定义跑成功,用户面更难发现(与 §6.2 第 1 条「查不到的留痕=没留痕」是同一把尺子的反向用法: 跑得通的假象=查不到的病灶)。 现读分派(同一病灶的三种表现): · 崩的一派=对象图形状:java(ModelParser.java:99-109 只在目标节点存在时 setTarget, TransitionModel.java:29-31 的 else 分支裸调 ⇒ NPE)、php(逐字同形 ⇒ Call to a member function execute() on null); · 无声停住的一派=c#(Models.cs 的 else if (Target != null) 守卫在,不崩,但既无解析期日志 也无落穿日志 ⇒ 义务 2 的「可诊断」没兑现); · 本来就停住的一派=go/python/node/moon/rust(按 id 现查目标,查不到就停;moon/rust 另有解析期 WARNING,go/python/node 至今零条日志 ⇒ 那笔属义务 2 未落地的欠账,见 issues/141 G4)。 本轮落地:java a2d2529/php c71d72e/c# 7fa7b95,三栈各配同判据用例、改前红样在案; c# 顺带补小写 subprocess 档(义务 3 的 c# 一角,java 早在 3d1fc98 已补)。

  3. 别名保留一代(一般原则):新增/统一类型键时,旧写法作为同值别名继续接受一代并标注 deprecate。 subprocess 档例外(owner 2026-10-01 裁定「历史旧账不管」):java/c# 的 wfSubProcess 与 1.8.36 代补的小写 subprocess 别名就地保留、不删不管——尤其不得删驼峰 subProcess (设计器与全部存量定义产出的正是驼峰,查表大小写敏感、未命中即丢节点); 原「先给 java/c# 补小写别名、下个代次再删」的计划随裁定作废。

发起申请节点约定(mldong 框架契约):start 后第一个任务节点必须是"发起申请"节点, assignee = "applicant"(引擎解析为流程发起人)。引擎不自动执行该节点,由调用方 (startAndExecute)启动后自动完成,流程推进到真正的审批节点。

4. 任务节点 properties 完整字典(snaker:task) ​

字段类型引擎说明
assigneestring✅固定参与者:逗号分隔多人;applicant = 发起人;token 优先按流程变量 key 解析(见 参与者解析)
assignmentHandlerstring✅动态参与者注册名(内置清单见 指南 07)
formstring✅表单标识 → formKey(发起页/办理页表单定位)
taskTypeint✅0=主办 1=协办 2=记录(记录类节点历史行档;引擎枚举含三值,历史行落值各栈形状不承诺统一——见规范 06 §4.6 approvalRecord)
performTypeint/string✅0=普通参与(多人任一完成即可)1=会签(每人独立任务);兼容字符串 '1'/'ALL'/'COUNTERSIGN'
countersignTypestring✅会签模式:PARALLEL(并行,全部完成推进)/ SEQUENTIAL(串行,依次流转)——仅两档;"按比例"不是独立模式=PARALLEL+countersignCompletionCondition 表达式组合(写 RATIO 串按缺省落 PARALLEL)
countersignCompletionConditionstring✅会签完成条件表达式(如 #nrOfCompletedInstances>=2);特殊值 ONE_VOTE_VETO(忽略大小写)= 开启一票否决,见 §4.2
candidateUsersstring⚠️候选人 userId 列表(逗号分隔)——不生成 actor,供 candidatePage 选人
candidateGroupsstring⚠️候选角色标识(逗号分隔)
candidateHandlerstring⚠️动态候选人处理注册名
reminderTimestring预留提醒时间(如 10:00)——无行为实现(到期提醒腿八栈均无)
reminderRepeatstring预留重复提醒间隔——无行为实现
expireTimestring✅期望完成时间
autoExecutestring预留自动执行配置——无行为实现(到期自动执行腿八栈均无)
callbackstring预留回调处理注册名——无行为实现(任务完成回调腿八栈均无)
preInterceptorsstring✅前置拦截器(逗号分隔多注册名)
postInterceptorsstring✅后置拦截器(persist 写库/权限判定;逗号分隔多注册名)
fieldobject✅字段权限声明(见 §4.1)
其他任意键anyext不识别 → 存入节点扩展属性 ext(任务变量回显,不参与执行)

候选人/会签键双源兼容:candidateUsers/candidateGroups/candidateHandler 与 countersignType/countersignCompletionCondition 既认 properties 顶层写法,也认旧版设计器的 field 扩展属性写法(field 内容即 §4.1 的 ext 容器);两源并存时的覆盖方向不承诺统一。

4.1 字段权限(field,SYNC persist 用) ​

  • 声明位置:任务节点 properties.field
  • 键格式(双兼容):PERMISSION_{表单字段全名}(含 f_ 前缀,前端 vben5-wf 约定,优先) 与 PERMISSION_{去前缀名}(旧格式)
  • 值:1=只读 / 2=可编辑 / 3=隐藏(缺省=可编辑)
  • 办理入口过滤:按任务节点权限过滤后再入流程变量——上游只读不可被下游绕过(可判定形状见 规范 08 场景 43)
  • 状态列:优先 {节点ID}_{状态码} 列,无则 {节点ID} 列

4.2 会签(countersign)行为 ​

模式行为推进条件
PARALLEL每个参与者一个独立任务全部完成
SEQUENTIAL一次一个任务,完成后流转给下一个参与者全部完成
PARALLEL+条件表达式每人一个任务(比例完成)完成数满足 countersignCompletionCondition
一票否决(ONE_VOTE_VETO)countersignCompletionCondition 设为 ONE_VOTE_VETO(忽略大小写)时,任一成员 submitType=20 会签不同意 → 节点立即推进否决者提交即合并

会签推进/否决后的残留处理:节点合并(merged)推进下一节点时,该节点仍 DOING 的会签任务一律废弃(taskState=99),不留孤儿待办——并行会签下其余成员的待办即被清空(对齐 mldong 内置引擎 abandonProcessTask 行为)。

会签拒绝(submitType=20)的默认语义 = 软拒绝:未配置 ONE_VOTE_VETO 时,否决者任务正常完成、countersignDisagreeFlag=1 记录为流程变量(供下游节点作参考),流程不阻断,按常规模式(全部完成 / 表达式)等待推进——会签意见默认只作参考、不阻断流程。

5. 决策节点 properties(snaker:decision) ​

字段位置引擎说明
expr节点✅节点级默认表达式(可选)
handleClass节点✅自定义决策器处理类注册名
expr出边✅分支条件表达式——决策出边必填(首条满足者胜)
text.value出边前端分支标签(钉钉模式渲染在线上;引擎解析器不读边标签)

决策路由规则:引擎按出边顺序求值,第一条 expr 为真的边获胜(命中即停,多真边并行流转不是 spec 语义——issues/234 丙-9 立法,八引擎已加 break);表达式语法见 指南 02 §5。

默认分支(无 expr 的出边)=可选超集,不承诺统一(issues/234 丙-10 → 队列 R93,2026-10-15 owner 拍 C):

  • 有兜底三栈,且选取顺序两档都许可:边序=在同一次遍历里遇到 expr-less 出边立即取用(go engine_impl.go、node engine.ts;于是 [e1(无 expr), e2(expr 真)] 走 e1);段序=先把全部带 expr 的边跑完,全 false 再取第一条 expr-less 边(python _evaluate_decision 两段循环;同一定义走 e2)。两档都合法,跨栈不保证同一条边走哪一支。
  • 无兜底五引擎:java/php/csharp/rust/moon 的 expr-less 出边只在自定义决策器(handleClass)返回的节点名匹配时才可选,表达式全 false 时不因它兜底(mldong-boot2 内置版与 java 同形)。
  • 下限(不可豁免):一条定义跑完全部出边仍无命中 ⇒ 必须显式报错,对外 99999999+msg 逐字 decision节点无法确定下一步执行路线(20010001 档,规范 06 §4.6 义务 2 的 issues/164 立法已覆盖此面),严禁静默停住——令牌不前进也不报错=办理人看到 code=0"成功"、实例挂在 state=10 且没有任何人的待办。
  • ⚠ 写给设计器/集成方:要用默认分支语义,流程定义必须按 go/node/python 三栈的形状,同一份定义部署到其余栈会在决策节点直接报错。要跨栈可携就别画无 expr 的决策出边,改成一条恒真表达式(如 #amount>=0)。
  • 📌 更正记账:audit 卡面(C2-L1 静-1,批2 时点)记的「rust/moon/go/node 此处静默停」与「python 独家末段回退硬走第一条边」两档读数已被 issues/161/164 轮修平——2026-10-15 逐仓现读八栈全显式抛同串,唯 php 抛的是 JeeflowException('无法确定下一节点') 无码(对外落 code=0=失败报成功,违反框架契约),随 audit8 行为并批轮钉成同码同串。

6. 自定义节点 properties(snaker:custom) ​

字段引擎说明
clazz✅处理器类路径(必填)
methodName✅方法名
args✅参数变量(逗号分隔的变量 key)
val✅返回值写入的变量 key

6.1 记录类型节点没有参与者是正常形态,不是缺陷(issues/141 G9 · 2026-09-29 owner 拍) ​

owner 原话:「这个得根据任务类型来,自定义类型这种记录类的,不会有参与人,是正常行为。」 ⇒ "参与者解析为空"要按节点类型分判,不许一刀切:

节点类型参与者为空时的正确形状禁止的形状(本轮抓到过实例)
任务类(task/approval,含会签)建待办行(task_state=10),参与者可以为零(java CreateTaskHandler 无条件建单)因为"没人可办"就跳过建行 ⇒ 实例停在 state=10 却零可办行(死锁黑洞;python 曾如此)
记录类(custom 这类自动执行/留痕节点)执行 clazz、落历史行(task_state=20)、令牌继续流转。它本来就不该有参与者,也不该有待办① 当任务类建 DOING 行;② 兜底把行挂给当前操作人(伪造一条他不该收到的待办);③ 直接跳过节点不建行(丢留痕)

三条硬结论:

  1. "参与者为空 ⇒ 兜底挂给当前操作人"这种写法八栈一律不许有(python 为消 demo_reset_test 长期红曾这么修过,本轮撤回)。
  2. 记录类节点不产生待办 ⇒ 任何"按待办数/可办人数"做的跨栈对账与种子矩阵计数,都要先把它从分母里摘出去。
  3. 判据分型:同一格不许既钉"任务节点必须建一行"又钉"custom 节点不该有 DOING 行"。 形状改了之后种子矩阵的数值要按新形状重算——那是"跟着裁定改数值", 与"改期望值蒙过红"是两件事,报告里必须分开写清是哪一种。

6.2 记录类历史行的三条硬要求(issues/142 §5 · 2026-09-30 owner 逐条拍) ​

上一节把"该不该建行"定了,这一节把"建出来的行要满足什么"定死。三条都是普查实读撞出来的:

  1. 历史行必须真落库——"落历史行"=wf_process_task 里查得到那一行 (task_state=20、参与者=当前操作人、task_parent_id 与行级首节点标记照建单不变量走), 只在聚合内存对象里 append 一条不算做到。 ⚠️ 现读基准自身违反本条:java CustomModel.exec 丢弃 createHistoryTask 的返回值只进 instance.tasks,而 persistTasks 只保存 exec.getProcessTaskList()、 updateInstance 的级联又只 UPDATE taskId != null 的行(ProcessTask.create 从不赋 taskId) ⇒ 这条 DONE 行永远进不了库;c# 同形。java 自家 JeeflowFullTest#test08CustomNode 对历史行零断言,所以这个洞测试照不出来。两栈本轮补 INSERT 腿。
  2. clazz 解析不了 ⇒ 记日志 + 照常落历史行 + 令牌继续流转,严禁抛错打断建单 (owner 09-30 拍;与 spec/04 那条"误配的相对档落穿→NULL、节点属性配错不该把流程炸掉" 是同一条哲学)。现读 java/c# 是显式报错(自定义模型[class=...]实例化对象失败),本轮跟改; python 已经是这个形状(记 WARNING 后继续),保持。"未注册处理器"与"clazz 为空串"要分档—— 两条都记日志继续,但日志文案要分别可诊断(c# 现在把两者合成同一个异常,覆盖面比 java 宽)。 处理器自身执行失败(反射到了、跑炸了)不在本条豁免内:照旧外抛,那是业务错误不是配错形状。 ⚠️ 豁免面边界(A 批收口补的细则——两栈当时各自解读成了一档):判据只有一句 「这一行处理器代码有没有被执行过」。
    • 没执行过=配错档 ⇒ 记日志继续:clazz 空串/属性缺失、clazz 非空但未注册、反射实例化失败、 methodName 未配或在处理器上找不到、参数形状对不上导致方法一行都没跑;
    • 执行过=业务错档 ⇒ 照旧外抛:IHandler.handle / HandleAsync / 方法体内部抛出的异常 (含 InvocationTargetException 解包出的目标异常)。 ⇒ methodName 找不到属配错档(配置项在调用之前就错、处理器零执行,与 clazz 写错、 与 spec/04 那条到期档落穿同族;外抛并不比一条带 class + method 名的日志更可诊断)。

1bis. 这条 DONE 行必须写 operator 与 finish_time(A 批收口补,同一把尺子:查不到的留痕=没留痕) ——processTask/doneList(state<>10 AND operator=?)与 processInstance/approvalRecord 都按这两列取数, 已完成行不带处理人和完成时间,在用户面上就等于这条留痕没落过。 operator=触发这次流转的当前操作人(与参与者列同值,是留痕主体不是待办),finish_time=该行落库时刻。 ⚠️ 现读 java/c# 的 ProcessTask.create 只用 operator 实参填 create_user/update_user, 既不填 operator 字段也不填 finish_time ⇒ 两栈跟改;python/php 已是这个形状。 反过来 expire_time 保持 NULL:记录类节点没有到期表达式可算(expireTime 是 task 节点专属属性, CustomParser 只映射 clazz/methodName/args/val),写它就得臆造属性, 与 issues/126 owner 口径"节点没配就保持 NULL、不许赋建单时刻"同向。 ⇒ 两列写、一列不写,别顺手一起补。 3. 记录类腿不解析参与者,所以"参与者为空"这一判据对它不适用; 反过来任务类零参与者必须建 DOING 行(第 6.1 表第一行), 现读 go / node / rust / moon 四栈是"actors 为空 ⇒ 一行不建" (engine_impl.go:716、engine.ts:786、engine.rs:444、engine.mbt:295) ——那正是 6.1 点名 python 曾犯的死锁黑洞形状,本轮四栈跟改。 附带:python 任务类腿现在还兜底挂当前操作人(engine.py:724-728),按硬结论 1 一并撤掉, 改成与 java 逐字同形的"建零参与者行"。

⇒ 这条改变的是用户可感知的形状(有没有一条查得到的留痕、待办列表里多不多一条), 所以任何一栈只做满一半(落库不建行、建行不落库、记日志但停在原地)都算违反本条。

7. 子流程节点 properties(snaker:subProcess) ​

⚠️ 子流程暂不进契约面(owner 2026-10-01 二拍:「可以暂不支持,后续再单独开」):类型键规范名是驼峰snaker:subProcess(§6.1 义务 1),但只有 java/c# 认这个串,且两栈的解析层只搬 form/version; 执行侧四态(issues/219 · audit8 C2-L3 R-6 现读更正):java 简化壳(StartSubProcessHandler 恒传 defineId=null、子实例起不来)、c# 抛错(20010002「没有流程定义」打断本次办理)、 php/rust/moon 纯空壳(不建子实例、不报错)、boot2 唯一真实现;php/rust/moon 只认小写 subprocess、go/python/node 无此档 ⇒ 六栈按未知档记日志后跳过(义务 2)。下表两字段属解析层契约(设计器输出什么、解析器读什么), 不承诺执行语义;真实现后续单独开案。

字段引擎说明
form✅子流程表单标识
version✅子流程版本号

8. 开始 / 结束 / 并行节点 ​

  • snaker:start / snaker:end:无专属属性(仅公共属性)
  • snaker:fork / snaker:join:无专属属性——fork 出边建议写 text.value 分支标签

9. 边属性(edges) ​

json5
{
  "id": "e1",
  "sourceNodeId": "start",     // 起点节点 id
  "targetNodeId": "apply",     // 终点节点 id
  "properties": { "expr": "amount > 1000" },  // 决策分支条件
  "text": { "value": "金额>1000" }            // 分支标签(决策/fork 边建议必填)
}
字段引擎说明
sourceNodeId / targetNodeId✅起止节点
pointsList / startPoint / endPoint布局画布连线坐标——拼成边 g 布局串(引擎不参与执行)
properties.expr✅决策出边条件表达式
text.value前端分支标签(钉钉模式渲染在线上,决策/fork 边建议必填;引擎解析器不读)

10. 完整示例(标准骨架) ​

json
{
  "name": "leave",
  "displayName": "请假审批",
  "type": "approval",
  "relTableName": "biz_leave",
  "persistMode": "SYNC",
  "nodes": [
    { "id": "start", "type": "snaker:start", "x": 100, "y": 200, "properties": {}, "text": { "value": "开始" } },
    { "id": "apply", "type": "snaker:task", "x": 250, "y": 200,
      "properties": { "form": "apply-form", "assignee": "applicant", "taskType": 0, "performType": 0 },
      "text": { "value": "发起申请" } },
    { "id": "leader", "type": "snaker:task", "x": 400, "y": 200,
      "properties": { "form": "leave-form", "assignee": "leader", "taskType": 0, "performType": 0,
        "postInterceptors": "jeeflowPersistPostInterceptor" },
      "text": { "value": "组长审批" } },
    { "id": "decision1", "type": "snaker:decision", "x": 550, "y": 200, "properties": {}, "text": { "value": "金额判断" } },
    { "id": "end", "type": "snaker:end", "x": 700, "y": 200, "properties": {}, "text": { "value": "结束" } }
  ],
  "edges": [
    { "id": "e1", "sourceNodeId": "start", "targetNodeId": "apply", "properties": {} },
    { "id": "e2", "sourceNodeId": "apply", "targetNodeId": "leader", "properties": {} },
    { "id": "e3", "sourceNodeId": "leader", "targetNodeId": "decision1", "properties": {} },
    { "id": "e4", "sourceNodeId": "decision1", "targetNodeId": "end",
      "properties": { "expr": "amount > 1000" }, "text": { "value": "金额>1000" } },
    { "id": "e5", "sourceNodeId": "decision1", "targetNodeId": "end",
      "properties": {}, "text": { "value": "默认" } }
  ]
}

11. 常见错误对照 ​

错误现象修复
多个 end 节点分支各自结束汇合到同一个 end
申请节点缺 assignee有任务行但零参与者(实例 DOING、无人可办=死锁黑洞)加 "assignee": "applicant"
决策出边缺 expr各栈不一(go/node 边序兜底/python 段序兜底/java·php·csharp·rust·moon 不兜底⇒落零命中报错):见上文 §5「默认分支=可选超集」要跨栈可携就每条出边补 expr
决策/fork 边缺 text线上无标签补 text.value
节点 id 重复任务名冲突保证 id 全流程唯一
custom 写 customClass解析不出 clazz,处理器不执行(引擎记日志后流转照常、历史行照落;内置版抛错打断办理)用 clazz(旧文档字段名,引擎不识别)
属性拼写错误静默进 ext,行为不生效(仅任务节点;其余节点类型直接丢弃,go/python/node 连 ext 都不进)对照本表逐键核对

jeeflow · 轻量级多语言工作流引擎