Layouts
Layouts define the UI structure of a class — which fields, relations, actions, and navigations are shown, and in what order. Each layout has a kind that determines where it is used.
Definition
{
"class": "my-project.employmentAgreement",
"kind": "OBJECT_DASHBOARD",
"label": "LABEL:contractDetails",
"tabPage": false,
"items": [...]
}
| Property | Type | Description |
|---|---|---|
class | QualifiedName | The class this layout applies to. |
name | string | Identifier for named layout variants (selected via ?name=xxx). |
label | string | Label key for the layout's title (tab or section). See Label keys. |
kind | LayoutKind | Determines where this layout is rendered. See Layout kinds. |
items | LayoutItem[] | Ordered list of items to display. |
tabPage | boolean | When true, this layout is rendered as a tab; when false (default), as the main section. |
order | integer | Controls the display sequence of panels and tabs. On OBJECT_FORM_DASHBOARD, layouts with the same order are presented as alternatives to choose from. Omit to preserve declaration order. |
condition | string | Formula expression evaluated against the current form state. The layout is hidden when the expression evaluates to false. Only meaningful on OBJECT_FORM_DASHBOARD. Omit to always show. |
create | boolean | Whether this layout is shown when creating a new object. Default: true. Only meaningful on OBJECT_FORM_DASHBOARD. |
modify | boolean | Whether this layout is shown when editing an existing object. Default: true. Only meaningful on OBJECT_FORM_DASHBOARD. |
Layout kinds
| Kind | Description |
|---|---|
OBJECT_DASHBOARD | Main detail view for a single object. Usually has a main section and optional tab layouts for related objects. |
RELATED_OBJECT | Compact inline detail panel when an object appears as a related item. |
OBJECT_FORM_DASHBOARD | Inline creation form for a new object. |
GENERAL_DASHBOARD | Dashboard not tied to a specific object (e.g. a home or search screen). Supports named variants via name. |
CLASS_DASHBOARD | List/table overview for all objects of a class. |
NAVIGATION_PANE | Items shown in the sidebar navigation. Usually contains NAVIGATE items. |
Label keys
Labels used in layouts and layout items (headers, tabs, button captions, static text) are label keys, resolved to translated text. The format is PREFIX:name, for example LABEL:contractDetails or CLASS_PLURAL:commons.document.
| Prefix | Resolves to |
|---|---|
LABEL:key | A general translation key from generalLabels in the class translation. |
CLASS_SINGULAR:class | Singular name of the named class, e.g. CLASS_SINGULAR:commons.document. |
CLASS_PLURAL:class | Plural name of the named class. |
FIELD_LABEL:field | Label for the named field. |
LIST_VALUE:list:key | Label for a standard list item. |
PLAIN_TEXT:text | Literal text, no lookup performed. |
BLANK | Empty string. |
Strings without a recognized prefix are treated as PLAIN_TEXT. Inline HTML is also supported.
Merging layouts across classes
When more than one class contributes a layout of the same kind for the same object — most commonly a commons class and an EXTENSION that targets it — the layouts are combined instead of shown separately. A panel (tabPage: false) merges into the object's main section; a tab (tabPage: true) merges into an existing tab when its label matches another contributing layout's label exactly. If the labels differ, the layouts stay as separate tabs.
This makes a layout's label double as the merge identity for a tab — it's the value an EXTENSION or another class must reuse to land its own fields inside an existing shared tab, rather than opening a second one next to it.
Overriding a shared tab label
Because label is the merge identity, changing it to rename a shared tab splits it into two tabs instead of renaming the original. And a bare, prefix-less label resolves as literal PLAIN_TEXT (see Label keys) with no translation lookup at all, so there is nothing to override.
To keep a shared tab's label overridable, the class that first declares it should use a LABEL: key with a matching generalLabels entry, rather than a plain-text string:
{
"kind": "OBJECT_DASHBOARD",
"tabPage": true,
"label": "LABEL:registrationDetails",
"items": [ ... ]
}
"translations": [
{
"language": "en",
"generalLabels": {
"registrationDetails": "Registration Details"
}
}
]
Another class can then override just the displayed text — without touching label, so the merge stays intact — by declaring its own translation scoped to the class the tab belongs to, using the class property described in Translations:
{
"version": 1,
"class": "my-project.companyExtension",
"kind": "EXTENSION",
"extends": "commons.company",
"translations": [
{
"language": "en",
"class": "commons.company",
"generalLabels": {
"registrationDetails": "Company Registration"
}
}
]
}
The class scope must match the class the merged tab belongs to (here commons.company), not the extension's own class — that's what resolves the override against the tab's LABEL:registrationDetails key.
Deterministic item order in a merged panel
Every layout item accepts an optional order (integer), separate from the layout-level order used for multi-step forms. It controls where an item lands relative to items contributed by other classes to the same merged panel or tab.
Absent order behaves as 0 — the same convention CSS flexbox's order property uses. Items without an explicit order keep their position relative to each other, so a layout where nothing declares order is unaffected by merging. A negative value moves an item toward the front of the merged panel, a large positive value toward the back:
{ "name": "priorityFlag", "order": -1 }
{ "name": "internalNote", "order": 100 }
Use this when your own class contributes a field to a tab that a commons class or another EXTENSION already fills, and that field needs to reliably appear first or last regardless of which other classes also contribute to the tab.
Layout items
Each item in items has a type that determines its structure. When type is omitted, FIELD is assumed.
Layout-level defaultValue always takes priority over the schema-level default: the field's defaultValue for FIELD items, RelationDefinition.defaultValue for RELATION items.
Every kind of item also accepts an optional order (integer) used to position it within a merged panel or tab.
FIELD
Displays a field.
{ "name": "jobTitle" }
{ "type": "FIELD", "name": "salary", "header": true }
| Property | Description |
|---|---|
name | The field name to display. |
header | When true, renders this field as the main title of the panel. |
defaultValue | Pre-filled value when the form opens for a new object. Accepts a static value (string, number, or boolean) or a token: today() for the current date, now() for the current date and time. |
showEmpty | When true, keeps the field visible on an OBJECT_DASHBOARD even when it has no value. Default: false — fields without a value are hidden. |
{ "type": "FIELD", "name": "startDate", "defaultValue": "today()" }
{ "type": "FIELD", "name": "hoursPerWeek", "defaultValue": 40 }
Use showEmpty for fields where the absence of a value is itself meaningful and should be visible at a glance, rather than the field silently disappearing from the dashboard:
{ "type": "FIELD", "name": "terminationDate", "showEmpty": true }
HEADER
A section heading.
{ "type": "HEADER", "label": "Employment details" }
| Property | Description |
|---|---|
label | The heading text, a LabelKey string, or inline HTML. |
ACTION
A button that triggers a script, opens a report, or acts on the current object.
{ "type": "ACTION", "name": "sendInvitation", "label": "Send invitation", "actionType": "SCRIPT" }
{ "type": "ACTION", "name": "invoicePDF", "label": "Print invoice", "actionType": "REPORT" }
{ "type": "ACTION", "label": "Delete", "actionType": "REMOVE" }
{ "type": "ACTION", "label": "Edit", "actionType": "MODIFY" }
{ "type": "ACTION", "label": "Merge", "actionType": "MERGE" }
| Property | Description |
|---|---|
name | The script or report name to invoke. Not required for REMOVE, MODIFY, or MERGE. |
label | Button caption or LabelKey string. |
actionType | What happens when the button is clicked. Defaults to SCRIPT when omitted. |
actionType values:
| Value | Description |
|---|---|
SCRIPT | Invokes the named script on the current object. Default when actionType is omitted. |
REPORT | Opens the named report for the current object in a new browser tab. See Reports. |
REMOVE | Deletes the current object. name is ignored. |
MODIFY | Opens the edit form for the current object. name is ignored. |
MERGE | Opens the merge screen for the current object. name is ignored. |
NAVIGATE
A navigation link to another class or screen.
{
"type": "NAVIGATE",
"class": "my-project.invoice",
"navigationKind": "CLASS_DASHBOARD",
"icon": "receipt"
}
| Property | Description |
|---|---|
class | The class to navigate to. Optional — see Omitting class below. |
relatedClass | Optional. Creates an object of relatedClass instead of class, linked to the current object through the relation relatedClass declares against class — see Creating a related object the current object isn't yet eligible for below. |
navigationKind | Which page to open. See the navigation kinds below. |
icon | Optional icon identifier. |
label | Optional LabelKey string for the displayed text. Takes priority over the class-derived fallback below. |
name | Selects a named general dashboard when navigationKind is DASHBOARD. This is a dashboard selector, not a display label — it has no effect on the rendered text. |
navigationKind values:
| Value | Description |
|---|---|
CLASS_DASHBOARD | Opens the list/table page for the target class. |
OBJECT_FORM_DASHBOARD | Opens an inline creation form for the target class (or relatedClass, when set). |
DASHBOARD | Opens a general dashboard, optionally named via name. |
Label resolution
When label is omitted, the displayed text falls back to CLASS_PLURAL:{class} — the plural name of class — and to BLANK when neither label nor class is set. Set label explicitly whenever the item's text shouldn't be tied to a single class's plural, for example a sidebar grouping that spans several classes, or an entry that opens the default dashboard.
Omitting class
class can be omitted entirely when navigationKind is DASHBOARD and name is not set — this opens the default, unnamed general dashboard without requiring an arbitrary anchor class:
{
"type": "NAVIGATE",
"navigationKind": "DASHBOARD",
"label": "LABEL:home",
"icon": "home"
}
Without a class, no plural fallback is available for the label, so declare label explicitly — otherwise the item renders with BLANK text.
Creating a related object the current object isn't yet eligible for
relatedClass, combined with navigationKind: OBJECT_FORM_DASHBOARD, opens a creation form for relatedClass linked to the current object through the relation relatedClass declares against class — even when the current object doesn't hold class in its type set yet, as long as it's structurally eligible for it (see How relations add classes).
{
"type": "NAVIGATE",
"class": "legalmanager.prospect",
"relatedClass": "legalmanager.salesDossier",
"navigationKind": "OBJECT_FORM_DASHBOARD",
"label": "PLAIN_TEXT:Add sales dossier",
"icon": "briefcase"
}
Placed on legalmanager.company's dashboard, this offers "Add sales dossier" even on a company that isn't a legalmanager.prospect yet. Submitting the form creates the sales dossier linked to the company through the relation salesDossier declares against prospect, and grants the company the prospect class as a side effect — no separate step to promote the company to a prospect first.
Without relatedClass, a NAVIGATE item can only open a form for class itself, so this action could only be offered once the current object already held prospect — which is exactly the gap relatedClass closes. Retrieving the created object (or the company, now carrying prospect) relies on the structural-eligibility fallback on GET.
RELATION
An editable relation field.
{ "type": "RELATION", "relatedClass": "commons.company", "editBehavior": "SEARCH" }
| Property | Description |
|---|---|
relatedClass | The class of the related object. |
editBehavior | UI widget for selecting objects: SEARCH (free-text), DROPDOWN, or CHECKBOX (for multiple with few options). |
defaultValue | Pre-filled relation when the form opens for a new object. Use the token me() to resolve to the currently logged-in user (fetched from /accounts/{tenantId}/me — the person the caller's profile links to). |
showEmpty | When true, keeps the relation visible on an OBJECT_DASHBOARD even when no object is linked. Default: false — relations without a value are hidden. |
The me() token is useful when a relation should default to the person who creates the record — for example, assigning a task or project to yourself:
{
"type": "RELATION",
"relatedClass": "commons.person",
"editBehavior": "SEARCH",
"defaultValue": "me()"
}
defaultValue may also be set on the RelationDefinition itself, in which case it applies to every layout showing the relation, including auto-generated ones — a layout item's own defaultValue overrides it. commons.memo's author relation is the reference example; see Memo archetype.
RELATED_OBJECT
Displays a related object as an inline sub-panel.
{ "kind": "RELATED_OBJECT", "relatedClass": "commons.person", "name": "compact" }
| Property | Description |
|---|---|
relatedClass | The class of the related object. |
name | Selects a named layout variant on the related class (omit for default). |
RELATED_VIEW
Displays a filtered list of related objects, typically as a tab.
{
"type": "RELATED_VIEW",
"fromClass": "my-project.project",
"relatedClass": "my-project.task",
"showTitle": true
}
| Property | Description |
|---|---|
fromClass | The class the relation originates from (the current object's class). |
relatedClass | The class of the related objects to list. |
filterClass | When set, restricts the list to this subclass of relatedClass. |
showTitle | Whether to show a section heading. |
name | Named view on relatedClass to use (omit for default). |
actions | Toolbar actions available on the list. Supported values: "NEW" (add button) and "EXPORT" (export button). Defaults to ["NEW", "EXPORT"] when omitted. |
alternativeViews | Optional list of additional named views the user can switch to. Same structure as for VIEW. class defaults to relatedClass when omitted. |
navigateBehaviour | How clicking a row opens its target. PAGE (default) navigates to a full page; OVERLAY_PAGE opens the target as an overlay on top of the current page. See Navigate behaviour. |
SEARCH
An embedded search widget for a class.
{ "type": "SEARCH", "class": "commons.person", "showTitle": true }
| Property | Description |
|---|---|
class | The class to search within. |
showTitle | Whether to show a section heading. |
TEXT
A static text block.
{ "type": "TEXT", "label": "This section shows employment history." }
| Property | Description |
|---|---|
label | Text content, a LabelKey string, or inline HTML. |
VIEW
Embeds a named or default View for a class.
{ "type": "VIEW", "class": "my-project.task", "name": "openTasks", "showTitle": true }
| Property | Description |
|---|---|
class | The class whose view is rendered. |
name | The named view to embed (omit for the default view). |
showTitle | Whether to show a section heading. |
actions | Toolbar actions available on the view. Supported values: "NEW" (add button) and "EXPORT" (export button). Defaults to ["NEW", "EXPORT"] when omitted. |
alternativeViews | Optional list of additional named views the user can switch to. When present, a dropdown is rendered next to the filter button. |
navigateBehaviour | How clicking a row opens its target. PAGE (default) navigates to a full page; OVERLAY_PAGE opens the target as an overlay on top of the current page. See Navigate behaviour. |
Each entry in alternativeViews:
| Property | Description |
|---|---|
name | The named view to load when selected. |
label | Optional display text (LabelKey string or literal). Falls back to name when omitted. |
class | Optional class override. Defaults to the item's class when omitted. |
{
"type": "VIEW",
"class": "legalmanager.clientAgreement",
"alternativeViews": [
{ "name": "inactiveAgreements", "label": "Inactive" },
{ "name": "archivedAgreements", "label": "Archived" }
]
}
SOURCE
A search widget that calls a producer script and lets the user pick a result. On selection, either an actor script is invoked or the selected fields are used to prefill the form.
{
"type": "SOURCE",
"sourceName": "CompanySearch",
"parameters": [
{ "name": "countryCode", "field": "country.countryCode" }
],
"script": "importCompany"
}
| Property | Description |
|---|---|
sourceName | Name of the producer script to call — matches {sourceName} in POST /sources/{tenantId}/{sourceName}. |
parameters | List of { name, field } entries. field is a dot-notation path into the current form state (e.g. "country.countryCode"). |
script | GENERAL-scope actor script to invoke when the user selects a result. The selected result's fields are passed as script parameters. Mutually exclusive with fieldMapping. |
fieldMapping | Maps result fields to form fields for client-side prefill on selection (e.g. { "registrationNumber": "kvkNumber" }). Mutually exclusive with script. |
A SOURCE item is only useful on an OBJECT_FORM_DASHBOARD. Use script when the selection should trigger server-side logic (create/update, full data enrichment). Use fieldMapping when you just want to prefill a few fields and let the user review before submitting.
Navigate behaviour
VIEW and RELATED_VIEW items support a navigateBehaviour property that controls what happens when a row is clicked.
| Value | Description |
|---|---|
PAGE | Navigates to a full page for the target object. Default when navigateBehaviour is omitted. |
OVERLAY_PAGE | Opens the target object as an overlay on top of the current page, without leaving the list. |
OVERLAY_PAGE is intended for cases where a full page navigation is disruptive to the surrounding context — for example, a document list where the user wants to quickly preview a file, or line items on an invoice that are more natural to view and edit in place than as a separate page.
{
"type": "RELATED_VIEW",
"fromClass": "my-project.invoice",
"relatedClass": "my-project.invoiceLine",
"navigateBehaviour": "OVERLAY_PAGE"
}
For commons.document and other file-backed classes, combining OVERLAY_PAGE with a FILE-typed field lets the overlay render an inline preview of the file. See Files for how the API determines whether a download URI is served inline or as an attachment.
View-level fallback
A View can also declare its own navigateBehaviour. This is used as a fallback when an embedding VIEW or RELATED_VIEW layout item does not declare navigateBehaviour itself — useful when a view should always open the same way, regardless of how many layout items embed it.
Resolution order:
- The layout item's own
navigateBehaviour, if set. - The embedded view's
navigateBehaviour, if set. PAGE.
{
"name": "openInvoices",
"defaultView": false,
"navigateBehaviour": "OVERLAY_PAGE",
"values": [
{ "name": "invoiceNumber" },
{ "name": "amount" }
]
}
With the view above, any VIEW or RELATED_VIEW layout item that embeds openInvoices opens rows as an overlay without needing to repeat "navigateBehaviour": "OVERLAY_PAGE" on every embed:
{ "type": "VIEW", "class": "my-project.invoice", "name": "openInvoices" }
Multi-step forms
The order and condition properties turn an OBJECT_FORM_DASHBOARD into a guided multi-step flow. Each layout in the form is a step; steps are shown in order sequence as the user fills in the form.
Rules:
- Layouts without
orderare shown in declaration order after any ordered layouts. - When a step has only one layout and its condition is met, it is shown directly.
- When a step has multiple layouts and all their conditions are met, they are shown as alternatives (the user picks one).
- A layout is hidden when its
conditionevaluates tofalse.
Example: add a company via register or manually
"layouts": [
{
"kind": "OBJECT_FORM_DASHBOARD",
"order": 0,
"label": "LABEL:chooseCountry",
"items": [
{ "type": "RELATION", "relatedClass": "commons.country", "editBehavior": "DROPDOWN" }
]
},
{
"kind": "OBJECT_FORM_DASHBOARD",
"order": 1,
"condition": "country.countryCode == 'nl' || country.countryCode == 'gb'",
"label": "LABEL:searchInRegister",
"items": [
{
"type": "SOURCE",
"sourceName": "CompanySearch",
"parameters": [
{ "name": "countryCode", "field": "country.countryCode" }
],
"script": "importCompany"
}
]
},
{
"kind": "OBJECT_FORM_DASHBOARD",
"order": 1,
"condition": "country != null",
"label": "LABEL:companyDetails",
"items": [
{ "name": "name", "header": true },
{ "name": "kvkNumber" },
{ "name": "city" },
{ "name": "postalCode" }
]
}
]
| Step | Situation | Shown |
|---|---|---|
| 0 | Always | Country picker |
| 1 | Unsupported country selected | Manual entry form only |
| 1 | Supported country (nl/gb) | Both alternatives: "Search in register" and "Manual entry" |
When the user searches in the register and selects a result, the importCompany script runs and handles the create-or-update logic server-side. The form does not need a separate review step.
Create vs. modify forms
By default an OBJECT_FORM_DASHBOARD layout is shown for both creating and editing an object. Use the create and modify flags to restrict a layout to one operation.
| Flag | Default | Effect when false |
|---|---|---|
create | true | Layout is hidden when creating a new object |
modify | true | Layout is hidden when editing an existing object |
Example: a field only settable on create
An invitation token should only appear on the create form — once the object exists it cannot be changed.
"layouts": [
{
"class": "my-project.invitation",
"kind": "OBJECT_FORM_DASHBOARD",
"modify": false,
"items": [
{ "type": "FIELD", "name": "token" },
{ "type": "FIELD", "name": "email" }
]
},
{
"class": "my-project.invitation",
"kind": "OBJECT_FORM_DASHBOARD",
"create": false,
"items": [
{ "type": "FIELD", "name": "status" },
{ "type": "FIELD", "name": "email" }
]
}
]
The first layout (with "modify": false) is shown when creating a new invitation and includes the token field. The second layout (with "create": false) is shown when editing an existing invitation and shows the status field instead.
Example: a form shown for both operations
Omitting both flags (or setting both to true) shows the same layout for create and modify — the default behaviour.
{
"class": "my-project.contract",
"kind": "OBJECT_FORM_DASHBOARD",
"items": [
{ "type": "FIELD", "name": "contractType" },
{ "type": "FIELD", "name": "startDate" }
]
}
Example: OBJECT_DASHBOARD layout
{
"class": "my-project.employmentAgreement",
"kind": "OBJECT_DASHBOARD",
"items": [
{ "type": "HEADER", "label": "Contract details" },
{ "type": "FIELD", "name": "contractType" },
{ "type": "FIELD", "name": "salary" },
{ "type": "FIELD", "name": "hoursPerWeek" },
{ "type": "HEADER", "label": "Parties" },
{ "type": "RELATION", "relatedClass": "commons.company", "editBehavior": "SEARCH" },
{ "type": "RELATION", "relatedClass": "commons.employee", "editBehavior": "SEARCH" },
{ "type": "HEADER", "label": "Actions" },
{ "type": "ACTION", "name": "extractFromDocument", "label": "Extract from document" }
]
}
Example: OBJECT_DASHBOARD with a tab
A main section layout and a separate tab for related tasks on the same class:
[
{
"class": "my-project.project",
"kind": "OBJECT_DASHBOARD",
"tabPage": false,
"items": [
{ "type": "FIELD", "name": "projectName", "header": true },
{ "type": "FIELD", "name": "startDate" },
{ "type": "FIELD", "name": "status" }
]
},
{
"class": "my-project.project",
"kind": "OBJECT_DASHBOARD",
"tabPage": true,
"label": "CLASS_PLURAL:my-project.task",
"items": [
{
"type": "RELATED_VIEW",
"fromClass": "my-project.project",
"relatedClass": "my-project.task",
"showTitle": false
}
]
}
]