Skip to main content

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 fields 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 ibanNumber value-archetype that requires the field's dataType to be TEXT. 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 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 class the relation leads to, by its qualified name (e.g. commons.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. It may be a relation the class declares or inherits, or a relation of the other class that points to this one.

Clients receive the relation under its REST name instead — the name it has in the data payload (data.dossier); see Publication on dashboards.

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 / cc on the commons.email archetype).

Namespace governance​

  • commons.* and system.* 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 inside commons or system. This is enforced at model-load time, as an extension of the existing system namespace guard.

Definition & loading​

Archetypes are declared on a kind: CONFIG class 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 (class commons.archetypes, kind: CONFIG).
  • 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 archetypeDefinitions across the tenant are aggregated into a registry (Map<QualifiedName, 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):

{
"version": 1,
"class": "commons.archetypes",
"kind": "CONFIG",
"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 }
]
}
]
}
PropertyDescription
archetypequalified name identifying the archetype.
kindOBJECT for an object-archetype (the only kind currently used by the pilot).
slotsThe named slots making up the contract.
slots[].nameSlot name, referenced by binding classes.
slots[].kindVALUE 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[].requiredWhether a binding must map this slot to be valid.
slots[].dataTypeFor a VALUE slot: the data type the mapped field must have. See 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) relation to commons.dossier:

