A few days ago, I shared how I built CrewForm — an open-source platform for orchestrating AI agent teams through a web UI. The "What's next" section mentioned an interactive workflow canvas. We shipped it. Here's how it works under the hood.
The problem
In the original version, pipeline teams were configured through a dropdown-based step list. It worked, but:
- You couldn't see the full flow at a glance
- Rearranging steps meant a lot of up/down clicking
- Non-technical team members found it confusing
- There was no visual feedback during execution
We needed a canvas where you could see agents as nodes, connections as edges, and execution status in real-time.
The stack
- React Flow — The canvas library. Handles nodes, edges, panning, zooming, selection, and minimap out of the box.
- Dagre — Auto-layout engine. Given a directed graph, it computes x/y positions for every node so nothing overlaps.
- SSE (Server-Sent Events) — Real-time execution state. The task runner pushes status updates, and the canvas highlights the active node.
Layout system: TB and LR
One design decision that took a few iterations: directional layouts.
Pipeline workflows naturally flow in one direction. Some people think top-to-bottom (like a flowchart), others think left-to-right (like a timeline). We support both.
const getLayoutedElements = (nodes, edges, direction = 'TB') => {
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setGraph({ rankdir: direction, nodesep: 80, ranksep: 100 });
dagreGraph.setDefaultEdgeLabel(() => ({}));
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: 220, height: 80 });
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
// ... map positions back to React Flow nodes
};
The tricky part was edge handles. React Flow connects edges to specific handles (connection points) on each node — top, bottom, left, right. When you toggle from TB to LR, every edge needs its sourceHandle and targetHandle updated:
function getHandleIds(direction: string) {
return direction === 'LR'
? { sourceHandle: 'right', targetHandle: 'left' }
: { sourceHandle: 'bottom', targetHandle: 'top' };
}
Without this, edges default to the first available handle (usually top), and you get connections going the wrong way. This was one of those bugs that seems obvious in hindsight but took a few rounds of debugging.
Inserting agents between steps
This was the feature that made the canvas feel like a real tool rather than a visualisation.
Right-click any edge → "Insert Agent Here" → pick an agent → done. The new agent splits the edge into two, and dagre re-layouts everything automatically.
The implementation:
- Remove the original edge (A → C)
- Insert the new node (B)
- Create two new edges (A → B, B → C)
- Re-run dagre layout
- Animate the transition with React Flow's
fitView()
Real-time execution
When a pipeline runs, the canvas comes alive:
- The active agent node gets highlighted
- Camera auto-follows the executing node
- A side panel shows the live transcript (streamed via SSE)
- Completed steps get a success indicator
This uses Supabase Realtime subscriptions on the task execution table. When the task runner updates a step's status, the canvas reflects it instantly.
Persisting layout direction
Small detail that matters: layout direction is saved per-team using an useRef to avoid stale closures in React's render cycle. When a user opens a team they configured as LR last week, it opens in LR.
The direction is stored in the team's JSONB configuration column alongside the graph structure, so it survives page reloads and re-renders without triggering unnecessary edge recalculations.
What I'd do differently
- Start with the canvas earlier. The dropdown-based config was a stepping stone, but users immediately understood the canvas better. Visual-first should have been day one.
-
Use
useReffor shared state from the start. React Flow callbacks capture stale closure values. We hit this bug twice before learning to use refs for anything that edges or layout callbacks depend on.
Try it
The visual canvas is live on crewform.tech and fully open-source:
If you've built anything with React Flow + dagre, I'd love to compare notes. And if you have ideas for the canvas — multi-select, copy-paste node groups, conditional branching — drop a comment or open an issue.

Top comments (0)