Skip to content

Data, seeds & naming ​

seeds ​

yaml
seeds:
  - name: order-statuses
    entity: OrderStatus
    rows:                                   # inline rows: small nomenclatures
      - { id: 1, name: DRAFT, stage: draft } # what the status MEANS to the lifecycle
      - { id: 2, name: ISSUED, stage: live }
      - { id: 8, name: CANCELLED, stage: cancelled }
      - { id: 9, name: VOIDED, stage: void }
  - name: cities
    entity: City
    rows:
      - { id: 1, name: Sofia, Country: 34 } # a foreign key by the relation's authored name (case-sensitive)
  - name: countries
    entity: Country
    file: data/countries.csv                # large sets: a developer-owned CSV in a subfolder
  - name: uoms-bg
    entity: UoM
    language: bg                            # a translation seed for a multilingual entity
    rows:
      - { id: 8, name: "Килограм" }

Generates a seed-import descriptor + CSV per seed. Two shapes:

  • rows: — inline seed data, right for small nomenclatures whose values are part of the flow (statuses, methods).
  • file: data/<name>.csv — an authored CSV under a data/ subfolder, right for bulk nomenclatures and prepopulated demo data. A foreign key is set by the relation name (Country: 34).

Normative

Row keys must match a field name, a to-one relation name (a collection has no column to set), or the stage marker below, exactly (case-sensitive). A key matching none of those is an authoring error, reported with the nearest declared name — see unrecognised keys. Accepting it would drop the column, and a dropped NOT NULL foreign key makes the import skip every row: a nomenclature that imports as zero rows, behind a fully green pipeline.

A seed with language: <code> is a translation seed: it fills the per-language values of a multilingual: true entity, carrying the base row's id plus the translatable fields only.

stage — what a status means to the lifecycle ​

A seed row of a status nomenclature (the target of a function: EntityStatus relation) may classify itself with stage, a closed vocabulary:

StageMeaning
draftNobody has issued it yet — visible to its author, not yet economically real.
liveIt counts: issued, sent, paid — anything in normal circulation.
cancelledWithdrawn before it ever became live.
voidDeliberately retired while keeping its number — out of circulation by design.

The classification exists because a status id is data, but its meaning is not: without it, "the rows that count" can only be expressed as a predicate over positional ids, repeated in every report and guard that needs it. With it, the meaning is declared once, where the nomenclature is defined, and consumers resolve it: a report's scope decides what an aggregate counts, and an event-driven create-from decides whether an already generated document still blocks its source.

Normative

stage is metadata, not data: it MUST NOT be emitted as a column of the seeded table. A row carrying stage MUST also carry the entity's primary key (the stage classifies that id). A value outside the vocabulary is an authoring error. An entity that declares its own stage property cannot be classified this way — the collision MUST be reported rather than resolved by guessing.

Status references — name, not number ​

Everywhere the file names a status — a transition's from and setStatus, a relation's init, a status-setting step's value, abortOn's status, a check's status / setStatus, immutableWhen, a lifecycle edge, a posting's event guard, a report's filter — the seeded name may be written instead of the id:

yaml
transitions:
  - { name: VoidInvoice, forEntity: Invoice, from: [ISSUED, SENT], setStatus: VOIDED, when: "Paid == 0" }
reports:
  - { name: OverdueInvoices, source: Invoice, filter: "balance > 0 AND Status != VOIDED", measures: ["sum(total)"] }

A status id is positional. Inserting a status into the middle of a nomenclature shifts every later id, and every guard authored against the old numbering keeps producing well-formed output that now means a different status — a defect no downstream check can see, because the emitted constant is valid. A name cannot be silently retargeted.

Normative

A status name is resolved against the seed rows of the nomenclature it belongs to, and the resolution happens before any other validation, so every later rule sees the resolved id. An unresolvable name is an authoring error naming the known statuses — never a silently-kept token. Numeric ids remain valid everywhere. A name has no ordering, so an ordering comparison against one (Status >= ISSUED) is an authoring error; express "the rows that count" as a scope. A nomenclature owned by another model is seeded there, so a name cannot be resolved against it — such a reference is an authoring error directing the author to the numeric id.

Multilingual data ​

Two independent things get translated: the data in multilingual entities, and the generated UI labels.

Data ​

Mark an entity multilingual: true and its string-typed properties gain per-language values in a sibling translation table. Every read overlays the translated values for the caller's requested language; untranslated content falls back to the default language. Author the translations as seeds with a language: code.

