Skip to main content

Overview

Plugins allow you to extend AutoMFlows’s capabilities by creating custom node types with your own functionality. You can create nodes for specific automation tasks, integrate external services, or add specialized workflow behaviors.

Easy to Build

Simple file structure and clear interfaces

TypeScript Support

Full type safety with TypeScript

Hot Reload

Automatically reload on changes

Share & Reuse

Easily share plugins with the community

Plugin Structure

A plugin is a directory in the plugins/ folder with the following files:
your-plugin/
  plugin.json          # Plugin manifest (required)
  handler.js           # Node handlers (required)
  handler.ts           # TypeScript source (optional)
  config.tsx           # Frontend config component (optional)
  icon.svg             # Custom icon (optional)
  README.md            # Plugin documentation (optional)
The only required files are plugin.json and handler.js. All other files are optional but recommended for better functionality and documentation.

Quick Start

1

Create plugin directory

Create a new folder in the plugins/ directory:
mkdir plugins/my-plugin
cd plugins/my-plugin
2

Create plugin manifest

Create plugin.json to define your plugin metadata:
plugin.json
{
  "name": "my-plugin",
  "version": "1.0.0",
  "description": "My custom AutoMFlows plugin",
  "author": "Your Name",
  "nodes": [
    {
      "type": "myplugin.customAction",
      "label": "Custom Action",
      "category": "Custom",
      "icon": "🎯",
      "description": "Performs a custom action",
      "handlerPath": "handler.js",
      "defaultData": {
        "field1": "default value"
      }
    }
  ]
}
3

Create node handler

Create handler.ts to implement your node logic:
handler.ts
import { BaseNode } from '@automflows/shared';
import { NodeHandler } from '../../backend/src/nodes/base';
import { ContextManager } from '../../backend/src/engine/context';

export class CustomActionHandler implements NodeHandler {
  async execute(node: BaseNode, context: ContextManager): Promise<void> {
    const page = context.getPage();
    
    if (!page) {
      throw new Error('No page available');
    }

    // Your custom logic here
    console.log('Executing custom action:', node.data);
  }
}

export default {
  'myplugin.customAction': CustomActionHandler,
};
4

Build and test

Compile TypeScript to JavaScript:
# From plugin directory
npx tsc handler.ts --target ES2020 --module commonjs
Restart the backend server to load your plugin:
# From project root
npm run dev:backend

Plugin Manifest

The plugin.json file defines your plugin’s metadata and available nodes.

Manifest Fields

FieldTypeRequiredDescription
namestringYesUnique plugin identifier (lowercase, no spaces)
versionstringYesSemantic version (e.g., “1.0.0”)
descriptionstringYesBrief description of the plugin
authorstringNoPlugin author name
nodesarrayYesArray of node definitions

Node Definition Fields

FieldTypeRequiredDescription
typestringYesUnique node type identifier (use plugin name prefix)
labelstringYesDisplay name shown in the UI
categorystringYesCategory name for grouping in the sidebar
iconstringYesEmoji or icon identifier
descriptionstringNoNode description
handlerPathstringYesPath to handler file relative to plugin root
configComponentPathstringNoPath to React config component
defaultDataobjectNoDefault values for node data fields
Always use a namespace prefix for node types (e.g., myplugin.action) to avoid conflicts with other plugins or core nodes.

Creating Node Handlers

Handlers implement the business logic for your nodes. They must implement the NodeHandler interface.

Handler Interface

interface NodeHandler {
  execute(node: BaseNode, context: ContextManager): Promise<void>;
}

Real-World Example: Fill Form Handler

Here’s a complete example from the example-plugin:
handler.ts
import { BaseNode } from '@automflows/shared';
import { NodeHandler } from '../../backend/src/nodes/base';
import { ContextManager } from '../../backend/src/engine/context';

interface FillFormNodeData {
  fields: Array<{
    selector: string;
    selectorType?: 'css' | 'xpath';
    value: string;
    timeout?: number;
  }>;
}

