Skip to content

Creating Custom Nodes

Voodflow ships a CLI scaffolding system to create and build custom nodes. A node is a self-contained unit composed of three files:

FileRole
{NodeClass}.phpPHP execution logic
components/{NodeClass}.jsxReact UI component displayed in the canvas
manifest.jsonMetadata, display settings, config schema, licensing

Overview: The Two-Command Workflow

php artisan voodflow:make-node MyNode       # 1. Scaffold
php artisan voodflow:build-node MyNode      # 2. Compile JSX → JS bundle

Step 1 — Scaffold: voodflow:make-node

bash
php artisan voodflow:make-node {name}
OptionDescription
{name}Node class name (e.g. SlackNode, OpenAiNode)
--interactiveForce fully interactive mode
--forceOverwrite existing files

The command launches an interactive wizard that collects:

Wizard Prompts

1. Node type

  trigger      — Starts a workflow (scheduled, event-driven, webhook)
  action       — Performs an operation (send, create, call)
  transform    — Reshapes or maps data
  flow         — Controls execution branching (if, switch, loop)
  data         — Reads or writes to a data store
  utility      — Developer tools (debug, code, inspect)
  ai           — AI/ML integrations
  notification — Sends alerts or messages
  integration  — Third-party API connectors

The type determines the default color, icon, and canvas group.

2. Tier

  CORE — Internal only (ships with Voodflow)

CORE tier is reserved for internal Voodflow nodes.

3. Author, description, optional metadata

  • Author name and URL
  • Short description (shown in the node picker)
  • Repository URL
  • License (default: MIT)
  • Logo filename (for a custom image icon instead of a Heroicon)

4. Multiple outputs (flow nodes only)

If the node type is flow, the wizard asks whether it has multiple output handles (like an IF or Switch node). You define each handle's ID, label, and color.

Output

After the wizard completes, the following structure is created:

packages/voodflow/voodflow/src/Nodes/Custom/{NodeClass}/
├── {NodeClass}.php
├── components/
│   └── {NodeClass}.jsx
└── manifest.json

Step 2 — Implement the PHP class

The generated {NodeClass}.php implements NodeInterface. Fill in the execute() method.

For a simple reference implementation, look at an existing core node such as DummyDataNode:

  • packages/voodflow/voodflow/src/Nodes/DummyDataNode/DummyDataNode.php

Minimal example: a simple transform node

This example reads a config value, resolves {{tags}}, and returns a merged output.

php
<?php

namespace Voodflow\Voodflow\Nodes\UppercaseNode;

use Voodflow\Voodflow\Contracts\NodeInterface;
use Voodflow\Voodflow\Execution\ExecutionContext;
use Voodflow\Voodflow\Execution\ExecutionResult;

class UppercaseNode implements NodeInterface
{
    public static function type(): string
    {
        return 'uppercase_node';
    }

    public static function defaultConfig(): array
    {
        return [
            'label' => 'Uppercase',
            'description' => '',
            'source_path' => 'text',
            'target_key' => 'uppercased',
        ];
    }

    public function execute(ExecutionContext $context): ExecutionResult
    {
        $sourcePath = $context->getConfig('source_path', 'text');
        $targetKey = $context->getConfig('target_key', 'uppercased');

        $value = data_get($context->input, $sourcePath);
        $value = is_string($value) ? $value : (string) $value;

        $resolved = $context->resolveTemplate($value);

        return ExecutionResult::success(array_merge($context->input, [
            $targetKey => mb_strtoupper($resolved),
        ]));
    }

    public function validate(array $config): array
    {
        $errors = [];

        if (empty($config['source_path'])) {
            $errors['source_path'] = 'Source path is required';
        }

        return $errors;
    }

    public static function definition(): array
    {
        return [
            ['key' => 'source_path', 'type' => 'text', 'label' => 'Source Path', 'required' => true],
            ['key' => 'target_key', 'type' => 'text', 'label' => 'Target Key', 'required' => true],
        ];
    }

    public static function supportsRetry(): bool
    {
        return true;
    }
}

ExecutionContext API

Method / PropertyTypeDescription
$context->inputarrayOutput of the previous node
$context->getConfig(string $key, mixed $default)mixedRead a node config value
$context->resolveTemplate(string $template)stringResolve {{tags}} against context
$context->getVariable(string $key)mixedRead a workflow-level variable
$context->setVariable(string $key, mixed $value)voidWrite a workflow-level variable
$context->getCredential(int $id, string $field)mixedDecrypt and return a credential field
$context->executionIdintCurrent execution ID
$context->workflowWorkflowThe Workflow Eloquent model

ExecutionResult API

php
// Pass data to the default output
ExecutionResult::success(array $data): ExecutionResult

// Terminate branch with error (interceptable by Catch node)
ExecutionResult::failure(string $message, string $code = 'ERROR'): ExecutionResult

// Route to a named output handle (flow nodes)
ExecutionResult::success($data)->toOutput('true')
ExecutionResult::success($data)->toOutput('false')

Step 3 — Customize the React component

The generated JSX file is a React component that integrates with the Voodflow canvas design system. You typically build the UI by composing a few shared primitives:

jsx
import {
    BaseNodeContainer,
    NodeHeader,
    CollapsedView,
    NodeConfigFields,
    FieldLabel,
    TextInput,
    SelectInput,
    StandardHandle,
    NODE_CONFIG_DEBOUNCE_MS,
    useStandardNodeBehavior,
} from "../../../../resources/js/components/nodes";

What these primitives do

