Apitomy Flow
A lightweight, visual workflow engine for orchestrating long-running project lifecycles. Designed as a standalone library/Quarkus extension that integrates into Apitomy products (starting with Axiom).
Motivation
Axiom needs workflow support to model project lifecycles — e.g. CVE triage (analyze → triage → mitigate or close) and SDLC for GitHub issues (plan → implement → PR → merge). Existing engines like Kogito/BPMN are too heavyweight. Apitomy Flow provides a streamlined alternative with visual definition and execution, purpose-built for Axiom's needs but generic enough to reuse.
Architecture
Two deliverables
-
apitomy-flow (engine library) — Graph model, execution engine, state persistence, condition evaluation. Defines a
NodeExecutorSPI that the host application implements for each node type. Ships as a Quarkus extension or plain JAR. Includes the React visual editor component. -
Axiom integration (in apitomy-axiom) — Implements
NodeExecutorfor each Axiom-specific node type. Integrates the visual editor into the Axiom UI. Wires workflow state to the project model.
Core Model: State Machine with Conditional Edges
A directed graph (not strictly a DAG — cycles are allowed for re-review/retry loops).
Workflow Definition (serializable as JSON):
Workflow {
id: string
name: string
description?: string
nodes: WorkflowNode[]
edges: WorkflowEdge[]
}
WorkflowNode {
id: string
type: "start" | "end" | "action" | "human-task" | "receive-event"
name: string
config: Record<string, any> // type-specific configuration
position: { x: number, y: number } // visual editor coordinates
}
WorkflowEdge {
id: string
source: string // source node id
target: string // target node id
condition?: string // expression evaluated against workflow context
priority: number // for ordering when multiple edges leave a node
isDefault: boolean // fallback when no conditions match
label?: string // display label for the edge
}
Workflow Instance (runtime state):
WorkflowInstance {
id: string
workflowId: string // reference to the definition
currentNodeId: string // which node the instance is at
status: "running" | "waiting" | "completed" | "failed"
context: Record<string, any> // accumulated data from completed nodes
history: HistoryEntry[] // path taken through the graph
createdOn: timestamp
updatedOn: timestamp
}
Node Types (5)
| Node Type | Purpose | Behavior |
|---|---|---|
| Start | Entry point | One per workflow. Immediately transitions to the next node. |
| Action | Automated work | Invokes a NodeExecutor. Blocks until executor returns a result. Result merges into context. |
| Human Task | Human approval/input | Blocks until a human responds. Has config for title, description, references, output schema (form fields). |
| Receive Event | Wait for external event | Blocks until a matching event arrives. Has config for event type filter and optional field matching. |
| End | Terminal state | Marks the instance as completed. Has config for outcome metadata (success, failure, reason). |
Branching: Conditions on Edges
- Each node can have multiple outgoing edges
- Each edge has an optional condition expression evaluated against the workflow context
- Edges are evaluated in priority order — first match wins
- One edge per node can be marked as default (fallback)
- Visually, edges display a small condition badge
Condition Expression Language
TBD — options:
- Simple property comparison (e.g. result.status == "not-affected")
- JSONPath or JMESPath expressions
- A minimal custom DSL
- SpEL (Spring Expression Language) — though not ideal for Quarkus
Execution Engine
When a node completes: 1. Retrieve the node's outgoing edges, sorted by priority 2. Evaluate each edge's condition against the workflow context 3. Transition to the first matching edge's target node (or the default edge) 4. Execute the target node (or enter a wait state for Human Task / Receive Event)
The engine must: - Persist state after each transition (survives server restarts) - Support long-running waits (days/weeks for human tasks and events) - Resume from persisted state on application startup - Track execution history (which nodes were visited, when, with what context)
Persistence
- Workflow definitions stored as JSON (single column, like Axiom's dashboard widgets)
- Workflow instances stored as rows with JSON context/history columns
- Uses the host application's existing database (PostgreSQL via Panache/Hibernate)
- The engine provides JPA entities; the host app includes them in its persistence unit
Visual Editor (React)
- Drag-and-drop node placement on a canvas
- Connect nodes with edges (click source → click target)
- Configure nodes via a properties panel
- Add conditions to edges via the properties panel
- Read-only visualization mode showing the current node highlighted and path taken
- Library: evaluate React Flow (reactflow.dev) as the canvas library
SPI: NodeExecutor
public interface NodeExecutor {
String nodeType(); // e.g. "action", "human-task", "receive-event"
CompletableFuture<NodeResult> execute(NodeExecutionContext context);
}
public interface NodeExecutionContext {
WorkflowNode node();
Map<String, Object> workflowContext();
Map<String, Object> nodeConfig();
}
public record NodeResult(
Status status, // COMPLETED, FAILED
Map<String, Object> output // merged into workflow context
) {}
The host application provides NodeExecutor implementations for each node type. The engine discovers them via CDI.
Axiom Integration Plan
Node Type Implementations
| Flow Node Type | Axiom Implementation |
|---|---|
| Action | Creates an Axiom task with the configured action type on the project. Completes when the task completes. Task output becomes node output. |
| Human Task | Creates an Axiom inbox item with the configured humanContext and outputSchema. Completes when the user responds. Response becomes node output. |
| Receive Event | Registers an event listener. Completes when a matching Axiom event arrives for the project. Event payload becomes node output. |
UI Integration
- Project detail page gets a "Workflow" tab showing the read-only visualization of the current workflow state
- Workflow definitions are managed in a new Settings section (alongside Action Types, Scheduled Jobs, etc.)
- Workflow definition editor uses the visual drag-and-drop editor
- A project can have one workflow assigned (optional). When assigned, the workflow starts on project creation.
Relationship to Lifecycle Hooks (#197)
Lifecycle hooks (issue #197) trigger individual action types on project events. Workflows are a more structured alternative — when a project has a workflow, the workflow controls the sequence of actions rather than individual hooks. Both can coexist: hooks for simple one-shot triggers, workflows for multi-step lifecycles.
Design Decisions Still Needed
- [ ] Condition expression language
- [ ] How to handle node execution failures (retry? error edges? fail the workflow?)
- [ ] Versioning: what happens to running instances when a workflow definition is updated?
- [ ] Can a workflow be restarted or reset to a specific node?
- [ ] Should the visual editor be a shared npm package or built into Axiom's UI directly?
- [ ] React Flow vs. other canvas libraries for the visual editor
- [ ] Should workflow definitions be shareable/exportable (JSON import/export)?
Example Workflows
CVE Triage
Start → Action(analyze-cve) → Human Task(triage: affected?)
→ [affected] Action(plan-mitigation) → Human Task(approve plan)
→ [approved] Action(implement-fix) → Action(verify-fix) → End(mitigated)
→ [rejected] → Action(analyze-cve) // loop back
→ [not-affected] Action(close-tracker) → End(not-affected)
GitHub Issue SDLC
Start → Action(analyze-and-plan) → Human Task(approve plan)
→ [approved] Action(implement) → Action(create-pr) → Receive Event(pr-merged) → End(completed)
→ [rejected] → Action(analyze-and-plan) // loop back
→ [wont-fix] End(wont-fix)
Influenced By
- AWS Step Functions — state machine model, Choice state, callback pattern for human tasks
- BPMN 2.0 — user task concept, gateway branching, event catch
- n8n — visual editor UX, conditions on edges rather than gateway nodes
- Temporal — durable execution, long-running wait states
Deliberately avoided full BPMN complexity (parallel gateways, sub-processes, compensation, complex event correlation).