Data model reference

Attribute settings
Every field an attribute definition accepts. Changeable answers the question that costs the most to get wrong — whether you can still alter this after records exist.
| Field | Accepts | Default | Changeable | What it does |
|---|---|---|---|---|
entityTypeId | The entity type's identifier | — | No | The type this attribute belongs to. Moving an attribute between types is not an edit; it is a new attribute. |
name | 1–100 chars, letters, digits, underscore; must start with a letter | — | No | The identifier used by matching rules, validation rules, and the API. Immutable because every rule that names it would otherwise break silently. |
slug | 1–100 chars, lowercase, digits, hyphen | Derived from the name | No | URL-safe identifier. |
displayName | 1–255 chars | — | Yes | The human label shown throughout the console. Safe to change at any time. |
description | Up to 2,000 chars | — | Yes | Guidance for whoever fills this in. |
dataType | One of the 12 types below | — | Yes | The value's shape. See the note on promotion below. |
subFields | 1–40 sub-fields | — | No | The named parts of a composite. Required when the type is composite, and fixed once created. |
rdmLookupTypeId | A lookup type | — | Yes | Constrains values to that list's codes. Required when the type is lookup. |
rdmHierarchyId | A hierarchy | — | Yes | Binds values to a level of a leveled hierarchy. Moves together with the level below. |
rdmHierarchyLevelOrder | Integer, 1 or greater | — | Yes | Which level of that hierarchy the value sits at. |
valueTypeLookupId | A lookup type | — | Yes | Supplies the per-value usage types (home, work, mobile). Distinct from rdmLookupTypeId: that one constrains the value, this one constrains the usage type attached to each value. |
isRequired | true or false | false | Yes | A required attribute blocks record creation when absent. |
isUnique | true or false | false | Yes | No two records of this type may share a value. |
isSearchable | true or false | true | Yes | Recorded intent only — see the note below. |
isPii | true or false | false | Yes | Recorded intent only — see the note below. |
defaultValue | Any value of the declared type | — | Yes | Applied when a record arrives without one. |
displayOrder | Integer, 0 or greater | 0 | Yes | Ordering within its group on entity screens and forms. |
attributeGroup | 1–100 chars, free text | — | Yes | The named section this appears under. Free text rather than a fixed list, so groups emerge per tenant. |
maxCardinality | Integer, 0 or greater | 1 | Yes | How many values one record may hold. See the warning below. |
metadata | Arbitrary object | {} | Yes | Your own annotations. The platform stores and returns them without interpreting them. |
Data types
string · number · integer · decimal · boolean · date · datetime ·
email · phone · json · lookup · composite
dataType is editable, which is not what most schema systems allow. The
intended use is promotion: an attribute that started as free-text string can
become a lookup bound to a controlled list once you have the reference data
for it — a diagnosis code moving from typed-in text to a bound clinical
terminology, for instance.
The binding rules are re-checked against the resulting state, so a promotion that would leave the attribute inconsistent is refused at that moment.
maxCardinality does not remove existing valuesThe limit is enforced when a value is written, not retroactively. Lower it from 3 to 1 and records that already hold three values keep all three; only subsequent writes are rejected.
So a reduction is not a cleanup. If you need the extra values gone, remove them deliberately — otherwise the model claims single-value while the data is not.
isSearchable and isPii do not yet change behaviourBoth flags are stored on the definition and returned by the API, and neither is currently acted upon.
- Turning
isSearchableoff does not remove the attribute from attribute-scoped search. Search resolves attributes by name; it never consults this flag. - Setting
isPiidoes not mask, redact, or restrict anything. Masking is driven entirely by field masking rules, which are configured separately and do not read this flag.
Use them to record intent, and treat them as documentation for your own team. Do not rely on either as a control. If you need an attribute hidden, write a masking rule; if you need it out of search, it currently cannot be excluded.
Composite attributes
A composite holds named sub-fields rather than one scalar. Matching can target a sub-field with dotted notation.
Each sub-field accepts:
| Field | Accepts | Default | What it does |
|---|---|---|---|
name | 1–100 chars, camelCase, must start with a letter | — | The identifier used in dotted paths, as in address.postalCode. |
displayName | 1–255 chars | — | The label shown for this part. |
dataType | One of the ten scalar types | — | The part's shape. |
isRequired | true or false | false | Whether this part must be present. |
maxLength | 1–10,000 | — | Length ceiling for text parts. |
format | Up to 100 chars | — | A format hint for this part. |
rdmLookupTypeId | A lookup type | — | Constrains this part to a controlled list — a country part bound to ISO 3166, for example. |
A composite may hold between 1 and 40 sub-fields.
A sub-field's type is drawn from the ten scalar types only — json and
composite are excluded. So an address may contain a postal code, but it
cannot contain another composite. One level, deliberately: nested composites
have no sensible dotted path and no comparator that could score them.
| Rule | Behaviour |
|---|---|
| A sub-field that does not exist | Rejected when the profile is saved |
| A scalar comparator on a whole composite | Rejected — two different composites would compare as equal |
| A composite-aware comparator on a whole composite | Allowed |
| A non-object value written to a composite attribute | Rejected, at both the API and the database |
The rejection of scalar comparators on whole composites is not a convenience check. Without it, two entirely unrelated addresses compare as equal and the entity type over-merges catastrophically.
Multi-value attributes
| Behaviour | Detail |
|---|---|
| Storage | Several values per attribute, each optionally carrying a usage type |
| Preferred | At most one preferred value per usage type on the golden record |
| Matching | Values are enumerated; the best-scoring pairing is taken |
| Blocking | Each value emits its own key |
| Survivorship | Grouped by usage type; each resolves independently |
Relationship types
| Setting | Purpose |
|---|---|
| Source and target entity types | What the relationship links |
| Cardinality | How many links are permitted per endpoint |
| Display name and inverse display name | So the link reads correctly from either end |
| Symmetric | Whether direction is meaningful |
| Acyclic | Whether cycles are rejected |
Acyclic matters for hierarchies: without it, an ownership chain can close into a loop that no rollup can resolve.
Validation on the model
Referenced configuration must exist and be active when you save. A profile naming an attribute that does not exist, or a rule naming a retired lookup type, is rejected at that moment rather than failing silently later.
This is the general contract: configuration that could not work is refused while you are looking at it.
Next
Last verified against commit a0765982 (2026-08-03)