How to Create and Customize Workflows in Modly: A Complete Guide

Modly workflows are JSON-based graphs managed through the useWorkflowsStore Zustand store, where you create pipelines by assembling nodes (operations) and edges (data flow), or extend functionality by registering custom processors in src/areas/workflows/nodes/ and updating nodeBehaviors.ts.

Modly is an open-source automation platform that treats workflows as portable, data-driven graphs. Whether you are building automated pipelines through the visual canvas or generating them programmatically, understanding how to create and customize workflows in Modly allows you to leverage the full power of its execution engine.

Understanding the Workflow Architecture

At its core, Modly represents workflows as plain JSON objects containing metadata, nodes, and edges. The system relies on several key components defined in the source code:

  • Workflow Model (src/shared/types/electron.d.ts): Defines the TypeScript interfaces for Workflow, WFNode, and WFEdge, specifying that each workflow contains id, name, description, optional folder, and arrays of nodes and edges.

  • State Management (src/shared/stores/workflowsStore.ts): The useWorkflowsStore Zustand store handles loading, saving, importing, exporting, and UI state management including open tabs and folder bookmarks.

  • Behavior Registry (src/areas/workflows/nodeBehaviors.ts): A lookup table that tells the runner which node types are passthrough, branch starters, scene outputs, or branch consumers.

  • Pre-flight Validation (src/areas/workflows/preflight.ts): Validates graphs before execution to detect missing inputs, cycles, or illegal merges.

  • Execution Engine (src/areas/workflows/workflowRunStore.ts): Walks the graph at runtime, resolves data sources, and invokes node processors.

Creating a New Workflow Programmatically

While you can click New Workflow in the UI and drag nodes onto the canvas, you can also create workflows directly via the store API. This is useful for generating workflows from templates or external systems.

import { useWorkflowsStore } from '@/shared/stores/workflowsStore'
import type { Workflow, WFNode, WFEdge } from '@shared/types/electron.d'

// 1. Create the skeleton
const newWorkflow: Workflow = {
  id:          `wf-${Date.now()}`,
  name:        'My Custom Workflow',
  description: 'Demo of a hand-crafted workflow',
  folder:      'Examples',          // optional folder name
  nodes:       [],
  edges:       [],
  createdAt:   new Date().toISOString(),
  updatedAt:   new Date().toISOString(),
}

// 2. Add nodes (example: an input node + a mesh-exporter node)
const inputNode: WFNode = {
  id:       'input',
  type:     'inputNode',
  position: { x: 250, y: 80 },
  data:     { inputType: 'image', enabled: true, params: {} },
}

const exporterNode: WFNode = {
  id:       'export',
  type:     'extensionNode',                  // uses a processor defined in areas/workflows/nodes/…
  position: { x: 250, y: 300 },
  data:     { extensionId: 'mesh-exporter', enabled: true, params: { export_format: 'glb' } },
}

newWorkflow.nodes.push(inputNode, exporterNode)

// 3. Wire them together
newWorkflow.edges.push({
  id:     `e-${inputNode.id}-${exporterNode.id}`,
  source: inputNode.id,
  target: exporterNode.id,
})

// 4. Persist the workflow
await useWorkflowsStore.getState().save(newWorkflow)

When you call save(), Modly writes a .json file into the workflows directory (configured via settings.workflowsDir). The UI automatically discovers new files on the next load() call.

Customizing Existing Workflows

Adding and Editing Nodes

In the visual interface, drag a new node from the palette onto the canvas. Internally, the UI creates a WFNode entry with type: 'extensionNode' and stores the selected extensionId in node.data.params. Edits to parameters are immediately persisted to the JSON structure.

Removing Nodes and Managing Edges

To delete a node programmatically while maintaining graph integrity, filter both the nodes array and edges array to remove references to the deleted node:

function deleteNode(workflow: Workflow, nodeId: string): Workflow {
  const nodes = workflow.nodes.filter(n => n.id !== nodeId)
  const edges = workflow.edges.filter(e => e.source !== nodeId && e.target !== nodeId)
  return { ...workflow, nodes, edges, updatedAt: new Date().toISOString() }
}

// Usage
const updated = deleteNode(existingWorkflow, 'node-id-123')
await useWorkflowsStore.getState().save(updated)

The store also provides moveOpenTab, openWorkflow, closeWorkflow, and removeFolder methods for managing UI state.

Importing and Exporting Workflows

Modly exposes file-picker bridges through the electron layer. Use the store methods to trigger OS dialogs:

// Import from JSON file
await useWorkflowsStore.getState().importFile()

