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
.blockswithout.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:
sanitizeEdgesensures 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →