How Modly Migrates Legacy Workflow Block Formats to the Modern Node/Edge Model

Modly automatically converts legacy block-based workflows to the modern node/edge graph model on-the-fly using the migrateWorkflow function in src/shared/stores/workflowsStore.ts, ensuring seamless backward compatibility without user intervention.

Modly stores workflows as a graph of nodes and edges, but early versions used a simpler blocks array format. When loading, importing, or opening workflows, the application detects the legacy structure and transforms it transparently. This article examines the complete migration pipeline as implemented in the lightningpixel/modly repository.

Legacy Workflow Detection

The migration triggers when a workflow object contains the old blocks array without nodes or edges properties. This check runs at every entry point in src/shared/stores/workflowsStore.ts, including load, importFile, and related methods.

// Detection logic at workflowsStore.ts#L89-L94
if (raw.blocks && !raw.nodes && !raw.edges) {
  return migrateWorkflow(raw)
}

The detection is intentionally conservative: any workflow with existing nodes or edges bypasses migration entirely, preventing double-conversion of already-modern formats.

The Seven-Step Migration Pipeline

The migrateWorkflow function executes a deterministic transformation sequence:

1. Create the Input Node

Legacy workflows store the input type (image or text) in a top-level input field. The migrator constructs an input node with a deterministic ID pattern:

// workflowsStore.ts#L36-L41
const inputNode: WFNode = {
  id: `input-${workflow.id}`,
  type: 'inputNode',
  position: { x: 0, y: 0 },
  data: { inputType: workflow.input },
}

2. Transform Blocks to Extension Nodes

Each legacy block ({id, extension, enabled, params}) becomes an extensionNode with calculated vertical positioning:

// workflowsStore.ts#L43-L48
const extensionNodes: WFNode[] = workflow.blocks.map((block, i) => ({
  id: block.id,
  type: 'extensionNode',
  position: { x: 0, y: 150 + i * 220 }, // 220px vertical spacing
  data: {
    extension: block.extension,
    enabled: block.enabled,
    params: block.params,
  },
}))

The 220-pixel vertical offset ensures reasonable default layouts in the React Flow canvas without expensive auto-layout algorithms.

3. Assemble Complete Node List

The input node prepends the extension nodes to form allNodes:

// workflowsStore.ts#L50-L51
const allNodes = [inputNode, ...extensionNodes]

4. Generate Sequential Edges

Edges connect each consecutive node in a linear chain (input → block-1 → block-2 …):

// workflowsStore.ts#L53-L57
const edges: WFEdge[] = []
for (let i = 0; i < allNodes.length - 1; i++) {
  const source = allNodes[i]
  const target = allNodes[i + 1]
  edges.push({
    id: `e-${source.id}-${target.id}`,
    source: source.id,
    target: target.id,
  })
}

5. Sanitize Edge Handles

The sanitizeEdges function runs post-migration to repair missing handle IDs and remove invalid connections:

// workflowsStore.ts#L4-L22
function sanitizeEdges(nodes: WFNode[], edges: WFEdge[]): WFEdge[] {
  // Repairs targetHandle = 'input-0', sourceHandle = 'output'
  // Eliminates "Couldn't create edge for target handle id: null" warnings
}

This step is critical for React Flow compatibility—legacy block formats had no concept of connection handles, so defaults must be injected.

6. Return Modern Workflow Object

The final output matches the current schema exactly:

// workflowsStore.ts#L59-L66
return {
  id: workflow.id,
  name: workflow.name,
  description: workflow.description,
  nodes: allNodes,
  edges: sanitizedEdges,
  createdAt: workflow.createdAt,
  updatedAt: new Date().toISOString(), // fresh timestamp
}

7. Transparent Store Integration

All store methods automatically invoke migrateWorkflow, ensuring the rest of the application works exclusively with node/edge representations regardless of persistence format.

Complete Migration Example

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

// Legacy format (as stored in pre-2024 workflows)
const legacy = {
  id: 'wf-123',
  name: 'Upscale Pipeline',
  description: 'Image enhancement workflow',
  input: 'image',
  blocks: [
    { id: 'b1', extension: 'upscale', enabled: true, params: { scale: 2 } },
    { id: 'b2', extension: 'filter', enabled: true, params: { type: 'gaussian' } },
  ],
  createdAt: '2024-01-01T00:00:00Z',
  updatedAt: '2024-01-02T00:00:00Z',
}

// Automatic conversion
const modern = migrateWorkflow(legacy)

// Result: React Flow-compatible graph with inputNode, extensionNodes, and edges

When the store loads workflows from the backend, migration happens transparently:

// Inside workflowsStore.ts load() method
for (const entry of raw as LegacyWorkflow[]) {
  const wf = migrateWorkflow(entry)  // legacy → modern
  this.workflows.set(wf.id, wf)
}

Key Implementation Files

File Purpose Key Exports
src/shared/stores/workflowsStore.ts Core migration logic migrateWorkflow, sanitizeEdges, LegacyWorkflow interface
src/shared/types/electron.d.ts Modern schema definitions Workflow, WFNode, WFEdge types
src/areas/workflows/preflight.ts App initialization Triggers load() on boot, invoking migration
src/electron/main/workflows.ts Backend persistence Stores both formats; frontend handles migration on read

Design Decisions in the Migration

Deterministic IDs over UUIDs: The input node uses input-${workflow.id} rather than random UUIDs, enabling idempotent re-migration and predictable debugging.

Fixed vertical layout: Rather than complex graph layout algorithms, the migrator uses simple arithmetic (150 + i * 220). This trades aesthetic optimization for reliability and speed.

Handle sanitization as separate pass: Separating edge creation from handle repair (sanitizeEdges) keeps the core migration readable while centralizing React Flow compatibility fixes.

Summary

  • Detection: Legacy workflows identified by presence of .blocks without .nodes/.edges
  • Transformation: Seven-step pipeline in migrateWorkflow (input node → block mapping → edge generation → sanitization)
  • Integration: Automatic invocation at all store entry points (load, importFile, etc.)
  • Compatibility: sanitizeEdges ensures React Flow rendering without handle errors
  • Transparency: Application code works exclusively with modern node/edge format regardless of source

Frequently Asked Questions

How does Modly detect whether a workflow needs migration?

Modly checks for the presence of a blocks array combined with the absence of nodes and edges arrays. This condition in workflowsStore.ts#L89-L94 reliably distinguishes legacy formats from modern graphs without false positives.

What happens to block parameters during migration?

All block parameters migrate unchanged into the extension node's data.params field. The transformation preserves extension, enabled, and params properties exactly as stored, ensuring no configuration loss.

Can migrated workflows be reverted to the block format?

No. The migration is one-directional—once converted to nodes and edges, workflows persist in the modern format. The original blocks array is discarded after successful migration.

Does migration affect workflow performance?

Migration runs synchronously on load for workflows under a few hundred blocks. The O(n) linear pass through blocks and deterministic operations introduce negligible overhead compared to I/O operations.

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 →