Entities & fields
Every entity becomes a table, a generated data-access layer + API, and a UI page. Primary keys are integers; composition is opt-in.
entities:
- name: Customer # PascalCase entity name
description: Buyer account
icon: user # an icon name for the generated navigation
group: master-data # navigation group in a shared shell
audit: true # adds CreatedAt / CreatedBy / UpdatedAt / UpdatedBy
fields: [ ... ]
relations: [ ... ]fields
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: name, type: string, required: true, length: 200 }
- { name: total, type: decimal }
- { name: active, type: boolean, defaultValue: "true" }| Key | Meaning |
|---|---|
name | field name, camelCase (PascalCased in the generated model) |
type | logical type (see below) |
primaryKey | marks the PK; must be an integer type |
generated | auto-increment (integer PKs only) |
required | NOT NULL; the generated required-value validation keys on this |
length | column length for string types |
defaultValue | column default |
unique | a UNIQUE constraint (e.g. a code or business key) |
precision / scale | override the decimal default (16, 2) |
readOnly | rendered read-only in the UI (e.g. a calculated total) |
major: false | keep the column off the compact list table (still on the detail page) |
size | form control width on a 12-column grid |
calculatedOnCreate / calculatedOnUpdate | an expression assigned to the property on insert / update |
calculatedActionOnCreate / calculatedActionOnUpdate | a server-side action call-out (see Calculated fields) |
number | turn a string field into a platform-numbered document field (see Document numbering) |
sensitive | strip this field from scoped (personal / partner) surfaces (see Scoped surfaces) |
Logical types
string, text, integer, int, long, decimal, double, boolean, date, timestamp, uuid, month, week.
Generators map each logical type to a physical column type. text is a large-object column; uuid is a 36-character string. month (a YYYY-MM value) and week (a YYYY-Www ISO-week value) are stored as short strings and render as month / week pickers.
Normative
Primary keys must be an integer type (integer / int / long). A non-integer auto-increment column is invalid, so a uuid or string primary key is rejected. uuid is valid for non-PK fields.
Entity-level attributes
| Attribute | Effect |
|---|---|
audit: true | adds the four standard audit columns, populated automatically |
multilingual: true | makes string properties translatable (see multilingual data) |
label: | a stored, read-only display name (see label) |
function: | an explicit presentation role (see function) |
order: | sequences form controls and list columns |
duplicable: true | adds a Duplicate button that clones a document through the normal create path |
imports: | injects import lines into the generated data-access layer (pairs with calculated actions) |
aggregate: true | on a document master's numeric field, keeps it equal to the sum of the items' same-named field |
kind: setting | marks the entity as nomenclature / configuration (see Setting entities) |
Control order
By default the generated UI controls follow declaration order - all fields first, then to-one relations last. Give an entity an order: list of property names to sequence them explicitly, interleaving fields and relations for a better layout:
- name: OrderItem
order: [Id, Order, Product, Name, Quantity, UoM, Price, Total]
fields: [ ... ]
relations: [ ... ]Names match field / relation names (case-insensitive). A partial order is fine - any property not listed keeps its default position and is appended after the listed ones.
Calculated fields
A field value can be derived instead of entered:
calculatedOnCreate/calculatedOnUpdate— an expression assigned to the property. Prefer a neutral arithmetic expression for numeric totals ("Quantity * Price","round(Net * 0.2, 2)"): the server evaluates it and the UI previews it live with the same evaluator. Date helpers such asdaysBetween,businessDaysBetweenandmonthsBetweenare available.
- { name: net, type: decimal, calculatedOnCreate: "Quantity * Price", calculatedOnUpdate: "Quantity * Price" }
- { name: days, type: decimal, readOnly: true, calculatedOnCreate: "businessDaysBetween(FromDate, ToDate)" }calculatedActionOnCreate/calculatedActionOnUpdate— a server-side call-out for logic beyond an expression. The value names a hand-written component; the intent emits no code for it. It runs server-side only (no live preview) and takes precedence over an expression on the same slot. To reference it by simple name, declareimports:on the entity:
entities:
- name: Invoice
imports: |
import example.invoices.InvoiceBarcodeAction;
fields:
- { name: barcode, type: string, calculatedActionOnCreate: InvoiceBarcodeAction }The implementation lives in the project's custom (escape-hatch) folder, never in the generated folder. For document numbers, use the first-class number attribute instead of a calculated action.
Document numbering
number: turns a string field into a platform-numbered document field. The platform owns a gap-free sequence per series, renders it through a format, and stamps the field automatically - no hand-written number generator.
# stamped on create (the number exists the moment the record is saved):
- { name: Number, type: string, number: { series: Proforma, format: "PF{seq:08}", stampOn: create } }
# stamped at a modeled issue step (a placeholder holds the field until then):
- name: Number
type: string
number:
series: SalesInvoice # documents sharing a sequence pass the same series
format: "SI-{year}-{seq:05}" # {seq} / {seq:0N} / {series} / scope tokens {year}, {<Field>}
scope: [year] # partitions the counter; omit for one continuous sequence
stampOn: issue # create | issueseries(default: the entity name) — the sequence identity. Give several document types the same series to share one running number.format(default{series}-{seq:06}) —{seq},{seq:0N}(zero-padded),{series}, and scope tokens{year}/{<Field>}.scope—yearand/or sibling field names; partitions the counter and supplies the format's scope tokens.stampOn—createstamps the real number on insert;issueputs a placeholder on the field at create and stamps the real number when the process reaches the wired step. Stamping is idempotent - re-issuing after an amend keeps the same number.
The field is read-only in the UI. Counters are visible and adjustable in the generated application's document-numbering settings.
label — a stored display name
A stored, read-only Name recomputed on every write, so lookups and dropdowns show a meaningful label instead of a raw id:
- name: SalesInvoice
label: "{Number} - {Date|yyyy MMMM} - {Customer.name}"Tokens are the entity's own fields or one-hop to-one relation properties ({Customer.name}); |format is a date pattern for temporal values. Deeper paths are rejected — compose by referencing the related entity's own label ({Parent.Name}). It is not allowed next to an authored name field, and a token must never reference a sensitive field.
function — the presentation role
Optional, and authoritative when set; inferred from structure otherwise.
- name: SalesInvoice
function: Document # header + line items + status pill + totals
- name: SalesInvoiceItem
function: DocumentItem # its line items (no "*Item" naming needed)Entity roles: Document, DocumentItem, Master, Detail, List, Setting, Calendar. Field role: DocumentTitle. Relation role: EntityStatus (a managed status badge). Board, Gantt and Timeline are reserved and rejected until those presentations are supported.
Setting entities
- name: Country
kind: settingkind: setting marks an entity as nomenclature / configuration. It is placed under a global Settings area instead of getting its own top-level perspective, and any relation targeting it resolves its dropdown there. Settings are still real entities (own table, seeds, FK columns) — only their UI placement differs.
checks — declarative validations
Row-level and document-level validations, enforced on write / on a status transition, with an authored message:
- name: JournalEntry
checks:
- { kind: itemsMin, count: 1, status: 2, message: "An entry needs at least one line" }
- { kind: itemsSumEqual, over: [debit, credit], status: 2, message: "Debits must equal credits" }
- name: JournalEntryItem
checks:
- { kind: exactlyOne, fields: [debit, credit], message: "Exactly one of debit / credit" }exactlyOne runs on every user write; itemsMin / itemsSumEqual are gated on a status transition, so drafting stays unconstrained and a failing transition aborts with the message.
immutableWhen / immutable — user-write immutability
- name: JournalEntry
immutableWhen: "Status == 2" # while POSTED, user update / delete are rejected (join terms with ||)
- name: InvoiceSnapshot
immutable: true # append-only: a frozen copy stored when a record is finalisedimmutableWhen requires a function: EntityStatus relation; immutable: true needs none and is mutually exclusive with it. System / workflow writes stay possible — corrections to an immutable record are flow-generated reversals, never edits.
hierarchy / leafOnly — tree entities
- name: Account
hierarchy: Parent # the tree edge (a self-relation)
relations:
- { name: Parent, kind: manyToOne, to: Account }
# elsewhere - only leaf accounts are referenceable (server-enforced):
- { name: Account, kind: manyToOne, to: Account, model: accounts, leafOnly: true }The list renders as an expandable tree; the server rejects cycles and leaf-only references to a node that has children.
See also
- Relations & multi-model — how entities reference one another, within and across models.
- Data, seeds & naming — how the physical table and column names are derived.
- DSL reference — one-line lookup for every construct.