Custom Node Development

Build your own node types with custom ports, configuration, and execution logic.

Nodes are the fundamental building blocks of every Floww workflow. While Floww ships with a library of built-in nodes, you can create your own to add new data sources, transformations, integrations, or any custom logic.

Node Anatomy

Every node on the canvas is composed of several visual and functional regions:

┌──────────────────────────────┐
│  [icon]  Node Display Name   │  ← Header (category color bar)
├──────────────────────────────┤
│                              │
│  ● input-1          output ● │  ← Ports (left = inputs, right = outputs)
│  ● input-2                   │
│                              │
├──────────────────────────────┤
│  ┌─────────────────────────┐ │
│  │ Config field 1          │ │  ← Configuration panel (collapsed by default)
│  │ Config field 2          │ │
│  └─────────────────────────┘ │
├──────────────────────────────┤
│  ✓ Completed · 42ms         │  ← Status bar (idle / running / complete / error)
└──────────────────────────────┘
  • Header — displays the node's icon and name. The top border color indicates the category.
  • Ports — input ports on the left, output ports on the right. Each port has a name and a data type. Wires connect output ports to input ports.
  • Configuration panel — user-editable fields defined by the node's config schema. Visible when the node is selected.
  • Status bar — shows the current execution state and timing.

Defining a Node

A custom node is defined as a plain JavaScript object (a node definition) that you register with Floww. Here is a complete example:

const myNode = {
  // Required: unique type identifier
  type: 'text-transform',

  // Required: display name shown in the UI
  displayName: 'Text Transform',

  // Category for the node palette (grouping)
  category: 'Text',

  // Description shown in the node palette tooltip
  description: 'Transform text with common string operations.',

  // Icon: path to an SVG file, or an inline SVG string
  icon: 'assets/text-transform.svg',

  // Input ports
  inputs: [
    { name: 'text', type: 'string', description: 'The input text' }
  ],

  // Output ports
  outputs: [
    { name: 'result', type: 'string', description: 'The transformed text' }
  ],

  // Configuration schema (see "Configuration Schema" section)
  config: {
    fields: [
      {
        key: 'operation',
        label: 'Operation',
        type: 'select',
        options: ['uppercase', 'lowercase', 'trim', 'reverse'],
        default: 'uppercase'
      }
    ]
  },

  // The execution function (see "The Execution Function" section)
  async execute(inputs, config, context) {
    const { text } = inputs;
    let result;

    switch (config.operation) {
      case 'uppercase':  result = text.toUpperCase(); break;
      case 'lowercase':  result = text.toLowerCase(); break;
      case 'trim':       result = text.trim(); break;
      case 'reverse':    result = text.split('').reverse().join(''); break;
      default:           result = text;
    }

    return { result };
  }
};

Ports and Types

Ports define what data flows into and out of a node. Each port declaration has a name, a type, and an optional description.

Supported data types

TypeWire colorDescription
stringGreenText values
numberBlueInteger or floating-point numbers
booleanYellowTrue or false
objectPurpleJSON objects / key-value maps
arrayOrangeOrdered lists of values
anyGrayAccepts any data type (no type checking)

Type checking happens at the VALIDATE stage of the execution lifecycle. If a string port receives a number, the runtime will attempt automatic coercion. If coercion fails, the node enters the ERROR state.

Use specific types
Prefer specific types over any. Typed ports give users visual feedback about compatibility (wire colors must match or be coercible) and help catch errors before execution.

Optional and multi-value ports

inputs: [
  { name: 'data', type: 'object', description: 'Primary data' },
  { name: 'fallback', type: 'object', optional: true, description: 'Used if data is empty' }
],
outputs: [
  { name: 'items', type: 'array', description: 'Processed items' },
  { name: 'count', type: 'number', description: 'Number of items' }
]

Ports marked optional: true do not require a wire connection. If no data arrives on an optional port, the input value is null.

Configuration Schema

The config object defines user-editable fields that appear in the node's configuration panel. Floww automatically generates the UI from this schema.

