Declare Node & Edge Types¶
Node and edge types are lightweight descriptors that define what data each kind of node/edge carries. A type can provide:
- a type name (
type) - a display label (
label) - node handles (
inputs/outputs) - handle connectivity controls (
input_connectable*/output_connectable*) - a schema for the
datapayload (schema)
Types are separate from editors. A type defines structure; an editor defines the UI used to edit it.

Complete runnable example¶
This script is a minimal, working example that produces the visualization shown above.
import param
import panel as pn
from panel_reactflow import EdgeType, NodeType, ReactFlow
pn.extension("jsoneditor")
class Job(param.Parameterized):
status = param.Selector(objects=["idle", "running", "done"])
retries = param.Integer(default=0)
decision_schema = {
"type": "object",
"properties": {
"question": {"type": "string", "title": "Question"},
"outcome": {
"type": "string",
"enum": ["yes", "no", "maybe"],
"title": "Outcome",
},
},
}
node_types = {
"job": NodeType(type="job", label="Job", schema=Job, inputs=["in"], outputs=["out"]),
"decision": NodeType(
type="decision",
label="Decision",
schema=decision_schema,
inputs=["in"],
outputs=["yes", "no"],
),
}
edge_types = {
"flow": EdgeType(
type="flow",
label="Flow",
schema={
"type": "object",
"properties": {"weight": {"type": "number", "title": "Weight"}},
},
),
}
nodes = [
{
"id": "j1",
"type": "job",
"label": "Fetch Data",
"position": {"x": 0, "y": 0},
"data": {"status": "idle", "retries": 0},
},
{
"id": "d1",
"type": "decision",
"label": "Valid?",
"position": {"x": 300, "y": 250},
"data": {"question": "Is data valid?", "outcome": "yes"},
},
{
"id": "j2",
"type": "job",
"label": "Process",
"position": {"x": 600, "y": 400},
"data": {"status": "running", "retries": 1},
},
]
edges = [
{"id": "e1", "source": "j1", "target": "d1", "type": "flow", "data": {"weight": 1.0}},
{"id": "e2", "source": "d1", "target": "j2", "type": "flow", "data": {"weight": 0.8}},
]
TASK_NODE_CSS = """
.react-flow__node-job {
background-color: white;
border-radius: 8px;
border: 1.5px solid #7c3aed;
}
.react-flow__node-decision {
background-color: white;
border-radius: 8px;
border: 1.5px solid green;
}
"""
flow = ReactFlow(
nodes=nodes,
edges=edges,
node_types=node_types,
edge_types=edge_types,
editor_mode="node",
sizing_mode="stretch_both",
stylesheets=[TASK_NODE_CSS]
)
pn.Column(flow, sizing_mode="stretch_both").servable()
How this code maps to the visualization¶
node_types["job"]andnode_types["decision"]define the two node kinds you see.inputsandoutputsdefine the left/right handles rendered on each node.edge_types["flow"]defines the edge payload schema used by both connections.nodescontrols labels (Fetch Data,Valid?,Process) and positions.editor_mode="side"makes selection open the schema-driven editor in the right panel.
Node type snippet¶
Use NodeType to define node handles and payload schema.
from panel_reactflow import NodeType
task_schema = {
"type": "object",
"properties": {
"status": {"type": "string", "enum": ["idle", "running", "done"]},
"priority": {"type": "integer"},
},
}
node_types = {
"task": NodeType(
type="task",
label="Task",
schema=task_schema,
inputs=["in"],
outputs=["out"],
),
}
Edge type snippet¶
Use EdgeType for edge payload schema and label.
from panel_reactflow import EdgeType
edge_types = {
"pipe": EdgeType(
type="pipe",
label="Pipe",
schema={
"type": "object",
"properties": {
"throughput": {"type": "number"},
"protocol": {"type": "string", "enum": ["tcp", "udp", "http"]},
},
},
),
}
Schema sources¶
The schema field accepts multiple inputs and normalizes them to JSON Schema.
| Source | Example |
|---|---|
| JSON Schema dict | {"type": "object", "properties": {...}} |
| Param class | A param.Parameterized subclass |
| Pydantic model | A pydantic.BaseModel subclass |
Param class shorthand¶
import param
from panel_reactflow import NodeType
class Job(param.Parameterized):
status = param.Selector(objects=["idle", "running", "done"])
retries = param.Integer(default=0)
node_types = {"job": NodeType(type="job", label="Job", schema=Job)}
Pydantic model shorthand¶
from pydantic import BaseModel
from panel_reactflow import NodeType
class Config(BaseModel):
host: str = "localhost"
port: int = 8080
node_types = {"config": NodeType(type="config", label="Config", schema=Config)}
Register on ReactFlow¶
Pass node_types and edge_types as dictionaries keyed by type name:
Types without a schema still work; they just do not get schema-driven validation or auto-generated forms.
Handle tooltips¶
By default, handles are plain connection points. You can add a tooltip (shown
on hover) by passing a dict with "id" and "label" and/or "type" instead
of a plain string:
from panel_reactflow import NodeType
node_types = {
"transform": NodeType(
type="transform",
label="Transform",
inputs=[{"id": "in", "label": "Data Input", "type": "DataFrame"}],
outputs=[
{"id": "success", "label": "Successful results", "type": "DataFrame"},
{"id": "error", "label": "Failed records", "type": "list"},
],
),
}
The tooltip combines both when present ("Data Input (DataFrame)"), or falls
back to whichever one is given. Plain strings and dicts can be mixed freely in
the same list:
Show a port's current value on click¶
Clicking a handle emits a "handle_clicked" event with the node id, handle
id, direction ("input"/"output") and a screen position. Clicking an edge
emits "edge_clicked" with the edge id and position. Use
ReactFlow.show_popup(content, position) in the handler to display whatever
you consider the "current value" for that port or connection:
def on_handle_clicked(payload, flow):
node_id, handle_id = payload["node_id"], payload["handle_id"]
value = live_values.get(node_id, {}).get(handle_id)
flow.show_popup(pn.pane.Markdown(f"**{handle_id}**: {value!r}"), payload["position"])
def on_edge_clicked(payload, flow):
source_id, source_port = edge_sources[payload["edge_id"]]
value = live_values.get(source_id, {}).get(source_port)
flow.show_popup(pn.pane.Markdown(f"**Value:** {value!r}"), payload["position"])
flow.on("handle_clicked", on_handle_clicked)
flow.on("edge_clicked", on_edge_clicked)
For popup_trigger="hover", register handle_hovered and edge_hovered to
show the popup and their corresponding handle_unhovered and edge_unhovered
events to call flow.close_popup(). Match the unhovered target against the
currently displayed one so a delayed leave event does not dismiss a newer
popup. See the runnable example below for both pairs of callbacks.
The default popup_hover_delay is 500 ms. After opening, an unhover event is
emitted when the pointer moves more than 10% of the shorter canvas dimension
from the anchor (at least 48 pixels) and is not over the popup. Click-opened
popups dismiss on outside click; either mode can dismiss programmatically via
flow.close_popup(). Use
popup_trigger="none" to disable the built-in inspection events.
panel-reactflow only provides the interaction events and overlay; looking up
"the current value" for a port is application-specific.
For a complete runnable graph with typed port hover tooltips and popups for both handles and edges, run:
Control handle connectivity¶
By default, all handles (inputs and outputs) are fully connectable — users can
drag edges from or to any handle. Use the *_connectable* flags to restrict
which connections are allowed.
Common patterns¶
Data source (output only)¶
A node that produces data but cannot accept incoming connections to its output:
from panel_reactflow import NodeType
source_type = NodeType(
type="data_source",
label="Data Source",
outputs=["data"],
output_connectable_start=True, # Can drag FROM output
output_connectable_end=False, # Cannot drag TO output
)
Data sink (input only)¶
A node that consumes data but cannot produce outgoing connections from its input:
sink_type = NodeType(
type="data_sink",
label="Data Sink",
inputs=["data"],
input_connectable_start=False, # Cannot drag FROM input
input_connectable_end=True, # Can drag TO input
)
Monitor node¶
A node that accepts input but whose output is status-only (one direction):
monitor_type = NodeType(
type="monitor",
label="Monitor",
inputs=["in"],
outputs=["status"],
input_connectable_start=False, # Cannot start edges from input
output_connectable_end=False, # Cannot end edges at output
)
All connectivity flags¶
| Flag | Default | Controls |
|---|---|---|
input_connectable |
True |
Whether input handles are connectable at all |
input_connectable_start |
True |
Whether edges can start from input handles |
input_connectable_end |
True |
Whether edges can end at input handles |
output_connectable |
True |
Whether output handles are connectable at all |
output_connectable_start |
True |
Whether edges can start from output handles |
output_connectable_end |
True |
Whether edges can end at output handles |
Complete example¶
import panel as pn
from panel_reactflow import NodeType, NodeSpec, EdgeSpec, ReactFlow
pn.extension("jsoneditor")
# Define node types with different connectivity patterns
node_types = {
"source": NodeType(
type="source",
label="Data Source",
outputs=["data"],
output_connectable_start=True,
output_connectable_end=False,
),
"transform": NodeType(
type="transform",
label="Transform",
inputs=["in"],
outputs=["out"],
# All connectable flags default to True
),
"sink": NodeType(
type="sink",
label="Data Sink",
inputs=["data"],
input_connectable_start=False,
input_connectable_end=True,
),
}
# Create a data pipeline
flow = ReactFlow(
nodes=[
NodeSpec(id="src", type="source", position={"x": 0, "y": 100}, data={}).to_dict(),
NodeSpec(id="tx", type="transform", position={"x": 250, "y": 100}, data={}).to_dict(),
NodeSpec(id="snk", type="sink", position={"x": 500, "y": 100}, data={}).to_dict(),
],
edges=[
EdgeSpec(id="e1", source="src", target="tx").to_dict(),
EdgeSpec(id="e2", source="tx", target="snk").to_dict(),
],
node_types=node_types,
sizing_mode="stretch_both",
)
flow.servable()
In this example:
- Users can drag from the source output to the transform input ✓
- Users cannot drag to the source output ✗
- Users can drag from the transform output to the sink input ✓
- Users cannot drag from the sink input ✗
The UI prevents invalid connections automatically — non-connectable handles show different cursor behavior and won't accept drag operations.