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/ Commands & actions
Building Tools

Commands & actions

Add a CLI subcommand from a typed function with @tool.command, and route menu or context actions with @tool.action — then invoke them via tool.dispatch.

TL;DR@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 — 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 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.

LearnContributing UI · The Tool object · Entity types

APITool · Context · CLI commands

ExamplesA CLI command · A dockable panel

Frequently asked questions

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.

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