This document contains key insights from analyzing the ActivePieces workflow builder implementation. These findings should guide our workflow builder development to match their sophisticated design patterns.
- Recursive Graph Building: The
buildGraphfunction recursively processes steps to create nodes and edges - Graph Structure: Each step generates its own subgraph with nodes and edges
- Merging: Child graphs are merged with parent graphs using
mergeGraphfunction - Offset Calculation: Uses
offsetGraphto position child elements relative to parents
// Main graph building function
buildGraph(step: FlowAction | FlowTrigger): ApGraph
// Offset graph by x,y coordinates
offsetGraph(graph: ApGraph, offset: { x: number; y: number }): ApGraph
// Merge two graphs together
mergeGraph(graph1: ApGraph, graph2: ApGraph): ApGraph
// Calculate graph bounding box
calculateGraphBoundingBox(graph: ApGraph): BoundingBoxAP_NODE_SIZE = {
STEP: { width: 260, height: 70 },
BIG_ADD_BUTTON: { width: 50, height: 50 },
ADD_BUTTON: { width: 18, height: 18 },
LOOP_RETURN_NODE: { width: 260, height: 70 }
}HORIZONTAL_SPACE_BETWEEN_NODES = 80 // Space between branches
VERTICAL_SPACE_BETWEEN_STEPS = 85 // Space between sequential steps
ARC_LENGTH = 15 // Radius for curved edges
LINE_WIDTH = 1.5 // Edge stroke width
LABEL_HEIGHT = 30 // Branch label height
LABEL_VERTICAL_PADDING = 12 // Padding for labelsVERTICAL_OFFSET_BETWEEN_LOOP_AND_CHILD = VERTICAL_SPACE_BETWEEN_STEPS * 1.5 + 2 * ARC_LENGTH
VERTICAL_OFFSET_BETWEEN_ROUTER_AND_CHILD = VERTICAL_OFFSET_BETWEEN_LOOP_AND_CHILD + LABEL_HEIGHTThis is the core algorithm for positioning branches horizontally:
const offsetRouterChildSteps = (childGraphs: ApGraph[]) => {
// 1. Calculate bounding boxes for all child graphs
const childGraphsBoundingBoxes = childGraphs.map(calculateGraphBoundingBox)
// 2. Calculate total width including spacing
const totalWidth =
childGraphsBoundingBoxes.reduce((acc, current) => acc + current.width, 0) +
HORIZONTAL_SPACE_BETWEEN_NODES * (childGraphs.length - 1)
// 3. Center branches relative to parent
let deltaLeftX = -(totalWidth - first.left - last.right) / 2 - first.left
// 4. Position each branch
return childGraphsBoundingBoxes.map((box, index) => {
const x = deltaLeftX + box.left
deltaLeftX += box.width + HORIZONTAL_SPACE_BETWEEN_NODES
return offsetGraph(childGraphs[index], { x, y: verticalOffset })
})
}enum ApEdgeType {
STRAIGHT_LINE = 'straightLineEdge',
LOOP_START_EDGE = 'loopStartLineEdge',
LOOP_RETURN_EDGE = 'loopReturnLineEdge',
ROUTER_START_EDGE = 'routerStartEdge',
ROUTER_END_EDGE = 'routerEndEdge'
}- Draws from router node to branch start
- Includes branch label as foreignObject
- Uses SVG path with arcs for smooth curves
- Conditional rendering based on branch emptiness
- Converges branches back to main flow
- Calculates vertical spacing dynamically
- Draws horizontal lines for first and last branches
FlowOperationType.ADD_BRANCH
FlowOperationType.DELETE_BRANCH
FlowOperationType.DUPLICATE_BRANCH
FlowOperationType.MOVE_BRANCHinterface RouterAction {
type: FlowActionType.ROUTER
settings: {
branches: Array<{
branchName: string
branchType: BranchExecutionType
conditions?: BranchCondition[][]
}>
}
children: (FlowAction | undefined)[]
}- Flow Version: Current flow state
- Operations: Queued operations with promises
- Selected Step: Current selection state
- Apply Operation: Central function for all flow modifications
- User triggers action
- Operation created with type and payload
- Optimistic UI update
- Operation queued for server sync
- Server response updates final state
ApStepCanvasNode: Main step component with drag supportApBigAddButtonCanvasNode: Large add button for empty branchesApLoopReturnCanvasNode: Special node for loop returnsApGraphEndWidgetNode: Invisible node for graph termination
ApStraightLineCanvasEdge: Simple vertical connectionsApRouterStartCanvasEdge: Router to branch connectionsApRouterEndCanvasEdge: Branch to merge point connectionsBranchLabel: Interactive label component within edges
const buildRouterChildGraph = (step: RouterAction) => {
// 1. Build child graphs for each branch
const childGraphs = step.children.map((branch, index) => {
return branch ? buildGraph(branch) : createBigAddButtonGraph(step, {...})
})
// 2. Apply horizontal offset algorithm
const childGraphsAfterOffset = offsetRouterChildSteps(childGraphs)
// 3. Create end node for convergence
const subgraphEndNode = createGraphEndNode(...)
// 4. Create start and end edges for each branch
const edges = createRouterEdges(childGraphsAfterOffset)
// 5. Return merged graph
return mergeGraphs(nodes, edges)
}- Empty branches show a BigAddButton node
- Button data includes parent step and branch index
- Clicking creates new action at specific location
- Positioned as foreignObjects in SVG
- Interactive with hover states
- Context menu for operations
- "Otherwise" branch is specially labeled
- Border changes on hover and selection
- Skipped nodes have accent background
- Dragging nodes become transparent
- Step numbers shown automatically
- Custom node/edge types registered once
- Memoized components with React.memo
- Selective re-renders based on step changes
- FitView with custom parameters for focus
- Bounding boxes calculated once per layout
- Graph structure cached until changes
- Offset calculations done in batches
- Node Size: AP uses 260x70px vs our 350x80px
- Branch Spacing: AP uses 80px vs our 180px
- Architecture: AP uses graph-based vs our direct positioning
- Branch Labels: AP uses edge components vs our separate nodes
- State Management: AP uses Zustand vs our Context API
- Adopt Graph-Based Architecture: More flexible and maintainable
- Reduce Branch Spacing: 80px provides better visual density
- Use Recursive Graph Building: Easier to handle nested structures
- Implement Operation Queue: Better state management
- Add Arc-Based Edge Routing: Smoother visual appearance
- Consider Zustand: More performant than Context API
- Add Empty Branch Buttons: Better UX for adding actions
- Implement Bounding Box Calculations: Precise layout control
- Layout Algorithm:
/activepieces/packages/react-ui/src/app/builder/flow-canvas/utils/flow-canvas-utils.ts - Constants:
/activepieces/packages/react-ui/src/app/builder/flow-canvas/utils/consts.ts - Router Edges:
/activepieces/packages/react-ui/src/app/builder/flow-canvas/edges/router-start-edge.tsx - Branch Labels:
/activepieces/packages/react-ui/src/app/builder/flow-canvas/edges/branch-label.tsx - Step Node:
/activepieces/packages/react-ui/src/app/builder/flow-canvas/nodes/step-node.tsx