# Commands & actions
URL: https://lsd.tools/docs/tools-commands-actions
Updated: 2026-07-18

> @tool.command turns a typed (ctx, ...) function into an "lsd <tool> <name>" CLI subcommand — required params become positional arguments, defaulted ones become flags, no argparse required. @tool.action registers a (ctx, **values) function under a route like app/<verb> or entity/<type>/<verb>, which becomes a menu or context-menu item and is callable with tool.dispatch(route, ...).

Steps make data; **commands** and **actions** make a tool *interactive*. A command is a CLI
subcommand; an action is a routed handler wired into menus and context menus. Both receive a
[`Context`](/docs/api-context) — not a `StepContext` — so they can touch the live project.

## Usage

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

tool = Tool("io")

@tool.command("csv2parquet", help="Convert a CSV to Parquet")
def csv2parquet(ctx: Context, src: str, dst: str = "out.parquet") -> int:
    Table.read_csv(src).write_parquet(dst)
    return 0
# → run it: lsd io csv2parquet data.csv --dst results.parquet
```

## Commands: a CLI from the signature

`@tool.command("name")` generates the subcommand `lsd <tool> <name>` straight from the function
signature — **zero argparse**. The mapping:

- the first `ctx: Context` parameter is **injected** (never on the command line);
- a parameter with **no default** → a **required positional** argument;
- a parameter **with a default** → an optional `--flag`;
- the returned `int` is the process **exit code** (`0` = success).

So `csv2parquet` above exposes `src` as positional and `dst` as `--dst`. See
[CLI commands](/docs/api-cli) for how `lsd` discovers your tool.

## Actions: routes into the UI

`@tool.action("route")` registers a `(ctx: Context, **values)` handler under a **route**, which
decides where it shows up:

- `app/<verb>` → an application-level command (a Run-menu item);
- `entity/<type>/<verb>` → a context-menu action on that entity type.

```python
@tool.action("app/refresh", label="Refresh cache", icon="lsd-refresh-symbolic")
def refresh(ctx: Context, **values) -> None:
    ctx.engine.run_all()
    ctx.ui.notify("cache refreshed")

@tool.action("entity/drillhole/desurvey", label="Desurvey")
def desurvey(ctx: Context, dip: float = 60.0, **values) -> None:
    ctx.entity.update(dip=dip)
    ctx.entity.run()
```

The `label` and `icon` decorate the generated menu item. At `tool.mount()`, `app/*` actions become
menu items; `entity/*/*` actions attach to the matching entity type's context menu.

## Dispatching an action

Actions are also callable directly by route with `tool.dispatch(...)` — handy from a view button,
a test, or another handler. If you don't pass a `ctx`, one is built from the mounted host:

```python
tool.dispatch("app/refresh") # ctx built from the host
tool.dispatch("entity/drillhole/desurvey", ctx, dip=75)
```

The `**values` you pass arrive as the handler's keyword arguments — the same values a form or view
would supply. Views wire their buttons to actions this way; see
[Contributing UI](/docs/tools-panels-overlays-views).

## Related

**Learn** — [Contributing UI](/docs/tools-panels-overlays-views) · [The Tool object](/docs/tools-tool-object) · [Entity types](/docs/tools-entities)

**API** — [Tool](/docs/api-tool) · [Context](/docs/api-context) · [CLI commands](/docs/api-cli)

**Examples** — [A CLI command](/docs/ex-cli-command) · [A dockable panel](/docs/ex-panel)

## FAQ
### Do commands and actions receive a StepContext like steps do?

No — they receive the richer Context, with live access to the host, engine, project, selection and ui. StepContext is only for steps, which must stay pure for the cache. Commands and actions are user-triggered, so they get the full Context.

### How does @tool.command decide positional args versus flags?

From the signature. A parameter with no default becomes a required positional argument; a parameter with a default becomes an optional --flag. The first parameter, ctx, is injected and never appears on the command line.
