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

> Modly seamlessly migrates legacy workflow block formats to the modern node edge model automatically. Discover how Modly ensures backward compatibility with its on-the-fly conversion.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: migration-guide
- Published: 2026-08-20

---

**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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/src/shared/stores/workflowsStore.ts), including `load`, `importFile`, and related methods.

```typescript
// 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:

```typescript
// 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:

```typescript
// 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`:

```typescript
// 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 …`):

```typescript
// 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:

```typescript
// 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:

```typescript
// 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

```typescript
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:

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/src/shared/stores/workflowsStore.ts) | Core migration logic | `migrateWorkflow`, `sanitizeEdges`, `LegacyWorkflow` interface |
| [`src/shared/types/electron.d.ts`](https://github.com/lightningpixel/modly/blob/main/src/shared/types/electron.d.ts) | Modern schema definitions | `Workflow`, `WFNode`, `WFEdge` types |
| [`src/areas/workflows/preflight.ts`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/preflight.ts) | App initialization | Triggers `load()` on boot, invoking migration |
| [`src/electron/main/workflows.ts`](https://github.com/lightningpixel/modly/blob/main/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.