export class FillFormHandler implements NodeHandler {
  async execute(node: BaseNode, context: ContextManager): Promise<void> {
    const data = node.data as FillFormNodeData;
    const page = context.getPage();

    if (!page) {
      throw new Error('No page available. Ensure Open Browser node is executed first.');
    }

    if (!data.fields || !Array.isArray(data.fields) || data.fields.length === 0) {
      throw new Error('Fields array is required for Fill Form node');
    }

    // Fill each field
    for (const field of data.fields) {
      if (!field.selector) {
        throw new Error('Selector is required for each field');
      }

      const timeout = field.timeout || 30000;
      const selector = field.selector;
      const value = field.value || '';

      try {
        if (field.selectorType === 'xpath') {
          await page.locator(`xpath=${selector}`).fill(value, { timeout });
        } else {
          await page.fill(selector, value, { timeout });
        }
      } catch (error: any) {
        throw new Error(`Failed to fill field with selector "${selector}": ${error.message}`);
      }
    }
  }
}

export default {
  'example.fillForm': FillFormHandler,
};

Utility Node Example

Some nodes don’t need to execute any browser actions:
handler.ts
import { BaseNode } from '@automflows/shared';
import { NodeHandler } from '../../backend/src/nodes/base';
import { ContextManager } from '../../backend/src/engine/context';

export class ShortcutHandler implements NodeHandler {
  async execute(_node: BaseNode, _context: ContextManager): Promise<void> {
    // Shortcut is a utility node - it doesn't execute anything
    // This handler exists only to satisfy the plugin interface
    return;
  }
}

export default {
  'shortcut.shortcut': ShortcutHandler,
};

Context Manager API

The ContextManager provides access to the workflow execution context:

Browser & Page Methods

MethodReturn TypeDescription
getPage()Page | nullGet the current Playwright Page object
getBrowser()Browser | nullGet the current Playwright Browser object
setPage(page)voidSet the current page
setBrowser(browser)voidSet the current browser

Variable & Data Methods

MethodReturn TypeDescription
getVariable(name)anyGet a workflow variable
setVariable(name, value)voidSet a workflow variable
getData(key)anyGet data from previous nodes
setData(key, value)voidSet data for next nodes

Example Usage

async execute(node: BaseNode, context: ContextManager): Promise<void> {
  // Get the browser page
  const page = context.getPage();
  
  // Get a workflow variable
  const username = context.getVariable('username');
  
  // Store data for next nodes
  context.setData('extractedText', await page.textContent('.result'));
  
  // Set a workflow variable
  context.setVariable('status', 'completed');
}

Frontend Config Components

Create custom React components for node configuration in the UI:
config.tsx
import { PluginConfigComponentProps } from '../../frontend/src/plugins/types';

export default function MyNodeConfig({ node, onChange }: PluginConfigComponentProps) {
  return (
    <div className="space-y-4">
      <div>
        <label className="block text-sm font-medium mb-1">Selector</label>
        <input
          type="text"
          className="w-full px-3 py-2 border rounded"
          value={node.data.selector || ''}
          onChange={(e) => onChange('selector', e.target.value)}
          placeholder="Enter CSS selector"
        />
      </div>
      
      <div>
        <label className="block text-sm font-medium mb-1">Timeout (ms)</label>
        <input
          type="number"
          className="w-full px-3 py-2 border rounded"
          value={node.data.timeout || 30000}
          onChange={(e) => onChange('timeout', parseInt(e.target.value))}
        />
      </div>
    </div>
  );
}

Best Practices

Always validate inputs and provide clear error messages:
if (!data.selector) {
  throw new Error('Selector is required for this node');
}

try {
  await page.click(data.selector);
} catch (error: any) {
  throw new Error(`Failed to click element: ${error.message}`);
}

Testing Your Plugin

1

Validate manifest

Ensure plugin.json is valid JSON:
cat plugin.json | jq .
2