// Export specific workflow to JSON
await useWorkflowsStore.getState().exportFile(workflow)

These methods wrap window.electron.workflows.import and window.electron.workflows.export, writing portable JSON files that can be shared across Modly installations.

Extending the Workflow Engine with Custom Nodes

To add a new node type (for example, a custom image filter), you must follow the extension pattern used by built-in nodes like mesh-exporter.

Step 1: Create the Processor

Create a processor file under src/areas/workflows/nodes/<your-node>/processor.ts. The function signature must match:

const processor = async (
  input: ProcessInput,
  params: Record<string, unknown>,
  context: ProcessContext,
): Promise<ProcessResult> => {
  // Your processing logic here
  return { output: transformedData }
}

export = processor

Reference the mesh-exporter implementation at src/areas/workflows/nodes/mesh-exporter/processor.ts for a complete example.

Step 2: Register the Extension

Add your extension to the registry in src/areas/workflows/mockExtensions.ts (or via runtime extension installation):

{
  id:            'my-filter',
  name:          'My Filter',
  description:   'Applies a custom image transformation',
  type:          'node',
  processorPath: 'areas/workflows/nodes/my-filter/processor.ts',
}

Step 3: Update Node Behaviors

If your node has special flow semantics (such as blocking execution or consuming branches), update src/areas/workflows/nodeBehaviors.ts:

const BEHAVIORS: Record<string, NodeBehavior> = {
  ...BEHAVIORS,
  myFilterNode: { 
    passthrough: false, 
    branchStarter: false, 
    sceneOutput: false, 
    branchConsumer: false 
  },
}

This ensures the runner's helper functions—resolveDataSource, nearestUpstreamWaits, and reachesSceneOutput—correctly handle your node during pre-flight and execution.

Step 4: Add UI Components (Optional)

Create React components under src/areas/workflows/components/ to expose node-specific parameters in the inspector panel. While the engine can run without custom UI components (using generic parameter inputs), dedicated components improve the user experience.

Step 5: Reload Extensions

After placing your files, reload extensions via Settings → Extensions → Reload, or restart Modly. The new node type will appear in the palette and be available for use in any workflow.

Pre-flight Validation and Debugging

Before execution, Modly calls preflight(workflow) from src/areas/workflows/preflight.ts to validate the graph. This function performs three critical checks:

  • Data Source Resolution: Verifies that all inputs can be satisfied using resolveDataSource
  • Cycle Detection: Identifies circular dependencies and illegal merges using nearestUpstreamWaits
  • Scene Output Validation: Ensures nodes that reach a scene output are not gated behind untriggered waitNode instances using reachesSceneOutput

If validation fails, the UI displays pre-flight issues in the workflow editor. Fix these by reconnecting edges or adding required input nodes before running the workflow.

Summary

  • Modly workflows are JSON graphs stored in the workflows directory, managed by the useWorkflowsStore Zustand store
  • Create workflows programmatically by constructing Workflow, WFNode, and WFEdge objects and calling save()
  • Customize workflows through the visual canvas or by manipulating the JSON structure directly, using importFile and exportFile for portability
  • Extend the engine by adding processor files in src/areas/workflows/nodes/, registering them in the extension list, and updating nodeBehaviors.ts if special flow semantics are required
  • Validate changes using the pre-flight checker in src/areas/workflows/preflight.ts before execution to catch graph errors early

Frequently Asked Questions

How do I save a workflow to a specific folder?

Use the optional folder property when creating the workflow object. This string value organizes workflows in the UI sidebar, though the actual .json file is stored in the root workflows directory defined by settings.workflowsDir. Folder metadata (names, colors, bookmarks) persists in localStorage under the key modly-workflow-folders.

Can I create workflows without using the UI?

Yes. Import useWorkflowsStore from @/shared/stores/workflowsStore and call getState().save(workflow) after constructing a valid Workflow object with nodes and edges. The store writes directly to the filesystem via the Electron bridge, bypassing the visual canvas entirely.

What is the difference between extensionNode and other node types?

extensionNode is a generic container type used for plugin-based processors. When type is set to extensionNode, the data.extensionId field tells the execution engine which processor file in src/areas/workflows/nodes/ to invoke. Built-in types like inputNode or waitNode have hardcoded logic in the runner, while extensionNode delegates to your custom processor.

Why does my custom node fail during pre-flight checks?

The pre-flight validator likely detected unreachable inputs, cycles, or improper branch connections. Check that you have updated nodeBehaviors.ts if your node blocks execution or consumes branches. Also verify that all required inputs are connected and that nodes reaching scene outputs are not gated behind unmet waitNode conditions.

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 →