defineCommand in lib/app/commands/. The command carries its own checks, so the web app, a scheduled job and a future AI assistant all get exactly the same rules.
What a command declares
Risk levels
The risk level is there for the AI assistant: it may run
read commands on its own, anything above needs the person’s confirmation, and money and irreversible always come after a preview the person has seen.The checks execute() runs
Every call goes throughexecute() in lib/app/command.ts, in this order. The first check that fails stops the call.
1
The module is on
If the command belongs to a module the agency turned off, it answers
MODULE_OFF (403).2
The user has the permission
Otherwise it answers
FORBIDDEN (403). Owners have every permission; admins have all but turning modules on and off; agents have travel.use.3
The input matches the schema
Otherwise it answers
INVALID_REQUEST (400), naming the first bad field and keeping every issue.4
Presenter mode, for confidential suppliers
Inside the agency’s scope, if
confidential(input) is true and presenter mode isn’t unlocked, it answers PRESENTER_REQUIRED (403).5
Run
run(input, ctx) does the work inside the agency’s scope. ctx holds the actor, whether presenter mode is on, and who is acting: ui, job or agent.Routes are one line
A route only connects a URL to a command:app/api/cancel/route.ts
commandRoute reads the input from the body (or the query string for GET), merges in route params such as :reference, runs the command and returns JSON. Two options cover the rest: from chooses where the input comes from, and respond shapes the answer (a 201 for something created, or a PDF for a document). When one URL serves several steps, such as preview, book and cancel for a rebooking, stepRoute("step", { ... }) picks the command by a body field.
Add a command
1
Define it
Add it to the right file in
lib/app/commands/ with defineCommand, a name, a risk, an input schema and a run that calls the domain service.2
Give it a route
Add a one-line route file that calls
commandRoute (or add it to an existing stepRoute).3
Register it for screens
Add its method and path to
ROUTES in lib/client/api.ts. The boundaries test fails until every command has a route there.4
Call it from a screen
Use
api("yourCommand", input, doing). The input and answer types come from the command itself.The catalogue
lib/app/catalog.ts lists every command (COMMANDS) and finds one by name (commandNamed). It’s the list a scheduled job or an assistant uses to know what it can do and how risky each action is.