How Modly Handles IPC Communication Between Electron Main and Renderer for Workflow Execution

Modly uses a preload script architecture with contextBridge.exposeInMainWorld to expose a safe window.electron API that forwards renderer calls to the main process via ipcRenderer.invoke, while workflow execution spawns a Python subprocess that streams progress back through ipcMain.emit.

Modly is an open-source Electron application for procedural 3D content generation. Its IPC (Inter-Process Communication) layer enables the renderer process UI to control workflow execution in the main process, which delegates heavy computation to a Python backend. This article examines the complete IPC pathway based on the lightningpixel/modly source code.

The Preload Script: Secure API Exposure

Electron's context isolation prevents direct access to Node.js APIs from the renderer. Modly solves this through electron/preload/electron-api.ts, which creates a controlled bridge.

Exposing Workflow Methods

The preload script uses contextBridge.exposeInMainWorld to attach methods to window.electron:

// electron/preload/electron-api.ts
contextBridge.exposeInMainWorld('electron', {
  workflow: {
    run: (args) => ipcRenderer.invoke('workflow:run', args),
    stop: () => ipcRenderer.send('workflow:stop'),
    onProgress: (callback) => {
      ipcRenderer.on('workflow:progress', (_, data) => callback(data))
      return () => ipcRenderer.removeAllListeners('workflow:progress')
    }
  },
  workflows: {
    list: () => ipcRenderer.invoke('workflows:list'),
    save: (workflow) => ipcRenderer.invoke('workflows:save', workflow),
    delete: (id) => ipcRenderer.invoke('workflows:delete', id),
    import: () => ipcRenderer.invoke('workflows:import'),
    export: (id) => ipcRenderer.invoke('workflows:export', id)
  }
})

Key patterns here:

  • ipcRenderer.invoke for request/response RPC (returns promises)
  • ipcRenderer.send for fire-and-forget commands
  • ipcRenderer.on with cleanup functions for event subscriptions

Main Process Handlers: IPC Registration

The main process registers handlers in electron/main/ipc-handlers.ts using ipcMain.handle and ipcMain.on:

Channel Handler Type Purpose
workflows:list ipcMain.handle Returns array of saved workflow JSON files
workflows:save ipcMain.handle Persists workflow definition to disk
workflows:delete ipcMain.handle Removes workflow file by ID
workflows:import ipcMain.handle Reads user-selected JSON, validates, stores
workflows:export ipcMain.handle Writes workflow to user-chosen path

These handlers perform filesystem operations through Node.js APIs—operations inaccessible to the isolated renderer.

Workflow Execution: The Complete IPC Flow

Running a workflow triggers Modly's most complex IPC sequence. The implementation spans three files and involves a Python subprocess.

Step 1: Renderer Initiates Execution

From the UI layer (typically workflowRunStore.ts):

// Start workflow execution
await window.electron.workflow.run({ 
  workflowId: 'terrain-generator-v2',
  overrides: { seed: 42, resolution: 2048 }
})

Step 2: Main Process Receives and Spawns

In electron/main/process-runner.ts, the handler:

  1. Loads the workflow JSON from workflows/ directory
  2. Validates the node graph
  3. Spawns Python via electron/main/python-bridge.ts
// electron/main/process-runner.ts (conceptual)
ipcMain.handle('workflow:run', async (event, { workflowId, overrides }) => {
  const workflow = await loadWorkflow(workflowId)
  const pythonProcess = spawnPythonBridge(workflow, overrides)
  
  pythonProcess.on('message', (status) => {
    // Forward Python progress to renderer
    event.sender.send('workflow:progress', {
      nodeId: status.currentNode,
      percent: status.progressPercent,
      logs: status.logLines
    })
  })
  
  return { pid: pythonProcess.pid }
})

Step 3: Bidirectional Progress Streaming

While Python executes, status flows back through:


Python subprocess → python-bridge.ts → process-runner.ts 
                                          ↓
                                   event.sender.send('workflow:progress')
                                          ↓
Renderer ← ipcRenderer.on('workflow:progress') ← preload re-exposure

The renderer subscribes via the preload's cleanup-aware API:

// Listen to execution progress
const unsubscribe = window.electron.workflow.onProgress(({ 
  nodeId, 
  percent, 
  logs 
}) => {
  updateNodeStatus(nodeId, percent)
  appendToLogPanel(logs)
})

// Cleanup on unmount or workflow completion
unsubscribe()

Step 4: Stopping Execution

The stop command uses ipcRenderer.send for immediate delivery without awaiting response:

// Immediate termination request
window.electron.workflow.stop()  // → ipcMain.on('workflow:stop', ...)

The main process kills the Python subprocess and emits a final workflow:progress with { status: 'cancelled' }.

IPC Communication Patterns in Modly

Modly employs three distinct IPC patterns depending on the use case:

Synchronous RPC with invoke/handle

  • Use when the renderer needs a result or confirmation
  • Examples: workflows:list, workflows:save, initial workflow:run (returns PID)

Event streaming with send/on

  • Use for ongoing, multi-message updates
  • Example: workflow:progress events during execution

Fire-and-forget with send/on

  • Use for commands requiring no acknowledgment
  • Example: workflow:stop

Renderer-Side State Management

The IPC API is consumed by two key renderer modules:

  • workflowsStore.ts (shared store) — wraps CRUD operations (list, save, delete) with reactive state
  • workflowRunStore.ts (execution store) — orchestrates run, stop, and progress subscription lifecycle

Preflight validation in src/areas/workflows/preflight.ts checks node connectivity and parameter validity before the IPC call, preventing invalid workflows from reaching the main process.

Security Considerations

Modly's IPC architecture follows Electron security best practices:

  • No nodeIntegration — renderer cannot access filesystem or spawn processes directly
  • Context isolation enabled — preload runs in separate context from renderer
  • IPC channel allowlisting — only predefined channels pass through preload
  • User input never eval'd — workflow JSON is parsed with JSON.parse, not eval

Summary

Frequently Asked Questions

How does Modly prevent renderer process from directly accessing the filesystem?

Modly enables Electron's context isolation and disables nodeIntegration. The renderer can only access filesystem operations through the explicitly exposed window.electron API in electron/preload/electron-api.ts, which forwards requests to the main process. This prevents arbitrary code execution and follows Electron's security guidelines.

Why does Modly use both ipcRenderer.invoke and ipcRenderer.send?

invoke provides request/response semantics suitable for CRUD operations where the renderer needs confirmation or data (e.g., workflows:list returning an array). send is fire-and-forget, used when the renderer doesn't need acknowledgment—like workflow:stop where immediate delivery matters more than response. Progress streaming uses on with callbacks because it involves multiple messages over time.

Where does the actual workflow computation happen if IPC connects renderer to main?

The main process delegates to a Python subprocess spawned through electron/main/python-bridge.ts. The main process acts as a coordinator: it manages the Python lifecycle, parses stdout/stderr, and forwards structured progress messages back to the renderer via IPC. This Python bridge handles generation algorithms, mesh processing, and file I/O that would block the main process.

Can multiple workflows run simultaneously in Modly?

The current architecture in process-runner.ts tracks a single Python subprocess per workflow execution. Multiple concurrent executions would require extending the runner to manage a process map (workflowId → subprocess) rather than singleton state. The IPC channels support this—workflow:run returns a PID that could identify specific instances—but the store layers would need corresponding updates to handle parallel execution state.

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 →