Build handler

Compile TypeScript if using .ts files:
npx tsc handler.ts --target ES2020 --module commonjs
3

Restart backend

Restart the backend server to load your plugin:
npm run dev:backend
Check server logs for plugin loading messages.
4

Verify in UI

  1. Refresh the frontend
  2. Check that your nodes appear in the sidebar under the specified category
  3. Drag a node onto the canvas
  4. Test the node configuration
5

Test execution

Create a workflow using your custom node and execute it to verify functionality.

Example Plugins

AutoMFlows includes several example plugins you can reference:

example-plugin

Fill Form and Scroll To nodes demonstrating form automation and scrolling

shortcut

Shortcut node for keyboard navigation between nodes (utility node example)

comment-box

Comment Box node for adding annotations to workflows

set-config-node

Set Config node for dynamic configuration management

Troubleshooting

Plugin not loading

Checklist:
  • Verify plugin.json is valid JSON (use jq or JSON validator)
  • Check that handler file path is correct
  • Review server logs for error messages
  • Ensure handler exports match node types in manifest

Handler not found

// ✓ Correct export format
export default {
  'myplugin.nodeType': MyHandler,
};

// ✗ Incorrect - missing default export
export const handlers = {
  'myplugin.nodeType': MyHandler,
};

Node not appearing in UI

  1. Check that plugin loaded successfully in server logs
  2. Verify node category is set correctly in manifest
  3. Refresh the frontend after plugin loads
  4. Clear browser cache if necessary

TypeScript errors

Ensure you have the correct imports:
import { BaseNode } from '@automflows/shared';
import { NodeHandler } from '../../backend/src/nodes/base';
import { ContextManager } from '../../backend/src/engine/context';

Sharing Your Plugin

To share your plugin with others:
1

Create repository

Create a Git repository with your plugin code:
cd plugins/my-plugin
git init
git add .
git commit -m "Initial plugin commit"
2

Add documentation

Include a comprehensive README.md with installation and usage instructions.
3

Publish

Push to GitHub, GitLab, or any Git hosting service:
git remote add origin <your-repo-url>
git push -u origin main
4

Installation instructions

Users can install your plugin by cloning it into their plugins/ directory:
cd plugins
git clone <your-repo-url> my-plugin
npm run dev:backend  # Restart to load plugin

Advanced Topics

Multiple Nodes in One Plugin

You can define multiple nodes in a single plugin:
plugin.json
{
  "name": "my-plugin",
  "nodes": [
    {
      "type": "myplugin.action1",
      "label": "Action 1",
      "handlerPath": "handler.js"
    },
    {
      "type": "myplugin.action2",
      "label": "Action 2",
      "handlerPath": "handler.js"
    }
  ]
}
handler.ts
export class Action1Handler implements NodeHandler {
  async execute(node: BaseNode, context: ContextManager): Promise<void> {
    // Action 1 logic
  }
}

export class Action2Handler implements NodeHandler {
  async execute(node: BaseNode, context: ContextManager): Promise<void> {
    // Action 2 logic
  }
}

export default {
  'myplugin.action1': Action1Handler,
  'myplugin.action2': Action2Handler,
};

Plugin Dependencies

If your plugin requires external npm packages:
  1. Create a package.json in your plugin directory
  2. Install dependencies
  3. Import and use them in your handler
package.json
{
  "name": "my-plugin",
  "version": "1.0.0",
  "dependencies": {
    "axios": "^1.6.0"
  }
}
handler.ts
import axios from 'axios';

export class ApiCallHandler implements NodeHandler {
  async execute(node: BaseNode, context: ContextManager): Promise<void> {
    const response = await axios.get(node.data.url);
    context.setData('apiResponse', response.data);
  }
}

Next Steps

Node Types

Learn about built-in node types

Workflow Engine

Understand how workflows execute

MCP Server

Build AI-powered workflow automation

API Reference

Explore the full API

Build docs developers (and LLMs) love