跪拜 Guibai
← Back to the summary

The Two-Tier State Machine That Keeps Workflow Engines From Lying to You

jeeflow series · Part 3 (Getting to Know)


Drawing a flowchart feels clean: Initiate → Department Approval → Finance Approval → End, a few arrows, neat and tidy.

But the engine must answer a different set of questions: After the process starts, where is it now? Someone rejects it during approval — what state is that? The initiator withdraws it — what are the others waiting for? An admin forcibly terminates it — what happens to the tasks?

Flowcharts can't answer these — state machines can. This is the "soul" of a workflow engine. This article explains jeeflow's two-tier state machine and 8 submission types in one go.

This content aligns with jeeflow v1.8.11. This series continues from the mldong rapid development framework ecosystem. It is recommended to read in order: Part 2 covered jeeflow's overall design (98KB / SPI system / DDD aggregate roots). This part dives into the state layer.


1. Why a "Two-Tier" State Machine?

A single process instance can have multiple tasks running simultaneously: in a parallel branch, task2 and task3 run at the same time. So the state must be split into two layers:

jeeflow's state machine is a cross-language contract (the four-language enum values are completely identical):

Tier Status Code Meaning
Instance 10 In Progress
Instance 20 Completed (reached end normally)
Instance 30 Withdrawn (by initiator)
Instance 40 Forcibly Terminated (by admin)
Instance 45 Rejected (dismissed/refused)
Instance 50 Suspended (paused)
Instance 99 Discarded
Task 10 Pending
Task 20 Completed
Task 30 / 40 / 50 Withdrawn / Terminated / Suspended (cascaded from instance)
Task 99 Discarded (cleaned up when rejecting/jumping over other in-progress tasks)

jeeflow two-tier state machine (v1.8.4)

Three easily overlooked design points:

  1. Instances only have "Completed," not "Approved" — Instance 20 is a terminal state, triggered after the last task completes;
  2. Tasks have "Discarded" (99) — When rolling back or jumping, pending tasks that are skipped must be discarded, otherwise the "to-do list" will have ghost tasks. This 99 state is something many self-built engines easily miss;
  3. Branch states rely on "cascading" — Withdrawal/termination/suspension are instance-level commands. Task states cascade and persist with the instance (updateInstance persists in the same connection, v1.0.1 contract). The engine never leaves a half-baked state where "the instance is stopped but tasks are still running."

2. submitType: How Business Actions Drive State

The state machine is a "static map"; submitType (submission type) is the "driving instruction." This is the ProcessSubmitTypeEnum from the mldong framework, fully aligned with jeeflow, with 8 values:

code Enum Caller Behavior Effect
0 APPLY executeProcessTask Initiate / Resubmit
1 AGREE executeProcessTask Agree
2 REJECT executeAndJumpToEnd Instance → 45 Rejected, no new pending tasks
3 ROLLBACK Trace back along edge to previous task node → executeAndJumpTask Return to previous approver, instance stays 10
4 JUMP executeAndJumpTask(..., taskName) Jump to a specified completed node (taskName from jumpable list)
5 RE_APPLY executeProcessTask Resubmit
6 ROLLBACK_TO_OPERATOR executeAndJumpToFirstTaskNode Re-execute the first task node, participant forced to initiator → Initiator receives new pending task, instance stays 10
20 COUNTERSIGN_DISAGREE executeProcessTask + countersignDisagreeFlag=1 Countersign veto

Behavioral details follow the jeeflow-doc specification (SPEC); each enum's engine method corresponds one-to-one across four languages (Java / Go / Python / Node with the same signature).

A few easily confused points, explained in detail:

Rejection (2) ≠ Rollback (3). Rejection is termination: executeAndJumpToEnd, instance directly becomes 45, no more pending tasks will be generated. Rollback is backtracking: trace upward along the incoming edge to find the nearest task node (skipping decision/fork/join), regenerate the task for the previous approver, instance remains in progress. One is a death sentence, the other is sent back for review — completely different semantics.

Rollback to initiator (6) is a closed loop. It relies on a convention: the first task node of every process must be the "Initiate Application" node (assignee is applicant, resolved to the process initiator). Rollback to initiator = re-execute the first task node, participant forced to the initiator. The initiator receives a new pending task, modifies and resubmits (5 RE_APPLY), and the process continues. Without this convention, rollback to initiator is impossible — this is why all shared processes in jeeflow follow the rule that "the first node is the application node."

Countersign veto (20) is a "one-vote veto." In parallel/serial countersigning, if any participant passes 20 + countersignDisagreeFlag=1, the entire countersign node is treated as vetoed, and other waiting tasks are discarded accordingly.

3. Dispatching Above the Engine: The Facade execute

In jeeflow's unified facade JeeflowFacade.flow(action, map), processTask/execute dispatches to the corresponding engine method based on submitType (aligned with boot3's ProcessTaskController.execute):

submitType Dispatch Target
0 APPLY / 1 AGREE / 20 Countersign Veto executeProcessTask
2 REJECT executeAndJumpToEnd
3 ROLLBACK executeAndJumpTask(target=null) (trace back along edge)
4 JUMP executeAndJumpTask(target=taskName)
5 Resubmit Business-side semantics (initiator re-initiates, facade passes through)
6 Rollback to Initiator executeAndJumpToFirstTaskNode

4. Design Insight: Why Expose "Submission Type" to the Caller?

A friend once asked: Isn't submission type an internal engine matter? Why make submitType a first-class citizen of the interface?

Because the business side is the initiator of process actions: the "Agree/Reject/Rollback/Jump" buttons on the page correspond to submitType=1/2/3/4 in the interface. The mldong ecosystem's frontend (vben5 + process designer) renders buttons and submits requests according to this enum set. The engine, interface, and frontend share the same semantics — this is the power of "contract alignment": swap the engine (e.g., jeeflow's four language implementations) without changing the contract, and the frontend requires zero modifications.

That's why you see completely consistent interface behavior in jeeflow's four demos: code=0 for success, msg field, full submitType enum, highLight/approvalRecord as independent endpoints — not a coincidence, but a contract.

Conclusion

The flowchart is the skin, the state machine is the bone, and submitType is the sinew. Once you understand "two-tier state + 8 submission types," you understand the core execution semantics of a workflow engine — later discussions on process definition, countersigning, rollback mechanisms, and persistence evolution are all built on this skeleton.

Next part preview: "Process Definition Design: A Complete LogicFlow JSON Breakdown" — How do nodes, edges, and expressions describe a process? 10 shared process JSONs dissected one by one.

Want to try it yourself? Four-language demo sites are online. Online experience: https://jeeflow-demo.mldong.com (Java :8080 / Go :8081 / Python :8100 / Node :8082 + unified frontend). Initiate a process, agree, rollback, withdraw — see the state machine turn clearly.

Related Links

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

亚雷

A two-tier state machine has clear boundaries.