跪拜 Guibai
← Back to the summary

A Workflow Engine's Entire Runtime Is One JSON File

Series Positioning: jeeflow Series Part 4 (Season 2 "Core Design" Part 1) Platform: Juejin (high code density, thorough principles) Material Version: Engine v1.8.15, pro site real-time screenshots Prerequisite Reading: Part 2 · jeeflow: What Does a 98KB Workflow Engine Look Like


1. Starting from a Flowchart

Open the jeeflow-pro integrated demo site (jeeflow-pro.mldong.com), and click on "Reimbursement Application" under "Process Design":

Reimbursement Application Process Designer

Four nodes, one straight line:

Start → Initiate Application → Department Manager Approval → Financial Review (Countersign) → General Manager Approval → End

Among them, the "Financial Review" node has a blue tag in the upper right corner — Parallel Countersign.

This diagram is for humans to read. But the engine doesn't consume diagrams; the engine consumes a JSON file.

The core question this article answers: What does this JSON look like? How does it turn "drawing" into "running a process"?


2. JSON Structure Anatomy

jeeflow's process definition file is a standard JSON, located in the jeeflow-java/jeeflow-core/src/test/resources/flows/ directory, loaded uniformly when the five-language demo starts.

Take the simplest 01-simple.json (simple approval process) as an example:

{
  "name": "simple",
  "displayName": "Simple Approval Process",
  "type": "approval",
  "instanceUrl": "/form/apply",
  "nodes": [ ... ],
  "edges": [ ... ]
}

The top level has only 5 fields:

Field Meaning Example
name Unique code (for program use) "simple"
displayName Display name (for humans) "Simple Approval Process"
type Process type "approval"
instanceUrl Initiation page route "/form/apply"
nodes / edges Node array + Edge array See below

2.1 Nodes

Each node is an object with core fields:

