Apitomy Flow 2.x Upgrade Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Upgrade Axiom from apitomy-flow-engine 1.0.4 / @apitomy/flow-ui 1.0.3 to 2.0.0, then adopt parallel fork/join execution and the new flow-ui features (simulation, diff viewer, import/export).
Architecture: Axiom uses Flow as an in-process, stateless orchestration library. Instance state
is serialized JSON in WorkflowRunEntity.instanceState; a stub NodeExecutorProvider parks every
node as PENDING and Axiom's Task system does the real work. The 2.x upgrade touches one service
(WorkflowExecutionService), two mappers, one REST resource, the OpenAPI spec, and the UI pages
that render workflow runs.
Tech Stack: Java 17+/Quarkus, Maven, JUnit 5, Jackson; React/TypeScript UI with
@apitomy/flow-ui.
Spec: This document (analysis section below serves as the spec). Flow 2.x reference docs:
~/git/apitomy/apitomy-flow/docs/user-guide/parallel-fork-join.md.
Global Constraints
- Flow coordinates:
io.apitomy:apitomy-flow-engine:2.0.0and@apitomy/flow-ui@2.0.0. Note: as of writing, only2.0.0-SNAPSHOTexists locally (nov2.0.0tag in apitomy-flow). Confirm the release artifact is available before starting; use2.0.0-SNAPSHOTfor local dev. - API-first: any REST payload change starts in
common/api/src/main/resources/openapi.json, thenmvn installto regenerate beans, then implement (see CLAUDE.md). - Back-compat: existing 1.x
instanceStateJSON in the DB must keep deserializing (Flow 2.x null-coerces missingactiveBranches/joinArrivals; do not break this). - 4-space indent, explicit types, Javadoc on public methods, JUnit 5.
Background: What changed in Flow 2.0.0
Breaking (affects Axiom):
WorkflowInstancerecord gainedList<ActiveBranch> activeBranchesandMap<String, List<String>> joinArrivals(inserted beforefailureReason). Jackson round-trip of old JSON still works (nulls coerced to empty).instance.currentNodeId()is now nullable: non-null only when exactly one branch is active. Axiom reads it inWorkflowExecutionService(task creation,persistInstanceState) andWorkflowRunBeanMapper.completeCurrentNode(...)throwsIllegalStateExceptionwhen multiple branches are waiting. New canonical API:completeNode(workflow, instance, nodeId, result).getActionInfo/getHumanTaskInfogained nodeId-addressed overloads:getActionInfo(workflow, instance, nodeId). The 2-arg forms only work with a single active branch.- Validation: warning
UNCONDITIONAL_MULTIPLE_EDGESremoved; multiple unconditional edges now fork. New ERROR codes:MIXED_FORK_EDGES,FORK_WITHOUT_JOIN,UNBALANCED_PARALLEL,CROSSING_PARALLEL_REGIONS,PARALLEL_BRANCH_REACHES_END,PARALLEL_REGION_CYCLE. HistoryEntrygainedbranchId(8th component); 7-arg constructor retained (branchId=null).- flow-ui TS types:
WorkflowInstance.currentNodeId: string | null, plus requiredactiveBranches: ActiveBranch[]andjoinArrivals: Record<string, string[]>.
New features:
- Parallel fork/join execution (engine):
ActiveBranch,ParallelRegions.analyze(Workflow), branch-aware history. - flow-ui:
SimulationPanel(interactive simulation + EL condition testing),WorkflowDiffViewer, import/export (serializeWorkflow,parseWorkflow,downloadWorkflowJson), canvas PNG export, parallel-region rendering hints.
NodeType is unchanged (RECEIVE_EVENT/WAIT already existed in 1.0.4).
Phase 1 — Mechanical upgrade (no new behavior)
Task 1: Bump engine dependency and get a clean compile
Files:
- Modify: app/pom.xml (apitomy-flow-engine 1.0.4 → 2.0.0)
Interfaces:
- Produces: compiling build against Flow 2.x; later tasks rely on engine.completeNode(...) and
instance.activeBranches() being available.
- [ ] Step 1: Edit
app/pom.xml, change theapitomy-flow-engineversion to2.0.0(or2.0.0-SNAPSHOTuntil released). - [ ] Step 2: Run
mvn -q compile -pl app -am. Expected: compiles (Axiom builds instances via Jackson/engine, not positional constructors). If any positionalWorkflowInstanceorHistoryEntryconstruction fails, switch to the builder / 8-arg form. - [ ] Step 3: Run
mvn -q test -pl app. Record failures for Tasks 2–4; do not fix here. - [ ] Step 4: Commit:
git commit -m "build: upgrade apitomy-flow-engine to 2.0.0"
Task 2: Branch-aware node completion in WorkflowExecutionService
Files:
- Modify: app/src/main/java/io/apitomy/axiom/app/WorkflowExecutionService.java
- Test: app/src/test/java/io/apitomy/axiom/app/WorkflowExecutionServiceTest.java (or nearest
existing test class covering onTaskCompleted)
Interfaces:
- Consumes: TaskEntity.nodeId (already persisted per task at creation time).
- Produces: onTaskCompleted advances the correct branch via
engine.completeNode(workflow, instance, task.nodeId, result).
- [ ] Step 1: Write a failing test: build a linear workflow, start it, complete the task, and
assert the run advances — but assert the service calls the nodeId-addressed path by using a
workflow where the task's
nodeIdis passed explicitly (after Step 3 the old 3-arg call is gone, so an existing green test plus a new parallel test in Task 5 is acceptable coverage; at minimum assertonTaskCompletedstill completes a single-branch run). - [ ] Step 2: Run the test; expected FAIL/compile error if referencing new behavior.
- [ ] Step 3: In
onTaskCompleted(~line 235), replaceworkflowEngine.completeCurrentNode(workflow, instance, result)withworkflowEngine.completeNode(workflow, instance, task.nodeId, result). - [ ] Step 4: In the task-creation paths (~lines 339–396), replace the 2-arg
getHumanTaskInfo(workflow, instance)/getActionInfo(workflow, instance)with the 3-arg nodeId overloads, and settask.nodeIdfrom the branch's node id rather thaninstance.currentNodeId()(see Task 5 for the multi-branch loop; for now passinstance.currentNodeId()guarded non-null). - [ ] Step 5: Run tests:
mvn -q test -pl app -Dtest=WorkflowExecutionServiceTest. Expected: PASS. - [ ] Step 6: Commit:
feat: use branch-addressed completeNode/getInfo APIs
Task 3: Nullable currentNodeId in persistence and mappers
Files:
- Modify: app/src/main/java/io/apitomy/axiom/app/WorkflowExecutionService.java (~line 436,
persistInstanceState)
- Modify: app/src/main/java/io/apitomy/axiom/app/WorkflowRunBeanMapper.java (lines 57, 84–88)
- Test: app/src/test/java/io/apitomy/axiom/app/WorkflowRunBeanMapperTest.java (create if absent)
Interfaces:
- Produces: run summary/detail beans tolerate currentNodeId == null (renders no current node
name); WorkflowRunEntity.currentNodeId column stores null when parallel-parked.
- [ ] Step 1: Write failing test: map a
WorkflowRunEntitywhosecurrentNodeIdis null and whose instance JSON contains twoactiveBranches; assert no NPE andcurrentNodeNameis null. - [ ] Step 2: Run test; expected FAIL (NPE or missing handling).
- [ ] Step 3: Guard all
instance.currentNodeId()dereferences with null checks; verify the DB column forworkflow_run.current_node_idis nullable (check DDL underapp/src/main/resources/db; add a migration only if it is NOT NULL). - [ ] Step 4: Run test; expected PASS.
- [ ] Step 5: Commit:
fix: tolerate null currentNodeId for parallel-parked runs
Task 4: Publish-time validation — new parallel error codes
Files:
- Modify (if needed): app/src/main/java/io/apitomy/axiom/app/rest/WorkflowDefinitionsResourceImpl.java
- Test: existing publish/validation tests
- [ ] Step 1: Review the validator usage: Axiom already filters
ValidationSeverity.ERRORgenerically, so the new codes flow through automatically. Add one test publishing a definition with a fork whose branches never rejoin; assert 400 and that the response body contains problem codeFORK_WITHOUT_JOIN. - [ ] Step 2: Run test; expected PASS with no code change (or minimal message mapping tweaks).
- [ ] Step 3: Remove any Axiom-side handling/suppression of the deleted
UNCONDITIONAL_MULTIPLE_EDGESwarning if present (grep for it). - [ ] Step 4: Commit:
test: cover Flow 2.x parallel validation codes on publish
Task 5: flow-ui bump and TS type fixes
Files:
- Modify: ui/package.json (@apitomy/flow-ui 1.0.3 → 2.0.0)
- Modify: ui/src/pages/WorkflowRunDetailPage.tsx, ui/src/components/WorkflowTab.tsx (nullable
currentNodeId, new required activeBranches/joinArrivals on any locally-constructed
WorkflowInstance values)
- [ ] Step 1: Bump the dependency, run
npm installinui/. - [ ] Step 2: Run
npm run build(ortsc --noEmit). Fix type errors: treatcurrentNodeIdasstring | null; when constructing instance objects for the viewer, includeactiveBranches: []andjoinArrivals: {}defaults for legacy data. - [ ] Step 3: Run UI tests/lint; expected PASS.
- [ ] Step 4: Commit:
build: upgrade @apitomy/flow-ui to 2.0.0
Phase 2 — Adopt parallel fork/join in Axiom
Task 6: Spawn one task per active branch
Files:
- Modify: app/src/main/java/io/apitomy/axiom/app/WorkflowExecutionService.java
- Test: app/src/test/java/io/apitomy/axiom/app/WorkflowExecutionParallelTest.java (new)
Interfaces:
- Consumes: instance.activeBranches() → List<ActiveBranch> where
ActiveBranch(String branchId, String nodeId).
- Produces: after startWorkflow/completeNode returns InstanceStatus.WAITING, Axiom creates a
TaskEntity for every active branch node that does not already have an open task
(idempotent on nodeId), instead of a single task for currentNodeId.
- [ ] Step 1: Write failing test: workflow
start → fork {actionA, actionB} → join → end. Trigger it; assert two openTaskEntityrows (nodeIds actionA and actionB). Complete actionA; assert run still WAITING and actionB's task still open. Complete actionB; assert the join fires and the run reaches COMPLETED (or the post-join node's task is created). - [ ] Step 2: Run test; expected FAIL (only one task created today).
- [ ] Step 3: Refactor the "create next task" logic into
private void createTasksForActiveBranches(Workflow workflow, WorkflowInstance instance, WorkflowRunEntity run)that loopsinstance.activeBranches(), skips nodes with an existing open task for the run, and usesgetActionInfo(workflow, instance, branch.nodeId())/getHumanTaskInfo(workflow, instance, branch.nodeId())per node type. Call it from both the trigger path and theonTaskCompletedWAITING branch. - [ ] Step 4: Run test; expected PASS. Run full
mvn -q test -pl app. - [ ] Step 5: Commit:
feat: parallel fork/join — one task per active branch
Task 7: Expose active branches over the REST API (API-first)
Files:
- Modify: common/api/src/main/resources/openapi.json — add to the workflow-run detail schema:
activeBranches: [{ branchId: string, nodeId: string, nodeName?: string }], make
currentNodeId/currentNodeName nullable, and add optional branchId to the run history entry
schema.
- Modify: app/src/main/java/io/apitomy/axiom/app/WorkflowRunBeanMapper.java
- Test: extend WorkflowRunBeanMapperTest
- [ ] Step 1: Edit the OpenAPI spec (prefer apicurio-data-models MCP tools), run
mvn installto regenerate beans incommon/api/target/generated-sources/jaxrs/. - [ ] Step 2: Write failing mapper test: entity with two active branches → bean lists both with
resolved node names via
workflow.findNodeById(...); history entries carrybranchId. - [ ] Step 3: Implement mapping in
WorkflowRunBeanMapperfrominstance.activeBranches()andHistoryEntry.branchId(). - [ ] Step 4: Run tests; expected PASS.
- [ ] Step 5: Commit:
feat(api): expose active branches and branch-aware history on runs
Task 8: Run-detail UI shows parallel state
Files:
- Modify: ui/src/pages/WorkflowRunDetailPage.tsx, ui/src/components/WorkflowTab.tsx
- [ ] Step 1: Pass the full instance (with
activeBranches/joinArrivals) toWorkflowViewerso flow-ui's parallel-region rendering highlights all active nodes. - [ ] Step 2: Replace single "current node" labels with a list of active branch node names when
activeBranches.length > 1. - [ ] Step 3: Build, lint, visual check (Chrome DevTools MCP against the dev server).
- [ ] Step 4: Commit:
feat(ui): render parallel branches on run detail
Phase 3 — Optional new flow-ui features (independent; prioritize with user)
Task 9: Workflow simulation panel in the definition editor — SUPERSEDED, do not implement
- ~~Add flow-ui's
SimulationPaneltoui/src/pages/WorkflowDefinitionDetailPage.tsx~~ This task was based on a wrong assumption and should be skipped entirely.WorkflowEditor(the component Axiom already renders bare on this page) has its own built-in canvas toolbar with a "Simulate" switch that toggles the realSimulationPanel, with zero integration work required — no opt-in prop, nothing to wire up. A custom page-level simulation panel was built during execution (commitec74189) and later identified as redundant duplicate UI, then removed (commitad4af51). Before touching this area again, confirm whatWorkflowEditor's toolbar already provides by reading its source (~/git/apitomy/apitomy-flow/ui/src/components/WorkflowEditor.tsx), not just the npm package's public export list.
Task 10: Visual diff between definition versions
- Axiom already versions definitions (
WorkflowDefinitionVersionEntity). Add a "Compare versions" view usingWorkflowDiffViewer+workflowDiff, fed by two version-content payloads from the existing definition-version endpoints. - Confirmed genuinely additive:
WorkflowEditorhas no comparison/diff feature of its own. Implemented as planned (commit4072043), unaffected by the Task 9/11 correction above.
Task 11: Import/export of definitions — SUPERSEDED, do not implement
- ~~Wire
downloadWorkflowJson/workflowFileNamefor export andparseWorkflowfor import~~ This task was also based on a wrong assumption and should be skipped entirely.WorkflowEditor's built-in toolbar already has Import (JSON), Export (JSON), and Image (PNG) buttons — more complete than what this task describes, since it already includes PNG export. Page-level duplicate buttons were built during execution (commit7c03bb2) and later removed (commitad4af51) once this was discovered.
Risks / notes
- Release availability: apitomy-flow HEAD is
2.0.0-SNAPSHOTwith nov2.0.0tag; confirm the 2.0.0 release exists in the repository Axiom resolves from before merging. - In-flight runs: old serialized instances deserialize fine (empty branch collections), and single-branch semantics are unchanged, so no data migration is needed.
- DB: verify
workflow_run.current_node_idis nullable before Phase 2 ships. - Ordering: Phase 1 tasks 1–5 must land together (one PR is fine); Phase 2 and each Phase 3 task are independently shippable.