LSD
Extending LSD · complete reference

Every extension point

LSD is built to be extended from a single import — lsdtools. This page catalogs everything a package can contribute: steps, entities, parameters & widgets, CLI commands, desktop menus, custom UI panels, events, and more. Each item is tagged Available, Partial, or Proposed — so this doubles as the roadmap for what's next.

New here? Start with the Quickstart. This page is the deep reference.

The contribution model Available

A package is plain Python. Two mechanisms cover almost everything:

  • Auto-registration — define a Step or Entity subclass (or a decorated function) and importing the module registers it. No boilerplate.
  • register(host) — an optional function in your package's __init__.py. It receives a PackageHost and pushes contributions (CLI commands, menus, views) via contribute(...).
python
from lsdtools import contribute, CliCommand, MenuItem, ViewContribution

def register(host):
    contribute(
        CliCommand("hello", "say hi", run=lambda a: print("hi")),
        MenuItem("Do it", callback=do_it, menu="run"),
        ViewContribution("my.panel", "My Panel", make_widget, location="sidebar"),
    )

Core (lsd) holds contributions in a registry; each front-end renders what it supports — the desktop renders views/menus, the CLI wires commands — so lsd itself imports neither GTK nor argparse.

1 · Steps Available

The unit of work. Decorate a function or subclass a base; both auto-register and appear in the Add step picker. (Full walkthrough in the Quickstart.)

Advanced step features

FeatureHow
Typed parametersparam(type, …) (class) or function args (decorator)
Output schema contractoutput_schema = pa.schema([...]) — validated after run()
Multiple inputs/outputsinput_ports=[…] / output_ports=[…] (or @shape(inputs=…, outputs=…))
Cross-entity wiringstep.bind_from("key", other_step) — drives run order
Read-only resultsparam(..., output=True) — written by run(), shown in the UI
Lifecycle hookon_attached() — called when added to an entity
python
class Join(ShapeStep):
    input_ports  = ["main", "lookup"]
    output_ports = ["joined"]
    key = param(str, default="id")
    def run(self, inputs):
        return inputs.main.join(inputs["lookup"], keys=self.key)

2 · Entities Available

Entities are the nodes in the project tree that hold steps. Subclass Entity to add a new creatable type — it appears in the desktop's Add entity menu and lsd entity add automatically.

python
from lsdtools import Entity, param

class Dataset(Entity):
    create_label = "Dataset"            # menu label (defaults to class name)
    source_url   = param(str, required=True, label="Source URL")
    icon         = param(str, default="lsd-folder-symbolic")
CapabilityWhat it gives you
creatable / create_labelShow (or hide) the type in “Add entity”; set its label
Capability flagscan_rename, can_delete, can_create_children, can_move, can_debug, has_context_menu, is_visible, sort_children
traitsFree-form tags (add_trait/has_trait) front-ends/packages can branch on
creatable_child_types()Restrict which entity types can be created under this one
get_actions()EntityActionAdd items to the entity's right-click context menu
CreateSpecDrive a “new child” flow (type picker + param form) from an action
python
from lsdtools import EntityAction

class Dataset(Entity):
    def get_actions(self):
        return [EntityAction("Refresh", callback=self._refresh, icon="lsd-refresh-symbolic")]

3 · Parameters & widgets Available

param() declares an input on a step or entity. The desktop renders an appropriate control; the CLI renders a flag. One declaration, both front-ends.

python
amount  = param(float, default=1.0, min=0, max=10, label="Amount", group="Tuning")
mode    = param(str, choices=["fast", "accurate"], default="fast")
notes   = param(str, widget="textarea", visibility="advanced")
secret  = param(str, widget="password")
folder  = param(str, widget="dir")
OptionEffect
type, default, requiredValue type, default, and whether it must be set
label, descriptionUI label + helptext / CLI help
choicesRenders a dropdown (combo); validates membership
min, maxNumeric range (validated; spin-button bounds)
widgetHint: password, dir, file, textarea
groupGroups params into tabs (desktop) / argument-groups (CLI)
visibilitynormal · advanced · hidden
validatorsExtra (value) → error? callables
outputRead-only result written by run()

Built-in form controls Available

The desktop maps each parameter to one of: text, multiline, numeric, checkbox, combo, file, directory, plus action controls (button, split-button, menu-button).

Proposed — custom parameter widgets. Proposed A public way to register your own control for a widget="my-thing" hint (e.g. a color picker, a query builder), so packages aren't limited to the built-in set. The widget field already exists on every parameter; the registry/extension API does not yet.

4 · CLI commands Available

Add a lsd <name> subcommand. configure(parser) adds arguments; run(args) executes and returns an exit code. Available to both lsd and the installed LSDCli.

python
from lsdtools import CliCommand, contribute

def _configure(p): p.add_argument("model", help="path to a .onnx file")
def _run(args): print("inspecting", args.model); return 0

def register(host):
    contribute(CliCommand("inspect-model", "summarise an ONNX graph",
                          configure=_configure, run=_run))
# → lsd inspect-model model.onnx

6 · Custom UI panels Available

Add a panel to the desktop — an editor tab or a sidebar section. The common way is declarative: describe fields + actions as data and LSD renders them with its own controls — no GTK import in your package. Action callbacks receive the panel's current field values.

python
from lsdtools import Panel, Field, Action, panel_view, contribute

def _greet(values):                # {field name → current value}
    print("hello", values["name"], values["times"])

def register(host):
    panel = Panel(
        title="Greeter",
        fields=[Field("name", default="world"),
                Field("times", kind="int", default=1, min=1, max=10)],
        actions=[Action("Greet", _greet, style="suggested-action")],
    )
    contribute(panel_view("my.greeter", "Greeter", panel, location="sidebar"))