{
  "id": "task1",
  "type": "snaker:task",
  "x": 300,
  "y": 200,
  "properties": {
    "width": 100,
    "height": 50,
    "form": "leave-form",
    "assignee": "leader",
    "taskType": 0,
    "performType": 0
  },
  "text": {
    "value": "Superior Approval"
  }
}
Field Purpose
id Unique node identifier, referenced by edges via sourceNodeId / targetNodeId
type Node type: snaker:start / snaker:task / snaker:decision / snaker:end
x / y Canvas coordinates (used by designer, engine doesn't care)
properties.assignee Approver expression — engine resolves via IUserProvider SPI
properties.performType Execution mode — 0=normal, 1=parallel countersign, 2=sequential countersign
properties.countersignType Countersign typePARALLEL / SEQUENTIAL / RATIO
properties.form Associated form route
text.value Node display text

2.2 Edges

Edges define the connection relationships between nodes:

{
  "id": "e3",
  "sourceNodeId": "decision1",
  "targetNodeId": "task2",
  "properties": {
    "expr": "amount > 1000"
  },
  "text": {
    "value": "Amount>1000"
  }
}

For normal edges, properties is an empty object {}. Conditional edges carry the expr field — this is the basis for the engine's routing decisions.

2.3 Node Type System

jeeflow has only 4 node types, covering all process patterns:

Type Identifier Purpose
Start Node snaker:start Process entry, auto-generated
Task Node snaker:task Manual approval / automated task
Decision Node snaker:decision Conditional branch gateway
End Node snaker:end Process exit

No "sub-process nodes", no "event nodes", no "timer nodes" — jeeflow's philosophy is to compose complex behaviors from a small set of primitives, rather than piling up node types.


3. Dissecting the 10 Shared Processes One by One

jeeflow's test resource directory contains 10 shared process JSONs (01-simple10-mixed-mode), shared by the five language engines and loaded automatically at startup.

3.1 Overview Table

# Filename Scenario Core Pattern Key JSON Feature
01 simple Simple Approval Single Task 1 task, assignee=leader
02 multi-task Sequential Multi-Node Linear Series Multiple tasks connected end-to-end
03 decision-expr Conditional Branch Expression Gateway Edge carries expr condition
04 fork-join Parallel Fork/Join fork+join Multiple parallel paths after fork, join converges
05 countersign-parallel Parallel Countersign Multi-person Simultaneous Approval performType:1 + countersignType:PARALLEL
06 countersign-sequential Sequential Countersign Approve One by One performType:2 + countersignType:SEQUENTIAL
07 countersign-ratio Ratio Countersign Pass Ratio Threshold countersignType:RATIO + ratio value
08 sequential-approve Sequential Approval Special Countersign Mode Create tasks one by one, complete one by one
09 with-reject Rejection Process Return to Upstream Edge points back to previous node
10 mixed-mode Mixed Mode Combination of All Above Conditional branch + countersign + rejection

3.2 Conditional Branch in Detail (03-decision-expr)

This is the most commonly used and most easily misunderstood pattern. Complete JSON structure:

{
  "name": "decision-expr",
  "displayName": "Decision Expression Process",
  "type": "approval",
  "nodes": [
    {
      "id": "start",
      "type": "snaker:start",
      "text": { "value": "Start" }
    },
    {
      "id": "apply",
      "type": "snaker:task",
      "properties": {
        "assignee": "applicant",
        "taskType": 0
      },
      "text": { "value": "Initiate Application" }
    },
    {
      "id": "decision1",
      "type": "snaker:decision",
      "properties": {
        "expr": "amount > 1000"
      },
      "text": { "value": "Amount>1000?" }
    },
    {
      "id": "task2",
      "type": "snaker:task",
      "properties": { "assignee": "manager" },
      "text": { "value": "Manager Approval" }
    },
    {
      "id": "task3",
      "type": "snaker:task",
      "properties": { "assignee": "director" },
      "text": { "value": "Director Approval" }
    },
    {
      "id": "end",
      "type": "snaker:end",
      "text": { "value": "End" }
    }
  ],
  "edges": [
    { "sourceNodeId": "start", "targetNodeId": "apply" },
    { "sourceNodeId": "apply", "targetNodeId": "decision1" },
    {
      "sourceNodeId": "decision1",
      "targetNodeId": "task2",
      "properties": { "expr": "amount > 1000" },
      "text": { "value": "Amount>1000" }
    },
    {
      "sourceNodeId": "decision1",
      "targetNodeId": "task3",
      "properties": { "expr": "amount <= 1000" },
      "text": { "value": "Amount≤1000" }
    },
    { "sourceNodeId": "task2", "targetNodeId": "end" },
    { "sourceNodeId": "task3", "targetNodeId": "end" }
  ]
}

Routing Logic:

decision1 node evaluates expr → iterates all outgoing edges → finds the first edge where expr is true → takes that path

Key points:

3.3 Parallel Countersign in Detail (05-countersign-parallel)

{
  "id": "task1",
  "type": "snaker:task",
  "properties": {
    "assignee": "userA,userB,userC",
    "performType": "1",
    "countersignType": "PARALLEL",
    "field": {
      "candidateUsers": "userA,userB,userC"
    }
  },
  "text": { "value": "Countersign Approval" }
}

Three key fields determine countersign behavior:

Field Value Meaning
performType "1" Countersign mode (0=normal, 1=parallel, 2=sequential)
countersignType "PARALLEL" Parallel countersign (everyone receives tasks simultaneously)
assignee "userA,userB,userC" Countersign personnel list

Engine Behavior:

  1. When the process reaches this node, tasks are created for all countersigners at once
  2. Each task is approved independently, without blocking each other
  3. The process only moves to the next node after everyone has passed
  4. If any node rejects → the entire countersign is rejected (one-vote veto, submitType=20)

Cross-language Difference: The Java engine creates all tasks at once; Python/Node engines create them one by one. Behavior is consistent, implementation strategy differs — this is a classic example of "contract alignment, implementation freedom".


4. From JSON to Business: 9 Real Scenarios on the Pro Site

The shared JSONs are "textbooks"; what runs on the pro site (jeeflow-pro.mldong.com) is "real combat".

4.1 Process Definition List

After logging in as superAdmin and entering "Process Definitions", you can see 28 records (9 business scenarios × multiple versions):

Pro Site Process Definition List

Process Name Unique Code Process Type Core Pattern
Leave Application biz_leave Attendance Management Conditional Branch (Days Gateway)
Purchase Application biz_purchase Business Management Conditional Branch (Amount Gateway)
Reimbursement Application biz_reimburse HR Management Sequential + Parallel Countersign
Seal Application biz_seal Business Management Simple Approval
Overtime Application biz_overtime Business Management Conditional Branch (Hours Gateway)
Contract Approval biz_contract Business Management Conditional Branch (Contract Amount)
Business Trip Application biz_business_trip Business Management Conditional Branch (Budget Gateway)
Position Transfer Application biz_transfer HR Management Sequential Multi-Node
Asset Requisition Application biz_asset Business Management Simple Approval

4.2 Conditional Branch in Action: Leave Application

Leave Application designer view:

Leave Application Process Designer

Conditional branch logic:

Initiate a 1-day personal leave with the admin account, the process trace is as follows:

Leave Process Trace

The green highlighted path clearly shows: Start → Applicant (Meng Lidong) → Conditional Branch (≤3 days path) → Department Manager Approval (Li Na) → End.

Corresponding JSON Core Structure:

{
  "nodes": [
    { "id": "apply", "type": "snaker:task",
      "properties": { "assignee": "applicant" },
      "text": { "value": "Initiate Application" } },
    { "id": "decision_days", "type": "snaker:decision",
      "properties": { "expr": "days > 3" } },
    { "id": "task_manager", "type": "snaker:task",
      "properties": { "assignee": "deptLeader" },
      "text": { "value": "Department Manager Approval" } },
    { "id": "task_director", "type": "snaker:task",
      "properties": { "assignee": "chargeLeader" },
      "text": { "value": "Supervising Leader Approval" } }
  ],
  "edges": [
    { "sourceNodeId": "decision_days", "targetNodeId": "task_manager",
      "properties": { "expr": "days <= 3" },
      "text": { "value": "≤3 days" } },
    { "sourceNodeId": "decision_days", "targetNodeId": "task_director",
      "properties": { "expr": "days > 3" },
      "text": { "value": ">3 days" } }
  ]
}

4.3 Amount Gateway in Action: Purchase Application

Purchase applications use the same conditional branch pattern, just with the judgment condition changed from "days" to "amount":

Purchase Application Process Designer

Admin initiates a 10,000 yuan purchase, process trace:

Purchase Process Trace

The path highlight shows it took the "≤20,000" branch, approved by Department Manager Li Na.

4.4 Parallel Countersign in Action: Reimbursement Application

This is the most complex scenario. Designer view:

Reimbursement Application Process Designer

Approval chain: Applicant → Department Manager → Financial Review (Parallel Countersign) → General Manager → End

Admin initiates a 5,000 yuan travel reimbursement, complete approval trace:

Reimbursement Process Trace

All four nodes passed:

Note: The actual countersign group has 6 people, not just the two words "Financial Review" seen on the designer. Countersign personnel are dynamically resolved by the assignee field + IUserProvider SPI, which can be a fixed list, department role, or even an expression calculation result.

4.5 Mapping Summary

Shared Process Pro Site Business Scenario JSON Pattern
03-decision-expr Leave Application (Days Gateway) Conditional Branch
03-decision-expr Purchase Application (Amount Gateway ≤20k) Conditional Branch
03-decision-expr Contract Approval (100k Threshold) Conditional Branch
03-decision-expr Business Trip Application (5000 Budget) Conditional Branch
03-decision-expr Overtime Application (4h Hours) Conditional Branch
05-countersign-parallel Reimbursement Application (Financial Countersign ALL) Parallel Countersign
01-simple Seal Application (Single Approval) Simple Approval
01-simple Asset Requisition (Single Approval) Simple Approval
02-multi-task Position Transfer (Multi-Node Sequential) Sequential Multi-Node

9 business scenarios = permutations and combinations of 4 JSON patterns.


5. Version Management and Designer

5.1 Multi-Version Coexistence

The pro site's process definition list shows 28 records — the same process (e.g., "Purchase Application") has multiple versions like v3, v4 coexisting.

The significance of version management:

5.2 Designer Operations

The process designer (based on the mldong-flow-designer-plus npm package) provides the following operations:

Function Description
Zoom Out/In Canvas zoom
Fit Auto-zoom to canvas size
Clear Clear canvas
View Data View/Edit underlying JSON
Import Import process definition from JSON file
Save Save current design to backend
Fullscreen Fullscreen editing mode

The designer is a pure frontend component, not bound to any backend — it produces JSON, the engine consumes JSON, and the two are decoupled through the JSON contract.


6. JSON as Protocol

Returning to the opening question: How does this JSON turn "drawing" into "running a process"?

The answer is three-layer decoupling:

┌─────────────┐     JSON      ┌──────────────┐     JSON      ┌──────────────┐
│  Process     │ ───────────→ │  Process      │ ───────────→ │  Workflow     │
│  Designer    │   Produces    │  Definition   │   Parses      │  Engine       │
│ (vben5-wf)   │    JSON       │  File (.json) │    JSON       │  (jeeflow)    │
└─────────────┘               └──────────────┘               └──────────────┘
     Frontend                       File                         Backend
  Not bound to engine           Not bound to frontend         Not bound to framework

This is the foundation of jeeflow's "one set of process definitions, four language implementations" — not a slogan, but a single JSON file.


References


Next Article Preview: Part 5 · Designing a Workflow Engine with DDD: Aggregate Roots and Rich Domain Models —— Why must ProcessInstance be an aggregate root? What is the responsibility boundary of ProcessTask as a child entity? Why can't an anemic domain model handle workflow scenarios?

Comments

Top 2 of 3 from juejin.cn, machine-translated. The original thread is authoritative.

Iism

I want to ask, if from a certain step, it's sent back to the previous step, how does jeeflow draw this?

mldong

The flowchart in the process designer only defines the forward flow path (from start to end). Sending back/rejecting does not need to draw reverse lines on the diagram. Sending back is a runtime behavior, not a design-time connection: When handling, the front end provides a 'Send Back' button. After clicking, the engine automatically lists all nodes that have been traversed (jumpAbleTaskNameList), and the approver chooses which step to send back to. The engine jumps back to the target node and regenerates the to-do item. The flowchart's responsibility is to define 'how the process goes'; which step to send back to is decided by the approver at runtime and does not need to be pre-drawn in the designer.

汪汪队首席上单

Free? What's the use?