Every entity needs an identity and a set of fields. Identity is a field carrying the pk flag; fields are a name, a type, and a small closed set of modifiers. The modifiers are declarative — they describe what a field is, and the platform turns that into column types, constraints, indexes, and validation.
Identity
The near-universal identity is a UUID primary key:
id uuid pk filterable[eq]pk marks the primary key; filterable[eq] says the field can be filtered by equality in a query. You never author proto field numbers or column order — identity is just a field with a flag.
Natural keys — a business identifier like a SKU or an order code — are modeled with unique, not pk. Chowk's product keeps a UUID id and a unique sku:
entity Product aggregate_root global {
doc "A sellable product in Chowk's catalog."
systems [CatalogSystem]
audit timestamps
indexes [
{ name "uk_product_id" kind unique fields [id] }
{ name "uk_product_sku" kind unique fields [sku] }
]
id uuid pk filterable[eq]
sku string required immutable filterable[eq] specs [Sku]
name string required
price Money required
status ProductStatus required
}The surrogate id is the stable internal identity; the sku is the human-facing natural key, kept unique by an index and shaped by a spec.
The field vocabulary
A field is <name> <type> [modifiers…]. The type is a built-in scalar (uuid, string, text, bool, int32, int64, timestamp, date, decimal, …), or the name of an enum or a value object (like Money). Append ? to make it optional.
The modifiers are a closed set — a fixed vocabulary, not free-form annotations:
| Modifier | Meaning |
|---|---|
pk | primary key |
required | must be present |
immutable | write-once — set at creation, never updated |
unique | a single-column uniqueness constraint (a natural key) |
filterable[eq, …] | queryable, with the allowed operators in brackets |
sortable | orderable in list queries |
specs [Name, …] | attach reusable field-shape rules (see Field specs) |
= <default> | a default value (a literal, or a bare enum member) |
relates <Entity> … | a foreign-key relationship (see Aggregates and entities) |
Reading a real spread, every modifier is a small declarative fact about the column:
id uuid pk filterable[eq]
customer_id uuid required immutable filterable[eq]
status OrderStatus required
sku string required immutable filterable[eq] specs [Sku]customer_id is required, set once, and filterable — a caller can list orders by customer but never move an order to a different one. sku adds specs [Sku], attaching a shape rule. Because the vocabulary is closed, a field declaration is fully checkable: the compiler knows exactly what each word means and generates the column, constraint, and index to match. There is no annotation whose effect you have to guess.
Fields flow outward automatically
You declare a field once, on the entity, and it propagates: the table column, the create/update command inputs that carry it, the query filters (filterable) and sort keys (sortable), the wire types, and the client-side validators are all derived from it. Change required to optional, or add a spec, and every layer re-derives. The field declaration is the single source of truth for that piece of data everywhere it appears.