ImportWhat it’s for
BaseNodeContainerStandard outer wrapper: selection states, border, background, and node color accents
NodeHeaderTitle bar: icon, label, tier badge, expand/collapse, and common actions
CollapsedViewCompact summary shown when the node is collapsed
NodeConfigFieldsThe standard “Label + Description” UI block used across nodes
FieldLabelConsistent label typography and spacing for custom fields
TextInput, SelectInputStyled inputs that match the canvas UI (including dark mode)
StandardHandleThe standard ReactFlow handle with consistent styling and positioning
NODE_CONFIG_DEBOUNCE_MSThe default debounce used to avoid spamming Livewire updates while typing
useStandardNodeBehaviorShared behavior hook (expanded state, selection, common update helpers)

Adding a configuration field

  1. Add state:
jsx
const [channel, setChannel] = useState(data.channel || "");
  1. Add JSX in the expanded view:
jsx
<div className="px-4 pb-4 border-t border-slate-100 dark:border-slate-800 pt-4 space-y-4">
    <div className="space-y-1">
        <FieldLabel>Slack Channel</FieldLabel>
        <TextInput
            value={channel}
            onChange={(e) => setChannel(e.target.value)}
            placeholder="#general"
            color={nodeColor}
        />
    </div>
</div>
  1. Persist in the debounced useEffect:
jsx
component.call("updateNodeConfig", { nodeId: id, label, description, channel });
// and in setNodes: data: { ...node.data, label, description, channel }
// add `channel, data.channel` to the dependency array

Available UI primitives

ComponentUse
TextInputSingle-line text
SelectInputDropdown (pass options={[...]})
FieldLabelLabel above a field
NodeConfigFieldsStandard label + description block
StandardHandleInput or output connection handle
CollapsedViewSummary shown when node is collapsed
BaseNodeContainerRoot wrapper with color/selection styles
NodeHeaderTitle bar with icon, badge, expand/delete

Step 4 — Build: voodflow:build-node

bash
php artisan voodflow:build-node ChatNode

What happens:

  1. Locates components/ChatNode.jsx
  2. Compiles with esbuild — React, ReactDOM, and ReactFlow are externalized
  3. Writes bundle to {node_dir}/dist/chat-node.js
  4. Copies bundle to public/js/voodflow/nodes/chat-node.js

After building, refresh your browser — the node appears in the canvas picker.

The bundle registers itself as window.VoodflowNode_ChatNode. Voodflow auto-discovers all bundles in public/js/voodflow/nodes/.


The manifest.json Reference

json
{
    "name": "chat-node",
    "display_name": "Chat",
    "version": "1.0.0",
    "author": "Acme Corp",
    "author_url": "https://acme.example.com",
    "description": "Post messages to chat channels",
    "category": "integration",
    "tier": "CORE",

    "display": {
        "iconType": "heroicon",
        "icon": "heroicon-o-chat-bubble-left-right",
        "color": "indigo",
        "logo": null
    },

    "php": {
        "class": "ChatNode",
        "namespace": "Voodflow\\Voodflow\\Nodes\\ChatNode"
    },

    "javascript": {
        "component": "VoodflowNode_ChatNode",
        "bundle": "dist/chat-node.js"
    },

    "voodflow": {
        "min_version": "1.0.0"
    },

    "license": {
        "type": "MIT"
    }
}

Field Reference

FieldTypeDescription
namestringKebab-case identifier, e.g. chat-node
display_namestringHuman-readable name shown in the UI
versionstringSemVer version
authorstringAuthor or organization name
descriptionstringShort description for the node picker
categorystringtrigger action transform flow data utility ai notification integration
tierstringCORE (internal only)
display.iconTypestringheroicon or image
display.iconstringHeroicon name or image filename
display.colorstringTailwind color: blue indigo amber emerald violet cyan etc.
display.logostring|nullLogo filename placed in the node directory (iconType: image)
php.classstringPHP class name
php.namespacestringFully qualified namespace
javascript.componentstringwindow variable name of the bundle
javascript.bundlestringRelative path to the compiled JS file
voodflow.min_versionstringMinimum Voodflow version required
license.typestringLicense identifier (e.g. MIT)

Development Tips

Hot-reload workflow

bash
# Edit your PHP or JSX, then:
php artisan voodflow:build-node MyNode
# → Refresh browser

Naming conventions

ArtifactConventionExample
PHP classPascalCase + Node suffixLlmChatNode
manifest.namekebab-casellm-chat-node
type() returnsnake_casellm_chat_node
JSX componentPascalCaseLlmChatNode

Type key uniqueness

The value returned by type() must be globally unique. Always prefix it:

php
// Good
public static function type(): string { return 'acme_chat_node'; }

// Bad — may conflict
public static function type(): string { return 'chat'; }

Complete Example: Simple Transform Node

bash
# 1. Scaffold
php artisan voodflow:make-node UppercaseNode
# Type: transform | Tier: CORE | Author: Your Name

UppercaseNode.php — execute method:

php
public function execute(ExecutionContext $context): ExecutionResult
{
    $sourcePath = $context->getConfig('source_path', 'text');
    $targetKey = $context->getConfig('target_key', 'uppercased');

    $value = data_get($context->input, $sourcePath);
    $value = is_string($value) ? $value : (string) $value;

    $resolved = $context->resolveTemplate($value);

    return ExecutionResult::success(array_merge($context->input, [
        $targetKey => mb_strtoupper($resolved),
    ]));
}
bash
# 2. Build
php artisan voodflow:build-node UppercaseNode

# 3. Test in canvas: connect after Dummy Data, set source_path = "text"
# 4. Inspect output with a Data Viewer node

Proprietary software — source-available. All rights reserved.