Interactive graph visualization for MTHDS method pipelines. Renders execution graphs from GraphSpec data — the canonical format produced by Pipelex when tracing pipeline execution.
Core graph logic (builders, layout, analysis) is pure TypeScript with no React dependency. The React layer provides a drop-in GraphViewer component powered by ReactFlow.
npm install @pipelex/mthds-uiReleased versions are listed on the npm page and the GitHub releases page.
If you render the React components, one setup step follows the install: the form controls that RunPanel and the graph's detail panel render need a stylesheet that your app loads itself. See Styling the form controls.
| Dependency | Required | Used by |
|---|---|---|
react, react-dom |
no | React layer (graph/react, form/react, timeline/react) |
shiki |
no | Syntax highlighting (shiki) |
| Dependency | License | Used by |
|---|---|---|
elkjs |
EPL-2.0 | Graph layout engine |
@xyflow/react |
MIT | Graph rendering (graph/react) |
@pipelex/mthds-form |
MIT | Run form panel + result view (form/react, graph/react) |
mthds |
MIT | The problem document's type on a failed node, types only |
elkjs (Eclipse Layout Kernel) is licensed under the Eclipse Public License 2.0. See NOTICE for details.
import { GraphViewer } from "@pipelex/mthds-ui/graph/react";
function MethodGraph({ graphspec }) {
return <GraphViewer graph={{ graphSpec: graphspec }} />;
}That's it. GraphViewer handles layout, styling, CSS variables, and all ReactFlow internals. All props except graph are optional with sensible defaults. The one exception to "styling" is the detail panel's data view, which shows a result through the form kernel's controls; those are styled by your app, as Styling the form controls describes.
ReactFlow accesses browser globals at module-evaluation time, so it cannot be server-side rendered. Use next/dynamic with ssr: false:
"use client";
import dynamic from "next/dynamic";
import type { GraphViewerProps } from "@pipelex/mthds-ui/graph/react";
import React from "react";
const GraphViewer = dynamic(
() =>
import("@pipelex/mthds-ui/graph/react").then((mod) => ({
default: mod.GraphViewer,
})),
{ ssr: false },
) as React.ComponentType<GraphViewerProps>;
export function MyGraph({ graphspec }) {
return <GraphViewer graph={{ graphSpec: graphspec }} />;
}graph carries the spec together with the artifacts that describe its pipes, and the detail panel uses them to lay a data node's value out. They must all come from one source, because the panel looks each value's descriptor up by the pipe_ref of the pipe that produced it in this graph: a run's own results, or a method's validate report beside that method's dry or static graph. A run's results carry all four, and graphArtifactsFrom reads them in one call:
import { GraphViewer, graphArtifactsFrom } from "@pipelex/mthds-ui/graph/react";
// results: { graph_spec, pipe_io_contracts, output_form, input_form }
const graph = graphArtifactsFrom(results);
return <GraphViewer graph={graph} />;Without pipeIoContracts and outputForm the panel shows each concept's structure table and no data tab. See docs/stuff-result-panel.md.
A failed node carries the MTHDS Protocol's problem document in its error (the Problem type of mthds/errors). Its detail panel shows the problem's title, code and detail, the pipe it failed in with the path down to it, and its next step, reading only the standard's core members, so a problem from any conforming runtime renders. The graph rings the pipe the run failed in: the node whose own problem's location names its pipe, not the controllers that failed because of it. See docs/run-problem.md.
GraphViewer never throws a spec it cannot draw out of render. A spec validateGraphSpec refuses, from a runtime newer or older than this package for instance, is drawn as an alert in place of the graph, naming the location refused and what was found there, and so is a spec the build or its first layout fails on. Your page stays up, and the next spec you pass is drawn afresh. A layout that fails after the reader changes the direction, the folds or the style is drawn as an alert over the dimmed graph instead, with the toolbar kept so they can change the view back. See docs/error-state.md.
| Prop | Type | Default | Description |
|---|---|---|---|
graph |
GraphArtifacts | null |
— | The spec and its descriptors, one source |
config |
GraphConfig |
DEFAULT_GRAPH_CONFIG |
Layout and visual configuration |
direction |
GraphDirection |
"LR" |
Layout direction: TB, LR, RL, BT |
showControllers |
boolean |
false |
Show controller group outlines |
onNavigateToPipe |
(pipeCode: string, status?: PipeStatus) => void |
— | Callback when a pipe node is clicked |
onReactFlowInit |
(instance: AppRFInstance) => void |
— | Access the underlying ReactFlow instance |
graphStyle |
GraphStyleId |
"detailed" |
The style the graph is drawn in: detailed or simple |
styleMenu |
boolean | GraphStyleId[] |
— | Offer a style menu in the toolbar (opt-in) |
onGraphStyleChange |
(style: GraphStyleId) => void |
— | Called when the style changes, so the host can keep the choice |
interactive |
boolean |
true |
false draws a still picture sized to its content |
The simple style draws the method as a flowchart in plain words: steps titled by their descriptions, only the method's inputs and final output as data, decisions with their outcomes, and loops as "for each". See docs/graph-styles.md.
The graph container uses position: absolute; inset: 0 to fill its parent. Make sure the parent element has position: relative and a defined height (e.g. h-full, flex-1, or an explicit height).
The still presentation (interactive={false}) is the exception: its container flows in its parent and takes the height the drawing needs, so the parent gives it a width and no height. See docs/still-presentation.md.
GraphSpec is the data format that describes a pipeline execution graph. It's generated by Pipelex when running or validating MTHDS method bundles.
# From a .mthds bundle file
pipelex-agent validate bundle my-method.mthds --view
# The output JSON contains a graphspec fieldOr programmatically via the Pipelex Python SDK:
from pipelex import Pipelex
px = Pipelex()
result = px.validate_bundle("my-method.mthds", view=True)
graphspec = result.graphspec # dict ready for JSON serializationFor authored-structure previews that should not run Pipelex, use the pure TypeScript static builder:
import { buildStaticGraphSpecFromToml } from "@pipelex/mthds-ui/static-graph";
const { spec, diagnostics } = buildStaticGraphSpecFromToml(tomlText);The returned spec has meta: { format: "mthds", mode: "static" } and can be
passed directly to GraphViewer. Static graphs hide runtime status/timing
chrome and are best-effort: diagnostics report incomplete or unresolved authored
content without preventing rendering.
See docs/static-graph.md for the full contract.
interface GraphSpec {
nodes: GraphSpecNode[];
edges: GraphSpecEdge[];
meta: {
format: "mthds";
mode?: "dry" | "live" | "static";
};
}
interface GraphSpecNode {
id: string;
pipe_code?: string; // e.g. "analyze_match"
pipe_type?: string; // e.g. "PipeLLM", "PipeSequence", "PipeExtract"
status?: string; // "succeeded", "failed", "running", "scheduled", "skipped"
io?: {
inputs?: IOItem[];
outputs?: IOItem[];
};
error?: Problem; // why a failed node failed: the problem document of "mthds/errors"
}
interface GraphSpecEdge {
id?: string;
source: string; // Source node ID
target: string; // Target node ID
kind: GraphSpecEdgeKind; // "data", "contains", "batch_item", etc.
label?: string;
}Nodes represent pipes (operations) and stuffs (data artifacts) in the pipeline:
- Pipe nodes — operations like
PipeLLM,PipeSequence,PipeExtract,PipeSearch - Stuff nodes — data flowing between pipes, typed by concepts (e.g.
CandidateProfile,Document)
| Kind | Description |
|---|---|
data |
Data flow — stuff produced by one pipe, consumed by another |
contains |
Containment — a controller pipe wraps child pipes |
batch_item |
Batch processing — items fanned out from a collection |
batch_aggregate |
Batch aggregation — items collected back |
parallel_combine |
Parallel results combined |
{
"nodes": [
{
"id": "extract_text",
"pipe_code": "extract_text",
"pipe_type": "PipeExtract",
"io": {
"inputs": [{ "digest": "input_doc", "name": "document", "concept": "Document" }],
"outputs": [{ "digest": "pages", "name": "pages", "concept": "TextPages" }]
}
},
{
"id": "summarize",
"pipe_code": "summarize",
"pipe_type": "PipeLLM",
"io": {
"inputs": [{ "digest": "pages", "name": "pages", "concept": "TextPages" }],
"outputs": [{ "digest": "summary", "name": "summary", "concept": "Summary" }]
}
}
],
"edges": []
}Full specification: docs.pipelex.com — Execution Graph Tracing
Controls layout and visual behavior. All fields are optional — DEFAULT_GRAPH_CONFIG provides sensible defaults.
import { DEFAULT_GRAPH_CONFIG } from "@pipelex/mthds-ui";
// Override specific settings
const myConfig = {
...DEFAULT_GRAPH_CONFIG,
direction: "LR",
nodesep: 80,
ranksep: 50,
};| Field | Type | Default | Description |
|---|---|---|---|
direction |
GraphDirection |
"LR" |
Layout direction |
graphStyle |
GraphStyleId |
"detailed" |
The style the graph is drawn in |
showControllers |
boolean |
false |
Show controller group boxes |
nodesep |
number |
50 |
Horizontal spacing between nodes |
ranksep |
number |
100 |
Vertical spacing between ranks |
edgeType |
EdgeType |
"bezier" |
Edge curve style |
initialZoom |
number | null |
null |
Override fit-view zoom (null = auto) |
panToTop |
boolean |
true |
Pan viewport to top after layout |
paletteColors |
Record<string, string> |
(see below) | CSS variable overrides for theming |
GraphViewer applies CSS custom properties from paletteColors on mount. Override colors by passing a custom config:
<GraphViewer
graph={{ graphSpec: graphspec }}
config={{
...DEFAULT_GRAPH_CONFIG,
paletteColors: {
...DEFAULT_GRAPH_CONFIG.paletteColors,
"--color-pipe": "#e06c75",
"--color-stuff": "#61afef",
"--color-bg": "#282c34",
},
}}
/>Default palette colors include:
| Variable | Purpose |
|---|---|
--color-pipe |
Pipe node border/accent |
--color-pipe-bg |
Pipe node background |
--color-stuff |
Stuff node border/accent |
--color-stuff-bg |
Stuff node background |
--color-edge |
Edge line color |
--color-batch-item |
Batch item edge color |
--color-batch-aggregate |
Batch aggregate edge color |
--color-bg |
Graph background |
--color-bg-dots |
Background dot pattern |
--font-sans |
Node font family |
--font-mono |
Code/controller font |
See graphConfig.ts for the full default palette.
| Import path | Content |
|---|---|
@pipelex/mthds-ui |
Pure-TS graph logic — types, builders, layout, controllers, config |
@pipelex/mthds-ui/graph/react |
React components — GraphViewer, label helpers, type converters |
@pipelex/mthds-ui/form |
The form kernel's React-free surface, re-exported |
@pipelex/mthds-ui/form/brand |
The kernel's brand entry: seed colours into readable colour roles |
@pipelex/mthds-ui/form/react |
RunPanel — a pipe's input form — and the kernel's controls |
@pipelex/mthds-ui/timeline |
A run's progress snapshot: types, path grammar, rows and time axis |
@pipelex/mthds-ui/timeline/react |
RunTimeline — a run's progress as growing bars or a checklist |
@pipelex/mthds-ui/shiki |
MTHDS syntax highlighting with shiki |
@pipelex/mthds-ui/tailwind.css |
Where the kernel's classes are, for a Tailwind 4 host |
@pipelex/mthds-ui/form-kernel.css |
The kernel's prebuilt stylesheet, for a host without Tailwind |
Use the graph logic without React — build nodes/edges, run layout, and feed the result to your own renderer:
import {
buildGraph,
getLayoutedElements,
applyControllers,
DEFAULT_GRAPH_CONFIG,
} from "@pipelex/mthds-ui";
// Build graph data from a GraphSpec
const { graphData, analysis } = buildGraph(graphspec, "bezier");
// Apply ELK layout
const { nodes, edges } = getLayoutedElements(graphData.nodes, graphData.edges, "TB");
// Optionally wrap nodes in controller groups
const final = applyControllers(nodes, edges, graphspec, analysis, true);RunPanel renders a pipe's input form from its IO contract: the fields, the readiness verdict on the Run button, and the wire-ready payload a run receives.
The form kernel ships as a dependency of this package, so there is nothing extra to install. Reach it through @pipelex/mthds-ui/form rather than importing it directly — a second declaration puts a second copy in your tree, and the kernel ships React contexts, so a provider you mount above the panel would stop resolving inside it.
import { getPipeIOContract } from "@pipelex/mthds-ui/form";
import { RunPanel } from "@pipelex/mthds-ui/form/react";
import "@pipelex/mthds-ui/form/react/RunPanel.css";
// Note the argument order — the kernel's README currently shows it wrong.
const contract = getPipeIOContract(pipeIoContracts, domain, pipeCode);
<RunPanel
contract={contract}
values={values}
onValuesChange={setValues}
onRun={(apiInputs) => execute(pipeCode, apiInputs)}
title={pipeCode}
theme="dark"
/>;onRun fires only once the kernel's run gate passes. This library renders and never executes: no API client, no upload, no storage resolution — the host injects all three.
The kernel's two other host seams travel the same way: fileSource lets a file field take a file the host already stores, and exampleSource lets the form be filled from one of the method's saved examples, asking first before it replaces what the person entered, then calling onFill so a host knows the form was reset rather than edited. A panel given neither offers neither. See docs/run-form-panel.md.
The controls inside RunPanel, and inside the graph's detail panel, are the form kernel's, styled with Tailwind classes over the shadcn tokens. No entry of this package imports a stylesheet for them: your app loads one, and which one depends on whether it runs Tailwind. Load one of the two, never both.
A host with Tailwind 4 follows the kernel's documented setup, A host that runs Tailwind, and imports this package's tailwind.css in place of that setup's @source line:
@import "tailwindcss";
@import "tw-animate-css";
@import "@pipelex/mthds-ui/tailwind.css";
@custom-variant dark (&:is(.dark *));
/* …and the @theme inline mapping of the shadcn tokens the kernel's setup lists. */tailwind.css points Tailwind at the copy of the kernel this package depends on, wherever pnpm or npm put it beside or under this package, so you write no path into node_modules and do not declare @pipelex/mthds-form yourself. One layout escapes it: when your tree holds two versions of this package, npm can nest one below a kernel hoisted further up, and the controls then render unstyled without a warning; docs/run-form-panel.md gives the one-line remedy. The rest of the kernel's setup stays yours: tw-animate-css animates the select popover and the tooltip, without the token mapping a class such as bg-background compiles to nothing, and the @custom-variant line, which a shadcn/ui codebase already has, keys dark: to the .dark class the kernel follows rather than to the operating system's preference.
A host without Tailwind imports the prebuilt stylesheet once, from its entry:
import "@pipelex/mthds-ui/form-kernel.css";It carries Tailwind's preflight, which resets the browser's default styles across your whole page (heading sizes, list markers, margins), and a cascade layer that lets every rule you write yourself win a tie against it.
Tailwind 3 hosts and Tailwind 4 hosts that configure a prefix are not supported: their builds cannot generate the kernel's unprefixed Tailwind 4 classes, and the prebuilt sheet cannot sit in their cascade, because its preflight must rank below their base styles while its utilities must rank above them.
Either way, the shadcn token values are yours to supply, and each must be a complete colour — hsl(240 10% 3.9%), #0a0711, oklch(…) — never a bare HSL triplet. Since kernel 0.8.0 the sheet emits background-color: var(--background) rather than hsl(var(--background)), so a triplet computes to something that is not a colour and the browser discards the declaration instead of overriding with it: green build, token inspectable, style silently absent.
To brand the controls with a team's own colours, @pipelex/mthds-ui/form/brand derives colour roles from up to three colours a person picked, every text colour readable on the theme's page: a host writes action onto --primary, accentInk onto the optional --link (the accent as text) and onto --ring (every focus the controls draw), and accentFill onto the optional --selected (the pale fill of a selected pill, tag or step). A role the team left unset comes back absent, so the host writes no token for it and keeps its own, and a page that defines neither --link nor --selected falls back to --primary for both.
Full contract, why this package stopped loading the kernel's stylesheet itself, the .dark bridge, the mthds-run-panel token hook and the brand tokens: docs/run-form-panel.md.
RunTimeline draws a running run as nested rows, one per invocation of a pipe, each with a bar on a shared time axis that grows while the invocation runs. Its only input is the progress snapshot of the hosted progress contract: a batch is one collapsible row with its counts and a strip of its items' states, whose items come a page at a time (batchPageSize, ten by default) with a jump to the next failed or stuck one, a condition's branch carries the outcome it took, and a retried or stuck invocation is marked on its own row.
import { RunTimeline } from "@pipelex/mthds-ui/timeline/react";
// Hand it every snapshot you read: it keeps the fresher one by the contract's
// rule, so a late answer from an older read never moves it backwards.
<RunTimeline snapshot={snapshot} theme="dark" />;timelineStyle="simple" draws the same run as a checklist of its steps in the method's own words, titled from the method's pipe registry the host passes as pipeRegistry, with each batch as one line of counts, for a reader who did not write the method; styleMenu lets the reader switch. See docs/timeline-styles.md.
The entry imports its own stylesheet, and the timeline themes itself with the same tokens as GraphViewer. The snapshot's types and the projection the component draws are in the React-free @pipelex/mthds-ui/timeline. Props, the freshness rule, what each row and bar shows, and the stories: docs/run-timeline.md.
Syntax-highlight MTHDS code with the bundled grammar and themes:
import { highlightMthds, getAvailableThemes } from "@pipelex/mthds-ui/shiki";
const html = await highlightMthds(mthdsSource, "pipelex-dark");npm install
make check # lint + format-check + typecheck
make test # unit tests (vitest)
make build # build to dist/Storybook's anonymous usage telemetry is disabled in .storybook/main.ts (core.disableTelemetry), because no test run, locally or in CI, may report to a third party: without it, every make test would send an event to storybook.js.org through the Storybook test project, and so would every make storybook. Every CI workflow also sets DO_NOT_TRACK=1.
MIT — see LICENSE.
This project depends on elkjs which is licensed under EPL-2.0. See NOTICE for third-party license details.