"archetypes": [
{
"archetype": "commons.document",
"slots": { "title": "name", "file": "file", "summary": "abstract", "date": "date", "folder": "folder", "container": "commons.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 relation to commons.dossier:

// commons/task.json
"archetypes": [
{ "archetype": "commons.event", "slots": { "title": "title", "when": "dueDate", "container": "commons.dossier" } }
]
// commons/reminder.json
"archetypes": [
{ "archetype": "commons.event", "slots": { "title": "title", "when": "date", "container": "commons.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 commons.dossier — a relation it inherits rather than declares itself. The resolver walks the inheritance chain to find that relation, exactly as it would for a value-slot mapped to an inherited field. Because the slot carries no target-archetype requirement, container can map to a relation to any class.

Email and invoice archetypes​

Beyond the commons.document / commons.event pair, the vocabulary includes four more object-archetypes:

ArchetypeBound by
commons.emailAddresscommons.emailAccount (and its subclasses commons.emailSender, commons.emailReceiver, commons.emailCcReceiver)
commons.emailcommons.emailMessage
commons.invoiceLinecommons.invoiceLine
commons.invoicecommons.invoice
{
"version": 1,
"class": "commons.archetypes",
"kind": "CONFIG",
"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 relations to commons.emailSender / commons.emailReceiver / commons.emailCcReceiver:

// commons/emailMessage.json
"archetypes": [
{
"archetype": "commons.email",
"slots": {
"subject": "subject", "date": "date",
"from": "commons.emailSender", "to": "commons.emailReceiver", "cc": "commons.emailCcReceiver",
"bodyHtml": "bodyHtml", "bodyText": "bodyText", "headers": "headers",
"attachments": "attachments", "container": "commons.dossier"
}
}
]

Each of those relation classes (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.

Memo archetype​

commons.memo is bound by commons.memo itself — a single class serving as both the archetype definition and its own reference implementation, unlike the document/event/email/invoice archetypes above where the binding class is a separate, richer type.

{
"archetype": "commons.memo",
"kind": "OBJECT",
"slots": [
{ "name": "body", "kind": "VALUE", "required": true, "dataType": "TEXTBLOCK" },
{ "name": "date", "kind": "VALUE", "required": true, "dataType": "DATETIME" },
{ "name": "author", "kind": "RELATION", "required": false },
{ "name": "title", "kind": "VALUE", "required": false, "dataType": "TEXT" },
{ "name": "labels", "kind": "VALUE", "required": false, "multiple": true, "dataType": "LIST" },
{ "name": "important", "kind": "VALUE", "required": false, "dataType": "BOOLEAN" },
{ "name": "container", "kind": "RELATION", "required": false },
{ "name": "followUp", "kind": "VALUE", "required": false, "dataType": "DATE" }
]
}

Only body and date are required — a feed renderer needs at minimum something to show and a moment to sort on. title, labels, important, author, and followUp are all optional; a memo without a title is still valid, and labels/importance/follow-up are decoration a model may leave out entirely.

commons.memo (the class) maps these onto its own fields:

// commons/memo.json
"archetypes": [
{
"archetype": "commons.memo",
"slots": {
"body": "memo", "date": "date", "author": "commons.author", "title": "title",
"labels": "labels", "important": "important", "container": "commons.dossier",
"followUp": "followUpDate"
}
}
]

Like container, the author slot sets no target-archetype requirement — any related class qualifies. The reference binding points it at commons.author, an ABSTRACT role class on commons.person in the same pattern as commons.customer/commons.supplier on commons.invoice (see How relations add classes): linking a person as a memo's author grants them the commons.author class, and the relation is labeled "Author" rather than "Person".

Default value tokens​

The author relation also illustrates a defaulting mechanism outside the archetype system itself: its definition sets "defaultValue": "me()", a token the client resolves to the current user via /accounts/{tenantId}/me — the person the caller's profile links to. Because the default sits on the relation definition rather than on a layout item, it applies to every layout that shows the relation, including the auto-generated form. See Default value tokens for the full mechanism, and Layouts for the equivalent per-layout override.

The date slot's field similarly defaults to now(), resolved server-side, so nobody has to type the timestamp their memo already carries. With title no longer required, commons.memo's name formula falls back to the date: {{#if title}}{{title}}{{else}}Memo {{date}}{{/if}}.

Validation & resolution (model load)​

Bindings are validated and resolved after extensions have been merged. For each binding, the platform checks:

  1. The archetype exists and is an OBJECT archetype.
  2. Every required slot is mapped.
  3. Every value-slot maps to an existing field with the matching data type — fields are looked up through the full inheritance chain.
  4. Every relation-slot maps to an existing relation whose target exposes the required archetype.

One check is strict: in a file in the current definition format, a relation-slot mapped to a REST name ("container": "dossier") instead of a class name is an error that refuses the model, with a message suggesting the class name. Files in format 0 used REST names; they are converted when loaded (see the changelog).

For the checks above, 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 /classes. For each class, the published shape is the archetype id plus its resolved slot-to-field map (ClientArchetype). In the published map, a relation-slot names the relation by its REST name (dossier), the key under which clients find its value in the data payload. 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 archetypes array on the dashboard, describing the type of the object itself. Endpoint: GET /metadata/{tenant}/dataObject?classes={...}.
  • Class / related list — archetypes on the ClientLayoutItemView, inherited by ClientLayoutItemRelatedView — i.e. published for both the VIEW and RELATED_VIEW layout items. Endpoints: GET /metadata/{tenant}/dashboard/{classes} (class dashboard), and the related-object tabs within an object dashboard.

The effective archetype set for a class is the union of its own bindings and those of every class 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 both commons.document and commons.event, so documents, tasks, and reminders can all be placed under a dossier.
  • folder is the subpath within that root (e.g. "/draft documents/may 2020"), and only exists on commons.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.

Open decision affecting this doc

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)​

  • Seven object-archetypes exist: commons.document (bound by commons.document), commons.event (bound by commons.task and commons.reminder), commons.memo (bound by commons.memo), commons.emailAddress (bound by commons.emailAccount and its subclasses), commons.email (bound by commons.emailMessage), commons.invoiceLine (bound by commons.invoiceLine), and commons.invoice (bound by commons.invoice).
  • container is used by commons.document, commons.event, commons.memo, commons.email, and commons.invoice — each binding maps it to the (inherited) dossier relation, and the slot sets no target-archetype requirement.
  • Constrained relation-slots are now in active use: commons.email's from/to/cc slots and commons.invoice's lines slot each require the target to expose a specific archetype (commons.emailAddress and commons.invoiceLine respectively). 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.