How Modly Migrates Legacy Workflow Formats to the Node-Based System

Modly automatically detects legacy block-list workflows and converts them to directed graphs of nodes and edges using the migrateWorkflow() function in src/shared/stores/workflowsStore.ts, ensuring seamless backward compatibility across versions.

Modly is an open-source workflow editor built on React Flow that stores workflows as directed graphs. Early versions of the application used a simpler block-list format consisting of an array of extension blocks, while the current architecture requires explicit nodes and edges arrays. When a workflow is loaded, Modly transparently migrates any legacy-format objects to the modern node-based representation before they enter the UI store, allowing users to edit historic files without manual conversion.

Legacy vs. Node-Based Workflow Formats

The evolution from Modly's early architecture to its current React Flow implementation required a fundamental shift in how workflows are serialized.

Legacy Block-List Format: Early workflow files stored operations as a sequential blocks array, where each block represented an extension with parameters. This format optionally included an input field (defaulting to 'image') but lacked explicit connection topology.

Modern Node-Based Format: Current workflows are directed graphs containing:

  • nodes: An array of WFNode objects including input nodes and extension nodes
  • edges: An array of WFEdge objects defining connections between node handles

The migration bridge between these formats lives entirely within the workflow store layer.

The Migration Pipeline in workflowsStore.ts

All migration logic resides in src/shared/stores/workflowsStore.ts. The system performs detection and conversion automatically when workflows are loaded from disk.

Legacy Detection in the load() Method

The entry point for migration is the load() method (lines 81-90). This method fetches workflow entries via the Electron bridge (window.electron.workflows.list()) and iterates through the results, casting each entry as LegacyWorkflow:

for (const entry of raw as LegacyWorkflow[]) {
    const wf = migrateWorkflow(entry)   // Migrates if needed
}

If the retrieved workflow already contains nodes and edges fields, migrateWorkflow() passes it through to sanitization. Otherwise, it triggers the full block-to-node conversion pipeline.

The migrateWorkflow() Entry Point

The migrateWorkflow(raw: LegacyWorkflow): Workflow function (lines 26-66) serves as the primary conversion engine. It first checks for modern format indicators:

// Lines 26-30: Early return for already-migrated workflows
if (raw.nodes && raw.edges) {
    return { ...raw, edges: sanitizeEdges(raw.edges, raw.nodes) }
}

If these fields are absent, the function proceeds to construct a new graph from the legacy blocks array.

Constructing Nodes from Legacy Blocks

The node construction phase (lines 32-48) transforms sequential blocks into a graph structure:

  1. Input Node Creation (lines 36-41): The function always generates an input node first, using the workflow's input field or defaulting to 'image'.

  2. Extension Node Mapping (lines 43-48): Each object in the legacy blocks array is converted to an extension node with preserved id, extension type, enabled status, and params.

These nodes are concatenated into allNodes, maintaining the original execution order as spatial positioning data.

Edge Wiring and Sanitization

After node construction, the system generates connections (lines 52-56) to create a linear processing chain:

// Connect each node to its successor
const edges: WFEdge[] = []
for (let i = 0; i < allNodes.length - 1; i++) {
    edges.push(createEdge(allNodes[i], allNodes[i + 1]))
}

The helper function sanitizeEdges (lines 4-24) then validates the edge list by removing dangling references to non-existent nodes and patching missing handle identifiers. This ensures graph integrity regardless of the source format.

The final function returns a complete Workflow object (lines 58-66) containing id, name, description, the constructed nodes and edges, and preserved timestamps.

Programmatic Migration Example

You can invoke the migration logic directly for batch conversions or testing. The following example demonstrates converting a legacy block-list workflow to the node-based format:

import { migrateWorkflow } from '@/shared/stores/workflowsStore'

// Example legacy workflow JSON (as read from disk)
const legacy = {
  id: 'w123',
  name: 'Legacy Example',
  description: '',
  input: 'image',
  blocks: [
    { id: 'b1', extension: 'stable-diffusion', enabled: true, params: {} },
    { id: 'b2', extension: 'mesh-optimizer', enabled: true, params: {} },
  ],
  createdAt: '2024-01-01T00:00:00Z',
  updatedAt: '2024-01-02T00:00:00Z',
}

// Convert to the node-based format
const workflow = migrateWorkflow(legacy)

// `workflow.nodes` now contains an input node + two extension nodes
// `workflow.edges` contains linear connections between them
console.log(workflow)

To load workflows through the UI store with automatic migration:

await useWorkflowsStore.getState().load()
// Every entry in the store has been passed through migrateWorkflow()

Key Files in the Migration Architecture

The migration system spans several files in the lightningpixel/modly repository:

  • src/shared/stores/workflowsStore.ts: Core store containing migrateWorkflow(), edge sanitization logic, and the load() method that triggers conversion.

  • src/shared/stores/workflowsStore.test.mjs: Unit tests verifying that legacy block-format workflows correctly transform into valid node and edge arrays.

  • src/shared/types/electron.d.ts: TypeScript definitions for Workflow, WFNode, and WFEdge that constrain the migration output.

  • src/areas/workflows/workflowRunStore.ts: Consumes migrated workflow objects when executing runs, expecting the modern node-based structure.

Additionally, readStoredFolders() in the workflows store contains a separate legacy guard (lines 49-53) that converts old plain-string folder arrays to the current object-based folder schema, ensuring persistence-layer compatibility.

Summary

  • Automatic Detection: The load() method in workflowsStore.ts identifies legacy workflows by checking for the absence of nodes and edges arrays.
  • Transparent Conversion: The migrateWorkflow() function transforms block-list formats into React Flow-compatible directed graphs without user intervention.
  • Node Construction: Legacy blocks become extension nodes prepended by an automatic input node (defaulting to 'image').
  • Linear Edge Generation: The system generates sequential connections between nodes and sanitizes them to remove invalid references.
  • Backward Compatibility: Both historic block-format files and modern node-based workflows coexist seamlessly in the UI.

Frequently Asked Questions

What triggers the legacy workflow migration in Modly?

The migration triggers automatically when useWorkflowsStore.load() fetches workflows from disk via the Electron bridge. The system casts raw entries as LegacyWorkflow types and passes them through migrateWorkflow(), which detects legacy formats by checking for the presence of nodes and edges properties. If these are missing, the conversion pipeline executes before the data reaches the UI store.

How does Modly handle missing input fields in legacy workflows?

When processing legacy workflows, the migration logic defaults the input node type to 'image' if the legacy input field is undefined or omitted (lines 36-41 in workflowsStore.ts). This ensures that every migrated workflow has a valid entry point node connecting to the first extension block, maintaining graph connectivity even for incomplete legacy files.

Can I manually migrate workflows outside the UI?

Yes. You can import migrateWorkflow directly from @/shared/stores/workflowsStore and invoke it programmatically with a legacy workflow object. This is useful for batch migrating file collections or testing conversion logic without running the full Electron application. The function returns a fully valid Workflow object compatible with Modly's current React Flow renderer.

Where are the migration unit tests located?

Unit tests verifying the legacy-to-node migration logic reside in src/shared/stores/workflowsStore.test.mjs. These tests validate that block arrays convert correctly to node lists, that edges form proper linear chains, and that sanitizeEdges correctly removes dangling references when nodes are deleted or reordered during migration.

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 →