# Example: multi-output mesh
URL: https://lsd.tools/docs/ex-multi-output
Updated: 2026-07-17

> A @tool.load with outputs=["vertices", "faces"] returns a dict of two Tables. Downstream, a shape reads a specific port with step.output("faces"). Engine().run(...).outputs gives every port as a Table.

Some nodes naturally produce more than one table. A mesh is the classic case: **vertices** and
**faces** are two datasets with different shapes. Declare `outputs=[...]`, return a dict, and wire
each output on its own port.

## The code

```python
# tool.py
from lsdtools import Tool, Table

tool = Tool("mesh", label="Mesh")


@tool.load(outputs=["vertices", "faces"])
def make_cube(size: float = 1.0) -> dict:
    """Emit a cube as two tables: 8 vertex positions and 12 triangle indices."""
    s = size / 2
    verts = [(x, y, z) for x in (-s, s) for y in (-s, s) for z in (-s, s)]
    tris = [
        (0, 1, 3), (0, 3, 2), # x = -s
        (4, 6, 7), (4, 7, 5), # x = +s
        (0, 4, 5), (0, 5, 1), # y = -s
        (2, 3, 7), (2, 7, 6), # y = +s
        (0, 2, 6), (0, 6, 4), # z = -s
        (1, 5, 7), (1, 7, 3), # z = +s
    ]
    return {
        "vertices": Table({"x": [v[0] for v in verts],
                "y": [v[1] for v in verts],
                "z": [v[2] for v in verts]}),
        "faces": Table({"a": [t[0] for t in tris],
                "b": [t[1] for t in tris],
                "c": [t[2] for t in tris]}),
    }


@tool.shape
def annotate(t: Table) -> Table:
    """Add an `index_sum` column (a + b + c) to each face."""
    return t.with_column("index_sum", t["a"] + t["b"] + t["c"])
```

## What each line does

- **`@tool.load(outputs=["vertices", "faces"])`** — declares two named output ports. The function
 must **return a dict** keyed by those names, each value a [`Table`](/docs/api-table).
- **`Table({...})`** — each output is built independently; they can have completely different
 columns and row counts (8 vertices vs 12 faces).
- **`@tool.shape` `annotate`** — a normal transform with one input port `t`. `t["a"] + t["b"] +
 t["c"]` is a whole-column [`Column`](/docs/api-table) expression; `with_column` appends it.
- **Wiring a specific port** happens at call time with `make_cube(...).output("faces")` — see below.

## Run it in Python

```python
from lsdtools import Engine

# read every output port of the load node
res = Engine().run(make_cube(size=2.0))
res.outputs["vertices"].print() # → 8 rows of x, y, z (each ±1.0)
res.outputs["faces"].print() # → 12 rows of a, b, c

# wire ONE named output ("faces") into the shape node
faces = annotate(make_cube(size=2.0).output("faces"))
Engine().run(faces).output.print()
# → a b c index_sum
# 0 1 3 4
# 0 3 2 5
# … 12 rows
```

`make_cube(...).output("faces")` returns a **port reference**; passing it as the shape's input wires
that exact output. `RunResult.outputs` is a `{port: Table}` dict, while `RunResult.output` is the
terminal step's primary table.

## Try changing it

- Add a `graph_outputs={"n_faces": int}` scalar to show the face count on the node card — see
 [live node metrics](/docs/ex-graph-outputs).
- Wire the `vertices` port into a second shape that recentres the mesh.
- Return a third output (`"normals"`) — just add it to `outputs=[...]` and the returned dict.

## Related

**Learn** — [Multiple outputs](/docs/tools-multi-output) · [Load steps](/docs/tools-load) · [Tables & data flow](/docs/tools-tables)

**API** — [Table](/docs/api-table) · [Engine](/docs/api-engine) · [Tool](/docs/api-tool)

**Examples** — [Live node metrics](/docs/ex-graph-outputs) · [File loader](/docs/ex-file-loader)
