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

> Master Modly workflows by learning to create and customize JSON-based pipelines. Assemble nodes and edges or register custom processors to build powerful data flows. Get the complete guide now.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: how-to-guide
- Published: 2026-08-19

---

**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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/preflight.ts)): Validates graphs before execution to detect missing inputs, cycles, or illegal merges.

- **Execution Engine** ([`src/areas/workflows/workflowRunStore.ts`](https://github.com/lightningpixel/modly/blob/main/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.

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

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

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

```typescript
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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/mockExtensions.ts) (or via runtime extension installation):

```typescript
{
  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`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/nodeBehaviors.ts):

```typescript
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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/nodeBehaviors.ts)** if special flow semantics are required
- **Validate changes** using the pre-flight checker in [`src/areas/workflows/preflight.ts`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/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.