EXTEND FLOWW
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
| Type | Wire color | Description |
|---|---|---|
string | Green | Text values |
number | Blue | Integer or floating-point numbers |
boolean | Yellow | True or false |
object | Purple | JSON objects / key-value maps |
array | Orange | Ordered lists of values |
any | Gray | Accepts 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.
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
| Type | Renders as | Extra options |
|---|---|---|
text | Single-line text input | placeholder, required, pattern |
textarea | Multi-line text area | rows, placeholder |
number | Number spinner | min, max, step, default |
boolean | Toggle switch | default |
select | Dropdown menu | options (array of strings or {label, value}) |
code | Code editor with syntax highlighting | language |
keyvalue | Dynamic key-value pair editor | (none) |
file | File picker | accept (file type filter) |
color | Color picker | default |
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 };
}
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:
| Requirement | Specification |
|---|---|
| Format | SVG only |
| ViewBox | 0 0 24 24 |
| Colors | Single color using currentColor (inherits from theme) |
| Stroke width | 1.5px or 2px for consistency with built-in icons |
| Padding | Keep a 2px margin inside the viewBox |
| Fills | Prefer 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>
currentColor, it automatically adapts to light and dark themes. Preview your node in both to ensure the icon is clearly visible against both backgrounds.