A command is a write: it mutates exactly one aggregate. Validate the caller's inputs, apply the change, persist the row — that is the whole shape of a command. It names the aggregate it acts on and the operation it performs (create, update, or delete), and everything else on it either shapes the inputs or constrains the write.
Here is the simplest useful command — a public create that adds a product to the Chowk catalog:
command CreateProduct {
aggregate Product
operation create
visibility public
doc "Add a product to the catalog (born DRAFT)."
systems [CatalogSystem]
set status = expr "'DRAFT'"
sku string required specs [Sku]
name string required
price Money required
}The fields at the bottom (sku, name, price) are the command's input — the wire message a client sends. required and specs [Sku] validate that input before any row is written, so a malformed SKU never reaches the database.
set — server-owned fields
Notice that status is not an input field. It is filled by a set clause:
set status = expr "'DRAFT'"A set writes a field from a server-side expression instead of from the client. Here it guarantees every product is born DRAFT — the client cannot choose the initial status, because it never supplies it. set is how you keep control of state the caller has no business setting: initial lifecycle status, derived totals, server timestamps.
An expression can read the command's own inputs, too. Chowk's ReserveStock moves a count from available to reserved using the incoming quantity:
command ReserveStock {
aggregate StockLevel
operation update
visibility private
set available = expr "available - req.quantity"
set reserved = expr "reserved + req.quantity"
id uuid required
quantity int32 required
}guard — a per-row precondition
A command can refuse to run based on the current state of the row it is about to change. That is a guard: a boolean checked against this (the row) before the write lands. Chowk's MarkOrderPaid only pays an order that is still pending:
command MarkOrderPaid {
aggregate Order
operation update
visibility public
doc "Mark an order paid (PENDING -> PAID). Guarded, so a repeat is a no-op."
guard "status == 'PENDING'"
set status = expr "'PAID'"
id uuid required
}The guard "status == 'PENDING'" makes the operation idempotent by construction: pay a pending order and it moves to PAID; send the same request again and the guard fails, so the second attempt is a safe no-op rather than a double-charge. A guard is the write-side equivalent of a state-machine transition rule — it encodes "this change is only valid from these states."
guardversuspolicy. Aguardasks "is this write allowed given the row's state?" (evaluated overthis). Apolicyasks "is this caller allowed to run this operation at all?" (evaluated overcaller.*). They are different questions and belong in different clauses — see Policies.
emits — announcing a fact
A command can announce a domain event when it succeeds. PlaceOrder declares emits OrderPlaced, so the moment an order is opened, that fact is published for anything that cares to react:
command PlaceOrder {
aggregate Order
operation create
visibility public
doc "Open an order (born PENDING) and announce it."
emits OrderPlaced
set status = expr "'PENDING'"
customer_id uuid required
total Money required
shipping_address Address required
}The command does not know or care who listens — it just states that an order was placed. A subscription in another sub-domain picks it up. (Not every event needs an explicit emits: a lifecycle event with trigger on_update fires automatically on the matching state change. See the Domain layer's Domain events page.)
visibility — public and private commands
Every command declares its visibility:
public— exposed as an HTTP endpoint a client can call.PlaceOrder,MarkOrderPaid, andCreateProductare all public.private— reachable only from inside the system: dispatched by a subscription or invoked as a saga step, never by an outside client. Chowk'sStartFulfillmentis private because it is driven by theOrderPlacedsubscription, andReserveStockis private because the checkout saga drives it.
Marking an internal command private keeps your public API honest: the surface a client sees is exactly the set of operations they are meant to invoke, and orchestration-only commands stay out of it.
Don't declare caller-sourced fields
One input rule is easy to get wrong: never declare a field the caller is already identified by. The tenant, the acting user, the session — all of these are resolved from the authenticated request at the edge and are in scope as caller.* for guards and policies. Declaring tenant_id or caller_user_id as a command input would create a wire contract saying "the client must send this" — but the client must not; the server sources it independently. Model only the fields that are genuinely the caller's to provide.