Skip to content

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.

yaml
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

yaml
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" }
KeyMeaning
namefield name, camelCase (PascalCased in the generated model)
typelogical type (see below)
primaryKeymarks the PK; must be an integer type
generatedauto-increment (integer PKs only)
requiredNOT NULL; the generated required-value validation keys on this
lengthcolumn length for string types
defaultValuecolumn default
uniquea UNIQUE constraint (e.g. a code or business key)
precision / scaleoverride the decimal default (16, 2)
readOnlyrendered read-only in the UI (e.g. a calculated total)
major: falsekeep the column off the compact list table (still on the detail page)
sizeform control width on a 12-column grid
calculatedOnCreate / calculatedOnUpdatean expression assigned to the property on insert / update
calculatedActionOnCreate / calculatedActionOnUpdatea server-side action call-out (see Calculated fields)
numberturn a string field into a platform-numbered document field (see Document numbering)
sensitivestrip 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

AttributeEffect
audit: trueadds the four standard audit columns, populated automatically
multilingual: truemakes 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: trueadds 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: trueon a document master's numeric field, keeps it equal to the sum of the items' same-named field
kind: settingmarks 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:

yaml
- 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 as daysBetween, businessDaysBetween and monthsBetween are available.
yaml
- { 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, declare imports: on the entity:
yaml
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.

yaml
# 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 | issue
  • series (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>}.
  • scopeyear and/or sibling field names; partitions the counter and supplies the format's scope tokens.
  • stampOncreate stamps the real number on insert; issue puts 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:

yaml
- 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.

yaml
- 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

yaml
- name: Country
  kind: setting

kind: 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:

yaml
- 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

yaml
- 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 finalised

immutableWhen 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

yaml
- 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

Released under the Apache License 2.0.