| name | retejs |
| description | This skill should be used when the user asks to "create a node editor", "build a visual workflow", "create a node-based interface", "add rete.js", "build a node graph", "create a dataflow editor", "build a visual programming interface", "connect nodes", or needs to implement visual node editors, workflow builders, or dataflow/control-flow processing using Rete.js v2 with Lit or vanilla JavaScript. |
| version | 0.1.0 |
Rete.js v2 Visual Editor Skill
Create professional node-based editors and visual programming interfaces using Rete.js v2. Supports custom nodes, connections, dataflow/control-flow processing, and comprehensive E2E testing.
Version: Rete.js 2.x | Documentation: https://retejs.org/docs
Core Concepts
Framework-Agnostic Architecture
Rete.js is NOT bound to any UI framework. For lightweight implementations without React/Vue/Angular:
- Use Lit (
@retejs/lit-plugin) - Web Components based, minimal overhead
- Use vanilla JS with manual DOM rendering for maximum control
Plugin System
Rete.js uses scope-based plugin architecture where signals propagate from parent to child:
import { Scope } from 'rete';
const parentScope = new Scope<number>('parent');
const childScope = new Scope<string, [number]>('child');
parentScope.addPipe((context) => {
console.log('parent', context);
return context;
});
parentScope.use(childScope);
await parentScope.emit(1);
Installation
NPM (Recommended for Lit)
npm i rete rete-area-plugin rete-connection-plugin @retejs/lit-plugin rete-render-utils
CDN (Vanilla JS)
<script src="https://cdn.jsdelivr.net/npm/rete/rete.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rete-area-plugin/rete-area-plugin.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rete-connection-plugin/rete-connection-plugin.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rete-render-utils/rete-render-utils.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@retejs/lit-plugin/retejs-lit-plugin.min.js"></script>
CDN namespaces: Rete, ReteAreaPlugin, ReteConnectionPlugin, ReteLitPlugin
Core Pattern (Lit-based)
import { NodeEditor, GetSchemes, ClassicPreset } from "rete";
import { AreaPlugin, AreaExtensions } from "rete-area-plugin";
import { ConnectionPlugin, Presets as ConnectionPresets } from "rete-connection-plugin";
import { LitPlugin, Presets } from "@retejs/lit-plugin";
type Schemes = GetSchemes<
ClassicPreset.Node,
ClassicPreset.Connection<ClassicPreset.Node, ClassicPreset.Node>
>;
type AreaExtra = LitArea2D<Schemes>;
const editor = new NodeEditor<Schemes>();
const area = new AreaPlugin<Schemes, AreaExtra>(container);
const connection = new ConnectionPlugin<Schemes, AreaExtra>();
const render = new LitPlugin<Schemes, AreaExtra>();
render.addPreset(Presets.classic.setup());
connection.addPreset(ConnectionPresets.classic.setup());
editor.use(area);
area.use(connection);
area.use(render);
const socket = new ClassicPreset.Socket("socket");
const nodeA = new ClassicPreset.Node("Node A");
nodeA.addOutput("out", new ClassicPreset.Output(socket, "Output"));
await editor.addNode(nodeA);
await area.translate(nodeA.id, { x: 100, y: 100 });
AreaExtensions.zoomAt(area, editor.getNodes());
Custom Node Classes
Extend ClassicPreset.Node to create custom nodes. Define width and height for layout compatibility. Add inputs, outputs, and controls in the constructor. Implement a data() method for dataflow engine integration.
For type-safe connections, extend ClassicPreset.Socket with isCompatibleWith() to restrict which sockets can connect.
See references/editor-examples.md for complete examples including NumberNode, AddNode, custom socket types, dynamic inputs, and dropdown controls.
Processing Engines
Dataflow Engine (Data-Driven)
Processes graphs where data flows from outputs to inputs:
import { DataflowEngine } from "rete-engine";
const engine = new DataflowEngine<Schemes>();
editor.use(engine);
const result = await engine.fetch(resultNode.id);
console.log(result);
engine.reset();
Control Flow Engine (Execution-Driven)
Processes graphs where execution flows through explicit control connections:
import { ControlFlowEngine } from "rete-engine";
class StartNode extends ClassicPreset.Node {
constructor() {
super("Start");
this.addOutput("exec", new ClassicPreset.Output(execSocket, "Execute"));
}
execute(_: never, forward: (output: "exec") => void) {
forward("exec");
}
}
class LogNode extends ClassicPreset.Node {
constructor(message: string) {
super("Log");
this.addInput("exec", new ClassicPreset.Input(execSocket, "Execute"));
this.addOutput("exec", new ClassicPreset.Output(execSocket, "Execute"));
this.addControl("message", new ClassicPreset.InputControl("text", { initial: message }));
}
execute(_: "exec", forward: (output: "exec") => void) {
const msg = (this.controls["message"] as ClassicPreset.InputControl<"text">).value;
console.log(msg);
forward("exec");
}
}
const engine = new ControlFlowEngine<Schemes>();
editor.use(engine);
engine.execute(startNode.id);
Hybrid Engine (Combined)
For nodes that need both data and execution flow:
const dataflow = new DataflowEngine<Schemes>(({ inputs, outputs }) => ({
inputs: () => Object.keys(inputs).filter(name => name !== "exec"),
outputs: () => Object.keys(outputs).filter(name => name !== "exec")
}));
const controlflow = new ControlFlowEngine<Schemes>(() => ({
inputs: () => ["exec"],
outputs: () => ["exec"]
}));
editor.use(dataflow);
editor.use(controlflow);
Connection Validation
import { getConnectionSockets } from "rete-render-utils";
function canCreateConnection(editor: NodeEditor<Schemes>, connection: Schemes["Connection"]) {
const { source, target } = getConnectionSockets(editor, connection);
return source && target && source.isCompatibleWith(target);
}
editor.addPipe((context) => {
if (context.type === "connectioncreate") {
if (!canCreateConnection(editor, context.data)) {
console.warn("Incompatible sockets");
return;
}
}
return context;
});
Import/Export (Serialization)
Serialize graphs by iterating editor.getNodes() and editor.getConnections(), capturing node positions from area.nodeViews. Deserialize by clearing the editor, recreating nodes via a type-keyed factory, preserving original IDs, and restoring connections.
See references/editor-examples.md for complete export/import patterns with SerializedGraph interfaces.
Essential Plugins
Auto Arrange
npm i rete-auto-arrange-plugin elkjs web-worker
import { AutoArrangePlugin, Presets as ArrangePresets } from "rete-auto-arrange-plugin";
const arrange = new AutoArrangePlugin<Schemes>();
arrange.addPreset(ArrangePresets.classic.setup());
area.use(arrange);
await arrange.layout();
Context Menu
npm i rete-context-menu-plugin
import { ContextMenuPlugin, Presets as ContextMenuPresets } from "rete-context-menu-plugin";
const contextMenu = new ContextMenuPlugin<Schemes>({
items: ContextMenuPresets.classic.setup([
["Number", () => new NumberNode()],
["Add", () => new AddNode()],
["Math", [
["Multiply", () => new MultiplyNode()],
["Divide", () => new DivideNode()]
]]
])
});
area.use(contextMenu);
render.addPreset(Presets.contextMenu.setup());
History (Undo/Redo)
npm i rete-history-plugin
import { HistoryPlugin, HistoryActions, HistoryExtensions, Presets as HistoryPresets } from "rete-history-plugin";
const history = new HistoryPlugin<Schemes, HistoryActions<Schemes>>();
history.addPreset(HistoryPresets.classic.setup());
area.use(history);
HistoryExtensions.keyboard(history);
await history.undo();
await history.redo();
Minimap
npm i rete-minimap-plugin
import { MinimapPlugin } from "rete-minimap-plugin";
const minimap = new MinimapPlugin<Schemes>();
area.use(minimap);
render.addPreset(Presets.minimap.setup({ size: 200 }));
Readonly Mode
npm i rete-readonly-plugin
import { ReadonlyPlugin } from "rete-readonly-plugin";
const readonly = new ReadonlyPlugin<Schemes>();
editor.use(readonly.root);
editor.use(area);
area.use(readonly.area);
readonly.enable();
readonly.disable();
Selection System
Selectable Nodes
const selector = AreaExtensions.selector();
const accumulating = AreaExtensions.accumulateOnCtrl();
AreaExtensions.selectableNodes(area, selector, { accumulating });
AreaExtensions.simpleNodesOrder(area);
selectableNodes.select(nodeId);
selectableNodes.select(nodeId, true);
selectableNodes.unselect(nodeId);
E2E Testing with Playwright
Test Rete.js editors using Playwright with data-testid attributes on nodes, sockets, and connections. Add test IDs via render.addPreset(Presets.classic.setup({ customize: { ... } })). Use the rete-qa package for regression testing across browsers.
See references/testing-guide.md for complete setup, test utilities, Page Object Models, and test suites covering node creation, connections, controls, serialization, and CI/CD integration.
Graph Operations (rete-structures)
npm i rete-structures
import { structures } from 'rete-structures';
const graph = structures(editor);
graph.roots();
graph.leaves();
graph.incomers(nodeId);
graph.outgoers(nodeId);
graph.predecessors(nodeId);
graph.successors(nodeId);
graph.filter(nodePredicate, connectionPredicate);
graph.union(otherGraph);
graph.difference(otherGraph);
graph.intersection(otherGraph);
Best Practices
Node Design
- Fixed dimensions - Define
width and height on custom nodes for minimap/arrange
- Descriptive labels - Use clear node names that explain purpose
- Typed sockets - Use custom socket classes for type safety
- Control validation - Validate control input values
Performance
- Limit visible nodes - Use virtualization for large graphs
- Debounce processing - Don't recompute on every change
- Cache engine results - Call
engine.reset() only when necessary
Testing
- Add data-testid attributes - Enable reliable E2E selectors
- Test serialization - Verify import/export roundtrips
- Test edge cases - Cycles, disconnected nodes, invalid connections
Additional Resources
Reference Files
For detailed patterns and complete code examples, consult:
references/editor-examples.md - Complete node implementations, custom sockets, serialization, plugin setup, and graph utilities
references/testing-guide.md - Playwright setup, Page Object Models, test suites, visual regression, and CI/CD integration
External Documentation