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:
| File | Role |
|---|---|
{NodeClass}.php | PHP execution logic |
components/{NodeClass}.jsx | React UI component displayed in the canvas |
manifest.json | Metadata, 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 bundleStep 1 — Scaffold: voodflow:make-node
php artisan voodflow:make-node {name}| Option | Description |
|---|---|
{name} | Node class name (e.g. SlackNode, OpenAiNode) |
--interactive | Force fully interactive mode |
--force | Overwrite 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 connectorsThe 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.jsonStep 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
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 / Property | Type | Description |
|---|---|---|
$context->input | array | Output of the previous node |
$context->getConfig(string $key, mixed $default) | mixed | Read a node config value |
$context->resolveTemplate(string $template) | string | Resolve {{tags}} against context |
$context->getVariable(string $key) | mixed | Read a workflow-level variable |
$context->setVariable(string $key, mixed $value) | void | Write a workflow-level variable |
$context->getCredential(int $id, string $field) | mixed | Decrypt and return a credential field |
$context->executionId | int | Current execution ID |
$context->workflow | Workflow | The Workflow Eloquent model |
ExecutionResult API
// 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:
import {
BaseNodeContainer,
NodeHeader,
CollapsedView,
NodeConfigFields,
FieldLabel,
TextInput,
SelectInput,
StandardHandle,
NODE_CONFIG_DEBOUNCE_MS,
useStandardNodeBehavior,
} from "../../../../resources/js/components/nodes";What these primitives do
| Import | What it’s for |
|---|---|
BaseNodeContainer | Standard outer wrapper: selection states, border, background, and node color accents |
NodeHeader | Title bar: icon, label, tier badge, expand/collapse, and common actions |
CollapsedView | Compact summary shown when the node is collapsed |
NodeConfigFields | The standard “Label + Description” UI block used across nodes |
FieldLabel | Consistent label typography and spacing for custom fields |
TextInput, SelectInput | Styled inputs that match the canvas UI (including dark mode) |
StandardHandle | The standard ReactFlow handle with consistent styling and positioning |
NODE_CONFIG_DEBOUNCE_MS | The default debounce used to avoid spamming Livewire updates while typing |
useStandardNodeBehavior | Shared behavior hook (expanded state, selection, common update helpers) |
Adding a configuration field
- Add state:
const [channel, setChannel] = useState(data.channel || "");- Add JSX in the expanded view:
<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>- Persist in the debounced
useEffect:
component.call("updateNodeConfig", { nodeId: id, label, description, channel });
// and in setNodes: data: { ...node.data, label, description, channel }
// add `channel, data.channel` to the dependency arrayAvailable UI primitives
| Component | Use |
|---|---|
TextInput | Single-line text |
SelectInput | Dropdown (pass options={[...]}) |
FieldLabel | Label above a field |
NodeConfigFields | Standard label + description block |
StandardHandle | Input or output connection handle |
CollapsedView | Summary shown when node is collapsed |
BaseNodeContainer | Root wrapper with color/selection styles |
NodeHeader | Title bar with icon, badge, expand/delete |
Step 4 — Build: voodflow:build-node
php artisan voodflow:build-node ChatNodeWhat happens:
- Locates
components/ChatNode.jsx - Compiles with esbuild — React, ReactDOM, and ReactFlow are externalized
- Writes bundle to
{node_dir}/dist/chat-node.js - 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 inpublic/js/voodflow/nodes/.
The manifest.json Reference
{
"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
| Field | Type | Description |
|---|---|---|
name | string | Kebab-case identifier, e.g. chat-node |
display_name | string | Human-readable name shown in the UI |
version | string | SemVer version |
author | string | Author or organization name |
description | string | Short description for the node picker |
category | string | trigger action transform flow data utility ai notification integration |
tier | string | CORE (internal only) |
display.iconType | string | heroicon or image |
display.icon | string | Heroicon name or image filename |
display.color | string | Tailwind color: blue indigo amber emerald violet cyan etc. |
display.logo | string|null | Logo filename placed in the node directory (iconType: image) |
php.class | string | PHP class name |
php.namespace | string | Fully qualified namespace |
javascript.component | string | window variable name of the bundle |
javascript.bundle | string | Relative path to the compiled JS file |
voodflow.min_version | string | Minimum Voodflow version required |
license.type | string | License identifier (e.g. MIT) |
Development Tips
Hot-reload workflow
# Edit your PHP or JSX, then:
php artisan voodflow:build-node MyNode
# → Refresh browserNaming conventions
| Artifact | Convention | Example |
|---|---|---|
| PHP class | PascalCase + Node suffix | LlmChatNode |
manifest.name | kebab-case | llm-chat-node |
type() return | snake_case | llm_chat_node |
| JSX component | PascalCase | LlmChatNode |
Type key uniqueness
The value returned by type() must be globally unique. Always prefix it:
// 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
# 1. Scaffold
php artisan voodflow:make-node UppercaseNode
# Type: transform | Tier: CORE | Author: Your NameUppercaseNode.php — execute method:
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),
]));
}# 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