Archetypes
Archetypes are a semantic type system layered on top of the data model. They let a DataClass declare what kind of thing an object represents — a document, an agenda item, an email — without the virtual database itself having any built-in notion of type meaning.
Why archetypes exist
The virtual database is deliberately generic: the metadata and business-logic layers only know about valueTypes and relations, never what a type means. That's why an object dashboard, by default, is built from generic building blocks — a title, n-1 relation links, tabs with tables for 1-n/m-n relations.
For some kinds of data that generic table view is semantically thin. A folder of documents, a list of agenda items, an inbox of emails — a user expects a recognizable, specialized view for these, not a generic table. But because a model builder is free to define their own DataClass types, the platform can't hard-code that specialized rendering against specific commons classes — it has no way to know that my-project.contract "is a document" unless something tells it so.
An archetype is that "something." It's a platform-defined contract that a class can bind to, declaring that it plays a certain semantic role.
This is explicitly not a presentation concept. Rendering is only one consumer:
- App rendering — a richer, specialized view instead of the generic table (the primary use case today).
- AI / MCP — understanding that "this object is a document / agenda item / email" so it can reason about it correctly. See AI Integrations.
- Gateways (future) — an archetype as an I/O contract, so a gateway can declare "I fill
commons.company" instead of hand-mapping dozens of individual target fields. See the company-register case below.
The generic field/table view always remains the fallback. Archetype-specific behavior is incremental and does not depend on any particular tenant's model.
Concept: archetype as a semantic contract
An archetype is a platform-defined semantic contract made up of named slots. A DataClass binds to an archetype and maps its own fields and relations onto those slots. The engine only ever knows about archetypes — never about concrete tenant types — and publishes the resolved binding so that any consumer can rely on it without needing to understand the tenant's actual model.
There are two layers:
- Value-archetypes (field level) — a semantic tag with a data-type requirement, for example an
ibanNumbervalue-archetype that requires the field'sdataTypeto beTEXT. Only needed when the raw data type doesn't already carry enough meaning on its own. Not yet implemented — see Pilot scope. - Object-archetypes (class level) — defined by a set of named slots. This is the primary mechanism and the focus of this page.
Slots
A slot is either:
- a value-slot — filled by a value type field, with a required
dataType, or - a relation-slot — filled by a relation, where the constraint on the target is structural: the target must expose a given archetype. It is not a nominal constraint ("the target must be class Y") — any class that happens to expose the required archetype qualifies, regardless of what it's called or how it inherits.
That target requirement is itself optional. A relation-slot can be defined with no archetype requirement at all, in which case any related type qualifies — see the container slot on commons.document and commons.event below.
A slot can be required or optional, and single- or multi-valued — a relation-slot for an email's to recipients, for instance, is multiple and required, while cc is multiple and optional.
In a binding, a relation-slot maps to the REST name of the target relation — the same name the relation is addressed by in the data payload (e.g. data.dossier) — rather than to a field name. The resolver looks this relation up through the class's full inheritance chain, exactly as it does for value-slots and fields.
Mapping, not provisioning
An archetype binding never injects fields into a class. The class keeps its own model exactly as defined, and simply maps its existing fields/relations onto the archetype's slots. This keeps the model builder fully in control: an existing or third-party class can satisfy an archetype without renaming any of its fields.
"archetypes": [
{
"archetype": "commons.document",
"slots": { "title": "name", "file": "file", "summary": "abstract", "date": "date", "folder": "folder" }
}
]
Here title maps to the field name — a field the class doesn't even declare itself, but inherits from commons.item (see Binding on a class).
Multiplicity
- A class can bind to multiple object-archetypes at once.
- A field can carry multiple value-archetypes.
- A slot itself can be multi-valued (e.g.
to/ccon thecommons.emailarchetype).
Namespace governance
commons.*andsystem.*are reserved for the platform — this is the stable, cross-tenant vocabulary that AI and gateways can safely build against.- A tenant may define its own archetypes, but only inside its own namespace (
my-project.*), never insidecommonsorsystem. This is enforced at model-load time, as an extension of the existingsystemnamespace guard.
Definition & loading
Archetypes are declared on a type: NONE DataClass via the archetypeDefinitions field, and loaded through the normal model-loading pipeline — which means custom models can define their own archetypes the same way commons does.
- The commons vocabulary lives in
datamodel/commons/archetypes.json(classcommons.archetypes,type: NONE). - That file is always loaded, because
commons.item— the universal base that every class reaches through inheritance — imports it. It doesn't depend on the tenant's own imports. - During model load, all
archetypeDefinitionsacross the tenant are aggregated into a registry (Map<TypeFQN, Archetype>), the same way lists and scripts are aggregated.
commons.document and commons.event each declare a container relation-slot alongside their value-slots — used to place the object under a parent dossier (see Folder hierarchy below):
{
"class": "commons.archetypes",
"type": "NONE",
"archetypeDefinitions": [
{
"archetype": "commons.document",
"kind": "OBJECT",
"slots": [
{ "name": "title", "kind": "VALUE", "required": true, "dataType": "TEXT" },
{ "name": "file", "kind": "VALUE", "required": true, "dataType": "FILE" },
{ "name": "summary", "kind": "VALUE", "required": false, "dataType": "TEXTBLOCK" },
{ "name": "date", "kind": "VALUE", "required": false, "dataType": "DATE" },
{ "name": "folder", "kind": "VALUE", "required": false, "dataType": "TEXT" },
{ "name": "container", "kind": "RELATION", "required": false }
]
},
{
"archetype": "commons.event",
"kind": "OBJECT",
"slots": [
{ "name": "title", "kind": "VALUE", "required": true, "dataType": "TEXT" },
{ "name": "when", "kind": "VALUE", "required": true, "dataType": "DATE" },
{ "name": "container", "kind": "RELATION", "required": false }
]
}
]
}
| Property | Description |
|---|---|
archetype | TypeFQN identifying the archetype. |
kind | OBJECT for an object-archetype (the only kind currently used by the pilot). |
slots | The named slots making up the contract. |
slots[].name | Slot name, referenced by binding classes. |
slots[].kind | VALUE for a value-slot, or RELATION for a relation-slot. A relation-slot may additionally require the target to expose a specific archetype; the container slot on commons.document and commons.event sets no such requirement, so any related type qualifies. |
slots[].required | Whether a binding must map this slot to be valid. |
slots[].dataType | For a VALUE slot: the ValueDataType the mapped field must have. See Value Data Types. |
Binding on a class
A class declares its bindings via the archetypes property, mapping each slot to one of its own fields or relations. Slots may map onto inherited fields and relations — a slot doesn't require the field or relation to be declared directly on the binding class.
commons.document binds commons.document, mapping title onto name (inherited from commons.item) and container onto its (inherited) dossier relation:
"archetypes": [
{
"archetype": "commons.document",
"slots": { "title": "name", "file": "file", "summary": "abstract", "date": "date", "folder": "folder", "container": "dossier" }
}
]
commons.task and commons.reminder both bind commons.event, each with its own value-slot mapping — commons.task maps when to its dueDate field, commons.reminder maps it to date — but both map container to the same inherited dossier relation:
// commons/task.json
"archetypes": [
{ "archetype": "commons.event", "slots": { "title": "title", "when": "dueDate", "container": "dossier" } }
]
// commons/reminder.json
"archetypes": [
{ "archetype": "commons.event", "slots": { "title": "title", "when": "date", "container": "dossier" } }
]
Binding a relation-slot: container
container is a good illustration of how a relation-slot binding works: it's declared directly on the commons.document and commons.event archetypes (not on a separate shared archetype), and each binding class maps it to dossier — a relation it inherits rather than declares itself. The resolver walks the inheritance chain to find dossier, exactly as it would for a value-slot mapped to an inherited field. Because the slot carries no target-archetype requirement, dossier can point at any related type.
Email and invoice archetypes
Beyond the commons.document / commons.event pair, the vocabulary includes four more object-archetypes:
| Archetype | Bound by |
|---|---|
commons.emailAddress | commons.emailAccount (and its subclasses commons.emailSender, commons.emailReceiver, commons.emailCcReceiver) |
commons.email | commons.emailMessage |
commons.invoiceLine | commons.invoiceLine |
commons.invoice | commons.invoice |
{
"class": "commons.archetypes",
"type": "NONE",
"archetypeDefinitions": [
{
"archetype": "commons.emailAddress",
"kind": "OBJECT",
"slots": [
{ "name": "address", "kind": "VALUE", "required": true, "dataType": "EMAIL" },
{ "name": "aliases", "kind": "VALUE", "required": false, "dataType": "EMAIL", "multiple": true }
]
},
{
"archetype": "commons.email",
"kind": "OBJECT",
"slots": [
{ "name": "subject", "kind": "VALUE", "required": true, "dataType": "TEXT" },
{ "name": "date", "kind": "VALUE", "required": true, "dataType": "DATETIME" },
{ "name": "from", "kind": "RELATION", "required": true, "archetype": "commons.emailAddress" },
{ "name": "to", "kind": "RELATION", "required": true, "multiple": true, "archetype": "commons.emailAddress" },
{ "name": "cc", "kind": "RELATION", "required": false, "multiple": true, "archetype": "commons.emailAddress" },
{ "name": "bodyHtml", "kind": "VALUE", "required": false, "dataType": "TEXTBLOCK" },
{ "name": "bodyText", "kind": "VALUE", "required": false, "dataType": "LONGTEXT" },
{ "name": "headers", "kind": "VALUE", "required": false, "dataType": "LONGTEXT" },
{ "name": "attachments", "kind": "VALUE", "required": false, "multiple": true, "dataType": "FILE" },
{ "name": "container", "kind": "RELATION", "required": false }
]
},
{
"archetype": "commons.invoiceLine",
"kind": "OBJECT",
"slots": [
{ "name": "description", "kind": "VALUE", "required": true, "dataType": "TEXT" },
{ "name": "quantity", "kind": "VALUE", "required": true, "dataType": "DECIMAL" },
{ "name": "unitPrice", "kind": "VALUE", "required": true, "dataType": "CURRENCY" },
{ "name": "amount", "kind": "VALUE", "required": true, "dataType": "CURRENCY" }
]
},
{
"archetype": "commons.invoice",
"kind": "OBJECT",
"slots": [
{ "name": "invoiceNumber", "kind": "VALUE", "required": true, "dataType": "TEXT" },
{ "name": "invoiceDate", "kind": "VALUE", "required": true, "dataType": "DATE" },
{ "name": "dueDate", "kind": "VALUE", "required": false, "dataType": "DATE" },
{ "name": "netAmount", "kind": "VALUE", "required": true, "dataType": "CURRENCY" },
{ "name": "taxAmount", "kind": "VALUE", "required": false, "dataType": "CURRENCY" },
{ "name": "amount", "kind": "VALUE", "required": true, "dataType": "CURRENCY" },
{ "name": "status", "kind": "VALUE", "required": false, "dataType": "LIST" },
{ "name": "customer", "kind": "RELATION", "required": true, "multiple": false },
{ "name": "supplier", "kind": "RELATION", "required": false, "multiple": false },
{ "name": "lines", "kind": "RELATION", "required": false, "multiple": true, "archetype": "commons.invoiceLine" },
{ "name": "attachments", "kind": "VALUE", "required": false, "multiple": true, "dataType": "FILE" },
{ "name": "container", "kind": "RELATION", "required": false }
]
}
]
}
Constrained relation-slots: requiring a target archetype
Unlike container, the from, to, cc, and lines slots each set an archetype requirement on the slot definition. This is the structural constraint mentioned under Slots: the target must expose the named archetype, regardless of what the target class is called or how it inherits.
commons.emailMessage binds commons.email, mapping from/to/cc onto its own emailSender / emailReceiver / emailCcReceiver relations:
// commons/emailMessage.json
"archetypes": [
{
"archetype": "commons.email",
"slots": {
"subject": "subject", "date": "date",
"from": "emailSender", "to": "emailReceiver", "cc": "emailCcReceiver",
"bodyHtml": "bodyHtml", "bodyText": "bodyText", "headers": "headers",
"attachments": "attachments", "container": "dossier"
}
}
]
Each of those relation types (commons.emailSender, commons.emailReceiver, commons.emailCcReceiver) is an ABSTRACT class inheriting from commons.emailAccount, which is where the commons.emailAddress binding actually lives:
// commons/emailAccount.json
"archetypes": [
{ "archetype": "commons.emailAddress", "slots": { "address": "emailAddress", "aliases": "emailAliases" } }
]
Because emailSender/emailReceiver/emailCcReceiver are ABSTRACT, they never exist as objects in their own right. They become concrete the moment a commons.person or commons.company is linked as the sender or a receiver on an email — the relation adds that class to the linked object (see How relations add classes), and with it the inherited commons.emailAddress binding. This is what lets the from/to/cc slots on commons.email resolve: the resolver checks that the linked object's effective archetype set includes commons.emailAddress, not that commons.emailSender itself declares a concrete binding.
commons.invoice illustrates a slot that stays unmapped: its binding maps every slot except lines — it does not currently map lines to its reverse relation from commons.invoiceLine. Since lines is optional, this is a valid binding, but it means a consumer reading the invoice's published archetype metadata will not find its line items through the lines slot today.
Validation & resolution (model load)
Bindings are validated and resolved after extensions have been merged. For each binding, the platform checks:
- The archetype exists and is an
OBJECTarchetype. - Every
requiredslot is mapped. - Every value-slot maps to an existing field with the matching data type — fields are looked up through the full inheritance chain.
- Every relation-slot maps to an existing relation whose target exposes the required archetype.
A violation is a soft warning, not a hard failure: the platform logs a warning, drops that binding, and the model continues to load. Only complete, usable bindings are published — a class with a broken binding simply falls back to the generic view for that archetype rather than blocking model load for the whole tenant.
Publication on dashboards
Archetype information is published on the dashboards, not on /types. For each type, the published shape is the archetype id plus its resolved slot-to-field map (ClientArchetype). A client reads slots — never the tenant-specific field names directly — and fetches the actual values from the data endpoint (/data/...). A slot with no value is simply omitted from the map.
- Object dashboard (self) — a top-level
archetypesarray on the dashboard, describing the type of the object itself. Endpoint:GET /metadata/{tenant}/dataObject?types={...}. - Class / related list —
archetypeson theClientLayoutItemView, inherited byClientLayoutItemRelatedView— i.e. published for both theVIEWandRELATED_VIEWlayout items. Endpoints:GET /metadata/{tenant}/dashboard/{type}(class dashboard), and the related-object tabs within an object dashboard.
The effective archetype set for a type is the union of its own bindings and those of every type it directly or indirectly inherits (deduplicated by archetype id). For a RELATED_VIEW with filterClass set (see Layouts), the inherited types of filterClass are included as well.
Published shape:
"archetypes": [
{
"archetype": "commons.document",
"slots": { "title": "name", "file": "file", "summary": "abstract", "date": "date", "folder": "folder", "container": "dossier" }
}
]
Folder hierarchy: container + folder
container and folder work together to render a folder hierarchy:
container(the relation, typically the dossier) is the folder root in a list view, and the navigation parent when viewing a single object. It's available on bothcommons.documentandcommons.event, so documents, tasks, and reminders can all be placed under a dossier.folderis the subpath within that root (e.g."/draft documents/may 2020"), and only exists oncommons.document— tasks and reminders don't have a folder subpath, only a container.
The client reads the relation's value from the data payload (data.dossier — a reference plus its display name); the metadata only ever supplies the slot-to-field mapping, never the value itself.
In the currently published shape ({ archetype, slots }), a client cannot tell from the metadata alone that container is a relation rather than a value field — it only knows this because it recognizes the commons.document / commons.event archetype ids. If it's later decided to publish the slot kind as well (to support generic MCP-style clients that don't hard-code known archetype ids), the published shape changes and this section needs to be updated accordingly.
Pilot scope (current state)
- Six object-archetypes exist:
commons.document(bound bycommons.document),commons.event(bound bycommons.taskandcommons.reminder),commons.emailAddress(bound bycommons.emailAccountand its subclasses),commons.email(bound bycommons.emailMessage),commons.invoiceLine(bound bycommons.invoiceLine), andcommons.invoice(bound bycommons.invoice). containeris used bycommons.document,commons.event,commons.email, andcommons.invoice— each binding maps it to the (inherited)dossierrelation, and the slot sets no target-archetype requirement.- Constrained relation-slots are now in active use:
commons.email'sfrom/to/ccslots andcommons.invoice'slinesslot each require the target to expose a specific archetype (commons.emailAddressandcommons.invoiceLinerespectively). See Constrained relation-slots. - Standalone value-archetypes (field-level semantic tags, as opposed to the class-level object-archetypes above) are planned but not yet defined.
- App-side rendering of archetype-specific views is a separate, later effort (in content-app).
Validation case: company-register
The company-register enrichment (registerCompany.json, a FunctionGateway) currently does a fully manual mapping of roughly 26 targets/parameters — matching canonical company-register field names to whatever the tenant's data model happens to call the equivalent fields. See Function Gateways for how targets/parameters work today.
An object-archetype commons.company — with slots for the canonical register fields — would let the gateway simply declare that it fills that archetype. The actual target fields would then follow from the class's own archetype binding, instead of being hand-wired per tenant. The boilerplate mapping disappears and the gateway becomes tenant-agnostic; action policy (OVERWRITE vs CREATE_ONLY, triggers) stays exactly where it is today, as gateway configuration.
This isn't implemented yet — it's included here because it validates the underlying design: gateways as archetype consumers, using an archetype as an I/O contract rather than a field-by-field mapping.