# Parameters vs ports
URL: https://lsd.tools/docs/tools-params-ports
Updated: 2026-07-18

> LSD reads a step's signature and sorts each parameter into one of four kinds by its annotation. A Table is an input port; a Source is an injected reader (load only); a StepContext is injected capabilities; anything else is a typed form field. The rules are enforced at import time, so mistakes surface when the package loads — never mid-click.

Every `@tool.*` step is a plain function. LSD reads its **signature** and sorts each parameter into
one of four kinds — by its **annotation**, not its name — to decide what becomes a wire, what gets
injected, and what becomes a form field.

## Usage

```python
from lsdtools import Tool, Table, StepContext

tool = Tool("demo")

@tool.shape
def clip(t: Table, lo: float, hi: float = 1.0, ctx: StepContext = None) -> Table:
    ctx.progress(0.5, "clipping")
    return t.filter((t["v"] >= lo) & (t["v"] <= hi))
# → t: input PORT · lo: required FIELD · hi: optional FIELD · ctx: INJECTED
```

## The classification rule

| Annotation | Becomes | Notes |
|---|---|---|
| `Table` / `pa.Table` | an **input port** | wired from an upstream node; a load step may have none |
| `Source` | an **injected reader** | `@tool.load` only; bound to the file/query the user picks |
| `StepContext` | **injected capabilities** | progress, cancel, scratch, emit; excluded from the cache key |
| anything else (`int`, `str`, `bool`, `float`, …) | a **typed parameter** | a form field; **no default ⇒ required**, **default ⇒ optional** |

The annotation is the whole signal. Rename `t` to `left` and it is still a port; drop the `Table`
annotation off it and it becomes a plain field.

## Ports: `Table` and `pa.Table`

A parameter annotated [`Table`](/docs/api-table) (or a raw `pyarrow.Table`) is an **input port** —
an edge you wire from an upstream node on the canvas, or pass positionally in Python:

```python
@tool.shape
def merge(a: Table, b: Table) -> Table: # two ports
    return a.join(b, on="id")

merge(load_a(), load_b()) # wire both ports
```

A load step is the *source* of data, so it may have **no** ports — see
[Load steps](/docs/tools-load). Shape and deliver steps have one or more.

## Injected: `StepContext` (and why not `Context`)

A [`StepContext`](/docs/api-context) parameter is filled in by the engine at run time — never by the
user, and never part of the cache key. Use it for `ctx.progress(...)`, `ctx.cancelled`,
`ctx.set_output(...)`, `ctx.emit(...)`, and `ctx.output_dir` / `ctx.scratch_dir`.

A `Context` (the richer object exposing live project state) is **rejected on a step** at import:

```python
@tool.shape
def bad(t: Table, ctx: Context) -> Table: # TypeError at import!
    ...
# → "a step must not read Context … use StepContext for progress/cancel/scratch"
```

A step must stay pure so the cache can trust it; `Context` is for
[commands, actions and views](/docs/tools-commands-actions), not steps.

## Everything else is a parameter

Any other annotation is a **typed parameter** that renders as a form field on the node. The type
drives the widget (`int` / `float` → number, `bool` → checkbox, `str` → text). A parameter with
**no default is required**; a parameter **with a default is optional**:

```python
@tool.load
def rows(n: int, seed: int = 0) -> Table: # n required, seed optional
    ...
```

To go beyond a plain auto-generated field — a form with its own buttons, or a custom editor — a tool
contributes a [view](/docs/tools-panels-overlays-views): a `View` of `param()` fields and `@action`
buttons the desktop docks beside the node. A domain view can also ride on a single parameter via
`Annotated[...]` — see the [file loader](/docs/ex-file-loader).

## Checked at import time

All of this is validated when the module is **imported** (when the package loads), so a mistake
never waits until someone clicks the node. The three guards:

- a `Table` port on `@tool.load` → *"a load step takes no Table input ports"*;
- a `Source` on `@tool.shape` / `@tool.deliver` → *"a Source parameter is only valid on @tool.load"*;
- a `Context` on any step → *"a step must not read Context … use StepContext"*.

Fix the signature and the whole package loads clean.

## Related

**Learn** — [The Tool object](/docs/tools-tool-object) · [Tables & data flow](/docs/tools-tables) · [Load steps](/docs/tools-load)

**API** — [Tool](/docs/api-tool) · [Context](/docs/api-context) · [Source](/docs/api-source)

**Examples** — [Your first tool](/docs/ex-hello-tool) · [Live node metrics](/docs/ex-graph-outputs)

## FAQ
### Why is my Source parameter rejected on a shape step?

A Source produces data, so it is only valid on @tool.load. On a shape or deliver step it raises a TypeError at import time. Wire a Table input port from an upstream load node instead.

### Why can't I annotate a step parameter Context?

A step must stay pure so the cache can trust it. A Context exposes live project state the cache can't see, so it is rejected at import. Use StepContext for progress, cancel, scratch and emit; use Context only in commands, actions and views.
