-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Panel Pipelines: Visual Data Flow with Parameterized Nodes
A next-generation replacement for panel.pipeline.Pipeline, built on ReactFlow.
Why Replace Panel Pipeline?
Panel Pipeline
was a pioneering attempt at visual workflows, but its limitations are well-known:
linear-first design, no interactive canvas (no drag/zoom/pan), navigation-centric
rather than data-flow-centric, no embedded previews inside nodes, and
long-standing unresolved bugs.
Tools like Daggr,
ComfyUI,
LangFlow, and
n8n have proven that visual node editors
are the natural interface for composable workflows. Panel deserves the same.
Core Concept: A Parameterized Class IS the Node
A pipeline stage is any param.Parameterized subclass. No special base class needed.
| Parameterized Concept | Pipeline Role |
|---|---|
param.Parameter declarations |
Inputs (data the node consumes) |
@param.output decorated methods |
Outputs (data the node produces) |
@param.depends view methods |
Display (live preview inside the node) |
| Parameter types & constraints | Auto-wiring and auto-input widgets |
import param
class TransformNode(param.Parameterized):
# Inputs
text = param.String(default="hello world")
mode = param.Selector(default="upper", objects=["upper", "lower", "title", "swapcase"])
# Output: Should we continue to use param.output. Or just param.depends methods?
@param.output(param.String)
@param.depends("text", "mode")
def result(self):
if not self.text:
return ""
return getattr(self.text, self.mode)()
# Display (view method — rendered inside the node)
# Shold param.depends methods be recognized as outputs?
@param.depends("text", "mode")
def preview(self):
result = getattr(self.text, self.mode)()
return pn.pane.Markdown(f"**{result}**")The Pipeline class introspects this and automatically:
- Creates auto-input widget nodes for
textandmode(unconnected params) - Wires the
resultoutput to any downstream stage with a matchingresultparameter - Renders the
preview()view method inside the node body - Sets up reactive updates so changes propagate through the graph
Pipeline API
from panel_reactflow import Pipeline
Pipeline(
stages=[("Name", ClassOrInstance), ...],
graph=None, # None = auto-infer edges; dict = explicit topology
layout_spacing=(350, 150), # (horizontal, vertical) pixels between nodes
auto_inputs=True, # auto-generate widget nodes for unconnected params
kwargs={}, # extra kwargs forwarded to ReactFlow
).servable()Question: Should this be a Pipeline class or just a def create_pipeline(...)->ReactFlow function?
Parameters
stages— List of(name, class_or_instance)tuples. Classes are instantiated automatically.graph— Explicit topology:{source_name: target_name | (t1, t2, ...)}. WhenNone, edges are inferred by matching@param.outputnames to downstream parameter names.layout_spacing—(horizontal, vertical)spacing in pixels.auto_inputs— WhenTrue, unconnected parameters get auto-generated widget nodes with an "INPUT" pill badge.kwargs— Extra keyword arguments forwarded toReactFlow(e.g.,min_height,show_minimap).
View resolution
Pipeline resolves what to display inside each stage node:
- View methods (preferred) — Public
@param.dependsmethods that are not@param.outputmethods. If a stage haspreview()orview()decorated with@param.depends, it's rendered in the node. - Output fallback — If no view methods exist,
@param.outputmethods are rendered. A single output is shown directly; multiple outputs are displayed in apn.Accordionwith named sections (all expanded).
Node styling
Pipeline nodes are visually distinguished:
- Auto-input nodes — Indigo border + gradient + "INPUT" pill badge (CSS class
rf-auto-input) - Stage nodes — Emerald border + gradient + "OUTPUT" pill badge (CSS class
rf-stage)
Question: What would the right styling here be?
Examples
1. Hello World — Single Stage
tmp/hello_world.py — The simplest pipeline: one stage, auto-input widgets.
import panel as pn
import param
from panel_reactflow import Pipeline
pn.extension("jsoneditor")
class StrTransformNode(param.Parameterized):
text = param.String("Hello World")
mode = param.Selector(default="upper", objects=["upper", "lower", "title", "swapcase"])
@param.output(param.String)
@param.depends("text", "mode")
def result(self):
if not self.text:
return ""
return getattr(self.text, self.mode)()
Pipeline(stages=[("Transform", StrTransformNode)]).servable()text and mode have no upstream connection, so Pipeline auto-generates input widget nodes for them. The result() output is rendered as the node's fallback view.
2. Text Pipeline — Two Stages with View Methods
tmp/text_pipeline.py — Auto-inferred edges, view methods in both stages.
import panel as pn
import param
from panel_reactflow import Pipeline
pn.extension("jsoneditor")
class TransformNode(param.Parameterized):
text = param.String(default="hello world")
mode = param.Selector(default="upper", objects=["upper", "lower", "title", "swapcase"])
@param.output(param.String)
@param.depends("text", "mode")
def result(self):
if not self.text:
return ""
return getattr(self.text, self.mode)()
@param.depends("text", "mode")
def preview(self):
if not self.text:
return pn.pane.Markdown("*No input yet*")
result = getattr(self.text, self.mode)()
return pn.pane.Markdown(f"**{result}**")
class DisplayNode(param.Parameterized):
result = param.String()
@param.depends("result")
def view(self):
return pn.pane.Alert(self.result or "Waiting...", alert_type="success")
Pipeline(
stages=[("Transform", TransformNode), ("Display", DisplayNode)],
).servable()Pipeline auto-infers the edge Transform.result -> Display.result by name matching. Transform shows preview(), Display shows view(). Auto-input widgets are created for text and mode.
3. Data Explorer — DataFrames with Dynamic Selectors
tmp/data_explorer.py — DataFrames flowing between nodes, dynamic column selectors, hvPlot chart.
import hvplot.pandas # noqa: F401
import numpy as np
import pandas as pd
import panel as pn
import param
from panel_reactflow import Pipeline
pn.extension("jsoneditor")
DATASETS = {
"iris": pd.DataFrame({
"sepal_length": np.random.normal(5.8, 0.8, 150),
"sepal_width": np.random.normal(3.0, 0.4, 150),
"petal_length": np.random.normal(3.7, 1.8, 150),
"petal_width": np.random.normal(1.2, 0.8, 150),
}),
"random": pd.DataFrame({
"x": np.random.randn(200),
"y": np.random.randn(200),
"size": np.random.uniform(1, 10, 200),
"value": np.random.uniform(0, 100, 200),
}),
}
class DataLoaderNode(param.Parameterized):
dataset = param.Selector(default="iris", objects=list(DATASETS.keys()))
@param.output(param.DataFrame)
@param.depends("dataset")
def data(self):
return DATASETS[self.dataset]
@param.depends("dataset")
def table(self):
return pn.pane.DataFrame(DATASETS[self.dataset], max_height=300)
class ChartNode(param.Parameterized):
data = param.DataFrame()
x_col = param.Selector(default="", objects=[""])
y_col = param.Selector(default="", objects=[""])
def __init__(self, **params):
super().__init__(**params)
self._update_col_options()
@param.depends("data", watch=True)
def _update_col_options(self):
if self.data is not None and len(self.data.columns):
cols = list(self.data.columns)
self.param.x_col.objects = cols
self.param.y_col.objects = cols
if self.x_col not in cols:
self.x_col = cols[0]
if self.y_col not in cols:
self.y_col = cols[1] if len(cols) > 1 else cols[0]
else:
self.param.x_col.objects = [""]
self.param.y_col.objects = [""]
self.x_col = ""
self.y_col = ""
@param.output()
@param.depends("data", "x_col", "y_col")
def plot(self):
if self.data is None or not self.x_col or not self.y_col:
return pn.pane.Markdown("*Waiting for data...*")
return self.data.hvplot.scatter(x=self.x_col, y=self.y_col, height=500, width=500)
Pipeline(
stages=[("Data", DataLoaderNode), ("Chart", ChartNode)],
).servable()DataLoaderNode.data auto-connects to ChartNode.data. ChartNode dynamically updates its x_col/y_col selector options when data arrives. table() is DataLoader's view method; plot() is Chart's output fallback.
4. Stock Analysis DAG — Diamond Topology
tmp/stock_dag.py — Fan-out and fan-in with explicit graph.
import hvplot.pandas # noqa: F401
import numpy as np
import pandas as pd
import panel as pn
import param
from panel_reactflow import Pipeline
pn.extension("jsoneditor")
class StockData(param.Parameterized):
symbol = param.String(default="AAPL")
days = param.Integer(default=252, bounds=(30, 1000))
@param.output(param.DataFrame)
@param.depends("symbol", "days")
def prices(self):
np.random.seed(hash(self.symbol) % 2**32)
dates = pd.date_range(end=pd.Timestamp.now(), periods=self.days)
price = 100 + np.cumsum(np.random.randn(self.days) * 1.5)
return pd.DataFrame({"date": dates, "price": price}).set_index("date")
class MANode(param.Parameterized):
prices = param.DataFrame()
window = param.Integer(default=20, bounds=(5, 100))
@param.output(param.DataFrame)
@param.depends("prices", "window")
def ma_data(self):
if self.prices is None:
return None
df = self.prices.copy()
df["ma"] = df["price"].rolling(self.window).mean()
return df
class RSINode(param.Parameterized):
prices = param.DataFrame()
period = param.Integer(default=14, bounds=(2, 50))
@param.output(param.DataFrame)
@param.depends("prices", "period")
def rsi_data(self):
if self.prices is None:
return None
delta = self.prices["price"].diff()
gain = delta.clip(lower=0).rolling(self.period).mean()
loss = (-delta.clip(upper=0)).rolling(self.period).mean()
rs = gain / loss
df = self.prices.copy()
df["rsi"] = 100 - (100 / (1 + rs))
return df
class ChartNode(param.Parameterized):
ma_data = param.DataFrame()
rsi_data = param.DataFrame()
@param.output()
@param.depends("ma_data", "rsi_data")
def plot(self):
if self.ma_data is None or self.rsi_data is None:
return pn.pane.Markdown("*Waiting for data from both branches...*")
price_plot = self.ma_data.hvplot.line(
y=["price", "ma"], ylabel="Price", title="Price & Moving Average",
legend="top_left", height=200,
)
rsi_plot = self.rsi_data.hvplot.line(
y="rsi", ylabel="RSI", title="RSI", color="orange", height=150,
)
return pn.Column(price_plot, rsi_plot, sizing_mode="stretch_width")
Pipeline(
stages=[
("Stock Data", StockData),
("MA", MANode),
("RSI", RSINode),
("Chart", ChartNode),
],
graph={"Stock Data": ("MA", "RSI"), "MA": "Chart", "RSI": "Chart"},
kwargs={"min_height": 600},
).servable()The explicit graph defines the diamond: Stock Data fans out to MA and RSI, which fan in to Chart. Auto-inputs are created for symbol, days, window, and period.
5. Multi-Output — One Method, Three Outputs
tmp/multi_output.py — A single @param.output method producing three named outputs, each flowing to its own downstream node.
import panel as pn
import param
from panel_reactflow import Pipeline
pn.extension("jsoneditor")
class Source(param.Parameterized):
text = param.String(default="Hello World")
@param.output(upper=param.String(), lower=param.String(), length=param.Integer())
@param.depends("text")
def split(self):
return self.text.upper(), self.text.lower(), len(self.text)
class UpperDisplay(param.Parameterized):
upper = param.String()
@param.depends("upper")
def view(self):
return pn.pane.Markdown(f"**{self.upper}**")
class LowerDisplay(param.Parameterized):
lower = param.String()
@param.depends("lower")
def view(self):
return pn.pane.Markdown(f"*{self.lower}*")
class LengthDisplay(param.Parameterized):
length = param.Integer()
@param.depends("length")
def view(self):
return pn.pane.Alert(f"Length: {self.length}", alert_type="info")
Pipeline(
stages=[
("Source", Source),
("Upper", UpperDisplay),
("Lower", LowerDisplay),
("Length", LengthDisplay),
],
).servable()Source.split() returns a tuple of 3 values. Pipeline auto-infers edges: upper -> Upper.upper, lower -> Lower.lower, length -> Length.length. The Source node displays all 3 outputs in an Accordion (expanded). Each downstream node shows its view method.
Implemented Features
-
Pipelineclass (src/panel_reactflow/pipeline.py) - Auto-inferred edges by matching
@param.outputnames to downstream parameter names - Explicit
graphdict for non-linear topologies (fan-out, fan-in, diamond) - Auto-input widget nodes for unconnected parameters
- Reactive wiring via
param.watch(upstream output changes propagate downstream) - View method resolution (public
@param.dependsmethods rendered in nodes) - Output fallback rendering (single output direct, multi-output in Accordion)
- Multi-output support (tuple-indexed extraction from
@param.output(a=..., b=..., c=...)) - Topological layout algorithm (BFS depth assignment, vertical stacking for fan-out)
- Configurable layout spacing
- Visual pill badges: "INPUT" (indigo) on auto-input nodes, "OUTPUT" (emerald) on stage nodes
- Forward
kwargsto ReactFlow (min_height, show_minimap, etc.) - Accepts both classes and instances as stages
- Unit tests (
tests/test_pipeline.py— 34 tests)
Open Issues
- Node is dragged instead of slider #27
- Make it possible to style the label #26
- EdgeSpec missing sourceHandle / targetHandle #25
- No way to hide port #24
- Add Auto Layout #23
- QuickStart example lacks polish #22
- WARNING:param.main: pn.extension was initialized but 'jsoneditor' extension was not loaded #21
- Please provide helpful docstrings #20
- Autoserialize edges and nodes #19
- Make how-to guides readable #18
- Select opens in another place #17
- NodeSpec does not have view parameter #16
- No way to make plot not overflow #15
- Plot looks stretched #14
- Cannot use arbitrary python objects as view #13
- Viewer: AttributeError: 'MyViewer' object has no attribute '_models' #12
Requirements
Customization
- Custom input widgets — Developers must be able to customize auto-input widgets in the same way
pn.Paramallows (e.g., specifying widget types, formatting, bounds overrides per parameter). - Custom output views — Developers must be able to customize how individual outputs are rendered in stage nodes, by providing panes, custom functions, or
Viewersubclasses (analogous topn.Param'swidgetsdict). - Customizable node styling — The default input/output pill badges and colors should look polished out of the box, but developers must be able to override them (custom CSS classes, colors, or disable badges entirely).
Execution control
- Output caching — Expensive output computations should be cacheable. The recommended pattern is
@pn.cacheon the output method; this should be documented with examples. - Manual vs. automatic execution — Developers must be able to choose between automatic reactive updates (current default: outputs recompute on any input change) and manual trigger mode (outputs recompute only on button click).
- Startup computation — Developers must be able to control whether outputs are computed on initialization (similar to
on_init=Trueinparam.depends). - Background execution — Long-running output computations should run in the background (e.g., via
pn.state.executeor threading) to keep the UI responsive. When possible, independent branches should compute in parallel. - Generators: Please also support output generators. I.e. sync and async functions that yield one element at the time. There is no reason to wait if one element is done - but the 9 others are not.
Visual feedback
- Stale/invalidated state — When an input changes but the downstream output has not yet recomputed, the affected nodes should be visually marked as stale (e.g., dimmed border, "stale" badge).
- Computing indicator — While an output is being recomputed, the node should show a spinner or loading overlay so the user knows work is in progress.
Documentation and examples
- Convert legacy Panel Pipeline examples — All examples from the Panel Pipeline How-To guides should be ported to this API and tested.
- Convert Gradio Daggr examples — All examples from Daggr should be ported to this API and tested.
- Convert langgraph examples: All examples from https://docs.langflow.org/ tutorials.
- ML and GenAI examples — Create showcase examples for machine learning workflows (training pipelines, inference chains) and generative AI (LLM chains, RAG pipelines).
- Function-to-node guide — Document how to wrap an existing function as a Pipeline stage with minimal boilerplate (input params from function signature, output from return value).
- Testing guide — Document how to unit-test individual stages in isolation and how to integration-test a full pipeline.
API design
- Helper node classes — Evaluate whether to provide ready-made base classes like
FnNode(wraps a plain function),PanelNode(wraps a Panel viewable), andInferenceNode(wraps an ML model), similar to Daggr's node types. - LLM-friendly error messages — All Pipeline errors (missing outputs, unresolved edges, type mismatches) should produce clear, actionable messages that an LLM coding assistant can interpret and fix without ambiguity.
- Resolve open issues above — Content-aware layout, Viewer resolution, node overflow, and type-based wiring should all be addressed.
Questions
- Should we display all input parameters in one node instead of individual nodes. Langgraph https://docs.langflow.org/ does the first, hugging face does the latter.
References
Panel Pipeline
param.output
Prior Art
- Daggr — Visual DAG builder for Gradio apps
- ComfyUI — Node-based Stable Diffusion workflow editor
- LangFlow — Visual LLM agent workflow builder
- Flowise — Drag-and-drop LLM flow builder
- n8n — Workflow automation platform