How to Create a Custom Modly Extension: Complete Guide to Manifest Fields and Required Structure

Creating a custom Modly extension requires a properly configured manifest.json file with eight mandatory fields—id, name, type, version, author, description, entry, and nodes—plus model-specific fields when building model extensions.

Modly extensions are self-contained packages that extend the platform's workflow capabilities through custom processing nodes or AI model integrations. The extension system, as implemented in lightningpixel/modly, uses a strict manifest validation process that runs at installation time to ensure compatibility and security.

Overview of the Extension Creation Process

Extensions in Modly follow a predictable lifecycle: folder creation → manifest definition → entry point implementation → local testing → optional publication. The core validation logic resides in electron/main/ipc-handlers.ts, specifically within the parseExtensionManifest() function (lines 911–923), where each manifest undergoes schema verification before the extension becomes available in the UI.

Extensions can be loaded from two sources:

  • Local folders under ~/.modly/extensions/ or the app's extensions/ directory
  • Remote GitHub repositories via the Install from GitHub dialog

Step 1: Create the Extension Folder Structure

Every Modly extension lives as a discrete folder containing at minimum a manifest.json file and an entry point script.


# Create local extension directory

mkdir -p ~/.modly/extensions/my-awesome-ext
cd ~/.modly/extensions/my-awesome-ext

# Required files

touch manifest.json
touch processor.js  # or your preferred entry filename

Modly scans registered extension directories at startup, parsing each manifest.json found in immediate subdirectories.

Step 2: Define Required Manifest Fields

The manifest.json file serves as the single source of truth for extension identity and capabilities. Based on the ParsedManifest type definition in electron/main/ipc-handlers.ts, these eight fields are mandatory for all extension types:

Field Type Description
id string Unique identifier, conventionally author/extension-name
name string Human-readable display name shown in the UI
type "process" | "model" Determines runtime behavior and validation rules
version string Semantic version (e.g., "1.0.0")
author string Creator name or organization
description string Short summary for extension browser
entry string Relative path to main JavaScript file
nodes array Workflow node definitions provided by this extension

Complete Base Manifest Example

{
  "id": "acme/text-processor",
  "name": "Text Utilities",
  "type": "process",
  "version": "1.0.0",
  "author": "Acme Corp",
  "description": "Common text transformation operations",
  "entry": "processor.js",
  "nodes": [
    {
      "id": "uppercase",
      "label": "To Uppercase",
      "description": "Converts input text to uppercase",
      "inputSchema": {
        "type": "object",
        "properties": {
          "text": { "type": "string" }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "text": { "type": "string" }
        }
      }
    }
  ]
}

Step 3: Implement Model-Specific Fields (When Applicable)

Model extensions require two additional fields beyond the base manifest. The extension-install-utils.ts file (line 37) explicitly validates these and throws an installation error if generator_class is absent:

  • generator_class: String name of the model generator class exported by the entry file
  • nodeClass (optional): Custom node class identifier for specialized UI rendering
{
  "id": "acme/custom-llm",
  "name": "Custom Language Model",
  "type": "model",
  "version": "1.0.0",
  "author": "Acme AI Lab",
  "description": "Fine-tuned model for domain-specific tasks",
  "entry": "generator.js",
  "generator_class": "DomainSpecificLLM",
  "nodes": [
    {
      "id": "generate",
      "label": "Generate",
      "description": "Run inference with custom model"
    }
  ]
}

Step 4: Write the Entry Point Script

The file referenced by entry must export the appropriate interface based on extension type.

Process Extension Entry Point

For type: "process", export an async run function receiving { input, params }:

// processor.js
export async function run({ input, params }) {
  const { text = '' } = input || {};
  const { addPrefix = false, prefix = '' } = params || {};
  
  const processed = addPrefix ? `${prefix}${text}` : text;
  
  return {
    text: processed,
    charCount: processed.length,
    wordCount: processed.split(/\s+/).filter(Boolean).length
  };
}

The runtime execution path flows through workflowRunStore.ts (lines 428–440), where window.electron.extensions.runProcess() ultimately invokes this function.

Model Extension Entry Point

For type: "model", export the generator class matching generator_class:

// generator.js
export class DomainSpecificLLM {
  constructor(config) {
    this.config = config;
  }
  
  async generate(prompt, options = {}) {
    // Model inference implementation
    return {
      text: `Generated response for: ${prompt}`,
      tokensUsed: prompt.length / 4
    };
  }
  
  async *stream(prompt, options = {}) {
    // Streaming implementation for real-time output
    yield { chunk: "Partial " };
    yield { chunk: "response" };
  }
}

Step 5: Define Node Specifications

Each object in the nodes array requires at minimum id and label. Full node schema enables rich workflow editor integration:

{
  "nodes": [
    {
      "id": "transform",
      "label": "Transform Text",
      "description": "Applies configurable text transformations",
      "inputSchema": {
        "type": "object",
        "required": ["text"],
        "properties": {
          "text": { "type": "string", "description": "Input text" }
        }
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "text": { "type": "string" },
          "stats": { "type": "object" }
        }
      },
      "paramSchema": {
        "type": "object",
        "properties": {
          "operation": {
            "type": "string",
            "enum": ["uppercase", "lowercase", "reverse"],
            "default": "uppercase"
          }
        }
      }
    }
  ]
}

Step 6: Test and Validate Locally

Install your extension through Modly's UI to verify manifest parsing:

  1. Open Extensions → Install from Local
  2. Select your extension folder
  3. Check for manifestError indicators in the UI
  4. Create a test workflow using your extension's nodes

Validation failures surface as error toasts with line references to parseExtensionManifest() in electron/main/ipc-handlers.ts.

Step 7: Publish via GitHub (Optional)

For distribution, push to a public repository and install via Install from GitHub. The installer (extension-install-utils.ts, lines 18–41) performs identical validation before registration:


# Repository structure for GitHub installation

my-modly-extension/
├── manifest.json
├── processor.js
└── README.md

Manifest Validation in Source Code

The validation pipeline enforces field presence through explicit checks:

File Function Lines Purpose
electron/main/ipc-handlers.ts parseExtensionManifest() 911–923 Schema validation, required field checks
electron/main/extension-install-utils.ts installExtension() 18–41 Installation orchestration, model field verification
electron/main/extension-install-utils.ts Model loader 37 generator_class presence check

Summary

  • Eight fields are mandatory in every Modly extension manifest: id, name, type, version, author, description, entry, and nodes
  • Model extensions require generator_class in addition to base fields
  • Process extensions export an async run({ input, params }) function
  • Model extensions export a class matching the generator_class name
  • Validation occurs in parseExtensionManifest() before any extension becomes available
  • Local testing uses Install from Local; distribution uses Install from GitHub

Frequently Asked Questions

What happens if I omit a required manifest field?

Modly rejects the extension during installation with a validation error. The parseExtensionManifest() function in electron/main/ipc-handlers.ts checks for all mandatory fields and returns a structured error object that the UI displays as a manifestError notification with specific field references.

Can I use a different entry filename than processor.js?

Yes. The entry field accepts any relative path within your extension folder. Process extensions default to processor.js only when entry is omitted, but explicit declaration is recommended for clarity. The runtime resolves this path relative to the extension's root directory.

How do I debug a failing extension installation?

Check the DevTools console for validation errors from parseExtensionManifest(). Enable verbose logging in Modly's settings to see full manifest parsing traces. Common failures include malformed JSON, missing generator_class for model types, or node definitions lacking required id and label properties.

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 →