Sandbox — LSD is in active testing. All systems operational·payments are test-mode (no real charges).
Overview Use Cases News Docs Tool Store Support Community Get LSD
All docs
Docs/ API Reference/ ViewerPayload
API Reference

ViewerPayload

ViewerPayload is what a viewer-delivering @tool.deliver returns — a JSON spec plus Arrow tables. Covers LayerPayload, ChartPayload, and LegendPayload.

ViewerPayload is what a viewer-delivering @tool.deliver step returns. It is symmetric to how a @tool.load step takes a Source: a load takes the input, a deliver returns the output artifact, and the framework does the plumbing on both ends. When a deliver returns a ViewerPayload, the framework

  • writes each table in tables to Parquet in the step's workspace,
  • persists the spec + table paths + routing as a *_payload.json sidecar,
  • emits the payload's topic with entity_id / step_id stamped centrally, and
  • reconstructs it generically on reopen from the sidecar — no per-step code.

So a package writes return LayerPayload(kind="lines", tables={"path": lines}) instead of hand-rolling ctx.emit("viewer/layer/set", ...) plus its own parquet-write and reconstruct-on-reopen dance. It is GTK-free and pyarrow-only (and pyarrow only at write time — import lsd stays cheap).

Usage#

Python
from lsdtools import Tool, Table, LayerPayload, ChartPayload, LegendPayload

tool = Tool("geo")

@tool.deliver
def show_lines(t: Table) -> LayerPayload:
    """Draw the incoming geometry as lines in the 3D viewer."""
    return LayerPayload(kind="lines", tables={"path": t}, style={"width": 1.5})

The return annotation is what marks a deliver as viewer-delivering — a ViewerPayload return is only valid on @tool.deliver (a load/shape returns a Table). Any View that watches this step's entity then receives the payload.

The three built-ins#

Each folds kind / style / title (and any extra keyword) into spec; tables may be empty.

Class Topic Constructor Typical use
LayerPayload viewer/layer/set LayerPayload(*, kind="", tables=None, style=None, **extra) 3D geometry — the geometry Parquet(s) ride in tables.
ChartPayload viewer/chart/set ChartPayload(*, kind="", title="", tables=None, **extra) A chart — spec-only by default (pass tables= only if table-backed).
LegendPayload viewer/legend/set LegendPayload(*, tables=None, **extra) A colour legend for a value column — spec-only.
Python
ChartPayload(kind="bar", title="Grades", series=[...])          # → viewer/chart/set
LegendPayload(title="Au g/t", colormap="grade", vmin=0.0, vmax=5.0)   # → viewer/legend/set

The base class#

ViewerPayload(spec=None, tables=None) is the base, exported from lsdtools alongside the three built-ins. spec is any JSON-serializable mapping (persisted verbatim, carried under the "spec" key); tables is {name: Table} — each written to Parquet, with the emitted/persisted payload carrying the paths, not the tables. A topic class attribute ("viewer/...") names the event-bus topic each subclass delivers on.

Subclassing for a custom viewer surface#

Define a new topic by subclassing ViewerPayload and setting topic. Registering it (so generic restore on reopen can map the persisted topic back to the class) uses the engine-internal lsd.flow.payload seam — the same module a custom front-end sink lives beside, so this recipe is a gui/product-tier task rather than a pure lsdtools package one:

Python
from lsd.flow.payload import ViewerPayload, register_payload

@register_payload
class HeatmapPayload(ViewerPayload):
    topic = "viewer/heatmap/set"

    def __init__(self, *, tables=None, **spec):
        super().__init__(spec=spec, tables=tables)

A viewer surface that renders this topic subscribes to it with a sink that gates each event on should_handle — the per-view routing rule (a viewer shows a payload iff it watches that payload's entity_id). The full recipe is on Rendering & payloads.

Where to start#

LearnRendering & payloads · Deliver steps

APITool.deliver · View · watch · Source

By LSD Team · Last updated Jul 17, 2026 Ask on Discord View as Markdown