跪拜 Guibai
← All articles
Backend

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

By mldong ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Most homegrown workflow engines get the happy path right and then crumble on rollback, withdrawal, and parallel-branch cleanup. A two-tier state machine with a dedicated Discarded task state and cascading instance-to-task updates eliminates the ghost tasks and half-dead instances that plague ad-hoc implementations. The submitType-as-contract pattern also means a team can swap the entire engine runtime without touching the UI layer.

Summary

A workflow diagram looks clean, but the engine underneath has to answer harder questions: where is the process right now, what happens when someone rejects a step, and how do you clean up tasks after a withdrawal or jump? jeeflow answers with a two-tier state machine that tracks instance state (In Progress, Completed, Rejected, Withdrawn, Terminated, Suspended, Discarded) separately from task state, with cascading updates that prevent orphaned tasks.

Eight submission types — APPLY, AGREE, REJECT, ROLLBACK, JUMP, RE_APPLY, ROLLBACK_TO_OPERATOR, and COUNTERSIGN_DISAGREE — act as the driving instructions. Each maps to a specific engine method and a specific UI button, creating a contract that stays identical across Java, Go, Python, and Node implementations. Rejection terminates the instance outright; rollback sends it back one step while keeping the instance alive. A countersign veto from any participant kills the entire countersign node.

The design exposes submitType as a first-class interface parameter because the business side owns the action. Frontend buttons, API calls, and engine dispatch all share the same enum, so swapping the engine implementation behind the facade requires zero frontend changes.

Takeaways
jeeflow separates process state into two tiers: instance state (10 In Progress, 20 Completed, 30 Withdrawn, 40 Terminated, 45 Rejected, 50 Suspended, 99 Discarded) and task state (10 Pending, 20 Completed, cascaded 30/40/50, 99 Discarded).
Task state 99 (Discarded) cleans up pending tasks that get skipped during a rollback or jump, preventing ghost entries in to-do lists.
Instance-level commands like withdrawal, termination, and suspension cascade to all tasks in the same database connection, so no task is left running after its instance stops.
Rejection (submitType 2) calls executeAndJumpToEnd and sets the instance to 45 with no new pending tasks; rollback (submitType 3) traces backward along edges to the previous task node and regenerates a task while keeping the instance at 10.
Rollback-to-initiator (submitType 6) depends on a convention: every process's first task node must be the application node with assignee set to applicant, so the engine can re-execute it and force the initiator as participant.
Countersign veto (submitType 20) lets any participant kill the entire countersign node; all other waiting tasks are discarded.
The unified facade JeeflowFacade.flow dispatches each submitType to a specific engine method, and the same enum values drive frontend button rendering in the vben5 process designer.
All four language implementations (Java, Go, Python, Node) share identical enum values, method signatures, and API response contracts (code=0, msg field, full submitType enum).
Conclusions

Exposing submitType as a first-class API parameter rather than hiding it inside the engine is a deliberate architectural choice that treats the UI, API, and engine as equal parties to a shared contract. This inverts the usual pattern where the engine owns semantics and the frontend adapts.

The Discarded task state (99) is the kind of detail that separates a workflow engine that works in demos from one that works after six months of production use. Skipped tasks that linger as pending are a common source of support tickets in homegrown systems.

Making the first task node a mandatory application node with a reserved assignee keyword (applicant) is a constraint that buys a lot: it makes rollback-to-initiator a generic operation rather than something that requires per-process custom logic.

jeeflow's cross-language contract alignment — same enums, same method signatures, same API response shapes across four runtimes — is a stronger guarantee than most polyglot workflow projects offer. It means the decision of which runtime to deploy can be made independently of the frontend and process definitions.

Concepts & terms
Two-tier state machine
A state machine split into instance-level state (tracking the overall process: In Progress, Completed, Rejected, etc.) and task-level state (tracking individual approval items: Pending, Completed, Discarded). Instance commands cascade to tasks so the two tiers never diverge.
submitType
An 8-value enum (APPLY, AGREE, REJECT, ROLLBACK, JUMP, RE_APPLY, ROLLBACK_TO_OPERATOR, COUNTERSIGN_DISAGREE) that maps each business action — clicking Approve, Reject, Rollback, etc. — to a specific engine method. It serves as the shared contract between UI, API, and engine across all four language implementations.
Task Discarded (99)
A task state indicating a pending task was invalidated because the process rolled back, jumped past it, or a countersign was vetoed. Without it, skipped tasks remain as ghost entries in to-do lists.
Countersign veto
In a parallel or serial countersign (multiple approvers), any single participant can cast a veto (submitType 20 with countersignDisagreeFlag=1), which terminates the entire countersign node and discards all other waiting tasks.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