config: {
  fields: [
    {
      key: 'url',
      label: 'Request URL',
      type: 'text',
      placeholder: 'https://api.example.com/endpoint',
      required: true
    },
    {
      key: 'method',
      label: 'HTTP Method',
      type: 'select',
      options: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
      default: 'GET'
    },
    {
      key: 'headers',
      label: 'Headers',
      type: 'keyvalue',
      description: 'Request headers as key-value pairs'
    },
    {
      key: 'body',
      label: 'Request Body',
      type: 'code',
      language: 'json',
      showWhen: { field: 'method', notEquals: 'GET' }
    },
    {
      key: 'timeout',
      label: 'Timeout (ms)',
      type: 'number',
      default: 5000,
      min: 100,
      max: 60000
    },
    {
      key: 'followRedirects',
      label: 'Follow Redirects',
      type: 'boolean',
      default: true
    }
  ]
}

Field types

TypeRenders asExtra options
textSingle-line text inputplaceholder, required, pattern
textareaMulti-line text arearows, placeholder
numberNumber spinnermin, max, step, default
booleanToggle switchdefault
selectDropdown menuoptions (array of strings or {label, value})
codeCode editor with syntax highlightinglanguage
keyvalueDynamic key-value pair editor(none)
fileFile pickeraccept (file type filter)
colorColor pickerdefault

Conditional visibility

Use showWhen to show or hide fields based on other field values:

// Show "body" field only when method is not GET
showWhen: { field: 'method', notEquals: 'GET' }

// Show "apiKey" field only when auth is enabled
showWhen: { field: 'useAuth', equals: true }

The Execution Function

The execute function is the heart of your node. It runs during the EXECUTE phase of the lifecycle and must return an object mapping output port names to values.

async execute(inputs, config, context) {
  // inputs: object with input port values
  //   e.g., { text: "hello world" }

  // config: object with configuration field values
  //   e.g., { operation: "uppercase" }

  // context: runtime utilities
  //   context.log(message)      — log to the workflow console
  //   context.progress(0-100)   — update the progress bar
  //   context.variable(name)    — read a workflow variable
  //   context.abort()           — check if execution was cancelled

  context.log('Starting text transformation...');
  context.progress(0);

  if (context.abort()) {
    throw new Error('Execution cancelled');
  }

  const result = inputs.text.toUpperCase();
  context.progress(100);

  // Return an object matching your output port names
  return { result };
}
Always return all output ports
Your execute function must return an object with a key for every declared output port. If you don't have data for an optional output, return null for that key. Missing keys will cause a runtime error.

Error handling

If your execution function throws an error, the node transitions to the ERROR state. The error message is displayed in the node's status bar and logged to the workflow console.

async execute(inputs, config, context) {
  const response = await fetch(config.url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  return { data: await response.json() };
}

Registering a Custom Node

Once you have a node definition, register it with Floww in your plugin's onLoad hook:

// In your plugin's index.js
const textTransformNode = require('./nodes/text-transform');

module.exports = {
  onLoad(floww) {
    floww.nodes.register(textTransformNode);

    // Register multiple nodes at once
    floww.nodes.registerAll([
      require('./nodes/json-query'),
      require('./nodes/csv-parse'),
      require('./nodes/regex-match')
    ]);
  },

  onUnload(floww) {
    // Nodes are automatically unregistered when the plugin unloads.
    // No manual cleanup needed.
  }
};

After registration, the node appears in the node palette under its declared category. Users can drag it onto the canvas like any built-in node.

Packaging

Custom nodes are distributed as part of a plugin. You can also create a standalone node package if your node has no other plugin logic:

my-text-nodes/
  manifest.json          # Plugin manifest with nodes:register permission
  index.js               # onLoad registers nodes, onUnload is empty
  nodes/
    text-transform.js    # Node definition
    regex-match.js       # Another node definition
  assets/
    text-transform.svg   # Node icon
    regex-match.svg      # Node icon

See the Plugin Packaging guide for details on zip structure and versioning.

Icon Guidelines

Every custom node should include an icon that appears in the node header and the node palette. Follow these guidelines for visual consistency:

RequirementSpecification
FormatSVG only
ViewBox0 0 24 24
ColorsSingle color using currentColor (inherits from theme)
Stroke width1.5px or 2px for consistency with built-in icons
PaddingKeep a 2px margin inside the viewBox
FillsPrefer stroke-based icons over filled shapes

Example icon SVG:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
     fill="none" stroke="currentColor" stroke-width="1.5"
     stroke-linecap="round" stroke-linejoin="round">
  <path d="M4 7V4h16v3" />
  <path d="M9 20h6" />
  <path d="M12 4v16" />
</svg>
Test your icon in both themes
Since the icon uses currentColor, it automatically adapts to light and dark themes. Preview your node in both to ensure the icon is clearly visible against both backgrounds.