# ViewerPayload
URL: https://lsd.tools/docs/api-viewerpayload
Updated: 2026-07-17

`ViewerPayload` is what a viewer-delivering [`@tool.deliver`](/docs/api-tool-deliver) step
**returns**. It is symmetric to how a [`@tool.load`](/docs/api-tool-load) step takes a
[`Source`](/docs/api-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
[`watch`](/docs/api-view-watch)es 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](/docs/tools-rendering).

## Where to start

**Learn** — [Rendering & payloads](/docs/tools-rendering) · [Deliver steps](/docs/tools-deliver)

**API** — [Tool.deliver](/docs/api-tool-deliver) · [View](/docs/api-view) · [watch](/docs/api-view-watch) · [Source](/docs/api-source)
