📦 How to Create a Custom Node in Borealis
This guide explains how to build your own node for the Borealis Workflow Automation Tool. It walks through the key structure, best practices, and configuration options. You'll find example code, inline commentary, and a copy-paste template at the bottom.
1. 📚 Node Anatomy: The Big Picture
Every Borealis node is a React component paired with an exported metadata object. Nodes communicate via the global window.BorealisValueBus
(shared memory), and can specify configurable fields for the sidebar via a config
array.
A node must:
- Export an object with keys:
type
,label
,description
,content
, andcomponent
- Implement a React component that uses ReactFlow handles and the value bus
- (Optionally) add a
config
array for sidebar-driven configuration - (Optionally) provide Markdown documentation in
usage_documentation
2. 🏗️ General Node File Structure
import React, { useEffect, useRef, useState } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
import { IconButton } from "@mui/material";
import SettingsIcon from "@mui/icons-material/Settings";
// Node Component
const MyNode = ({ id, data }) => {
// ...state and logic...
return (
<div className="borealis-node">
<Handle type="target" position={Position.Left} className="borealis-handle" />
{/* ...UI... */}
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
// Export node metadata object
export default {
type: "MyNodeType",
label: "My Node",
description: "What this node does...",
content: "Short content summary...",
component: MyNode,
// Optional for Node Configuration Sidebar:
config: [ /* ... */ ],
usage_documentation: `...`
};
3. ⚙️ Node Configuration Sidebar (config
)
To enable sidebar configuration, add a config
array to your export object:
config: [
{ key: "value", label: "Value", type: "text" },
{
key: "operator",
label: "Operator",
type: "select",
options: ["Add", "Subtract", "Multiply", "Divide", "Average"]
}
]
Supported field types:
text
- simple inputselect
- dropdown (must specifyoptions
)- More can be added as needed
Each config field is automatically rendered in the sidebar when a node is selected.
4. 🧑🎨 Tips for Sidebar-Configurable Nodes
- Use
config
for all user-tunable parameters. - Keys in
config
must match data keys in your node'sdata
prop. - Sidebar will auto-update and write changes to
window.BorealisValueBus
and node state. - Add usage documentation in
usage_documentation
for help inside the editor.
5. 🧩 Copy-Paste Node Template
import React, { useEffect, useState, useRef } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
/**
* TemplateNode Component
*/
const TemplateNode = ({ id, data }) => {
const { setNodes } = useReactFlow();
const edges = useStore((state) => state.edges);
const [renderValue, setRenderValue] = useState(data?.value || "");
const valueRef = useRef(renderValue);
// Manual input handler (when no upstream)
const handleManualInput = (e) => {
const newValue = e.target.value;
valueRef.current = newValue;
setRenderValue(newValue);
window.BorealisValueBus[id] = newValue;
setNodes((nds) =>
nds.map((n) =>
n.id === id ? { ...n, data: { ...n.data, value: newValue } } : n
)
);
};
useEffect(() => {
let currentRate = window.BorealisUpdateRate;
let intervalId;
const runNodeLogic = () => {
const inputEdge = edges.find((e) => e.target === id);
const hasInput = Boolean(inputEdge && inputEdge.source);
if (hasInput) {
const upstreamValue = window.BorealisValueBus[inputEdge.source] || "";
if (upstreamValue !== valueRef.current) {
valueRef.current = upstreamValue;
setRenderValue(upstreamValue);
window.BorealisValueBus[id] = upstreamValue;
setNodes((nds) =>
nds.map((n) =>
n.id === id ? { ...n, data: { ...n.data, value: upstreamValue } } : n
)
);
}
} else {
window.BorealisValueBus[id] = valueRef.current;
}
};
intervalId = setInterval(runNodeLogic, currentRate);
const monitor = setInterval(() => {
const newRate = window.BorealisUpdateRate;
if (newRate !== currentRate) {
clearInterval(intervalId);
currentRate = newRate;
intervalId = setInterval(runNodeLogic, currentRate);
}
}, 250);
return () => {
clearInterval(intervalId);
clearInterval(monitor);
};
}, [id, edges, setNodes]);
const inputEdge = edges.find((e) => e.target === id);
const hasInput = Boolean(inputEdge && inputEdge.source);
return (
<div className="borealis-node">
<Handle type="target" position={Position.Left} className="borealis-handle" />
<div className="borealis-node-header">
{data?.label || "Node Template"}
</div>
<div className="borealis-node-content">
<div style={{ marginBottom: "8px", fontSize: "9px", color: "#ccc" }}>
{data?.content || "Template acting as a design scaffold for designing nodes for Borealis."}
</div>
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>
Template Location:
</label>
<input
type="text"
value={renderValue}
onChange={handleManualInput}
disabled={hasInput}
style={{
fontSize: "9px",
padding: "4px",
background: hasInput ? "#2a2a2a" : "#1e1e1e",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px",
width: "100%"
}}
/>
</div>
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
export default {
type: "Node_Template",
label: "Node Template",
description: `Node structure template to be used as a scaffold when building new nodes for Borealis.`,
content: "Template acting as a design scaffold for designing nodes for Borealis.",
component: TemplateNode,
config: [
{ key: "value", label: "Value", type: "text" }
],
usage_documentation: `
### Node Template
A reference implementation of a simple, single-value node for Borealis.
- Accepts manual or upstream input
- Configurable via sidebar
- Uses the value bus for live updates
`.trim()
};
Happy Node Building!
If you need help, check out the sample nodes and open a PR or issue with your questions.