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/ Building Tools/ Shape steps
Building Tools

Shape steps

A @tool.shape node transforms data — one or more Table input ports in, a Table out. Learn its signature and how to combine several inputs into one.

TL;DRDecorate a function with @tool.shape to make a transform node. Each Table parameter is an input port wired from upstream; the function returns a Table. Add plain parameters for form fields, several Table ports to join or merge inputs, and a StepContext for progress and cancel.

A shape step is a transform: it takes one or more Table input ports and returns a Table. It is the workhorse node — filter, join, reshape, compute.

Usage#

Python
from lsdtools import Tool, Table

tool = Tool("scores")

@tool.shape
def top(t: Table, limit: int = 10) -> Table:
    """Keep the highest-scoring rows."""
    return t.sort("score", descending=True).head(limit)
# → t: input port · limit: a number field

t: Table is an input port you wire from an upstream node; limit: int is a parameter rendered as a number field.

Combining multiple input ports#

Add more than one Table parameter and each becomes its own input port on the node — wire a different upstream node into each. Combine them however you like and return one table:

Python
@tool.shape
def enrich(facts: Table, dims: Table, on: str = "id") -> Table:
    """Join a facts table to a dimensions table."""
    return facts.join(dims, on=on, how="left")

enrich(load_facts(), load_dims(), on="id") # wire both ports in Python

On the canvas the node shows two inlets, facts and dims; in Python you pass them positionally or by keyword. The parameter name is the port label — so name your ports for what they carry.

Parameters alongside ports#

Everything that isn't a Table (or StepContext) is a form field. Mix ports and parameters freely; a parameter with no default is required, one with a default is optional:

Python
@tool.shape
def bin_col(t: Table, column: str, bins: int = 10) -> Table:
    """Bucket a numeric column into `bins` equal ranges."""
    ...

Progress and cancel#

For long transforms, add a StepContext parameter to report progress and honour cancellation. It is injected by the engine and excluded from the cache key:

Python
from lsdtools import Tool, Table, StepContext

@tool.shape
def heavy(t: Table, ctx: StepContext = None) -> Table:
    out = []
    for i, part in enumerate(split(t)):
        if ctx.cancelled:
            break
        ctx.progress(i / len(t), "processing")
        out.append(transform(part))
    return concat(out)

Emitting several tables#

A shape step can produce named output ports with outputs=[...] and return a dict — useful for a split or a decomposition. See Multiple outputs:

Python
@tool.shape(outputs=["keep", "reject"])
def partition(t: Table, floor: float = 0.0) -> dict:
    mask = t["score"] >= floor
    return {"keep": t.filter(mask), "reject": t.filter(~mask)}

LearnLoad steps · Deliver steps · Tables & data flow

APITool · Table · Context

ExamplesClean a CSV · Multi-output mesh

Frequently asked questions

How do I make a node that reads two upstream tables?

Give the function two Table parameters. Each becomes its own input port on the node; wire a different upstream node into each. Inside, combine them with join, or any logic you like, and return one Table.

Can a shape step read a file directly?

No — a Source parameter is only valid on @tool.load, and is rejected on a shape step at import. Put the read in a load node and wire its Table into the shape step's port.

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