Field kinds: text, password, textarea, int, float, bool, file, dir, or any field with choices=[…] (a dropdown). Same controls the inspector uses.

Escape hatch — a raw widget Available

Need something the declarative panel can't express? Contribute a factory() that returns any GTK 4 widget. Full power; couples that package to GTK (as the built-in DL viewer does).

python
from lsdtools import ViewContribution, contribute

def make_panel():
    import gi; gi.require_version("Gtk", "4.0")
    from gi.repository import Gtk
    return Gtk.Label(label="Anything you can build in GTK")

def register(host):
    contribute(ViewContribution("my.custom", "Custom", make_panel, location="editor"))
Real example — lsd-studio. The in-app authoring panel (generate steps with AI, open in VS Code, hot-reload) is itself a package built only on lsdtools — a declarative panel + host services, no GTK, no lsd.*. It's the proof the whole authoring UX can be built (and sold) on the public API. lsd package lint confirms it stays clean.

7 · Events Available

Subscribe to the app's lifecycle with host.on(event, handler) inside register. Handlers are async-capable and receive Event(type, data).

Pipeline & engine events

EventWhen
engine/run/started · …/finishedA run begins / ends
engine/entity/started · finished · blockedPer-entity execution
pipeline/started · finishedA pipeline's overall span
pipeline/step/started · skipped · finished · failedPer-step (cache hits emit skipped)
entity/child/added · removed · moved · entity/renamedTree edits

Desktop events

EventWhen / payload
desktop/project/opened · closed · savedProject lifecycle (path, engine)
desktop/menu/buildCollect EngineActions (see Menus)
desktop/ui/themeTheme changed (dark/light)
desktop/packages/loaded · installed · removedPackage lifecycle
desktop/scripts/reloadedLoose project scripts hot-reloaded

8 · Host services Available

register(host) receives a PackageHost. It transparently proxies the event bus (host.on/emit) and exposes optional services so a package can act on the app without importing the desktop:

python
def register(host):
    eng = host.engine                 # current project's engine (or None)
    host.notify("Indexing finished", "info")
    host.open_in_editor("steps.py")    # open a file in the code editor
    host.request_reload()             # ask the app to reload project scripts
Status. The PackageHost API, the event proxy, and the desktop implementations now ship — register(host) receives a host whose engine/notify/open_in_editor/request_reload are wired to the running app. (notify records to the log today; an on-screen toast is a small future polish.) Off the desktop — e.g. the CLI — service calls remain safe no-ops/None.

9 · Step hooks & agents Available

The lsd-agent package wraps pipeline execution with hooks — for human-in-the-loop approval, auditing, or AI assistance — without changing your steps.

python
from lsd_agent import run_with_hooks, HITLHook, AuditHook

run_with_hooks(engine, "sales", hooks=[HITLHook(), AuditHook()])
# HITLHook pauses for approval before each step; AuditHook records what ran.

Write your own by subclassing BaseHook (pre/post-step callbacks with a StepContext).

AI step generation Available

Describe a step in plain English and have it written for you. The public lsdtools.generate_step bridges to the private lsd-agent engine, which calls Claude with your project's live API context (the same cheat-sheet from §10) so the output uses exactly the steps and params available here. The generated step lands in your steps.py and hot-reloads.

python
from lsdtools import generate_step
generate_step("a deliver step that writes inputs.main to a parquet file", project_dir=".")

In the desktop: Develop → Generate Step with AI…. From the terminal: lsd ai "keep the top 100 rows by value". Needs an ANTHROPIC_API_KEY (or pass a custom completer=).

10 · Packaging & lifecycle Available

TaskCommand / API
Scaffold a packagelsd package new my-package
Manifestlsd_package.json (name, version, deps, entry, register)
Check the boundarylsd package lint . (errors on lsd.* imports)
Install / enable / disablelsd package install · enable · disable
Add a folder you're editingdesktop Packages → Add local… — registered live (by reference, not copied) and pinned to the project
Seal & signlsd package pack → a signed .lsdpkg
Publish to the marketplacelsd package publish (lint + seal) → register, then users lsd market install <name>
Find the installed runtimefrom lsdtools import find_runtimeLSDCli.exe
Describe the API (editors / AI)lsd api describe --json · from lsdtools import describe_api
Write an AI context filelsd api context --out LSD_CONTEXT.md (the desktop also refreshes it on “Open in VS Code”)
One rule. Import only from lsdtools; never lsd.*. lsd package lint enforces it (CI-friendly). See the Quickstart.

Roadmap — what's proposed Proposed

The plan, distilled from the gaps above. Review and we'll prioritize, then implement.

ItemWhy
Custom param widgets in panelsDeclarative panels now cover the common cases (see §6). Next: pluggable custom controls (color picker, query builder…) for both panels and the inspector. panels shipped
On-screen notificationsA desktop toast for host.notify() (it records to the log today). Host services are otherwise wired. recently shipped
Custom parameter widgetsPublic registry so a package can supply a control for a custom widget= hint (color picker, query builder, …).
Typed editor stubslsd api describe --json + lsd api context (Markdown cheat-sheet) ship. Next: a generated .pyi for inline editor type-checking. describe + context shipped
Inline AI iterationStep generation ships (§9: lsdtools.generate_step / lsd ai / Develop → Generate Step with AI). Next: in-place edit/refine of an existing step and auto-wiring it into the pipeline. generate shipped
Toolkit-agnostic views beyond panelsLonger-term: editor surfaces that aren't tied to GTK at all.

← Back to the Quickstart · Create an account →