Modly React Flow Workflow Editor Architecture and Backend Integration Explained

Modly's workflow editor is built on React Flow for the visual canvas, uses Zustand for state management with undo/redo history, and communicates with its Python backend through Electron IPC and a dedicated Python bridge.

Modly is an open-source AI workflow platform that combines a React-based visual editor with a Python execution backend. This article breaks down exactly how the React Flow workflow editor architecture works, how state flows through the application, and how the frontend integrates with the backend services that execute AI pipelines.

React Flow Canvas and Node System

The visual editor centers on the React Flow library, which provides the draggable node canvas and edge connections. All workflow editing happens in src/areas/workflows/WorkflowsPage.tsx, the main editor component.

Canvas Setup and Event Handlers

WorkflowsPage.tsx initializes the <ReactFlow> element with several critical callbacks:

  • onDrop – handles dragging new nodes from the sidebar onto the canvas
  • onConnect – creates edges between nodes when users draw connections
  • onKeyDown – binds keyboard shortcuts (Ctrl+Z for undo, Ctrl+Y for redo)
  • isValidConnection – validates connections before they're created

The nodeTypes prop registers all available node components, mapping type strings like 'extensionNode' to their React component implementations in src/areas/workflows/nodes/.

Node Definitions and Edge Rendering

Each node type is a self-contained React component. Custom edges are rendered by WorkflowEdge.tsx, which handles styling and arrow indicators. Nodes support nested containers through a parentId field, enabling visual grouping of related operations.

Coordinate Conversion and Layout

The screenToFlowPosition utility in src/areas/workflows/utils converts screen pixels to the canvas's internal coordinate system. This ensures nodes are placed accurately regardless of zoom level or pan position.

Zustand State Management and History

Modly uses Zustand for state management, with the core store defined in src/shared/stores/workflowsStore.ts.

Store Structure and Actions

The store maintains:

  • nodes – array of React Flow node objects with positions and data
  • edges – array of connection objects linking node handles
  • historyRef – stack of past states for undo/redo functionality

Key actions include:

setNodes: (updater: Node[] | ((nodes: Node[]) => Node[])) => void;
setEdges: (updater: Edge[] | ((edges: Edge[]) => Edge[])) => void;
undo: () => void;
redo: () => void;

The useWorkflow() selector provides convenient access throughout the component tree.

Undo/Redo Implementation

History is managed as a stack of { nodes, edges } snapshots. Before each state change, the current state is pushed to historyRef.current.past. The undo action replaces current state with historyRef.current.past.pop() and pushes the replaced state to historyRef.current.future. Redo reverses this flow.

Connection Validation and Type Safety

Before any edge is created, isValidConnection runs two validation checks in WorkflowsPage.tsx:

Type Compatibility Check

const srcType = getNodeOutputType(getNode(connection.source) as Node, allExtensions);
const tgtType = getNodeInputType(
  getNode(connection.target) as Node,
  connection.targetHandle,
  allExtensions,
);
if (srcType && tgtType && srcType !== tgtType) return false;

This prevents connecting incompatible data types between nodes.

Cycle Detection

const stack = [connection.target];
const seen = new Set<string>();
while (stack.length) {
  const id = stack.pop()!;
  if (id === connection.source) return false;
  if (seen.has(id)) continue;
  seen.add(id);
  for (const e of edges) if (e.source === id) stack.push(e.target);
}

This depth-first search ensures workflows remain directed acyclic graphs (DAGs), which is required for deterministic execution order.

Frontend-Backend Integration via Electron IPC

The renderer process never accesses files or Python directly. All backend communication flows through a structured IPC layer.

API Hook: useApi.ts

src/shared/hooks/useApi.ts provides a thin wrapper around window.electron?.api, exposing methods like:

saveWorkflow: (workflow: WorkflowDef) => Promise<ApiResult<void>>;
runWorkflow: (params: { workflowId: string }) => Promise<ApiResult<{ jobId: string }>>;
listModels: () => Promise<ApiResult<ModelInfo[]>>;

Usage in components:

import useApi from '@/shared/hooks/useApi';

const runWorkflow = async (workflowId: string) => {
  const { data, error } = await useApi().runWorkflow({ workflowId });
  if (error) {
    console.error('Run failed:', error);
  } else {
    console.log('Run started, job id:', data.jobId);
  }
};

Preload and IPC Handler Registration

electron/preload/electron-api.ts exposes a safe API object to the renderer via contextBridge.exposeInMainWorld('electron', { api: {...} }). The actual implementations live in electron/main/ipc-handlers.ts, which registers listeners for each exposed method.

Python Bridge Execution

electron/main/python-bridge.ts spawns a child Python process and manages bidirectional JSON communication. When runWorkflow is invoked:

  1. IPC handler receives the request
  2. Workflow definition is serialized to JSON
  3. Python bridge sends the workflow to the backend process
  4. Backend executes the AI pipeline node by node
  5. Status updates, logs, and progress stream back through the bridge
  6. Results are returned to the renderer via the original Promise

Supporting Services

Complete Example: Adding a Node Programmatically

import { useStore } from '@/shared/stores/workflowsStore';
import { screenToFlowPosition } from '@/areas/workflows/utils';

const addExtensionNode = (extensionId: string, clientX: number, clientY: number) => {
  const position = screenToFlowPosition({ x: clientX, y: clientY });
  useStore.getState().setNodes((nodes) => [
    ...nodes,
    {
      id: crypto.randomUUID(),
      type: 'extensionNode',
      position,
      data: { extensionId, enabled: true, params: {} },
    },
  ]);
};

This pattern from WorkflowsPage.tsx shows how drag-drop operations translate screen coordinates into canvas state updates.

Summary

  • React Flow provides the visual canvas in WorkflowsPage.tsx with configurable node types and edge rendering
  • Zustand store in workflowsStore.ts manages nodes, edges, and undo/redo history as immutable snapshots
  • Connection validation enforces type compatibility and prevents cycles before edges are created
  • Electron IPC layer (useApi.ts, preload, handlers) keeps the renderer sandboxed while enabling full backend access
  • Python bridge executes workflows as child processes with streaming status updates
  • Supporting services handle model downloads, artifact management, and filesystem persistence

Frequently Asked Questions

What state management library does Modly use for its workflow editor?

Modly uses Zustand for state management. The workflowsStore.ts file defines the store with actions like setNodes, setEdges, undo, and redo, plus selectors like useWorkflow() that components consume.

How does Modly prevent invalid connections between nodes?

The isValidConnection callback in WorkflowsPage.tsx runs two checks: type compatibility using getNodeOutputType and getNodeInputType, and cycle detection via depth-first search that traverses existing edges to ensure no path leads back to the source node.

Why does Modly use Electron IPC instead of direct filesystem access?

The renderer process is sandboxed for security. All file operations, Python execution, and model management happen in the main process. The IPC layer in useApi.ts provides a controlled interface that validates requests and prevents unauthorized access to system resources.

Can Modly workflows run without the Electron frontend?

The Python backend defined in python-bridge.ts accepts JSON workflow definitions and can execute them independently. However, the visual editor, undo/redo system, and interactive debugging features require the React Flow frontend.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →