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:
- Instance state: where the overall process is
- Task state: where a specific approval item is
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) |

Three easily overlooked design points:
- Instances only have "Completed," not "Approved" — Instance 20 is a terminal state, triggered after the last task completes;
- 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;
- Branch states rely on "cascading" — Withdrawal/termination/suspension are instance-level commands. Task states cascade and persist with the instance (
updateInstancepersists 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
- jeeflow documentation site: https://jeeflow-doc.mldong.com
- Open-source demo site (online experience): https://jeeflow-demo.mldong.com
- Integrated demo site: https://jeeflow-pro.mldong.com
- Engine repositories (GitHub · mldong organization):
jeeflow-java/jeeflow-go/jeeflow-python/jeeflow-node/jeeflow-ui
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
A two-tier state machine has clear boundaries.