yaml
languages: [en, bg]        # top level: the languages THIS module provides translations for
entities:
  - name: UoM
    kind: setting
    multilingual: true
    fields:
      - { name: id,   type: integer, primaryKey: true, generated: true }
      - { name: name, type: string,  required: true, length: 100 }

The set of languages the whole stack supports is a platform concern, never defined per module. The top-level languages: only declares which languages this module provides.

A report reads the same data, so it reads it in the same language: a column bound to a translatable property is shown translated, whether the report is rooted at the multilingual entity or reaches it through a relation. A report grouping by a multilingual nomenclature therefore shows the same term as the pages beside it — before this was stated, a status column could read DRAFT next to a list reading the translated word, from the same record.

Normative

Every read of a translatable property of a multilingual: true entity is served in the caller's requested language, and a report column bound to such a property is such a read. What a report matches is unaffected: a report's filter:, its scope and any condition applied to it are evaluated against the stored, untranslated values — so translating content can never change which rows a report returns, only how they read. A property with no translation for the requested language, and a caller who requested none, both read the stored value.

The language of a render ​

A render — the interactive print, a versioned copy, a document or report attached to a message — has a language of its own: chosen in the print action, or declared by language: / languageFrom:. That language selects the template, and it is also the language the render's data is read in. A document names its language in its heading and must not contradict it in its body: a Bulgarian invoice reading ФАКТУРА over a payment method reading Bank transfer is one document in two languages.

The distinction matters because a render is not a read by a caller. The interactive print has one, but the reader's browsing language is not the language they just chose to print in; a copy minted by a workflow, or a document attached by a scheduled or event-driven message, has no caller at all. Those last two are the renders nobody re-reads before they leave — the archived copy an audit is answered from, and the copy the counterparty receives with no application around it.

Normative

The language a render is produced in is the requested language for every read performed to produce it, including reads made where there is no caller to ask. A render never resolves its data in a language other than the one its template was selected in.

The per-property fallback is unchanged: a property with no translation in the render's language reads its stored value, so a render language the model provides no translations for produces a document in the default language rather than an error.

What a render selects stays evaluated against the stored, untranslated values — a print template's row filter and an attached report's filter: / scope alike — so a language can change how a rendered document reads but never what it contains.

A render's language applies to that render alone: consecutive renders in different languages are each produced in their own.

Keys are not translated ​

Some string properties are keys, not labels — the column a posting's determination rule matches on, the business key an arrival's lookup resolves a relation by, a code another model refers to. Such a value identifies a row; it is not text to be read in the reader's language. Declare it translatable: false:

yaml
entities:
  - name: PostingRule
    kind: setting
    multilingual: true
    fields:
      - { name: id,           type: integer, primaryKey: true, generated: true }
      - { name: name,         type: string }                       # a label - translated
      - { name: documentType, type: string, translatable: false }  # a key - never translated

Without the marker the comparison is on a moving target: the read overlay shows the translated value, saving the row from the UI writes the translated value into the stored column, and from then on the literal the intent was authored with matches nothing. Nothing fails — the rule simply stops applying, in one language, for as long as it takes someone to read the data and notice.

Normative

A property marked translatable: false carries no per-language value: no column in the entity's translation table, never overlaid on a read, read as stored in a report column, and refused in a translation seed. The default is true, and declaring it explicitly changes nothing. The marker must be able to mean something: on an entity that is not multilingual: true, or on a property that is not string-typed, it is an authoring error rather than an accepted no-op. A property a determination rule matches on, and a property a business-key lookup resolves by, must not be translated — matching on a translated property is an authoring error naming this marker.

UI labels ​

Generation also emits a per-project translation catalogue for every generated label: entity names (a humanised singular plus a plural form), field labels, form and report names, and report column headers. The default locale is generated for you; a translator adds a sibling locale folder with the same keys. The UI renders through these keys, falling back to the baked default label for any key a locale has not translated.

Naming and tables ​

  • The top-level name: is the intent's identity. Single-file outputs are named after it; the physical table prefix is its upper-snake form.
  • Physical table names are intent-prefixed: <INTENT>_<ENTITY> in upper-snake (ORDERS_ORDER), applied consistently across the data model, reports and seed imports. This dodges reserved words and cross-project collisions in a shared schema.
  • Property names are PascalCase in the generated model (loanedOn → LoanedOn); physical columns stay UPPER_SNAKE. You author in lower camelCase.
  • A multilingual entity's translations land in a sibling <TABLE>_LANG table.

Because every table is intent-prefixed, many independent intent models share one schema without colliding — the foundation of a multi-model application.

See also ​

Released under the Apache License 2.0.