The Two-Tier State Machine That Keeps Workflow Engines From Lying to You
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.
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.
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.