Building a Lightweight Approval Flow with AI: Why Flowable Wasn't the Right Fit
The author details building a lightweight approval flow end-to-end using AI (Codex), covering process modeling, dynamic forms, conditional and parallel branches, task routing, path replay, and analytics, while explaining why Flowable was rejected due to excessive tables, steep learning curve, integration difficulty, and the need for a lightweight, configurable solution.
1. Overview
1.1 Introduction
Over the past month the author used an AI coding agent (Codex) to implement a lightweight approval flow end-to-end. The 1.0 version delivers a complete business loop: process modeling, dynamic forms, node configuration, instance initiation, to-do/done/cc lists, approval details, runtime path replay, and an analytics dashboard. Nearly all front-end and back-end code was written by Codex across multiple continuous tasks. The human role was to set goals, define boundaries, choose between key design alternatives, and accept the final result; Codex handled repository exploration, plan refinement, code changes, test runs, bug location, and iteration.
1.2 Questions the 1.0 Version Answers
Will a process model change affect running instances?
How to ensure condition branches use the same judgment logic at design, preview, and runtime?
Under what conditions should countersign, any-sign, and sequential approval proceed?
How to prevent the same task from being processed twice when two users click approve simultaneously?
How to handle condition expressions that reference a form field after that field is deleted?
Can the system accurately replay the actual nodes and path an instance traversed after completion?
Can administrators pinpoint exactly where a process is stuck, rather than just seeing “4 approvals in progress”?
2. Implemented Functionality
The system forms a closed loop from design to execution to observability.
Model management : drafts, publish, deactivate, copy, version control. A model contains basic info, dynamic form, node tree, and extra settings. Publishing creates an immutable version; new instances use the active version while running instances stay on their original definition.
Dynamic form design embedded in the model. Fields drive both the initiation UI and condition-branch evaluation, so form and flow design are tightly coupled.
Node types : approval, cc, condition branch, parallel branch. Approval nodes support manual, auto-approve, auto-reject, and three multi-user strategies: countersign (all must complete), any-sign (first approval ends others), sequential (one after another).
Runtime preview : while filling the form, the front end polls the back end every 500 ms (with a request-ID guard) to show the predicted approval path.
Tracking views : my applications, to-do, done, cc lists; detail page shows form snapshot, approval path, actual flow diagram, and transition log.
Analytics dashboard : period started, period completed, currently processing, P50/P90 duration, pass rate, real-time backlog, process health ranking, node bottlenecks. Metrics definitions: started/completed by time range; processing/backlog by current stock; pass rate excludes cancelled; P50/P90 only on finished approved/rejected instances; long-running threshold is explainable, not a fake SLA; trends fill empty day/week/month buckets.
3. Why Build a Custom Lightweight Approval Flow 3.1 Motivation The author previously worked on systems with approval flows and found the code itself not inherently hard, especially with today’s AI coding ability. The real challenge is understanding business scenarios, identifying key boundaries, and trading off complexity vs. generality. Existing internal systems required large amounts of adapter code for each new business integration, prompting the decision to build a reusable lightweight engine. 3.2 Why Not Flowable Flowable is a mature Apache 2.0 BPM platform with BPMN/CMMN/DMN engines, embeddable via Java/Spring or REST, and features async executors, persistent timers, retry, dead-letter handling, and configurable history levels. “Cannot embed”, “license cost”, or “insufficient features” are not valid reasons. The trade-off is whether the current stable, controlled approval needs justify pulling in BPMN semantics, a full runtime, job system, history system, and the associated team learning cost. Four concrete reasons: Too many tables : Flowable 6.8 creates 79 tables in the business database, complicating migration, backup, and monitoring. High learning curve : BPMN 2.0 (swimlanes, subprocesses, events, gateways, boundary events) is hard for developers and impractical for business users (e.g., HR creating a leave process). Deep integration difficulty : Even with a custom front-end designer, mapping to Flowable’s core (user system, permissions, transactions) is hard because the execution logic lives inside the framework. Lightweight & configurable : A business-friendly tree of nodes with drag-and-drop, dynamic form fields directly usable in conditions, full code control. 4. Handing a Full-Stack Approval Requirement to Codex AI succeeds when given a bounded, contract-driven engineering environment, not a scattered prompt. The author placed Vue front-end, Flow back-end, and system-settings service in one full-stack workspace with repo-level rules: Every business feature must check front-end and back-end impact together. Back-end interface VO is the contract source. Process nodes are a tree; modifications must consider childNode , branches, and parent-child linkage. One task must deliver interface, page integration, and joint debugging. New behavior requires tests. Two repos checked separately, no auto-commit. Human vs. AI responsibilities: <code>Human: propose business goal → define boundaries → choose design → accept result AI: investigate code → make plan → implement front/back → run tests → fix issues</code> If business trade-offs are also delegated to AI, the result may be “code looks complete but no one knows why it was designed that way.” 5. Real Collaboration Examples with AI 5.1 Requirements Emerge Iteratively: Form Ownership Initial idea: manage forms independently for reuse across processes. Deeper analysis revealed that condition branches and node permissions reference fields; a shared mutable form could break multiple published processes when a field is deleted. After several rounds, the decision converged: templates are reusable, but each process model owns its own form copy; form and node tree are saved, validated, and published together; runtime instances use the published snapshot. 5.2 Dashboard: From Metric Discussion to Full-Stack Delivery in One Task Starting from a vague “dashboard around FlowModel, FlowInstance, FlowTask”, Codex first framed it as an organizational operations board (not personal to-do), prioritizing backlog/bottleneck discovery over volume stats. It then defined precise statistical calibers (listed above). Once confirmed, a single task added back-end aggregation API, query objects, VO, service, SQL, and tests, plus front-end API, types, data transforms, chart page, and tests. Verification included targeted dashboard tests, front-end ESLint, production build, and back-end module tests. The “end-to-end” value: from metric definition onward, interface fields, statistical calibers, and page explanations stay consistent — even the P50/P90 tooltip text was synced back to the back-end OpenAPI field description. 5.3 The Hard Part Isn’t Generating Forms, It’s Maintaining Cross-Module Invariants Dynamic forms can use mature components; the difficulty is the relationship between form fields and process conditions. Example: a condition node checks “leaveDays > 3”. If an admin renames, deletes, or makes the field non-required, will the process still execute correctly? The solution centralized rules in a utility function that walks the node tree, collects referenced fields, and synchronizes rule.field on rename. Save/publish validation checks for duplicate fields, existence of referenced fields, and required status of condition fields; deleting a referenced field is blocked. Date fields use a unified submission format to avoid drift between design, runtime, and back-end comparison. This cross-module invariant — keeping form, process tree, interface, and executor speaking the same language long-term — is the real engineering challenge. <code>function visit(node?: ModelNode) { if (!node) return; for (const group of node.condition?.conditionGroups ?? []) { for (const rule of group.conditionRules) { // collect references, or sync rule.field on rename } } node.conditionNodes?.forEach(visit); visit(node.childNode); }</code> 5.4 AI Leaves Bugs; the Key Is a Feedback Loop Parallel branches reused the condition-node type but don’t need condition expressions. The publish validator initially treated all condition nodes as regular branches, causing “condition config cannot be empty” errors on parallel flows. The fix task didn’t just patch the logic; it first added two regression cases: Parallel branch child without condition should pass. Regular condition node without condition should still fail. The first case failed before the fix; then an exemption was added only for parallel nodes, followed by targeted and full Flow module tests. All 38 back-end tests passed. This mirrors real engineering: AI makes mistakes, the goal isn’t zero errors but a reproducible, verifiable, convergent feedback loop. 6. Core Engine: Four Tables and Core Controls The engine compresses to four core entities: FlowModel : editable process model containing form, node tree, initiation scope, admins. FlowDefinition : snapshot created on each publish, with incrementing version. FlowInstance : one actual application, stores definitionId , business data, status, current node. FlowTask : manual to-do and node execution records, stores assignee, approval mode, status, comment. Node structure uses a business-friendly JSON tree instead of BPMN XML: <code>class FlowNode { String key; Integer type; FlowNode childNode; // sequential successor List<FlowNode> conditionNodes; // branch collection ProcessNodeCondition condition; FlowNode parentNode; // added at runtime, not serialized ... }</code> Ordinary nodes link via childNode ; condition/parallel containers expand multiple branches via conditionNodes . The parser back-fills parent references before execution so a branch reaching its end can walk up to find the common successor after convergence. This model cannot express full BPMN semantics but is intuitive for fixed office approvals and lets front-end and back-end share the same node contract. 6.1 Publish Creates a Definition Snapshot, Not an Overwrite Overwriting the running config would break in-flight instances. Therefore model and definition are separate. Publish flow: <code>lock FlowModel → validate node tree & form references → copy to new FlowDefinition → increment version → write back model’s activeDefinitionId / activeVersion</code> New instances can only use the currently active published definition; once created, an instance is bound to its definitionId . Subsequent model edits don’t affect running instances. This also enables accurate historical form and node-tree replay on the detail page. 6.2 Executor Handling of Conditions and Parallel Branches DefaultProcessExecutor is the routing hub, dispatching by node type to start, approval, cc, condition, parallel, and end logic. Condition branches use first-match-by-priority: <code>for conditionBranch in branches: if matches(instance.variables, conditionBranch.condition): execute(conditionBranch.childNode) break</code> Condition groups and intra-group rules support AND/OR; numeric, date, string, and collection comparisons are implemented via controlled operators — no arbitrary script execution. A no-condition branch serves as default at the end. Parallel branches launch all sub-branches. Each branch increments a completion counter on reaching the join point; only when completed count equals total branches does the instance proceed to the common successor. Note: “parallel” here means business-semantic parallel to-dos (multiple tasks created in the same execution chain), not multi-threaded async execution. Approval nodes handle three multi-user strategies: Countersign: all tasks must complete before continuing. Any-sign: first approval cancels other running tasks of the same node. Sequential: current assignee approves → next assignee task created → last completer continues. 6.3 Concurrency Protection: Preventing Double Processing of the Same Task A common race condition: two requests submit the same to-do simultaneously. Current implementation uses SELECT ... FOR UPDATE to lock the process instance, then performs a conditional atomic update to complete the task. The update WHERE clause includes: Task still in running state. Task not deleted. Current organization matches. Current assignee matches. Node type is indeed an approval node. Only a row count of 1 counts as success; otherwise returns “task already processed”. This protects the current approval action but is not a general command-idempotency system with request IDs — the article does not conflate the two. 6.4 Front End Is Not a Second Copy of Execution Rules The initiation page listens to form data, debounces 500 ms, then calls the back end to compute the predicted path, using an incrementing request ID to discard stale responses: <code>const requestId = ++routeRequestId; const nodes = await calculateFlowRoute({ definitionId, varMap: normalizeFormData(formData), }); if (requestId === routeRequestId) { routeNodes.value = nodes; }</code> Deliberate boundary: front end provides instant feedback but does not duplicate the condition executor. The path is always computed by the back end against the published definition and form variables, so preview and real execution share the exact same rules and never drift. The same flow component serves three states: design (editable), preview (shows predicted path), runtime (highlights actual hit branch with statuses: in-progress, approved, rejected, cancelled). 6.5 End-to-End Walk-Through of One Application From the user view it’s a form submit; from the engine view it passes definition selection, permission check, instance creation, variable persistence, node dispatch, task creation, condition evaluation, concurrency protection, and state persistence. 7. When to Build Custom vs. Use a Mature Engine Consider lightweight custom build when: Processes are mainly stable human approvals. Node types and routing rules are limited in number. Product semantics are tightly bound to existing org, form, and permission systems. No requirement for BPMN model interchange. Team accepts long-term ownership of state machine, concurrency, migration, and audit responsibilities. Sufficient automated tests and clear capability boundaries exist. Re-evaluate Flowable et al. when: Cross-system long-running processes and service-task orchestration appear. Large numbers of timers, async tasks, retries, compensations are needed. BPMN standard modeling, import/export, or cross-team collaboration required. Subprocesses, events, complex gateways keep growing. Higher demands for history audit, ops management, process governance. Team unwilling to maintain a custom runtime long-term. AI lowers implementation cost but does not automatically eliminate maintenance responsibility. A system quickly written by AI today doesn’t mean future compatibility, migration, data repair, and incident handling come for free. 8. Conclusion This practice reinforces that the highest value of AI coding agents isn’t “generate a page in ten seconds” but whether they can continuously traverse requirements, models, interfaces, implementation, tests, and fixes under explicit constraints to land a real business loop. Plasticene Flow does not attempt to re-implement a BPM platform. It picks a small, stable problem space: human approval, conditional routing, parallel to-dos, version snapshots, dynamic forms, runtime tracing — and deeply integrates that slice into the existing back-office. Not using Flowable doesn’t mean Flowable is bad; choosing custom doesn’t mean custom is inherently lighter. The choice stands on three premises: boundaries are clear enough, integration payoff with existing systems is large enough, and the team is willing to own the missing generic capabilities. AI made this choice feasible, but the system’s direction is still decided by human judgment of boundaries.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
