Anatomy

The data model, shown next to what it draws — never in isolation. Each block below is one thing (a node, a connector tag, a connector state) rendered live, with its TypeScript (the render call) and its JSON (the tracer's output) one tab away. Flip the Preview / Source / Data tabs to line the three up.

For the prose reference of the same shapes, see Pipeline Graph JSON.

The node card

A node is one stage — a @ls.udf — as a 156 × 72 card. Everything on it comes straight from the node JSON; nothing is styled by hand.

detect_objects
transform · 11
idle
On the cardJSON fieldNotes
Kind dot (top-left square)kindTints by category — source green, transform blue, model/review purple, filter amber, merge grey, sink red.
TitletitleThe UDF name. id is the stable key (may differ on fan-out: semantic_match_2).
Meta line transform · 1→1kind · inputs.lengthoutputs.lengthThe is port counts, not columns.
Left dotsinputsOne input port per UDF parameter.
Right dotoutputsA UDF returns one table → a single ["out"] port. A sink has none ([]).
Status dot + label (footer)statusidle here; drives tint + the pulse when running.
(not drawn on the card)columnsThe result schemaboxes, classes, confidence. Surfaced in the source inspector, not as ports.

Ports vs columns is the one thing to internalise: inputs are ports (one per parameter), outputs is a single port (the whole result table), and the return column names live in columns — a schema, not more ports.

Connector tags

An edge carries exactly one structural tag, kind, decided by the tracer. It's the only style an edge stores; everything else about an edge is derived from status.

data — the value flows through (solid)

The default. The source's result table is consumed by the target.

detect_objects
transform · 11
idle
save_dataset
sink · 10
idle

mask — the source gates the target (dashed)

A gate, not data flow. The source (a review veto or a confidence/consensus mask) only decides which rows of the target survive. In Python this is labels[consensus] — a boolean selector — so it reads as a gate without adding a filter node. Rendered dashed and slightly fainter in the settled states.

review_boxes
review · 11
idle
save_dataset
sink · 10
idle
TagMeaningLookProduced by
datasource result flows into targetsolidpassing a UDF result as an argument
masksource only filters/gates targetdashed, faintera boolean mask used as a selector (labels[mask])

Connector states

An edge stores no colour, width, or animation. Its flow — how it looks right now — is derived at render time from the status of its two endpoint nodes, via one shared function:

ts
edgeFlow(src: NodeStatus, dst: NodeStatus):
  'running' | 'queued' | 'stalled' | 'error' | 'ok' | 'idle'

Six states, one derived value. Below, each swatch is the real FLOW[state] styling, next to the status pair that produces it (this board is built straight from the exported edgeFlow + FLOW, so it can't drift from the component):

runningrunningidle · data is flowing now
queuedokidle · produced, waiting on downstream
stalledstaleidle · upstream went stale
errorerroridle · an endpoint failed
okokok · both settled ok
idleidleidle · nothing has run
FlowDerived whenLook
runningsrc running, or (src ok & dst running)blue, marching dashes
queuedsrc ok, dst still idlegrey, slow drift
stalledsrc staleamber, gentle breath
erroreither endpoint erroredred, tight dashes
oksrc ok & dst okgreen, solid
idleanything elsefaint, solid

Note the spelling: a node's status is stale, but the edge flow it derives is stalled. Because flow is derived, you never store or diff it — keep node status live (via statusById) and every edge restyles itself. That single source of truth is what makes the live runner cheap.


Next: Pipeline Graph JSON is the full data-model reference · Architecture & Roadmap covers the internals and what's planned.