Model-Driven Entity Definition
Schema id: model-driven-entity@1
Machine-readable schema: model-driven-entity.schema.json
The canonical, plain-text representation of a Model-Driven Apps entity. This is the source of truth an entity is validated against — the same definition the designer produces, an API call submits, an AI assistant generates, and a Solution import carries. Validation is enforced identically for every origin, so a hand-written or generated file that conforms to this schema is safe to import.
Shape
A definition is a JSON object with these members:
| Field | Type | Required | Notes |
|---|---|---|---|
logicalName | string | yes | Publisher-prefixed, lower snake-case (e.g. par_invoice). |
displayName | string | yes | Singular label (e.g. Invoice). |
displayCollectionName | string | yes | Plural label (e.g. Invoices). |
icon | string | no | Icon key from the platform's curated icon set, shown next to the table in an app's navigation (§4.4). Omit for the default glyph. |
publisherPrefix | string | yes | Short lower-case prefix (e.g. par). |
primaryField | string | yes | Logical name of the primary (name) field; must appear in fields. |
primaryImage | string | no | Logical name of the record's image (§6.16) — the picture shown beside a record in grids and lookups. Must name a field of type Image in fields. Omit for a table with no picture. |
secondaryField | string | null | no | Logical name of the field shown as the record's secondary text — the smaller second line under its name wherever records are listed without a grid, such as a lookup panel (§6.13.5). Must name a field in fields that is neither the primary field nor a File/Image column. |
hierarchyParentField | string | null | no | Logical name of the Lookup that makes this table a hierarchy — the field holding each record's parent, pointing back at this same table (a category's parent category, a unit's parent unit). Must name a field in fields of type Lookup whose targetEntity is this table. It grants nothing and forces nothing: the table still renders as a grid until a view nests its records. What it is for is that being a hierarchy is a property of the data rather than of one view — a tree view's top level defaults its child link to it. |
personField | string | null | no |
constraints | array | no | Rules the database enforces, not the application (see below) — a unique column, or a period that may not overlap another in the same scope. |
fields | array | yes | One or more field definitions (see below). |
relationships | array | no | One-to-Many relationships backed by lookup fields. |
isAuditEnabled | boolean | no | Enables change auditing for the entity (Section 6.5). Defaults to false. |
states | array | no | The States a record of this table can be in (§6.14) — its lifecycle. See below. Absent in a document written before states existed, which is how an importer tells that apart from a table with none. |
reasons | array | no | The Reasons, each belonging to exactly one State. See below. |
statusValueBase | integer | no | The floor of the range this table's added State and Reason options are numbered from — the owning publisher's option-value base (§6.14). The seeded Active/Inactive sit outside it at 0 and 1: every table has them and they mean the same thing everywhere, so they need no namespace. |
nextStatusValue | integer | no | The table's status high-water mark — the next value it will hand out. Stored rather than derived from the options, for the same reason an option set's is: a removed option's number is retired, and re-deriving would hand it to the next new one while records may still hold it. |
Field
| Field | Type | Required | Notes |
|---|---|---|---|
logicalName | string | yes | Lower snake-case. |
displayName | string | yes | |
dataType | string | yes | One of Text, MultilineText, Integer, Decimal, Boolean, DateTime, Lookup, PolymorphicLookup, File, Image, OptionSet, State, Reason. |
isRequired | boolean | no | |
isPrimary | boolean | no | True for the single primary field. |
maxLength | integer | no | For Text (1–4000). |
precision, scale | integer | no | For Decimal (scale ≤ precision ≤ 38). |
targetEntity | string | conditional | Required when dataType is Lookup; the referenced entity's logical name. |
targetEntities | string[] | conditional | Required when dataType is PolymorphicLookup; the tables a value may point at, in picker order. At least two, none repeated. |
isAuditExcluded | boolean | no | Excludes the field from change auditing when the entity is audited (Section 6.5). Defaults to false. |
PolymorphicLookupfields are provisioned as two columns: the field's ownuuidcolumn holding the record's id, and a sibling<column>_typetextcolumn holding the target table's logical name. Neither carries a foreign key — a single column cannot reference several tables — so integrity is the write path's: it checks the target exists, and a target deleted afterwards reads as an unresolved reference.On the wire the two halves travel as one scalar,
<table>:<id>, so a record'svaluesstay a flat map of field logical name to scalar and the discriminator column never surfaces as a field. That is the platform's existing convention for a self-describing reference (recordfile:<id>,webresource:par_/app.js).The table's logical name is stored rather than a number, because a numeric discriminator would index into
targetEntities— so reordering the targets in the designer would silently re-point every stored row at a different table. A record's owner columns use a numeric enum instead, which is consistent rather than contradictory: their five values are platform concepts fixed in code, not a list a maker edits.Each entry in
targetEntitiesgets its own backing relationship, which is what makes the referenced table's form offer a Related tab for these records.
File/Imagefields are provisioned as a text column holding a scheme-qualified reference —recordfile:<id>for a file kept in the environment's own schema, orobjectstore:<key>for one large enough to have spilled to Object Storage. The bytes are uploaded to the field's own file endpoint rather than written through the record API, and are served under the record's own authorization. Which of the two stores holds a file is decided by its size and by nothing a maker sets.
Relationship
| Field | Type | Required | Notes |
|---|---|---|---|
logicalName | string | yes | Lower snake-case. |
type | string | yes | OneToMany. |
referencedEntity | string | yes | The "one" side. |
referencingEntity | string | yes | The "many" side (carries the lookup). |
lookupField | string | yes | The lookup field on the referencing entity that backs the relationship. |
State and Reason
A State is where a record is in its lifecycle; a Reason says which flavour of that state it is in. They travel with the table because they are part of what the maker designed.
🔴 They are not an option set. Active on an Invoice and Active on a Contact are not the same
choice, and modelling them as one shared list would make renaming one rename the other.
State:
| Field | Type | Required | Notes |
|---|---|---|---|
value | integer | yes | The stored value, preserved across an import: the source environment's records already hold it, so Dev and Test must agree about what it means. |
displayName | string | yes | The label. |
color | string | no | The colour its badge is drawn in. |
icon | string | no | The glyph on its badge. |
defaultReason | integer | no | Which of this state's reasons a record entering it gets. Absent means the first one, which is what makes the setting optional. |
Reason:
| Field | Type | Required | Notes |
|---|---|---|---|
state | integer | yes | The value of the State this reason belongs to. Keyed by value rather than by row id, because a value is what a record stores and what survives an export. |
value | integer | yes | The stored value. |
displayName | string | yes | The label. |
color · icon | string | no | As for a State. |
The containment is the point: Won and Lost are both reasons a deal is Inactive, and offering
them while it is Active would be nonsense.
Example
{
"logicalName": "par_invoice",
"displayName": "Invoice",
"displayCollectionName": "Invoices",
"publisherPrefix": "par",
"primaryField": "par_name",
"fields": [
{ "logicalName": "par_name", "displayName": "Name", "dataType": "Text", "isPrimary": true, "isRequired": true, "maxLength": 200 },
{ "logicalName": "par_total", "displayName": "Total", "dataType": "Decimal", "precision": 18, "scale": 2 },
{ "logicalName": "par_account", "displayName": "Account", "dataType": "Lookup", "targetEntity": "par_account" }
],
"relationships": [
{ "logicalName": "par_account_invoices", "type": "OneToMany", "referencedEntity": "par_account", "referencingEntity": "par_invoice", "lookupField": "par_account" }
]
}
Versioning
Older schema versions are kept alongside new ones so already-deployed solutions
keep validating after the schema evolves. When a breaking change is needed, a new
version (model-driven-entity@2) is published rather than editing this one.