Component schema — UI Protocol reference
Component schemas of the ObjectStack UI Protocol: ActionButtonProps and 43 more — each property with its type, default and a TypeScript example.
Source: packages/spec/src/ui/component.zod.ts
TypeScript Usage
import { ActionButtonPropsSchema, ActionGroupPropsSchema, ActionIconPropsSchema, ActionMenuPropsSchema, ElementButtonPropsSchema, ElementDefinitionListPropsSchema, ElementFilterPropsSchema, ElementFormPropsSchema, ElementImagePropsSchema, ElementMetadataViewerPropsSchema, ElementNumberPropsSchema, ElementRecordPickerPropsSchema, ElementRepeaterPropsSchema, ElementTextInputPropsSchema, ElementTextPropsSchema, ObjectCalendarPropsSchema, ObjectFormPropsSchema, ObjectGanttPropsSchema, ObjectGridPropsSchema, ObjectKanbanPropsSchema, ObjectMapPropsSchema, ObjectMasterDetailFormPropsSchema, ObjectMetricPropsSchema, ObjectTimelinePropsSchema, ObjectTreePropsSchema, PageAccordionProps, PageCardProps, PageContainerProps, PageHeaderProps, PageTabsProps, RecordActivityProps, RecordAlertActionSchema, RecordAlertProps, RecordChatterProps, RecordDetailsProps, RecordHighlightsField, RecordHighlightsProps, RecordHistoryProps, RecordLineItemsProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, RecordRelatedListProps, ReferenceRailEntrySchema } from '@objectstack/spec/ui';
import type { ActionButtonProps, ActionGroupProps, ActionIconProps, ActionMenuProps, ElementDefinitionListProps, ElementNumberProps, ElementRecordPickerProps, ElementRepeaterProps, ObjectCalendarProps, ObjectFormProps, ObjectGanttProps, ObjectGridProps, ObjectKanbanProps, ObjectMapProps, ObjectMasterDetailFormProps, ObjectMetricProps, ObjectTimelineProps, ObjectTreeProps, PageContainerProps, RecordAlertAction, RecordAlertProps, RecordHighlightsField, RecordHistoryProps, RecordLineItemsProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, ReferenceRailEntry } from '@objectstack/spec/ui';
// Validate data
const result = ActionButtonPropsSchema.parse(data);ActionButtonProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Action name, forwarded to the action runner. Optional: an inline page button is not a registered object action, and without a name the runner dispatches on actionType alone. With no actionType, or an empty one, the runner dispatches on the name instead, so a name of url or navigation runs the url navigator and its target is judged as a url target, and onSuccess is judged beside the executor the name names |
| label | string | optional | Button text. A literal string, placed as-is — localize through the translation bundle entry for this component id, not an inline locale map |
| icon | string | optional | Lucide icon name drawn left of the label, resolved through the shared action-icon resolver (an unknown name draws no icon) |
| actionType | string | optional | Executor the action runner dispatches to — built in: script, url, modal, flow, api, form; a handler registered under another name is dispatched too. navigation, the runner's alias of url, is refused: write url. Write it here, not as type: on a page component type is the component itself |
| variant | Enum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'primary'> | optional | Button variant — the Button primitive's vocabulary, plus primary, which renders as default (renderer default: default) |
| size | Enum<'default' | 'sm' | 'lg' | 'icon' | 'md'> | optional | Button size — the Button primitive's vocabulary, plus md, which renders as default (renderer default: default) |
| visible | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Visibility predicate — a boolean, a CEL string, or a { dialect, source } envelope, evaluated against the row the host binds; the button is not rendered when it is FALSE, and a predicate that fails to evaluate hides it. Omit for always-visible |
| disabled | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Disabled predicate — a boolean, a CEL string, or a { dialect, source } envelope; the button is shown but cannot be pressed while it is TRUE. Omit for never-disabled |
| params | any | optional | Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user before the action runs; an object is forwarded as the static parameter values |
| description | string | optional | Action description, forwarded to the runner — the parameter dialog shows it under its title |
| target | string | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per actionType. For the url navigator (actionType: 'url', or no or an empty actionType and a name of url or navigation): an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
| openIn | Enum<'self' | 'new-tab'> | optional | For a url action: self navigates in place, new-tab opens a new browser tab |
| method | string | optional | HTTP method for an api action, forwarded to the runner |
| bodyExtra | any | optional | Static request-body fields for an api action, forwarded to the runner |
| bodyShape | any | optional | How an api action shapes its request body, forwarded to the runner |
| operation | Enum<'update'> | optional | Declarative single-record write, forwarded to the runner together with patch: ActionSchema's own operation, whose one member is update. The runner reads it before any executor, performs the write through the platform action route and runs no executor of its own. Judged as ActionSchema judges it: beside it the executor key is absent or script, target, method, bodyExtra, bodyShape, recordIdField and onSuccess are refused, a patch or params gives the values to write, and list_toolbar is not a location |
| patch | Record<string, any> | optional | Static field values the declarative operation: 'update' writes, forwarded to the runner: ActionSchema's own record of field values, merged UNDER the values params supplies. Refused on a row without operation: 'update' (it would be silently dropped) |
| confirmText | string | optional | Confirmation question asked before the action runs |
| successMessage | string | optional | Toast shown when the action succeeds |
| errorMessage | string | optional | Toast shown when the action fails, in place of the raw error |
| refreshAfter | boolean | optional | Refresh the surrounding data after the action runs |
| undoable | boolean | optional | Offer an Undo affordance after an update action |
| recordIdField | string | optional | Row field whose value identifies the record the action acts on |
| locations | Enum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | 'record_related' | 'record_section'>[] | optional | Action locations, forwarded to the runner — the console uses them to tell a record-scoped action from an object-level one |
| toast | any | optional | Toast behaviour, forwarded to the runner |
| resultDialog | any | optional | One-shot result dialog for a value the response shows exactly once, forwarded to the runner |
| onSuccess | { navigate: string; openIn?: Enum<'self' | 'newTab'> } | optional | Post-success navigation, forwarded to the runner: ActionSchema's own { navigate, openIn } block. Taken beside an api or script executor, or a handler a host registers under another name; refused beside the built-ins url, navigation, modal, flow and form, on a row that names no executor, and beside operation: 'update', whose write the runner performs before any executor. navigate is an http:// or https:// URL, a mailto: address, an in-app path starting with / or ./, or a template that opens with ${, judged after interpolation — every other scheme and a protocol-relative //host are refused; openIn is self (default) or newTab |
| objectName | string | optional | Object the action acts on, forwarded to the runner — the console dispatches to it instead of the page's object. Omit to act on the page's object |
Nested Shape: ActionButtonProps.onSuccess
| Property | Type | Required | Description |
|---|---|---|---|
| navigate | string | ✅ | Route/URL template navigated to after the action succeeds. Interpolates ${param.*} (params-dialog values), ${ctx.*} (origin/apiBase/user/org/recordId/selection) and ${result.*} (the action's server response payload — NEW with this key, e.g. ${result.id}). An http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ (an SPA route hop); a template that opens with ${ is judged after interpolation. Every other scheme and a protocol-relative //host are refused. Renderers MUST encodeURIComponent values in query positions. |
| openIn | Enum<'self' | 'newTab'> | optional (default: "self") | Where to perform the post-success navigation: 'self' (default — in-place SPA navigation, immune to popup blocking) or 'newTab'. Closed enum — no general navigation DSL. |
ActionGroupProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| actions | { name?: string; label?: string; icon?: string; type?: string; … }[] | optional | The actions in this group, in order — each an action object the group draws and runs itself (name, label, icon, type, target, visible, disabled, size, …); a member's executor is its type |
| display | Enum<'inline' | 'dropdown'> | optional | Display mode: inline renders every action as a button row; dropdown renders one trigger button and lists the actions in its menu (renderer default: inline) |
| location | Enum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | 'record_related' | 'record_section'> | optional | Render only the members whose locations include this location. Omit to render every member |
| label | string | optional | Dropdown trigger text (renderer default: Actions). Inline mode renders no group label. A literal string — localize through the translation bundle entry for this component id |
| icon | string | optional | Lucide icon name on the dropdown trigger. Inline mode renders no group icon |
| variant | Enum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'> | optional | Button variant for the dropdown trigger and for every inline member that sets none — the Button primitive's vocabulary (renderer default: outline) |
| size | Enum<'default' | 'sm' | 'lg' | 'icon'> | optional | Button size for the dropdown trigger and for every inline member that sets none — the Button primitive's vocabulary |
| visible | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Visibility predicate for the whole group — a boolean, a CEL string, or a { dialect, source } envelope, evaluated against the row the host binds. Omit for always-visible |
Nested Shape: ActionGroupProps.actions[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Action name, forwarded to the action runner, and the item's text when there is no label. Optional: a member is not a registered object action, and the runner dispatches a nameless one on its type. With no type, or an empty one, the runner dispatches on the name instead, so a name of url or navigation runs the url navigator and its target is judged as a url target, and onSuccess is judged beside the executor the name names |
| label | string | optional | The item's text. A literal string, placed as-is — localize through the translation bundle entry for this component id |
| icon | string | optional | Lucide icon name drawn before the label, resolved through the shared action-icon resolver (an unknown name draws no icon) |
| type | string | optional | Executor the action runner dispatches to — built in: script, url, modal, flow, api, form; a handler registered under another name is dispatched too. navigation, the runner's alias of url, is refused: write url. A member is an action entry, so its executor is type (on an action:button node it is actionType) |
| variant | Enum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'primary'> | optional | Item variant — the Button primitive's vocabulary, plus primary (drawn as default). An inline action:group button draws it (falling back to the group's variant); a dropdown or action:menu item draws destructive red and every other variant plainly |
| visible | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Visibility predicate — a boolean, a CEL string, or a { dialect, source } envelope, evaluated against the row the host binds; the item is not drawn when it is FALSE, and a predicate that fails to evaluate hides it. Omit for always-visible |
| disabled | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Disabled predicate — a boolean, a CEL string, or a { dialect, source } envelope; the item is drawn but cannot be pressed while it is TRUE, and a predicate that fails to evaluate disables it. Omit for never-disabled |
| tags | Enum<'separator-before'>[] | optional | Item tags — separator-before draws a divider above the item in a dropdown or menu (not above the first item); no other tag is drawn |
| params | any | optional | Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user before the action runs. Only a type: 'api' member takes any other value, forwarded as its request payload (use bodyExtra for that); on every other member a non-array params is refused — author an action with static parameter values as its own action:button node |
| description | string | optional | Action description, forwarded to the runner — the parameter dialog shows it under its title |
| target | string | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per type. For the url navigator (type: 'url', or no or an empty type and a name of url or navigation): an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
| openIn | Enum<'self' | 'new-tab'> | optional | For a url action: self navigates in place, new-tab opens a new browser tab |
| method | string | optional | HTTP method for an api action, forwarded to the runner |
| bodyExtra | any | optional | Static request-body fields for an api action, forwarded to the runner |
| bodyShape | any | optional | How an api action shapes its request body, forwarded to the runner |
| operation | Enum<'update'> | optional | Declarative single-record write, forwarded to the runner together with patch: ActionSchema's own operation, whose one member is update. The runner reads it before any executor, performs the write through the platform action route and runs no executor of its own. Judged as ActionSchema judges it: beside it the executor key is absent or script, target, method, bodyExtra, bodyShape and onSuccess are refused, a patch or params gives the values to write, and list_toolbar is not a location |
| patch | Record<string, any> | optional | Static field values the declarative operation: 'update' writes, forwarded to the runner: ActionSchema's own record of field values, merged UNDER the values params supplies. Refused on a row without operation: 'update' (it would be silently dropped) |
| confirmText | string | optional | Confirmation question asked before the action runs |
| successMessage | string | optional | Toast shown when the action succeeds |
| errorMessage | string | optional | Toast shown when the action fails, in place of the raw error |
| refreshAfter | boolean | optional | Refresh the surrounding data after the action runs |
| locations | Enum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | …>[] | optional | Action locations, forwarded to the runner — an action:group with a location draws only the members that list it, and the console uses them to tell a record-scoped action from an object-level one |
| toast | any | optional | Toast behaviour, forwarded to the runner |
| resultDialog | any | optional | One-shot result dialog for a value the response shows exactly once, forwarded to the runner |
| onSuccess | { navigate: string; openIn?: Enum<'self' | 'newTab'> } | optional | Post-success navigation, forwarded to the runner: ActionSchema's own { navigate, openIn } block. Taken beside an api or script executor, or a handler a host registers under another name; refused beside the built-ins url, navigation, modal, flow and form, on a row that names no executor, and beside operation: 'update', whose write the runner performs before any executor. navigate is an http:// or https:// URL, a mailto: address, an in-app path starting with / or ./, or a template that opens with ${, judged after interpolation — every other scheme and a protocol-relative //host are refused; openIn is self (default) or newTab |
| objectName | string | optional | Object the action acts on, forwarded to the runner — the console dispatches to it instead of the page's object. Omit to act on the page's object |
| size | Enum<'default' | 'sm' | 'lg' | 'icon' | 'md'> | optional | Inline button size — the Button primitive's vocabulary, plus md (drawn as default), falling back to the group's size. A dropdown item reads no size |
ActionIconProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Action name, forwarded to the action runner; also the aria-label when there is no label. Optional, as for action:button. With no actionType, or an empty one, the runner dispatches on the name instead, so a name of url or navigation runs the url navigator and its target is judged as a url target, and onSuccess is judged beside the executor the name names |
| label | string | optional | Accessible label and tooltip text (and its first letter stands in when no icon resolves). A literal string — localize through the translation bundle entry for this component id |
| icon | string | optional | Lucide icon name, resolved through the shared action-icon resolver (renderer registration default play) |
| actionType | string | optional | Executor the action runner dispatches to — built in: script, url, modal, flow, api, form; a handler registered under another name is dispatched too. navigation, the runner's alias of url, is refused: write url. Write it here, not as type |
| variant | Enum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'primary'> | optional | Button variant — the Button primitive's vocabulary, plus primary, which renders as default (renderer default: ghost) |
| visible | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Visibility predicate — a boolean, a CEL string, or a { dialect, source } envelope, evaluated against the row the host binds; the icon is not rendered when it is FALSE. Omit for always-visible |
| disabled | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Disabled predicate — a boolean, a CEL string, or a { dialect, source } envelope; the icon is shown but cannot be pressed while it is TRUE. Omit for never-disabled |
| description | string | optional | Action description — the tooltip text when there is no label, and forwarded to the runner for the parameter dialog |
| params | any | optional | Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user; an object is forwarded as the static parameter values |
| target | string | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per actionType. For the url navigator (actionType: 'url', or no or an empty actionType and a name of url or navigation): an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
| openIn | Enum<'self' | 'new-tab'> | optional | For a url action: self navigates in place, new-tab opens a new browser tab |
| method | string | optional | HTTP method for an api action, forwarded to the runner |
| bodyExtra | any | optional | Static request-body fields for an api action, forwarded to the runner |
| bodyShape | any | optional | How an api action shapes its request body, forwarded to the runner |
| operation | Enum<'update'> | optional | Declarative single-record write, forwarded to the runner together with patch: ActionSchema's own operation, whose one member is update. The runner reads it before any executor, performs the write through the platform action route and runs no executor of its own. Judged as ActionSchema judges it: beside it the executor key is absent or script, target, method, bodyExtra, bodyShape and onSuccess are refused, a patch or params gives the values to write, and list_toolbar is not a location |
| patch | Record<string, any> | optional | Static field values the declarative operation: 'update' writes, forwarded to the runner: ActionSchema's own record of field values, merged UNDER the values params supplies. Refused on a row without operation: 'update' (it would be silently dropped) |
| confirmText | string | optional | Confirmation question asked before the action runs |
| successMessage | string | optional | Toast shown when the action succeeds |
| errorMessage | string | optional | Toast shown when the action fails, in place of the raw error |
| refreshAfter | boolean | optional | Refresh the surrounding data after the action runs |
| locations | Enum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | 'record_related' | 'record_section'>[] | optional | Action locations, forwarded to the runner — the console uses them to tell a record-scoped action from an object-level one |
| toast | any | optional | Toast behaviour, forwarded to the runner |
| resultDialog | any | optional | One-shot result dialog for a value the response shows exactly once, forwarded to the runner |
| onSuccess | { navigate: string; openIn?: Enum<'self' | 'newTab'> } | optional | Post-success navigation, forwarded to the runner: ActionSchema's own { navigate, openIn } block. Taken beside an api or script executor, or a handler a host registers under another name; refused beside the built-ins url, navigation, modal, flow and form, on a row that names no executor, and beside operation: 'update', whose write the runner performs before any executor. navigate is an http:// or https:// URL, a mailto: address, an in-app path starting with / or ./, or a template that opens with ${, judged after interpolation — every other scheme and a protocol-relative //host are refused; openIn is self (default) or newTab |
| objectName | string | optional | Object the action acts on, forwarded to the runner — the console dispatches to it instead of the page's object. Omit to act on the page's object |
Nested Shape: ActionIconProps.onSuccess
| Property | Type | Required | Description |
|---|---|---|---|
| navigate | string | ✅ | Route/URL template navigated to after the action succeeds. Interpolates ${param.*} (params-dialog values), ${ctx.*} (origin/apiBase/user/org/recordId/selection) and ${result.*} (the action's server response payload — NEW with this key, e.g. ${result.id}). An http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ (an SPA route hop); a template that opens with ${ is judged after interpolation. Every other scheme and a protocol-relative //host are refused. Renderers MUST encodeURIComponent values in query positions. |
| openIn | Enum<'self' | 'newTab'> | optional (default: "self") | Where to perform the post-success navigation: 'self' (default — in-place SPA navigation, immune to popup blocking) or 'newTab'. Closed enum — no general navigation DSL. |
ActionMenuProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| actions | { name?: string; label?: string; icon?: string; type?: string; … }[] | optional | The menu's actions, in order — each an action object the menu draws and runs itself (name, label, icon, type, target, visible, disabled, tags, …); a member's executor is its type |
| label | string | optional | Trigger text and accessible label; omit for an icon-only trigger labelled "More actions". A literal string — localize through the translation bundle entry for this component id |
| icon | string | optional | Lucide icon name on the trigger (renderer default: the horizontal ellipsis) |
| variant | Enum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'> | optional | Trigger button variant — the Button primitive's vocabulary (renderer default: ghost) |
| size | Enum<'default' | 'sm' | 'lg' | 'icon'> | optional | Trigger button size — the Button primitive's vocabulary (renderer default: icon) |
| visible | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Visibility predicate for the whole menu — a boolean, a CEL string, or a { dialect, source } envelope, evaluated against the row the host binds; a predicate that fails to evaluate hides it. Omit for always-visible |
Nested Shape: ActionMenuProps.actions[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Action name, forwarded to the action runner, and the item's text when there is no label. Optional: a member is not a registered object action, and the runner dispatches a nameless one on its type. With no type, or an empty one, the runner dispatches on the name instead, so a name of url or navigation runs the url navigator and its target is judged as a url target, and onSuccess is judged beside the executor the name names |
| label | string | optional | The item's text. A literal string, placed as-is — localize through the translation bundle entry for this component id |
| icon | string | optional | Lucide icon name drawn before the label, resolved through the shared action-icon resolver (an unknown name draws no icon) |
| type | string | optional | Executor the action runner dispatches to — built in: script, url, modal, flow, api, form; a handler registered under another name is dispatched too. navigation, the runner's alias of url, is refused: write url. A member is an action entry, so its executor is type (on an action:button node it is actionType) |
| variant | Enum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'primary'> | optional | Item variant — the Button primitive's vocabulary, plus primary (drawn as default). An inline action:group button draws it (falling back to the group's variant); a dropdown or action:menu item draws destructive red and every other variant plainly |
| visible | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Visibility predicate — a boolean, a CEL string, or a { dialect, source } envelope, evaluated against the row the host binds; the item is not drawn when it is FALSE, and a predicate that fails to evaluate hides it. Omit for always-visible |
| disabled | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Disabled predicate — a boolean, a CEL string, or a { dialect, source } envelope; the item is drawn but cannot be pressed while it is TRUE, and a predicate that fails to evaluate disables it. Omit for never-disabled |
| tags | Enum<'separator-before'>[] | optional | Item tags — separator-before draws a divider above the item in a dropdown or menu (not above the first item); no other tag is drawn |
| params | any | optional | Action parameters, forwarded to the runner: an array is the list of inputs to collect from the user before the action runs. Only a type: 'api' member takes any other value, forwarded as its request payload (use bodyExtra for that); on every other member a non-array params is refused — author an action with static parameter values as its own action:button node |
| description | string | optional | Action description, forwarded to the runner — the parameter dialog shows it under its title |
| target | string | optional | Executor target, forwarded to the runner: the URL, script name, flow name or API endpoint, per type. For the url navigator (type: 'url', or no or an empty type and a name of url or navigation): an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused |
| openIn | Enum<'self' | 'new-tab'> | optional | For a url action: self navigates in place, new-tab opens a new browser tab |
| method | string | optional | HTTP method for an api action, forwarded to the runner |
| bodyExtra | any | optional | Static request-body fields for an api action, forwarded to the runner |
| bodyShape | any | optional | How an api action shapes its request body, forwarded to the runner |
| operation | Enum<'update'> | optional | Declarative single-record write, forwarded to the runner together with patch: ActionSchema's own operation, whose one member is update. The runner reads it before any executor, performs the write through the platform action route and runs no executor of its own. Judged as ActionSchema judges it: beside it the executor key is absent or script, target, method, bodyExtra, bodyShape and onSuccess are refused, a patch or params gives the values to write, and list_toolbar is not a location |
| patch | Record<string, any> | optional | Static field values the declarative operation: 'update' writes, forwarded to the runner: ActionSchema's own record of field values, merged UNDER the values params supplies. Refused on a row without operation: 'update' (it would be silently dropped) |
| confirmText | string | optional | Confirmation question asked before the action runs |
| successMessage | string | optional | Toast shown when the action succeeds |
| errorMessage | string | optional | Toast shown when the action fails, in place of the raw error |
| refreshAfter | boolean | optional | Refresh the surrounding data after the action runs |
| locations | Enum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | …>[] | optional | Action locations, forwarded to the runner — an action:group with a location draws only the members that list it, and the console uses them to tell a record-scoped action from an object-level one |
| toast | any | optional | Toast behaviour, forwarded to the runner |
| resultDialog | any | optional | One-shot result dialog for a value the response shows exactly once, forwarded to the runner |
| onSuccess | { navigate: string; openIn?: Enum<'self' | 'newTab'> } | optional | Post-success navigation, forwarded to the runner: ActionSchema's own { navigate, openIn } block. Taken beside an api or script executor, or a handler a host registers under another name; refused beside the built-ins url, navigation, modal, flow and form, on a row that names no executor, and beside operation: 'update', whose write the runner performs before any executor. navigate is an http:// or https:// URL, a mailto: address, an in-app path starting with / or ./, or a template that opens with ${, judged after interpolation — every other scheme and a protocol-relative //host are refused; openIn is self (default) or newTab |
| objectName | string | optional | Object the action acts on, forwarded to the runner — the console dispatches to it instead of the page's object. Omit to act on the page's object |
ElementButtonProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| label | string | Record<string, string> | ✅ | Button display label |
| variant | Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'link'> | optional (default: "primary") | Button visual variant |
| size | Enum<'small' | 'medium' | 'large'> | optional (default: "medium") | Button size |
| icon | string | optional | Lucide icon name rendered inside the button, left or right of the label per iconPosition. Read on this component — the renderer resolves it through lucide-react's icons map via the shared resolveIcon helper every action:* site uses (a PascalCase normaliser plus a one-entry rename map), NOT the LazyIcon slot the container icons use; the two paths accept different spellings, and an unknown name here renders nothing rather than a fallback glyph. |
| iconPosition | Enum<'left' | 'right'> | optional (default: "left") | Icon position relative to label |
| disabled | boolean | optional (default: false) | Disable the button |
| action | { type?: Enum<'script' | 'url' | 'modal' | 'flow' | 'api' | 'form'>; name?: string; label?: string | Record<string, string>; target?: string; … } | optional | Inline action executed on click |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: ElementButtonProps.action
| Property | Type | Required | Description |
|---|---|---|---|
| type | Enum<'script' | 'url' | 'modal' | 'flow' | 'api' | 'form'> | optional (default: "script") | Action functionality type — the dispatch route. The declarative single-record field write is not a type: it is operation: 'update' + patch on the default script route. |
| name | string | optional | Machine name (lowercase snake_case) |
| label | string | Record<string, string> | optional | Display label |
| target | string | optional | URL, Script Name, Flow ID, or API Endpoint. Supports ${param.X} and ${ctx.X} interpolation. For type:'url': an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ — every other scheme and a protocol-relative //host are refused. |
| openIn | Enum<'self' | 'new-tab'> | optional | For type:'url' — where to open target. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of params (which is user-input-collection only). |
| method | Enum<'POST' | 'PATCH' | 'PUT' | 'DELETE'> | optional | HTTP method for type:"api" actions. Defaults to POST. |
| params | { name?: string; field?: string; objectOverride?: string; label?: string | Record<string, string>; … }[] | optional | Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in bodyExtra). |
| bodyExtra | Record<string, any> | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). {{page.<var>}} tokens are resolved by the runtime. This — not params — is where a payload goes. |
| confirmText | string | Record<string, string> | optional | Confirmation message before execution. On a registered action, pairing this with a non-empty params is refused — that opens a second dialog for one decision; put the question on description instead. Correct on a param-LESS action, where the confirm is the only dialog there is. |
| successMessage | string | Record<string, string> | optional | Success message shown after the action succeeds. On a type: 'api' or type: 'script' action it may interpolate ${result.*} — the server response payload, the same scope onSuccess.navigate declares (e.g. ${result.id}). On a registered action that declares outcomeMessages, the entry named by the response's outcome is shown instead, and this message is the fallback. |
| errorMessage | string | Record<string, string> | optional | Error message to show when the action fails (overrides the raw error). |
| refreshAfter | boolean | optional (default: false) | Refresh view after execution |
| opensInNewTab | boolean | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
Nested Shape: ElementButtonProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
ElementDefinitionListProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| items | { term: string; description?: any }[] | optional | Term/description pairs, in order. Omitted or empty renders the "No details" empty state |
| columns | Enum<1 | 2> | optional | Grid columns from the small breakpoint up — the NUMBER 1 or 2 (renderer default: 1) |
| inline | boolean | optional | Put each term and its description on one baseline-aligned row instead of stacking them |
Nested Shape: ElementDefinitionListProps.items[number]
| Property | Type | Required | Description |
|---|---|---|---|
| term | string | ✅ | The term — rendered as the row's label, as-is |
| description | any | optional | The value shown under (or beside) the term — a string or number as-is, an object as JSON; omitted renders an em dash |
ElementFilterProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | never | optional | [REMOVED] element:filter property object was removed in @objectstack/spec 17 (ADR-0049) — the whole element:filter element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the element:filter component; list surfaces own their filtering — use a view's userFilters quick-filter bar or the list toolbar's filter builder. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| fields | never | optional | [REMOVED] element:filter property fields was removed in @objectstack/spec 17 (ADR-0049) — the whole element:filter element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the element:filter component; list surfaces own their filtering — use a view's userFilters quick-filter bar or the list toolbar's filter builder. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| targetVariable | never | optional | [REMOVED] element:filter property targetVariable was removed in @objectstack/spec 17 (ADR-0049) — the whole element:filter element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the element:filter component; list surfaces own their filtering — use a view's userFilters quick-filter bar or the list toolbar's filter builder. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| layout | never | optional | [REMOVED] element:filter property layout was removed in @objectstack/spec 17 (ADR-0049) — the whole element:filter element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the element:filter component; list surfaces own their filtering — use a view's userFilters quick-filter bar or the list toolbar's filter builder. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| showSearch | never | optional | [REMOVED] element:filter property showSearch was removed in @objectstack/spec 17 (ADR-0049) — the whole element:filter element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the element:filter component; list surfaces own their filtering — use a view's userFilters quick-filter bar or the list toolbar's filter builder. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| aria | never | optional | [REMOVED] element:filter property aria was removed in @objectstack/spec 17 (ADR-0049) — the whole element:filter element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the element:filter component; list surfaces own their filtering — use a view's userFilters quick-filter bar or the list toolbar's filter builder. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
ElementFormProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | never | optional | [REMOVED] element:form property object was removed in @objectstack/spec 17 (ADR-0049) — the whole element:form element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound object-form block"), so every key on this element was a capability claim nothing kept. Delete the element:form component and use the object-bound object-form block instead — it is rendered, designer-publishable, and carries the same intent (objectName, fields, mode, submitText). Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| fields | never | optional | [REMOVED] element:form property fields was removed in @objectstack/spec 17 (ADR-0049) — the whole element:form element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound object-form block"), so every key on this element was a capability claim nothing kept. Delete the element:form component and use the object-bound object-form block instead — it is rendered, designer-publishable, and carries the same intent (objectName, fields, mode, submitText). Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| mode | never | optional | [REMOVED] element:form property mode was removed in @objectstack/spec 17 (ADR-0049) — the whole element:form element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound object-form block"), so every key on this element was a capability claim nothing kept. Delete the element:form component and use the object-bound object-form block instead — it is rendered, designer-publishable, and carries the same intent (objectName, fields, mode, submitText). Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| submitLabel | never | optional | [REMOVED] element:form property submitLabel was removed in @objectstack/spec 17 (ADR-0049) — the whole element:form element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound object-form block"), so every key on this element was a capability claim nothing kept. Delete the element:form component and use the object-bound object-form block instead — it is rendered, designer-publishable, and carries the same intent (objectName, fields, mode, submitText). Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| onSubmit | never | optional | [REMOVED] element:form property onSubmit was removed in @objectstack/spec 17 (ADR-0049) — the whole element:form element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound object-form block"), so every key on this element was a capability claim nothing kept. Delete the element:form component and use the object-bound object-form block instead — it is rendered, designer-publishable, and carries the same intent (objectName, fields, mode, submitText). Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| aria | never | optional | [REMOVED] element:form property aria was removed in @objectstack/spec 17 (ADR-0049) — the whole element:form element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion — "use the object-bound object-form block"), so every key on this element was a capability claim nothing kept. Delete the element:form component and use the object-bound object-form block instead — it is rendered, designer-publishable, and carries the same intent (objectName, fields, mode, submitText). Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
ElementImageProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| src | string | ✅ | Image URL or attachment field |
| alt | string | optional | Alt text for accessibility |
| fit | Enum<'cover' | 'contain' | 'fill'> | optional (default: "cover") | Image object-fit mode |
| height | number | optional | Fixed height in pixels |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: ElementImageProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
ElementMetadataViewerProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | Enum<'state_machine' | 'flow' | 'permission'> | ✅ | Metadata view kind (ADR-0051): state_machine | flow | permission |
| name | string | ✅ | Target metadata item name; resolved package-scoped (ADR-0048), then dependencies (ADR-0046 §3.3) |
| object | string | optional | Owning object — required for object-scoped kinds: state_machine is a rule ON an object (ADR-0020), permission renders a matrix FOR one; omit for top-level flow |
| mode | Enum<'diagram' | 'matrix' | 'summary'> | optional | Render form; defaults per type (diagram for flow/state_machine, matrix for permission) |
| detail | Enum<'business' | 'technical'> | optional (default: "business") | Authoring altitude (ADR-0051 §3.4): business collapses technical flow nodes to business steps + approvals. NOT access (cf. book.audience); permission projection is automatic and render-time, never set here |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: ElementMetadataViewerProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
ElementNumberProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | never | optional | [REMOVED] element:number property object was removed in @objectstack/spec 18 (ADR-0087 D2) — it was a flat second spelling of the node-level dataSource.object, and the element now reads its query from dataSource only, so a value written here reaches no query. Use dataSource.object on the component node, a sibling of type rather than a key inside properties. Move the key; the value (an object name) is unchanged, and where dataSource already sets it, delete this one, since the binding's value always won. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| field | string | optional | Field to aggregate |
| aggregate | Enum<'count' | 'sum' | 'avg' | 'min' | 'max'> | ✅ | Aggregation function |
| filter | never | optional | [REMOVED] element:number property filter was removed in @objectstack/spec 18 (ADR-0087 D2) — it was a flat second spelling of the node-level dataSource.filter, and the element now reads its query from dataSource only, so a value written here reaches no query. Use dataSource.filter on the component node, a sibling of type rather than a key inside properties. Move the key; the value (a ViewFilterRule array) is unchanged, and where dataSource.filter already has rules, append these to it, since the two always AND-combined. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| format | Enum<'number' | 'currency' | 'percent'> | optional | Number display format |
| prefix | string | optional | Prefix text (e.g. "$") |
| suffix | string | optional | Suffix text (e.g. "%") |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: ElementNumberProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
ElementRecordPickerProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | never | optional | [REMOVED] element:record_picker property object was removed in @objectstack/spec 18 (ADR-0087 D2) — it was a flat second spelling of the node-level dataSource.object, and the picker now reads its query from dataSource only, so a value written here reaches no query. Use dataSource.object on the component node, a sibling of type rather than a key inside properties. Move the key; the value (an object name) is unchanged, and where dataSource already sets it, delete this one, since the binding's value always won. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| labelField | string | optional | Field rendered as each row's text (default name) |
| valueField | string | optional | Field whose value is written into the bound page variable (default id) |
| label | string | Record<string, string> | optional | Control label rendered above the select |
| filter | never | optional | [REMOVED] element:record_picker property filter was removed in @objectstack/spec 18 (ADR-0087 D2) — it was a flat second spelling of the node-level dataSource.filter, and the picker now reads its query from dataSource only, so a value written here reaches no query. Use dataSource.filter on the component node, a sibling of type rather than a key inside properties. Move the key; the value (a ViewFilterRule array) is unchanged, and where dataSource already sets it, delete this one, since the binding's value always won. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| sort | never | optional | [REMOVED] element:record_picker property sort was removed in @objectstack/spec 18 (ADR-0087 D2) — it was a flat second spelling of the node-level dataSource.sort, and the picker now reads its query from dataSource only, so a value written here reaches no query. Use dataSource.sort on the component node, a sibling of type rather than a key inside properties. Move the key; the value (a [{ field, order }] array) is unchanged, and where dataSource already sets it, delete this one, since the binding's value always won. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| limit | never | optional | [REMOVED] element:record_picker property limit was removed in @objectstack/spec 18 (ADR-0087 D2) — it was a flat second spelling of the node-level dataSource.limit, and the picker now reads its query from dataSource only, so a value written here reaches no query. Use dataSource.limit on the component node, a sibling of type rather than a key inside properties. Move the key; the value (a positive integer) is unchanged, and where dataSource already sets it, delete this one, since the binding's value always won. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| targetVariable | never | optional | [REMOVED] element:record_picker property targetVariable was removed in @objectstack/spec 17 (ADR-0049) — it was a declarative hint no renderer ever read: the live binding runs the other direction, resolved from the page variable whose source names this component's id, so authoring only targetVariable bound nothing while reporting success. Delete the key; to bind the picked record id, declare it on the variable — variables: [{ name: '<var>', type: 'record_id', source: '<this component id>' }]. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| placeholder | string | Record<string, string> | optional | Placeholder text |
| emptyText | string | Record<string, string> | optional | Text shown when the query returns no records (default "No records") |
| displayField | never | optional | [REMOVED] element:record_picker property displayField was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — it was a required declaration no renderer ever read, while the renderer honoured labelField for the same thing and defaulted to name. Rename the key to labelField; the value (a field name) is unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| searchFields | never | optional | [REMOVED] element:record_picker property searchFields was removed in @objectstack/spec 17.0.0 (ADR-0049) — the picker renders a plain single-select with no search input, so no renderer ever read it and it narrowed nothing. Delete the key. To restrict which records the picker offers, use the component-level dataSource.filter, which the query path does apply. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| multiple | never | optional | [REMOVED] element:record_picker property multiple was removed in @objectstack/spec 17.0.0 (ADR-0049) — the picker is a single-select Select and the bound page variable holds one record id, so multiple: true selected nothing extra and reported success. Delete the key; multi-record selection is not implemented on this element. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: ElementRecordPickerProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
ElementRepeaterProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | never | optional | [REMOVED] element:repeater property object was removed in @objectstack/spec 18 (ADR-0087 D2) — it was a second door onto the list's query, which the list reads from the node-level dataSource binding first, keeping this key only as a fallback for metadata written before it. Use dataSource.object on the component node, a sibling of type rather than a key inside properties. Move the key; the value (an object name) is unchanged, and where dataSource already sets it too, decide which of the two values the list should use and keep that one on dataSource. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| titleField | string | optional | Field shown first on each line, emphasized |
| fields | (string | { field: string })[] | optional | Fields shown after the title on each line, in order — a bare field name, or { field } |
| filter | never | optional | [REMOVED] element:repeater property filter was removed in @objectstack/spec 18 (ADR-0087 D2) — it was a second door onto the list's query, which the list reads from the node-level dataSource binding first, keeping this key only as a fallback for metadata written before it. Use dataSource.filter on the component node, a sibling of type rather than a key inside properties. Move the key; the value (a ViewFilterRule array) is unchanged, and where dataSource already sets it too, decide which of the two values the list should use and keep that one on dataSource. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| sort | never | optional | [REMOVED] element:repeater property sort was removed in @objectstack/spec 18 (ADR-0087 D2) — it was a second door onto the list's query, which the list reads from the node-level dataSource binding first, keeping this key only as a fallback for metadata written before it. Use dataSource.sort on the component node, a sibling of type rather than a key inside properties. Move the key; the value (a [{ field, order }] array) is unchanged, and where dataSource already sets it too, decide which of the two values the list should use and keep that one on dataSource. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| limit | never | optional | [REMOVED] element:repeater property limit was removed in @objectstack/spec 18 (ADR-0087 D2) — it was a second door onto the list's query, which the list reads from the node-level dataSource binding first, keeping this key only as a fallback for metadata written before it. Use dataSource.limit on the component node, a sibling of type rather than a key inside properties. Move the key; the value (a positive integer) is unchanged, and where dataSource already sets it too, decide which of the two values the list should use and keep that one on dataSource. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| emptyText | string | optional | Copy shown when the query returns no records (renderer default: "No records"). A literal string — localize through the translation bundle entry for this component id |
| divided | boolean | optional | Draw a separator between lines (renderer default: true) |
Nested Shape: ElementRepeaterProps.fields[number]
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name |
ElementTextInputProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| inputType | Enum<'text' | 'email' | 'number' | 'tel' | 'url' | 'password'> | optional (default: "text") | Native input type — drives keyboard/validation affordance and how the bound value is coerced (number → numeric). |
| label | string | Record<string, string> | optional | Field label shown above the input |
| placeholder | string | Record<string, string> | optional | Placeholder text shown when empty |
| defaultValue | string | number | optional | Initial value; seeds the bound page variable on mount |
| required | boolean | optional (default: false) | Mark the field as required |
| disabled | boolean | optional (default: false) | Disable the input |
| description | string | Record<string, string> | optional | Helper text shown below the input |
| targetVariable | never | optional | [REMOVED] element:text_input property targetVariable was removed in @objectstack/spec 17 (ADR-0049) — it was a declarative hint no renderer ever read: the live binding runs the other direction, resolved from the page variable whose source names this component's id, so authoring only targetVariable bound nothing while reporting success. Delete the key; to bind the typed value, declare it on the variable — variables: [{ name: '<var>', type: 'string', source: '<this component id>' }]. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: ElementTextInputProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
ElementTextProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| content | string | Record<string, string> | ✅ | Text or Markdown content — a plain string, or an inline locale map |
| variant | Enum<'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'body' | 'caption' | 'overline'> | optional (default: "body") | Text style variant |
| align | Enum<'left' | 'center' | 'right'> | optional (default: "left") | Text alignment |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: ElementTextProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
ObjectCalendarProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | optional | Object this calendar binds to. Optional because the component-level dataSource binding can supply the object instead |
| calendar | { startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … } | optional | Calendar field config { startDateField, endDateField?, titleField?, colorField?, allDayField? } — the same block a list view's calendar declares; startDateField is required |
| defaultView | Enum<'month' | 'week' | 'day'> | optional | Initial view mode |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Base query filter — the ViewFilterRule array form [{ field, operator, value }, ...], the one filter orthography every filter door in this map shares. The MongoDB-style record form is refused — see migration element-data-source-and-object-block-filter-rule-array |
| sort | { field: string; order: Enum<'asc' | 'desc'> }[] | optional | Row order for the fetched events — the SortItem array form [{ field, order }, ...], the one sort orthography every declared sort door on this platform shares; lowered to the wire $orderby. The legacy string clause (name desc) is refused — see migration object-block-sort-item-array |
| data | any[] | optional | Pre-fetched records — skips the internal fetch |
| staticData | any[] | optional | Static inline records |
| locale | string | optional | Locale override for the calendar chrome |
| loading | boolean | optional | External loading state (honoured only alongside data) |
| navigation | { mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … } | optional | Event-click navigation config — the same block ListViewSchema.navigation declares ({ mode, size, openNewTab, preventNavigation }). The renderer's own default is { mode: 'drawer' } when the key is absent; it is documented rather than declared, so a parsed calendar carries the key only when the author wrote it |
Nested Shape: ObjectCalendarProps.calendar
| Property | Type | Required | Description |
|---|---|---|---|
| startDateField | string | ✅ | Field providing the event start date/time |
| endDateField | string | optional | Field providing the event end date/time (defaults to a single-day event) |
| titleField | string | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
| colorField | string | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value |
| allDayField | string | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day |
Nested Shape: ObjectCalendarProps.filter[number]
View filter rule
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to filter on |
| operator | Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …> | ✅ | Filter operator |
| value | string | number | boolean | null | (string | number)[] | optional | Filter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: icontains takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
Nested Shape: ObjectCalendarProps.sort[number]
Sort field and direction pair
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to sort by |
| order | Enum<'asc' | 'desc'> | ✅ | Sort direction |
Nested Shape: ObjectCalendarProps.navigation
| Property | Type | Required | Description |
|---|---|---|---|
| mode | Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'> | optional (default: "page") | |
| view | never | optional | [REMOVED] view.list.navigation.view was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the form view to open for a record detail, and no layer resolved a view by that name: the value was passed straight into the navigation-MODE argument of the console's onNavigate, where anything other than edit or view matched no branch, so the key selected nothing and could silently deaden the row click. Delete the key; to choose what opens for a record, assign a record page to the object and let isDefault pick the one that opens — page assignment is the machinery that resolves a detail layout, and a list view's navigation block only decides HOW the detail is surfaced (mode, size). |
| preventNavigation | boolean | optional (default: false) | Disable standard navigation entirely |
| openNewTab | boolean | optional (default: false) | Force open in new tab (applies to page mode) |
| size | Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'> | optional (default: "auto") | Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel width; page mode ignores it. |
| width | string | number | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the size bucket. |
ObjectFormProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | optional | Object this form creates/edits. Optional because the component-level dataSource binding can supply the object instead |
| recordId | string | number | optional | Record to load (edit/view modes) |
| mode | Enum<'create' | 'edit' | 'view'> | optional | Form mode |
| formType | Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'> | optional | Form presentation |
| layout | Enum<'vertical' | 'horizontal'> | optional | Field layout — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set columns |
| columns | number | optional | Number of field columns (multi-column forms), honoured under either layout |
| fields | string[] | optional | Field names to draw, in order — bare names selecting from the object's fields and from customFields. A { name } or { field } object entry is refused: a per-form label or required override goes on a sections[].fields entry |
| customFields | { name: string; label?: string; description?: string; type?: string; … }[] | optional | Field definitions merged over the set generated from the object's metadata — each a closed inline field { name, label?, type?, required?, … }: a member naming a field the object declares replaces that field's whole definition, any other is added after the generated fields. With no object behind the form, the members are its only fields |
| sections | { name?: string; label?: string; description?: string; collapsible?: boolean; … }[] | optional | Form sections — wizard steps, tab panes or stacked groups: { name?, label?, description?, collapsible?, collapsed?, visibleWhen?, columns?, pane?, fields }, or { group, columns?, pane? } to inherit an object field group. Each fields entry is a field name, the form view's { field, … } entry, or an inline form field { name, type, … } |
| title | string | Record<string, string> | optional | Form title |
| description | string | Record<string, string> | optional | Form description (rendered by the drawer/modal presentations) |
| defaultTab | string | optional | Initially active tab (tabbed) |
| tabPosition | Enum<'top' | 'bottom' | 'left' | 'right'> | optional | Tab strip position (tabbed) |
| allowSkip | boolean | optional | Allow skipping steps (wizard) |
| showStepIndicator | boolean | optional | Show the step indicator (wizard) |
| splitDirection | Enum<'horizontal' | 'vertical'> | optional | Split direction (split) |
| splitSize | number | optional | Split panel size in percent (split) |
| splitResizable | boolean | optional | Allow resizing the split (split) |
| drawerSide | Enum<'top' | 'bottom' | 'left' | 'right'> | optional | Drawer side (drawer) |
| drawerWidth | string | number | optional | Drawer width (drawer) |
| modalSize | Enum<'sm' | 'default' | 'lg' | 'xl' | 'full'> | optional | Modal size (modal) |
| modalCloseButton | boolean | optional | Show the modal close button (modal) |
| contentLayout | Enum<'simple' | 'tabbed'> | optional | How the modal presentation lays out its sections (modal) — 'simple' stacks them (the default); 'tabbed' puts each section on its own tab once more than one section has a field to show |
| confirmOnDiscard | boolean | optional | Confirm before discarding edits (drawer/modal) |
| submitText | string | Record<string, string> | optional | Submit button label |
| cancelText | string | Record<string, string> | optional | Cancel button label |
| nextText | string | Record<string, string> | optional | Next-step button label (wizard) |
| prevText | string | Record<string, string> | optional | Previous-step button label (wizard) |
| showSubmit | boolean | optional | Show the submit button |
| showCancel | boolean | optional | Show the cancel button |
| showReset | boolean | optional | Show the reset button |
| submitBehavior | { kind: 'thank-you'; title?: string; message?: string } | { kind: 'redirect'; url: string; delayMs?: integer } | { kind: 'continue' } | { kind: 'next-record' } | optional | What happens after a successful submit — the same block a form view's submitBehavior declares: { kind: 'thank-you', title?, message? }, { kind: 'redirect', url, delayMs? } (a relative url, interpolating declared record fields as {{record.field_name}}), { kind: 'continue' } or { kind: 'next-record' }. Takes precedence over navigateOnSuccess and resetOnSuccess |
| successMessage | string | Record<string, string> | optional | Toast message on successful submit |
| resetOnSuccess | boolean | optional | Reset the form after a successful submit |
| navigateOnSuccess | string | optional | Relative path to navigate to after a successful create/update — {id} / {recordId} are replaced with the saved record's id, URL-escaped; an absolute URL is refused at submit and reported on the success toast. Ignored when submitBehavior is set — prefer submitBehavior |
| readOnly | boolean | optional | Render every field read-only |
| initialValues | Record<string, any> | optional | Prefill values (create mode) |
| initialData | Record<string, any> | optional | Alternate spelling of initialValues the renderer also reads |
| mobile | { stickyActions?: boolean; stepper?: boolean | 'auto'; stepperMinFields?: integer; stepperFieldsPerStep?: integer; … } | optional | Phone presentation options, each opt-in — { stickyActions?, stepper?, stepperMinFields?, stepperFieldsPerStep?, fullscreenLongText? }. Read by the flat (simple, sectionless) form |
Nested Shape: ObjectFormProps.customFields[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | The field's name: its identity, and the key its value is submitted under. A member naming a field the object declares replaces that field's whole definition |
| label | string | optional | The label drawn beside the control (a plain string) |
| description | string | optional | Help text drawn under the control |
| type | string | optional | The widget the field renders as — a built-in input type (input, textarea, select, …) or a field widget (email, field:markdown, …); the renderer's default input when omitted |
| inputType | string | optional | The HTML type of the built-in input (email, tel, number, date, …) |
| widget | string | optional | A widget to render instead of the one type resolves to |
| required | boolean | optional | Refuse the submit while the field is empty |
| disabled | boolean | optional | Draw the control greyed out and not interactive |
| readonly | boolean | optional | Draw the value plainly, not editable |
| hidden | boolean | optional | Do not draw the field (its value still submits) |
| placeholder | string | optional | Placeholder text in the empty control |
| options | { label: string; value: string | number | boolean; description?: string; visibleWhen?: string | object }[] | optional | The choices of a select / radio / checkboxes field (and a lookup's static options) — { label, value, description?, visibleWhen? }, value a string, a number or a boolean |
| validation | { required?: string; minLength?: object; maxLength?: object; min?: object; … } | optional | Extra rules checked at submit — { required?, minLength?, maxLength?, min?, max? }, each bound rule a { value, message }, and required the message a required field shows |
| dependsOn | string | (string | { field: string; param?: string })[] | optional | The field(s) this field's options depend on: the form gates the field until they are set and re-evaluates its options as they change — a field name, or the object field's list of names / { field, param } entries |
| visibleWhen | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Predicate (CEL) — the field is drawn only when TRUE |
| readonlyWhen | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Predicate (CEL) — the field is read-only when TRUE |
| requiredWhen | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Predicate (CEL) — the field is required when TRUE |
| colSpan | integer | optional | Absolute column span (1-4), clamped to the form grid's column count |
| span | Enum<'auto' | 'full'> | optional | Relative width: 'auto' (the default) sizes the field from its widget and the column count; 'full' takes the whole row |
| group | string | optional | The object field group (fieldGroups[].key) this field is drawn in when the form derives its sections from the object's groups |
| multiple | boolean | optional | Hold several values instead of one (file, image, lookup, user and select fields) |
| rows | integer | optional | Height of the textarea / markdown editor, in text rows |
| accept | string[] | optional | Upload types a file field's picker offers, as MIME types or extensions (e.g. ["image/*", ".pdf"]) |
| dimensions | integer | optional | Vector dimensionality a vector field prints beside its value |
| reference | string | optional | The object a lookup / user field's picker queries |
| min | number | optional | Minimum value — the native control's min, enforced by the browser at submit |
| max | number | optional | Maximum value — the native control's max, enforced by the browser at submit |
| minLength | integer | optional | Minimum character count — the native control's minlength |
| maxLength | integer | optional | Maximum character count — the control's ceiling (a textarea also draws its counter) |
| pattern | string | optional | Regular expression the value must match, as a string — the native control's pattern, enforced by the browser at submit |
| returnType | Enum<'number' | 'text' | 'boolean' | 'date' | 'currency'> | optional | The value type a formula field displays (number / text / boolean / date / currency) |
| summaryOperations | { object: string; field: string; function: Enum<'count' | 'sum' | 'min' | 'max' | 'avg'>; relationshipField?: string; … } | optional | The roll-up a summary field displays — the object field's own { object, field, function, … } |
| columns | { name: string; label?: string; type?: Enum<'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' | 'select' | 'lookup' | 'file'>; width?: number; … }[] | optional | The columns of a grid field — the strict, name-keyed inline grid column a relationship field's inlineColumns takes |
| minRows | number | optional | A grid field's minimum row count: its Remove action is disabled at this many rows (no minimum when unset). Read only by the grid widget |
| maxRows | number | optional | A grid field's maximum row count: its Add and Duplicate actions are disabled, and no blank entry row is drawn, at this many rows (no maximum when unset). Read only by the grid widget |
| allowAdd | boolean | optional | Whether a grid field offers Add, and each row's Duplicate (a duplicate is an add): on unless false. A read-only or disabled grid offers neither. Read only by the grid widget |
| allowDelete | boolean | optional | Whether a grid field offers each row's Delete: on unless false. A read-only or disabled grid never offers it. Read only by the grid widget |
| allowReorder | boolean | optional | Whether a grid field's rows can be reordered by dragging: on unless false. A read-only or disabled grid never offers it. Read only by the grid widget |
| totalField | string | optional | The CHILD column a grid field sums into its footer total — the name of one of its columns; no total shows when unset. Not the PARENT field a master-detail or record:line_items sum is saved to: those blocks spell that totalField and the child column amountField, and the grid writes no parent field. Read only by the grid widget |
| addLabel | string | optional | Label of a grid field's Add button, also named in its empty state (the locale's own wording when unset). A plain string. Read only by the grid widget |
| sortField | string | optional | A field on each row that a grid field stamps with the row's index (0, 1, 2, …) on every change, so the order a drag-reorder leaves is saved with the rows (rows carry no position when unset). A row field, not one of columns: a column of that name has its typed value overwritten. Read only by the grid widget |
Nested Shape: ObjectFormProps.sections[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Stable section identifier (snake_case) — the heading resolves through objects.<object>._sections.<name>.label |
| label | string | optional | Section heading (a plain string) |
| description | string | optional | Text drawn under the heading |
| collapsible | boolean | optional | Draw a disclosure control on the heading, so a reader can close the section and open it again. collapsed: true implies it |
| collapsed | boolean | optional | Start the section closed (implies collapsible) |
| visibleWhen | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Predicate (CEL) — the whole section, heading and fields, is drawn only when TRUE |
| columns | integer | optional | Field-grid columns for this section (1-4), a number |
| pane | Enum<'primary' | 'secondary'> | optional | The split form's panel this section renders in; omitted → the first section 'primary', the others 'secondary' |
| group | string | optional | Field group key (snake_case) whose members and presentation this section inherits, from the object's fieldGroups. Mutually exclusive with fields, and with every key the group itself declares (name, label, description, visibleWhen, and a true collapsible / collapsed) |
| fields | (string | { field: string; type?: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; options?: object[]; reference?: string; … } | { name: string; label?: string; description?: string; type?: string; … })[] | optional | The section's fields, in order — each a field name, the form view's { field, … } entry overriding that object field, or an inline form field { name, type, … }. Omit only when group supplies the members |
Nested Shape: ObjectFormProps.submitBehavior[kind='redirect']
| Property | Type | Required | Description |
|---|---|---|---|
| kind | 'redirect' | ✅ | |
| url | string | ✅ | Where the browser goes after a successful submit. (1) RELATIVE paths only — it must start with /, and absolute or protocol-relative URLs are refused, which is what closes the open-redirect face; (2) interpolation ONLY from declared record fields, spelled {{record.field_name}}, and every interpolated value is URL-escaped when the redirect is built; (3) a verbatim redirect on the resolved relative path is the intended consumption. To send the browser OUT of the app, use an app navigation item ({ type: 'url', url }) instead. |
| delayMs | integer | optional |
Nested Shape: ObjectFormProps.mobile
| Property | Type | Required | Description |
|---|---|---|---|
| stickyActions | boolean | optional | Pin the submit / cancel bar to the bottom of a phone-width viewport |
| stepper | boolean | 'auto' | optional | One-step-at-a-time wizard for a flat form — true always, 'auto' only on a phone-width viewport once the form has stepperMinFields fields, false (the default) never |
| stepperMinFields | integer | optional | The field count at which stepper: 'auto' steps up (default 8) |
| stepperFieldsPerStep | integer | optional | Fields shown per stepper step (default 1) |
| fullscreenLongText | boolean | optional | Offer a fullscreen editor on textarea and rich-text fields |
ObjectGanttProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | optional | Object this gantt binds to — the THIRD record source resolveRecordSourceConfig resolves, after data and staticData. Optional because the component-level dataSource binding can supply the object instead |
| data | { provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> } | optional | Data source binding (ViewDataSchema — discriminated on provider: object | value | schema), read FIRST by resolveRecordSourceConfig. Static inline rows live at { provider: 'value', items: [...] } or at staticData; the bare-array shortcut is refused |
| staticData | any[] | optional | Inline records — read SECOND by resolveRecordSourceConfig, wrapped into a { provider: 'value' } config |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Base query filter — the ViewFilterRule array form [{ field, operator, value }, ...], the one filter orthography every filter door in this map shares; lowered to the wire $filter. The MongoDB-style record form is refused — see migration element-data-source-and-object-block-filter-rule-array |
| sort | { field: string; order: Enum<'asc' | 'desc'> }[] | optional | Task order for the fetched bars — the SortItem array form [{ field, order }, ...], the one sort orthography every declared sort door on this platform shares; lowered to the wire $orderby. The legacy string clause (name desc) is refused — see migration object-block-sort-item-array |
| gantt | { startDateField: string; endDateField: string; titleField: string; progressField?: string; … } | optional | Gantt-timeline configuration, the author face — the same block ListViewSchema.gantt declares, and the one the renderer validates this node against. Taken WHOLE when present: the flat top-level spelling beside it is ignored |
| navigation | { mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … } | optional | Task-click navigation config — the same block ListViewSchema.navigation declares ({ mode, size, openNewTab, preventNavigation }), mode one of the seven NavigationModeSchema values. The renderer's own default is { mode: 'drawer' } when the key is absent |
| label | string | Record<string, string> | optional | Gantt label — the second link of the exported PNG/PDF file-name chain, after gantt.exportFileName and before the bound object's own label |
| skipWeekends | boolean | optional | Measure duration and auto-schedule math in WORKING days, skipping Saturdays and Sundays |
| holidays | string[] | optional | Additional non-working dates for the working calendar, ISO yyyy-mm-dd strings; folded into a Set for the duration math |
| persistLayout | boolean | optional | Opt OUT of layout and filter-chip persistence — only an explicit false disables it; the storage key is objectName:viewName |
| viewName | string | optional | Layout-persistence scope, the second half of the objectName:viewName storage key (renderer default 'default') |
| markers | { date: string; label?: string; color?: string }[] | optional | Extra vertical reference lines drawn like the Today marker — each { date, label?, color? }: date places the line (a date outside the drawn range draws none), label is drawn against it, color paints it |
| criticalPath | boolean | optional | Seed the critical-path highlight ON; the toolbar toggle stays available either way |
| showBaselines | boolean | optional | Render the planned-vs-actual baseline bars — ON unless an explicit false disables it |
| readOnly | boolean | optional | Disable every write path on this gantt and lock the record drawer |
| mobileReadOnly | boolean | optional | Auto read-only on narrow viewports — ON unless an explicit false disables it |
Nested Shape: ObjectGanttProps.data[provider='object']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'object' | ✅ | |
| object | string | ✅ | Target object name |
Nested Shape: ObjectGanttProps.data[provider='value']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'value' | ✅ | |
| items | any[] | ✅ | Static data array |
Nested Shape: ObjectGanttProps.data[provider='schema']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'schema' | ✅ | |
| schemaId | string | ✅ | Schema identifier — typically the metadata type name |
| schema | Record<string, any> | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. |
Nested Shape: ObjectGanttProps.filter[number]
View filter rule
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to filter on |
| operator | Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …> | ✅ | Filter operator |
| value | string | number | boolean | null | (string | number)[] | optional | Filter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: icontains takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
Nested Shape: ObjectGanttProps.sort[number]
Sort field and direction pair
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to sort by |
| order | Enum<'asc' | 'desc'> | ✅ | Sort direction |
Nested Shape: ObjectGanttProps.gantt
| Property | Type | Required | Description |
|---|---|---|---|
| startDateField | string | ✅ | Field providing the task start date |
| endDateField | string | ✅ | Field providing the task end date |
| titleField | string | ✅ | Field displayed as the task title |
| progressField | string | optional | Field providing the task completion percentage |
| dependenciesField | string | optional | Field listing the task's predecessor (dependency) record ids |
| colorField | string | optional | Field to derive each bar color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else a semantic color token derived from the value |
| parentField | string | optional | Field holding the parent task id (builds the summary → step tree) |
| typeField | string | optional | Field whose value maps to task/summary/milestone |
| baselineStartField | string | optional | Baseline (planned) start field |
| baselineEndField | string | optional | Baseline (planned) end field |
| groupByField | string | optional | Field to group leaf tasks by (synthesized summary rows). NO leading or trailing whitespace: the group accessor reads this name off every task verbatim, so a padded spelling drops every task into one ungrouped bucket. |
| resourceView | boolean | optional | Render a per-resource workload histogram instead of the timeline |
| assigneeField | string | optional | Resource field to bucket load by (resource view) |
| effortField | string | optional | Per-task load units (resource view; default 1) |
| capacity | number | optional | Per-resource capacity ceiling; loads above this flag overload |
| tooltipFields | (string | { field: string; label?: string })[] | optional | Fields to surface in the hover tooltip, in display order |
| quickFilters | { field: string; label?: string; options?: (string | object)[] }[] | optional | Multi-select filter dropdowns rendered above the chart |
| autoZoomToFilter | boolean | optional | When true (default), filtering zooms the range to the filtered tasks |
| viewMode | Enum<'day' | 'week' | 'month' | 'quarter' | 'year'> | optional | Timeline granularity — one column per day/week/month/quarter/year (also the resource-view column granularity; renderer default 'day') |
| borderColorField | string | optional | Field carrying a per-task alert stroke color — any CSS color or semantic palette name (red, orange, …): the bar keeps its fill and gains an outline plus halo in that color (e.g. red for overdue, orange for due-soon; typically a server-computed alert field). Empty or null means no stroke |
| lockField | string | optional | Field marking a row view-only (truthy means locked): a locked bar cannot be dragged or resized, its progress cannot be dragged, no dependency can be drawn from it and its inline-edit and edit/delete menu entries are hidden — clicking it (open drawer, jump) still works. Independent of the global readOnly; freezes individual levels while siblings stay editable |
| objectField | string | optional | Field carrying the row's OWN object API name, for mixed-object trees (a row set composing parent-object rows with child-object rows): the detail drawer and its full-page link follow each row's real object instead of the view's bound object. Empty or missing falls back to the bound object |
| summaryExtent | Enum<'children' | 'self'> | optional | How a summary bar's span is computed. 'children' (renderer default) rolls the bar up from its children — min start, max end, duration-weighted progress — and ignores the record's own dates; 'self' renders the record's OWN start, end and progress and falls back to rollup only for records without dates (use it when the parent's schedule is authoritative, e.g. a shift plan whose work-order children are locked history) |
| defaultCollapsedDepth | integer | optional | Auto-collapse tree nodes at or below this 0-indexed depth on first render (roots are depth 0): every node at that depth or deeper that has children starts folded; the user can still expand them. Omit to start fully expanded |
| dependencyTypes | boolean | optional | Whether the backing store persists dependency link TYPES (fs, ss, ff, sf); renderer default true. Set false when dependencies are bare predecessor ids: the link menu hides the type switcher (a switch would be silently reverted on refetch) and drag-created links are always finish-to-start |
| timeZone | string | optional | Business time zone, an IANA name such as 'Asia/Shanghai': the chart's calendar — shift bands, day columns, snapping, the today line, date labels — renders in this zone's wall time for every viewer instead of the browser's zone; a datetime value is still written as the real instant, and a date value as the calendar day it was dropped on in this zone's calendar (YYYY-MM-DD). An invalid name falls back to the browser zone with a console warning |
| exportFileName | string | optional | Base name for exported PNG and PDF files (e.g. the view's display label — the host's view schema often reaches the renderer stripped of label); falls back to the object schema label, then the object API name. A timestamp suffix is always appended |
| interactions | { move?: boolean; resize?: boolean; progress?: boolean; link?: boolean } | optional | Per-interaction switches, each defaulting to true: allow bar moves but pin durations (resize: false), or keep the dependency UI read-only (link: false). They only narrow what readOnly and row locks already allow |
| timeSegments | { dayStart?: string; bands: object[]; showMidnight?: boolean } | optional | Shift segmentation for the day-mode timeline: splits each shift-day (starting at dayStart) into the configured bands — a two-tier header (date over band), per-band tints and drag/resize snapping to band boundaries. No shift concept is hardcoded; bands are pure config. Off when omitted |
Nested Shape: ObjectGanttProps.navigation
| Property | Type | Required | Description |
|---|---|---|---|
| mode | Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'> | optional (default: "page") | |
| view | never | optional | [REMOVED] view.list.navigation.view was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the form view to open for a record detail, and no layer resolved a view by that name: the value was passed straight into the navigation-MODE argument of the console's onNavigate, where anything other than edit or view matched no branch, so the key selected nothing and could silently deaden the row click. Delete the key; to choose what opens for a record, assign a record page to the object and let isDefault pick the one that opens — page assignment is the machinery that resolves a detail layout, and a list view's navigation block only decides HOW the detail is surfaced (mode, size). |
| preventNavigation | boolean | optional (default: false) | Disable standard navigation entirely |
| openNewTab | boolean | optional (default: false) | Force open in new tab (applies to page mode) |
| size | Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'> | optional (default: "auto") | Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel width; page mode ignores it. |
| width | string | number | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the size bucket. |
Nested Shape: ObjectGanttProps.markers[number]
| Property | Type | Required | Description |
|---|---|---|---|
| date | string | ✅ | Where the line stands — an ISO date (2026-07-01, read as that day on the chart's own calendar) or date-time string; a date that does not parse, or falls outside the drawn range, draws no line |
| label | string | optional | Text drawn against the line |
| color | string | optional | Line colour, any CSS colour (renderer default: the theme's primary colour) |
ObjectGridProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | optional | Object this grid binds to. Optional because the component-level dataSource binding can supply the object instead |
| label | string | Record<string, string> | optional | Grid label — used as the table caption and export file title |
| title | string | Record<string, string> | optional | Fallback for label (the renderer reads label || title) |
| description | string | Record<string, string> | optional | One line of help text drawn above the grid's rows — a string, or an inline locale map resolved against the display locale |
| emptyState | { title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object } | optional | What the grid draws instead of an empty table: { title, message, icon, link } — the list view's own empty-state shape |
| columns | string[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[] | optional | Columns — all field-name strings, or all column entries { field, label?, width?, align?, hidden?, sortable?, … }, the same union a list view's columns declares. One spelling per list: an array mixing strings and column objects is refused |
| fields | string[] | optional | Field-name fallback the grid reads when columns is absent — bare field names (['name', 'amount']); write column decoration such as label or width on columns |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Base query filter — the ViewFilterRule array form [{ field, operator, value }, ...], the one filter orthography every filter door in this map shares; lowered to the wire $filter. THE key, singular — not the plural misspelling. The MongoDB-style record form is refused — see migration element-data-source-and-object-block-filter-rule-array |
| defaultFilters | never | optional | [REMOVED] object-grid property defaultFilters was removed in @objectstack/spec 18 (ADR-0087 D2) — it was the legacy second spelling of filter: the same rules, read only when filter lowered to nothing, so one intent had two spellings and a grid authoring both silently ignored this one. Use filter. Rename the key where filter is empty; the value (a ViewFilterRule array, [{ field, operator, value }, ...]) is unchanged. Where filter already has rules, delete this key: the grid never read it there. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| sort | { field: string; order: Enum<'asc' | 'desc'> }[] | optional | Initial row order — the SortItem array form [{ field, order }, ...], the one sort orthography every declared sort door on this platform shares; lowered to the wire $orderby. The legacy string clause (name desc) is refused — see migration object-block-sort-item-array |
| defaultSort | never | optional | [REMOVED] object-grid property defaultSort was removed in @objectstack/spec 17 (ADR-0049) — it was the legacy second spelling of sort: a single { field, order } pair read only when sort was absent, so one intent had two spellings and a grid authoring both silently ignored this one. Rename the key to sort and wrap the value in an array (defaultSort: { field, order } becomes sort: [{ field, order }]); the pair itself is unchanged. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| pagination | { pageSize?: integer; pageSizeOptions?: integer[] } & Record<string, any> | optional | Pagination config ({ pageSize, pageSizeOptions, … }); its presence enables paging. pageSize and every pageSizeOptions entry is a positive integer — the accept set the view arm's PaginationConfigSchema already rules; the bag stays open, so other keys pass through unvalidated |
| pageSize | integer | optional | Flat page-size shorthand, a positive integer; pagination.pageSize wins when both are set |
| showPagination | boolean | optional | Show the pager (read only when pagination is absent) |
| searchableFields | string[] | optional | Fields the toolbar search queries; a non-empty list enables search |
| showSearch | boolean | optional | Show the search box (read only when searchableFields is absent) |
| rowHeight | Enum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'> | optional | Row height — one of compact, short, medium, tall, extra_tall; the same values a list view's rowHeight takes |
| grouping | { fields: object[] } | optional | Row grouping config |
| aggregations | { field: string; type: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'> }[] | optional | Per-group aggregations drawn in a grouped grid's group headers — [{ field, type }], type one of count, sum, avg, min, max, count_distinct (count is the group's row count, whatever field names) |
| conditionalFormatting | { condition: string | object; style: Record<string, string> }[] | optional | Conditional formatting rules — [{ condition, style }], the same rules a list view declares: the first rule whose CEL condition holds applies its CSS style map to the row |
| rowColor | { field: string; colors?: Record<string, string> } | optional | Row colour by field value — { field, colors }, the same block a list view's rowColor declares |
| selection | { type?: Enum<'none' | 'single' | 'multiple'> } | optional | Selection config { type } — none, single or multiple, the same block a list view's selection declares |
| selectable | boolean | Enum<'single' | 'multiple'> | optional | Legacy selection shorthand, read only when selection is absent — true (multiple), false (off), 'single' or 'multiple'. Prefer selection |
| rowActions | string[] | optional | Per-row action names — edit / delete select the generic entries, any other name resolves against the object's actions; the same list a list view's rowActions declares |
| bulkActions | string[] | optional | Bulk action names shown on selection — the same list a list view's bulkActions declares; a full def goes on bulkActionDefs |
| batchActions | string[] | optional | Alternate spelling of bulkActions that the renderer reads FIRST (batchActions ?? bulkActions) — the same action-name list. Prefer bulkActions |
| bulkActionDefs | { name: string; label?: string; icon?: string; variant?: Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'>; … }[] | optional | Inline bulk-action definitions (full defs, not names) — the same BulkActionDef entries a list view's bulkActionDefs declares |
| navigation | { mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … } | optional | Row-click navigation config — the same block ListViewSchema.navigation declares ({ mode, size, openNewTab, preventNavigation }) |
| editable | boolean | optional | Enable inline cell editing |
| singleClickEdit | boolean | optional | Enter cell edit on single click (default true when editable) |
| keyboardNavigation | boolean | optional | Arrow-key cell navigation on the WAI-ARIA grid pattern: the grid's data cells become one Tab stop that the arrow keys, Home / End and Ctrl+Home / Ctrl+End move between. Defaults to on when the grid renders editable — editable set and the viewer allowed to edit; a grid that renders read-only keeps every cell its own Tab stop unless this is true, and false turns it off on an editable grid |
| resizable | boolean | optional | Allow column resize (the renderer default is on) |
| resizableColumns | never | optional | [REMOVED] object-grid property resizableColumns was removed in @objectstack/spec 17.7.0 (ADR-0049) — it was the legacy second spelling of resizable, read only when resizable was absent, so one switch had two spellings and a grid authoring both silently ignored this one. Use resizable. Rename the key; the value (a boolean) is unchanged. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| reorderableColumns | boolean | optional | Allow column drag-reorder |
| frozenColumns | number | optional | How many leading columns stay frozen (default 1) |
| showColumnTypeIcons | boolean | optional | Show field-type icons in column headers |
| exportOptions | { formats?: Enum<'csv' | 'xlsx' | 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … } | optional | Export config — the object { formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }, the same block a list view's exportOptions declares, with formats drawn from csv, xlsx and json. A bare format array is refused: the grid reads exportOptions.formats, so write { formats: ['csv', 'xlsx'] } |
| operations | { create?: boolean; update?: boolean; delete?: boolean; export?: boolean } | optional | Built-in affordance toggles { create?, update?, delete?, export? } (booleans). A declared block replaces the grid's default: update / delete not named are withheld from the row menu, create enables the add-row affordance, and export: false hides the export menu |
| data | { provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> } | optional | Data source binding (ViewDataSchema — discriminated on provider: object | value | schema). Static inline rows live at { provider: 'value', items: [...] }; the bare-array shortcut is refused — see migration object-grid-data-view-data-converged |
| staticData | any[] | optional | Deprecated bare-array static-rows shortcut the renderer still reads. Prefer data: { provider: 'value', items: [...] } |
Nested Shape: ObjectGridProps.emptyState
| Property | Type | Required | Description |
|---|---|---|---|
| title | string | Record<string, string> | optional | Heading of the empty state |
| message | string | Record<string, string> | optional | Line of text below the heading |
| icon | string | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph |
| link | { label: string | Record<string, string>; url: string } | optional | The one next step an empty list names: a link the user follows, { label, url } — for example to the how-to page that explains what belongs here when nobody can add a row. It opens a page or an address and runs no action. The console draws it in a list's empty state (the grid and gallery visualizations) and in an object-grid block's, beside the add-record action where one is offered. |
Nested Shape: ObjectGridProps.columns[number]
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name (snake_case) |
| label | string | Record<string, string> | optional | Display label override |
| width | number | optional | Column width in pixels |
| align | Enum<'left' | 'center' | 'right'> | optional | Text alignment |
| hidden | boolean | optional | Hide column by default |
| sortable | boolean | optional | Allow sorting by this column |
| resizable | boolean | optional | Allow resizing this column |
| wrap | boolean | optional | Allow text wrapping |
| type | string | optional | Renderer type override (e.g., "currency", "date") |
| pinned | Enum<'left' | 'right'> | optional | Pin/freeze column to left or right side |
| summary | Enum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …> | { type: Enum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …>; field?: string } | optional | Footer aggregation for this column — the function alone, or { type, field } to aggregate another field |
| prefix | { field: string; type?: Enum<'badge' | 'text'> } | optional | Field rendered inline before this cell value |
| link | boolean | optional | Functions as the primary navigation link (triggers View navigation) |
| action | string | optional | Registered Action ID to execute when clicked |
Nested Shape: ObjectGridProps.filter[number]
View filter rule
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to filter on |
| operator | Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …> | ✅ | Filter operator |
| value | string | number | boolean | null | (string | number)[] | optional | Filter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: icontains takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
Nested Shape: ObjectGridProps.sort[number]
Sort field and direction pair
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to sort by |
| order | Enum<'asc' | 'desc'> | ✅ | Sort direction |
Nested Shape: ObjectGridProps.grouping
| Property | Type | Required | Description |
|---|---|---|---|
| fields | { field: string; order?: Enum<'asc' | 'desc'>; collapsed?: boolean }[] | ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's groupBy |
Nested Shape: ObjectGridProps.aggregations[number]
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field whose values the function aggregates within each group (ignored by count) |
| type | Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'> | ✅ | Aggregation function — count (the group's row count), sum, avg, min, max or count_distinct |
Nested Shape: ObjectGridProps.conditionalFormatting[number]
| Property | Type | Required | Description |
|---|---|---|---|
| condition | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | ✅ | Predicate (CEL) to evaluate. |
| style | Record<string, string> | ✅ | CSS styles to apply when condition is true |
Nested Shape: ObjectGridProps.rowColor
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field whose value is looked up in the colors map below to pick a row colour (typically a select/status field). The map is what does the colouring — with no colors, no row is ever coloured, whatever this field holds. Author-time diagnostic view/row-color-without-colors reports that combination. |
| colors | Record<string, string> | optional | Map of field value to row colour. The spellings that actually paint a row are not free-form: objectui plugin-grid's useRowColor hands a value already written as a complete Tailwind background class (bg-red-200) straight through, otherwise lower-cases and trims it and resolves it through its own closed vocabulary of colour NAMES (red, blue, slate, … each mapping to bg-NAME-100), and returns undefined for anything else. A hex, an rgb() or a CSS variable parses here, publishes, and colours no row — Tailwind v4 has no runtime, so no class can be fabricated from one. Author-time diagnostic view/row-color-unresolvable-value reports a value that cannot resolve. |
Nested Shape: ObjectGridProps.selection
| Property | Type | Required | Description |
|---|---|---|---|
| type | Enum<'none' | 'single' | 'multiple'> | optional (default: "none") | Selection mode |
Nested Shape: ObjectGridProps.bulkActionDefs[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Stable identifier — the audit-log action key, and (for an aggregate def) the name of the object action to dispatch. |
| label | string | optional | Button + dialog-header text. Plain string: an authored def is not i18n-resolved (declare a real action and name it in bulkActions to get localization). |
| icon | string | optional | Lucide icon name (e.g. "user-check", "trash"). |
| variant | Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'> | optional | Visual treatment of the button. |
| operation | Enum<'update' | 'delete' | 'custom'> | ✅ | What the executor does: 'update'/'delete' are data-plane mass mutations; 'custom' dispatches an object action (see execution). |
| execution | Enum<'perRecord' | 'aggregate'> | optional | For operation: 'custom' — 'aggregate' dispatches the named action ONCE for the whole selection, carrying every id in params._selectedIds. Required on a custom def: the per-record form is declared as bulkActions: ['<name>'] instead. |
| patch | Record<string, any> | optional | For operation: 'update' — static field values applied to every selected record, merged UNDER the user-supplied params so a fixed value can be declared without exposing it in the dialog. |
| params | { name: string; label?: string; help?: string; type: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; … }[] | optional | Inputs collected once before the run. Omit to skip the params step and go straight to confirm. |
| confirmText | string | optional | Confirmation text shown above the affected-record summary. |
| confirmLabel | string | optional | Custom Confirm button label (default: "Run"). |
| visible | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Eligibility predicate (CEL) — a string or a {dialect, source} envelope, i.e. action.visible without its boolean-literal arm: a per-record predicate has nothing to say as a constant. Evaluated once PER SELECTED RECORD with that record bound: the button is offered when at least one passes, the run covers only those, and the rest are reported as skipped. A record-free predicate (features.x, current_user.y) therefore behaves as a plain button-level gate. Fail-closed — a predicate that faults excludes the record. |
| requiredPermissions | string[] | optional | [ADR-0066 D4] Capability gate on the button, action.requiredPermissions semantics verbatim: absent or empty always passes, several are AND-ed, and a client that cannot resolve the caller's capabilities fails OPEN (the server stays the authority). This key exists for INLINE defs — notably the update/delete data-plane forms, which dispatch no action and so have nothing to inherit a gate from; a def promoted from bulkActions: ['<name>'] (or an aggregate def naming a declared action) inherits the action's own declaration instead. On a data-plane def the gate governs visibility only — the write itself is still authorized by the data API's object permissions and server hooks. |
| maxRecords | integer | optional | Selection size above which the run is blocked. Set it on defs whose server work is expensive — an aggregate def carries every selected id in one request. |
| batchSize | integer | optional | Records per executor batch (default 200). Data-plane operations only — an aggregate run is a single call by definition. |
Nested Shape: ObjectGridProps.navigation
| Property | Type | Required | Description |
|---|---|---|---|
| mode | Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'> | optional (default: "page") | |
| view | never | optional | [REMOVED] view.list.navigation.view was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the form view to open for a record detail, and no layer resolved a view by that name: the value was passed straight into the navigation-MODE argument of the console's onNavigate, where anything other than edit or view matched no branch, so the key selected nothing and could silently deaden the row click. Delete the key; to choose what opens for a record, assign a record page to the object and let isDefault pick the one that opens — page assignment is the machinery that resolves a detail layout, and a list view's navigation block only decides HOW the detail is surfaced (mode, size). |
| preventNavigation | boolean | optional (default: false) | Disable standard navigation entirely |
| openNewTab | boolean | optional (default: false) | Force open in new tab (applies to page mode) |
| size | Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'> | optional (default: "auto") | Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel width; page mode ignores it. |
| width | string | number | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the size bucket. |
Nested Shape: ObjectGridProps.exportOptions
| Property | Type | Required | Description |
|---|---|---|---|
| formats | Enum<'csv' | 'xlsx' | 'json'>[] | optional | Formats offered in the export menu (default: ['csv', 'json']). XLSX is delivered by the server stream only. |
| maxRecords | integer | optional | Maximum number of records to export; 0 or absent = unlimited |
| includeHeaders | boolean | optional | Include column headers in the exported file (default true) |
| fileNamePrefix | string | optional | Download file name prefix — replaces the object label and suppresses the view label in the generated file name |
| streaming | boolean | optional | Set false to force the client-side export path (csv/json only) instead of the server stream |
Nested Shape: ObjectGridProps.operations
| Property | Type | Required | Description |
|---|---|---|---|
| create | boolean | optional | Show the add-row affordance (also gated by the user's create permission) |
| update | boolean | optional | Offer the row menu's generic Edit entry |
| delete | boolean | optional | Offer the row menu's generic Delete entry |
| export | boolean | optional | false hides the export menu that exportOptions enables |
Nested Shape: ObjectGridProps.data[provider='object']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'object' | ✅ | |
| object | string | ✅ | Target object name |
Nested Shape: ObjectGridProps.data[provider='value']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'value' | ✅ | |
| items | any[] | ✅ | Static data array |
Nested Shape: ObjectGridProps.data[provider='schema']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'schema' | ✅ | |
| schemaId | string | ✅ | Schema identifier — typically the metadata type name |
| schema | Record<string, any> | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. |
ObjectKanbanProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | optional | Object this board binds to. Optional because the component-level dataSource binding can supply the object instead |
| groupBy | string | optional | Field whose values become the board columns |
| columns | string[] | { id: string; title: string; cards?: object[]; limit?: integer; … }[] | optional | Swimlane definitions — all lane objects { id, title, cards?, limit?, className?, collapsed? } (one per groupBy value), or all bare value strings (drawn only on a board with no groupBy). NOT a field projection; one spelling per list |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Base query filter, handed to the wire $filter — the ViewFilterRule array form [{ field, operator, value }, ...], the one filter orthography every filter door in this map shares. The MongoDB-style record form is refused — see migration element-data-source-and-object-block-filter-rule-array |
| limit | integer | optional | Maximum number of records loaded onto the board (row cap); lowered to the query's top-level $top (renderer default 100). The component-level dataSource.limit wins when both are set; a bound view's pagination.pageSize fills this key only when it is unset — and on this face unset is the whole rule, because every cap this key accepts is one the binding gate already treats as authored |
| data | any[] | optional | Static inline cards — bypasses the object query |
| cardTitle | string | optional | Field rendered as each card title |
| titleField | string | optional | Legacy fallback for cardTitle (the board reads cardTitle || titleField). Prefer cardTitle |
| cardFields | string[] | optional | Fields rendered on each card |
| swimlaneField | string | optional | Field for horizontal swimlanes (in addition to columns) |
| grouping | { fields: object[] } | optional | View grouping config; its first field is the swimlane fallback |
| quickAdd | never | optional | [REMOVED] object-kanban property quickAdd was removed in @objectstack/spec 17 (ADR-0049) — the board forwarded it, but the per-column affordance is gated on both quickAdd and onQuickAdd, and onQuickAdd is a host-supplied function JSON cannot carry and no producer ever put on an object-kanban node, so authoring it was a parse-clean no-op. Delete the key; object-kanban offers no quick-add control. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| coverImageField | string | optional | Image field rendered as the card cover |
| conditionalFormatting | { condition: string | object; style: Record<string, string> }[] | optional | Card conditional formatting rules — [{ condition, style }], the same rules a list view declares: the first rule whose CEL condition holds applies its CSS style map to that card |
| navigation | { mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … } | optional | Card-click navigation config — the same block ListViewSchema.navigation declares ({ mode, size, openNewTab, preventNavigation }). The renderer's own default is { mode: 'drawer' } when the key is absent; it is documented rather than declared, so a parsed board carries the key only when the author wrote it |
Nested Shape: ObjectKanbanProps.columns[number]
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Lane id — the groupBy value whose records land in this lane (matched as a string) |
| title | string | ✅ | Lane heading, localized against the groupBy picklist's option labels |
| cards | ({ id: string; title: string } & Record<string, any>)[] | optional | A static board's own cards, drawn ahead of the records bucketed into this lane — each { id, title, … }, the rest of the card being the record row's own values |
| limit | integer | optional | WIP limit — the card count at which the lane warns; never reaches the query |
| className | string | optional | CSS class names applied to the lane |
| collapsed | boolean | optional | Whether the lane first renders collapsed — a title spine with its cards withheld, which the viewer can reopen |
Nested Shape: ObjectKanbanProps.filter[number]
View filter rule
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to filter on |
| operator | Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …> | ✅ | Filter operator |
| value | string | number | boolean | null | (string | number)[] | optional | Filter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: icontains takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
Nested Shape: ObjectKanbanProps.grouping
| Property | Type | Required | Description |
|---|---|---|---|
| fields | { field: string; order?: Enum<'asc' | 'desc'>; collapsed?: boolean }[] | ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's groupBy |
Nested Shape: ObjectKanbanProps.conditionalFormatting[number]
| Property | Type | Required | Description |
|---|---|---|---|
| condition | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | ✅ | Predicate (CEL) to evaluate. |
| style | Record<string, string> | ✅ | CSS styles to apply when condition is true |
Nested Shape: ObjectKanbanProps.navigation
| Property | Type | Required | Description |
|---|---|---|---|
| mode | Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'> | optional (default: "page") | |
| view | never | optional | [REMOVED] view.list.navigation.view was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the form view to open for a record detail, and no layer resolved a view by that name: the value was passed straight into the navigation-MODE argument of the console's onNavigate, where anything other than edit or view matched no branch, so the key selected nothing and could silently deaden the row click. Delete the key; to choose what opens for a record, assign a record page to the object and let isDefault pick the one that opens — page assignment is the machinery that resolves a detail layout, and a list view's navigation block only decides HOW the detail is surfaced (mode, size). |
| preventNavigation | boolean | optional (default: false) | Disable standard navigation entirely |
| openNewTab | boolean | optional (default: false) | Force open in new tab (applies to page mode) |
| size | Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'> | optional (default: "auto") | Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel width; page mode ignores it. |
| width | string | number | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the size bucket. |
ObjectMapProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | optional | Object this map binds to — the THIRD record source getDataConfig resolves, after data and staticData. Optional because the component-level dataSource binding can supply the object instead |
| data | { provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> } | optional | Data source binding (ViewDataSchema — discriminated on provider: object | value | schema), read FIRST by getDataConfig. Static inline rows live at { provider: 'value', items: [...] } or at staticData; the bare-array shortcut is refused |
| staticData | any[] | optional | Inline records — read SECOND by getDataConfig, wrapped into a { provider: 'value' } config |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Base query filter — the ViewFilterRule array form [{ field, operator, value }, ...], the one filter orthography every filter door in this map shares; lowered to the wire $filter. The MongoDB-style record form is refused — see migration element-data-source-and-object-block-filter-rule-array |
| sort | { field: string; order: Enum<'asc' | 'desc'> }[] | optional | Marker order for the fetched records — the SortItem array form [{ field, order }, ...], the one sort orthography every declared sort door on this platform shares; lowered to the wire $orderby. The legacy string clause (name desc) is refused — see migration object-block-sort-item-array |
| map | { latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … } | optional | Map field config, the author face — the same block ListViewSchema.map declares, and the one the renderer validates this node against. Taken WHOLE when present: the flat top-level spelling beside it is ignored |
| mapStyle | string | optional | MapLibre style URL or spec, overriding the public demo tiles. Read before map.style; NOT the base node style, which is an inline CSS record |
| navigation | { mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … } | optional | Marker-click navigation config — the same block ListViewSchema.navigation declares ({ mode, size, openNewTab, preventNavigation }), mode one of the seven NavigationModeSchema values |
| enableClustering | boolean | optional | Group nearby markers into clusters. Absent, the renderer clusters only above 100 markers |
Nested Shape: ObjectMapProps.data[provider='object']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'object' | ✅ | |
| object | string | ✅ | Target object name |
Nested Shape: ObjectMapProps.data[provider='value']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'value' | ✅ | |
| items | any[] | ✅ | Static data array |
Nested Shape: ObjectMapProps.data[provider='schema']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'schema' | ✅ | |
| schemaId | string | ✅ | Schema identifier — typically the metadata type name |
| schema | Record<string, any> | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. |
Nested Shape: ObjectMapProps.filter[number]
View filter rule
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to filter on |
| operator | Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …> | ✅ | Filter operator |
| value | string | number | boolean | null | (string | number)[] | optional | Filter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: icontains takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
Nested Shape: ObjectMapProps.sort[number]
Sort field and direction pair
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to sort by |
| order | Enum<'asc' | 'desc'> | ✅ | Sort direction |
Nested Shape: ObjectMapProps.map
| Property | Type | Required | Description |
|---|---|---|---|
| latitudeField | string | optional | Field providing the marker latitude (used with longitudeField) |
| longitudeField | string | optional | Field providing the marker longitude (used with latitudeField) |
| locationField | string | optional | Field providing a combined location — a "lat,lng" string or a { lat, lng } object — as the alternative to the latitudeField/longitudeField pair |
| titleField | string | optional | Field displayed as the marker title (popup heading, mobile record card, and what the map search box matches on) |
| descriptionField | string | optional | Field displayed as the marker description |
| zoom | number | optional | Initial zoom level (1-20). Omit to let the renderer fit the camera to the queried records |
| center | [number, number] | optional | Initial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records |
| style | string | optional | Map style URL — the MapLibre style document the renderer loads in place of the public demo tiles. The component-level mapStyle is read FIRST and wins when both are present; this is NOT the inline CSS style record a component node carries |
Nested Shape: ObjectMapProps.navigation
| Property | Type | Required | Description |
|---|---|---|---|
| mode | Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'> | optional (default: "page") | |
| view | never | optional | [REMOVED] view.list.navigation.view was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the form view to open for a record detail, and no layer resolved a view by that name: the value was passed straight into the navigation-MODE argument of the console's onNavigate, where anything other than edit or view matched no branch, so the key selected nothing and could silently deaden the row click. Delete the key; to choose what opens for a record, assign a record page to the object and let isDefault pick the one that opens — page assignment is the machinery that resolves a detail layout, and a list view's navigation block only decides HOW the detail is surfaced (mode, size). |
| preventNavigation | boolean | optional (default: false) | Disable standard navigation entirely |
| openNewTab | boolean | optional (default: false) | Force open in new tab (applies to page mode) |
| size | Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'> | optional (default: "auto") | Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel width; page mode ignores it. |
| width | string | number | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the size bucket. |
ObjectMasterDetailFormProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | optional | PARENT object. Optional because the component-level dataSource binding can supply the object instead |
| recordId | string | number | optional | Parent record to load (edit mode) |
| mode | Enum<'create' | 'edit'> | optional | Form mode |
| formType | Enum<'simple' | 'tabbed'> | optional | Parent form presentation — the two variants the renderer honours for the parent half |
| sections | { name?: string; label?: string; description?: string; collapsible?: boolean; … }[] | optional | Parent form sections — the same section shape object-form takes: { name?, label?, description?, collapsible?, collapsed?, visibleWhen?, columns?, pane?, fields } or { group, columns?, pane? } |
| fields | string[] | optional | Parent field names to draw, in order — bare names, as on object-form; a { name } or { field } object entry is refused |
| details | { childObject: string; relationshipField?: string; columns?: object[]; formFields?: string[]; … }[] | optional | Detail collections — each a strict entry ({ childObject, title?, addLabel?, columns?, relationshipField?, … }) whose columns are the inline grid columns a relationship field's inlineColumns takes; the FK and columns auto-derive from child metadata when omitted |
| title | string | Record<string, string> | optional | Form title |
| submitText | string | Record<string, string> | optional | Submit button label |
| cancelText | string | Record<string, string> | optional | Cancel button label |
| showSubmit | boolean | optional | Show the submit button |
| initialValues | Record<string, any> | optional | Prefill values for the parent (create mode) |
| initialData | Record<string, any> | optional | Alternate spelling of initialValues the renderer also reads |
| taxRateField | string | optional | Child field holding the per-line tax rate (line-items totals) |
Nested Shape: ObjectMasterDetailFormProps.sections[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Stable section identifier (snake_case) — the heading resolves through objects.<object>._sections.<name>.label |
| label | string | optional | Section heading (a plain string) |
| description | string | optional | Text drawn under the heading |
| collapsible | boolean | optional | Draw a disclosure control on the heading, so a reader can close the section and open it again. collapsed: true implies it |
| collapsed | boolean | optional | Start the section closed (implies collapsible) |
| visibleWhen | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Predicate (CEL) — the whole section, heading and fields, is drawn only when TRUE |
| columns | integer | optional | Field-grid columns for this section (1-4), a number |
| pane | Enum<'primary' | 'secondary'> | optional | The split form's panel this section renders in; omitted → the first section 'primary', the others 'secondary' |
| group | string | optional | Field group key (snake_case) whose members and presentation this section inherits, from the object's fieldGroups. Mutually exclusive with fields, and with every key the group itself declares (name, label, description, visibleWhen, and a true collapsible / collapsed) |
| fields | (string | { field: string; type?: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; options?: object[]; reference?: string; … } | { name: string; label?: string; description?: string; type?: string; … })[] | optional | The section's fields, in order — each a field name, the form view's { field, … } entry overriding that object field, or an inline form field { name, type, … }. Omit only when group supplies the members |
Nested Shape: ObjectMasterDetailFormProps.details[number]
| Property | Type | Required | Description |
|---|---|---|---|
| childObject | string | ✅ | Child object whose records are entered inline |
| relationshipField | string | optional | FK on the child pointing back to the parent (auto-detected from the child's master_detail/lookup field when omitted) |
| columns | { name: string; label?: string; type?: Enum<'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' | 'select' | 'lookup' | 'file'>; width?: number; … }[] | optional | Editable grid columns (derived from the child object when omitted). Each entry is the strict, name-keyed inline grid column a relationship field's inlineColumns takes ({ name, label?, type?, … } — objectui GridColumn); identity-only entries ({ name }) hydrate everything else from the child object's fields. Unknown keys and the retired field spelling are refused at parse. |
| formFields | string[] | optional | Child field names for the per-row expand form. When omitted they are derived from the child object's fields — except on an entry that names both relationshipField and at least one column, which is kept as authored: nothing is derived, and the per-row form is offered only when inlineMode is 'form', where it draws the child object's full field list |
| inlineMode | Enum<'grid' | 'form'> | optional | Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. When omitted it is resolved from the relationship field's inlineEdit, else from the child object's shape — except on an entry that names both relationshipField and at least one column, which is kept as authored: nothing is resolved, the collection renders as a grid, and the per-row form is offered only when formFields lists more fields than columns |
| amountField | string | optional | Numeric child column summed for the running total and the totalField rollup. When omitted it is picked from the grid's number and currency columns: a computed one, else one named amount, total, subtotal, line_total, line_amount or net_amount, else the last currency column, else the last numeric one — except on an entry that names relationshipField and at least one column and gives every column a type. The renderer keeps that entry exactly as authored, so nothing is picked. With no amountField authored or picked, the sums read a child column named amount, and the grid shows a running total only when totalField is set |
| sortField | never | optional | [REMOVED] object-master-detail-form property details[].sortField was removed in @objectstack/spec 17 (ADR-0087 D2) — the console reads no authored value: the field the line grid stamps with each line's position on drag-reorder is derived from the child object, so an authored sortField was accepted and dropped. Delete the key. For a drag-reorder to be saved, give the child object a field named position / sort_order / sequence / line_no / line_number / sort: the renderer stamps the child's first field with one of those names — except on an entry that names relationshipField and at least one column and gives every column a type, which the renderer keeps exactly as authored and stamps no line position on. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| totalField | string | optional | Parent field to receive the rolled-up sum |
| title | string | optional | Section title |
| minRows | number | optional | Minimum number of rows |
| maxRows | number | optional | Maximum number of rows |
| addLabel | string | optional | Add-row button label |
ObjectMetricProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | optional | Object this metric aggregates. Optional because the component-level dataSource binding can supply the object instead |
| label | string | Record<string, string> | optional | Metric label |
| description | string | Record<string, string> | optional | Helper text under the value |
| title | string | Record<string, string> | optional | Drill-down panel title; defaults to the metric label |
| icon | string | optional | Lucide icon name drawn in the metric tile header, inside the colorVariant-tinted square. Read on this component — ObjectMetricWidget forwards it to MetricWidget, which resolves it with getLazyIcon (the LazyIcon module: kebab-case or PascalCase, degrading to the Database glyph on an unknown name). |
| colorVariant | Enum<'default' | 'blue' | 'teal' | 'orange' | 'purple' | 'success' | 'warning' | 'danger'> | optional | Icon container color variant |
| aggregate | { field?: string; function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; groupBy?: string | object } | optional | Aggregation run against the object: { field?, function, groupBy? } — function one of count / sum / avg / min / max / count_distinct (field needed for all but count), groupBy a field name or a { field, dateGranularity } node, absent for one number over every row |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Filter the aggregation is scoped by — the ViewFilterRule array form [{ field, operator, value }, ...], the one filter orthography every filter door in this map shares. The MongoDB-style record form is refused — see migration element-data-source-and-object-block-filter-rule-array |
| format | string | optional | Number format pattern (e.g. '0,0', '$0,0', '0%') |
| currency | string | optional | ISO currency code (e.g. 'USD') — enables currency formatting |
| prefix | string | optional | Static prefix before the formatted value |
| suffix | string | optional | Static suffix after the formatted value |
| invert | boolean | optional | Display 1 - value for opposite-signal gauges (compliance/uptime) |
| variant | Enum<'card' | 'bare'> | optional | Layout variant |
| fallbackValue | string | number | optional | Static value shown when no data source is available |
| trend | { value: number; label?: string | Record<string, string>; direction?: Enum<'up' | 'down' | 'neutral'> } | optional | Static trend badge { value, label?, direction? } — value painted as a percentage, direction up / down / neutral. A compareTo-derived trend replaces it |
| drillDown | { enabled?: boolean; title?: string; target?: Enum<'drawer' | 'dialog' | 'navigate'>; columns?: string[]; … } | optional | Click-through drill config { enabled?, title?, target?, columns?, maxRows?, report? } — opens the records behind the number, scoped by the metric's own filter — or draws the report report defines; a present block is on unless enabled: false. filter and mode are refused: a metric tile has no click context and no row |
| compareTo | { kind: Enum<'previousPeriod' | 'previousYear'> } | optional | Period-over-period comparison { kind } — previousPeriod or previousYear, shifting the date macros in the tile's own filter. dimension is refused: this tile never reads a dataset time dimension |
Nested Shape: ObjectMetricProps.aggregate
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | optional | Field to aggregate — required for every function but count, which counts rows |
| function | Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'> | ✅ | Aggregation function — count, sum, avg, min, max or count_distinct |
| groupBy | string | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string } | optional | Field the rows are grouped by, or a { field, dateGranularity } date-bucket node. Omit it for the one number over every row (_all) |
Nested Shape: ObjectMetricProps.filter[number]
View filter rule
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to filter on |
| operator | Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …> | ✅ | Filter operator |
| value | string | number | boolean | null | (string | number)[] | optional | Filter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: icontains takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
Nested Shape: ObjectMetricProps.trend
| Property | Type | Required | Description |
|---|---|---|---|
| value | number | ✅ | The change shown on the badge, painted as a percentage (12 reads 12%) |
| label | string | Record<string, string> | optional | Badge caption — a string or an inline locale map. The tile-level description outranks it in the one caption slot they share |
| direction | Enum<'up' | 'down' | 'neutral'> | optional | Arrow beside the value; omit it for no arrow |
Nested Shape: ObjectMetricProps.drillDown
| Property | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | optional | Turn the tile's drill on or off; the block being present already means on, so this is only needed to force it off |
| title | string | optional | Drill drawer/dialog heading; defaults to the tile's own title, then its label. A metric tile has no click context, so an ${event.*} token here resolves to nothing |
| target | Enum<'drawer' | 'dialog' | 'navigate'> | optional | Where the drilled list opens: 'drawer' (default, side sheet), 'dialog' (centered modal), or 'navigate' (skip the in-place view and open the object's full list page; needs host drill navigation, else falls back to 'drawer') |
| columns | string[] | optional | Field names to show as columns in the drilled list (default: the table's own columns) |
| maxRows | integer | optional | Rows per page in the drilled list |
| report | { name: string; label: string | Record<string, string>; description?: string | Record<string, string>; type?: Enum<'tabular' | 'summary' | 'matrix' | 'joined'>; … } | optional | Drill into a report instead of the record list — a report definition (the same shape as reports[]): { name, label, dataset, values, … }, or a joined report whose every block binds a dataset. The drawer draws it as a report, with the metric's filter joined into its runtimeFilter |
Nested Shape: ObjectMetricProps.compareTo
| Property | Type | Required | Description |
|---|---|---|---|
| kind | Enum<'previousPeriod' | 'previousYear'> | ✅ | Comparison window: previousPeriod (equal-length, immediately before) or previousYear (−1 calendar year) |
ObjectTimelineProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | optional | Object this timeline binds to. Optional because the component-level dataSource binding can supply the object instead — this block registers through ElementDataSourceGate, which lowers the binding onto this key before the renderer sees the node |
| timeline | { startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … } | optional | Timeline configuration, the author face — the same block ListViewSchema.timeline declares: { startDateField, endDateField, titleField, groupByField, colorField, scale }. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Base query filter — the ViewFilterRule array form [{ field, operator, value }, ...], the one filter orthography every filter door in this map shares; lowered to the wire $filter. The MongoDB-style record form is refused — see migration element-data-source-and-object-block-filter-rule-array |
| sort | { field: string; order: Enum<'asc' | 'desc'> }[] | optional | Row order for the fetched entries — the SortItem array form [{ field, order }, ...], the one sort orthography every declared sort door on this platform shares; lowered to the wire $orderby. The legacy string clause (name desc) is refused — see migration object-block-sort-item-array |
| limit | integer | optional | Maximum number of records loaded onto the rail (row cap); lowered to the query's top-level $top (renderer default 100). A timeline renders one rail with no pagination control, so this is the author's window rather than a page size |
| data | any[] | optional | Pre-fetched records — read FIRST as the rail's row source, ahead of the data-scope binding and the fetch, and composed into entries through the same timeline field bindings a fetched row takes; authoring it suppresses the object query entirely. Distinct from items, which is the already-composed entry shape and wins over this key when both are written |
| items | { time?: string; title?: string; description?: string; variant?: Enum<'default' | 'success' | 'warning' | 'danger' | 'info'>; … }[] | optional | Static inline entries — read ahead of every record source, data above included, and bypasses the object query entirely (the renderer becomes a pass-through). Each entry is the kind this node's variant selects: a feed entry { time?, title, description?, variant?, icon?, content?, className? } (variant absent / vertical / horizontal), or a gantt row { label, items? } (variant: 'gantt') whose bars are { title?, startDate?, endDate?, variant? }, each date a string or epoch milliseconds |
| variant | Enum<'vertical' | 'horizontal' | 'gantt'> | optional | Rail layout (renderer default vertical). ⚠️ gantt needs authored items: the object-bound path composes flat feed entries, which the gantt branch cannot draw, and refuses that combination with a named diagnostic instead of drawing an empty chart |
| dateFormat | Enum<'short' | 'long' | 'iso'> | optional | How each entry's date is rendered (renderer default short): short / long are locale-formatted, iso is the locale-free machine form |
| rowLabel | string | optional | Header label for the gantt row column — read by the gantt branch only, which on this block needs authored items |
| minDate | string | optional | Pin the gantt axis start (ISO yyyy-mm-dd) instead of deriving it from the rows; only a non-empty value is honoured |
| maxDate | string | optional | Pin the gantt axis end (ISO yyyy-mm-dd) instead of deriving it from the rows; only a non-empty value is honoured |
| descriptionField | string | optional | Field rendered as each entry's description (renderer default description). Declared FLAT because the timeline block has no member for it — it is the only spelling this binding has |
| mapping | { title?: string; date?: string; description?: string; variant?: string } | optional | Record-to-entry field mapping { title?, date?, description?, variant? }, each a field name — the binding record read BETWEEN the timeline block and the flat fallbacks. Its variant member (the field whose value picks each marker colour, renderer default variant) is the only spelling that binding has |
| navigation | { mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … } | optional | Entry-click navigation config — the same block ListViewSchema.navigation declares ({ mode, size, openNewTab, preventNavigation }) |
Nested Shape: ObjectTimelineProps.timeline
| Property | Type | Required | Description |
|---|---|---|---|
| startDateField | string | ✅ | Field for timeline item start date |
| endDateField | string | optional | Field for timeline item end date |
| titleField | string | ✅ | Field to display as timeline item title |
| groupByField | string | optional | Field to group timeline rows. NO leading or trailing whitespace: the renderer reads this name off every row verbatim, so a padded spelling drops every row into one ungrouped band. |
| colorField | string | optional | Field to derive each item color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the timeline default marker color |
| scale | Enum<'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'> | optional (default: "week") | Default timeline scale |
Nested Shape: ObjectTimelineProps.filter[number]
View filter rule
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to filter on |
| operator | Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …> | ✅ | Filter operator |
| value | string | number | boolean | null | (string | number)[] | optional | Filter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: icontains takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
Nested Shape: ObjectTimelineProps.sort[number]
Sort field and direction pair
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to sort by |
| order | Enum<'asc' | 'desc'> | ✅ | Sort direction |
Nested Shape: ObjectTimelineProps.items[number]
| Property | Type | Required | Description |
|---|---|---|---|
| time | string | optional | When it happened — an ISO 8601 date (or date-time) string, formatted by the row's dateFormat |
| title | string | optional | The entry's heading — required on a feed entry (the arm a feed variant selects) |
| description | string | optional | Secondary line under the title |
| variant | Enum<'default' | 'success' | 'warning' | 'danger' | 'info'> | optional | Marker colour (renderer default default) |
| icon | string | optional | Emoji or short text drawn inside the marker |
| content | any | optional | Child page components drawn below the description — a component node or a list of them. Held opaque: not judged on this row |
| className | string | optional | Tailwind classes for the entry |
| label | string | optional | Row label, drawn in the row-label gutter — required on a gantt row (the arm variant: 'gantt' selects) |
| items | { title?: string; startDate?: string | number; endDate?: string | number; variant?: Enum<'default' | 'success' | 'warning' | 'danger' | 'info'> }[] | optional | The row's bars, each { title?, startDate?, endDate?, variant? } |
Nested Shape: ObjectTimelineProps.mapping
| Property | Type | Required | Description |
|---|---|---|---|
| title | string | optional | Field whose value is each entry's title — read after timeline.titleField, ahead of the flat titleField (renderer default name) |
| date | string | optional | Field whose value is each entry's date — read after timeline.startDateField / timeline.dateField, ahead of the flat spellings |
| description | string | optional | Field whose value is each entry's description — read ahead of descriptionField (renderer default description) |
| variant | string | optional | Field whose value picks each entry's marker colour (renderer default variant) — the only spelling this binding has |
Nested Shape: ObjectTimelineProps.navigation
| Property | Type | Required | Description |
|---|---|---|---|
| mode | Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'> | optional (default: "page") | |
| view | never | optional | [REMOVED] view.list.navigation.view was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the form view to open for a record detail, and no layer resolved a view by that name: the value was passed straight into the navigation-MODE argument of the console's onNavigate, where anything other than edit or view matched no branch, so the key selected nothing and could silently deaden the row click. Delete the key; to choose what opens for a record, assign a record page to the object and let isDefault pick the one that opens — page assignment is the machinery that resolves a detail layout, and a list view's navigation block only decides HOW the detail is surfaced (mode, size). |
| preventNavigation | boolean | optional (default: false) | Disable standard navigation entirely |
| openNewTab | boolean | optional (default: false) | Force open in new tab (applies to page mode) |
| size | Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'> | optional (default: "auto") | Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel width; page mode ignores it. |
| width | string | number | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the size bucket. |
ObjectTreeProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | optional | Object this tree binds to — the THIRD record source resolveRecordSourceConfig resolves, after data and staticData. Optional because either of the first two rungs resolves the source without it: data can name the object itself ({ provider: 'object', object }) and staticData needs none. ⚠️ NOT supplied by the component-level dataSource binding the sibling blocks name — this renderer registers no such gate |
| data | { provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> } | optional | Data source binding (ViewDataSchema — discriminated on provider: object | value | schema), read FIRST by resolveRecordSourceConfig. Static inline rows live at { provider: 'value', items: [...] } or at staticData; the bare-array shortcut is refused |
| staticData | any[] | optional | Inline records — read SECOND by resolveRecordSourceConfig, wrapped into a { provider: 'value' } config |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Base query filter — the ViewFilterRule array form [{ field, operator, value }, ...], the one filter orthography every filter door in this map shares; lowered to the wire $filter. The MongoDB-style record form is refused — see migration element-data-source-and-object-block-filter-rule-array |
| tree | { parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } | optional | Tree/hierarchy configuration, the author face — the same block ListViewSchema.tree declares: { parentField?, labelField?, fields?, defaultExpandedDepth? }. parentField auto-detects from the object schema when omitted |
| navigation | { mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … } | optional | Row-click navigation config — the same block ListViewSchema.navigation declares ({ mode, size, openNewTab, preventNavigation }), mode one of the seven NavigationModeSchema values |
Nested Shape: ObjectTreeProps.data[provider='object']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'object' | ✅ | |
| object | string | ✅ | Target object name |
Nested Shape: ObjectTreeProps.data[provider='value']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'value' | ✅ | |
| items | any[] | ✅ | Static data array |
Nested Shape: ObjectTreeProps.data[provider='schema']
| Property | Type | Required | Description |
|---|---|---|---|
| provider | 'schema' | ✅ | |
| schemaId | string | ✅ | Schema identifier — typically the metadata type name |
| schema | Record<string, any> | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. |
Nested Shape: ObjectTreeProps.filter[number]
View filter rule
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to filter on |
| operator | Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …> | ✅ | Filter operator |
| value | string | number | boolean | null | (string | number)[] | optional | Filter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: icontains takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
Nested Shape: ObjectTreeProps.tree
| Property | Type | Required | Description |
|---|---|---|---|
| parentField | string | optional | Single-parent pointer field (auto-detected from the object schema when omitted) |
| labelField | string | optional | Field rendered indented in the first column (defaults to "name") |
| fields | string[] | optional | Additional fields rendered as flat columns alongside the label |
| defaultExpandedDepth | integer | optional | Initial expansion depth (0 = roots only; omit = expand all) |
Nested Shape: ObjectTreeProps.navigation
| Property | Type | Required | Description |
|---|---|---|---|
| mode | Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'> | optional (default: "page") | |
| view | never | optional | [REMOVED] view.list.navigation.view was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the form view to open for a record detail, and no layer resolved a view by that name: the value was passed straight into the navigation-MODE argument of the console's onNavigate, where anything other than edit or view matched no branch, so the key selected nothing and could silently deaden the row click. Delete the key; to choose what opens for a record, assign a record page to the object and let isDefault pick the one that opens — page assignment is the machinery that resolves a detail layout, and a list view's navigation block only decides HOW the detail is surfaced (mode, size). |
| preventNavigation | boolean | optional (default: false) | Disable standard navigation entirely |
| openNewTab | boolean | optional (default: false) | Force open in new tab (applies to page mode) |
| size | Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'> | optional (default: "auto") | Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel width; page mode ignores it. |
| width | string | number | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the size bucket. |
PageAccordionProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| items | { label: string | Record<string, string>; icon?: string; collapsed?: boolean; children: any[] }[] | ✅ | |
| allowMultiple | boolean | optional (default: false) | Allow multiple panels to be expanded simultaneously |
| variant | Enum<'flush' | 'card'> | optional (default: "flush") | Panel framing: 'flush' draws a divider under each panel; 'card' leaves the border to each panel's own content |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: PageAccordionProps.items[number]
| Property | Type | Required | Description |
|---|---|---|---|
| label | string | Record<string, string> | ✅ | Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time |
| icon | string | optional | Lucide icon name rendered in the panel trigger, left of the label. Read on this component — the renderer draws it via LazyIcon; contrast the item value beside it, which the renderer overwrites with panel-<index>. |
| collapsed | boolean | optional (default: false) | |
| children | any[] | ✅ | Child components |
Nested Shape: PageAccordionProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
PageCardProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| title | string | Record<string, string> | optional | Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time |
| bordered | boolean | optional (default: true) | |
| actions | never | optional | [REMOVED] page:card property actions was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — no renderer ever read it: objectui's card renderer builds its <Card> from title, bordered, children and footer only, has no actions area, and the component registry never published it as an input, so an authored value was accepted and dropped. Delete the key and author the buttons as components in the card's children or footer (element:button, record:quick_actions), which is what actually renders. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| children | any[] | optional | Card content components, in order (the card body slot) |
| body | never | optional | [REMOVED] page:card property body was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — it was a second spelling of the composition slot every other container calls children, and the renderer reads both. Rename the key to children; the value (an array of child components) is unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| footer | any[] | optional | Card footer components (slot) |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: PageCardProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
PageContainerProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| children | any[] | optional | Child components rendered inside this container, in order |
PageHeaderProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| title | string | Record<string, string> | optional | Page title. Omit to let the renderer derive the heading from the record (the default for record pages) — set explicitly on non-record pages (dashboard, landing) with no record to derive from. |
| subtitle | string | Record<string, string> | optional | Page subtitle |
| icon | never | optional | [REMOVED] page:header property icon was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — no renderer ever read it: objectui resolves icon only per header action (action.icon), never off the header's own props bag, and the component registry never published it as an input, so an authored value was accepted and dropped. Delete the key. The header's own identity is drawn by the record chrome (recordChrome, on by default) and each action carries its own icon. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| breadcrumb | never | optional | [REMOVED] page:header property breadcrumb was removed in @objectstack/spec 17 (ADR-0087 D2) — no renderer ever drew a trail for it: objectui drew an empty slot and nothing filled it, and the navigation trail is drawn once, by the app shell's header. Delete the key, whether it was true or false; the shell's trail is unchanged. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| actions | string[] | optional | Action IDs the header draws, resolved against the actions declared on the page's object. An authored list is the whole set: an action that declares record_header (or record_more) in its locations but is not named here is not drawn, and omitting the key draws none of the object's actions. A named action that declares neither location renders nowhere. The host's own Edit / Share / Delete chrome is separate from this list. |
| recordChrome | boolean | optional (default: true) | Render the record chrome — the title as a record chip with its follow star and copy-id button. Set false on a non-record page (dashboard, landing) to fall back to the bare heading layout. |
| showStar | boolean | optional (default: true) | Show the follow (favourite) star beside the record title. Part of the record chrome — no effect when recordChrome is false. |
| showCopyId | boolean | optional (default: true) | Show the copy-record-id button beside the record title. Part of the record chrome — no effect when recordChrome is false. |
| maxVisible | integer | optional | How many header actions render as inline buttons before the rest fold into the overflow menu (renderer default 3). |
| mobileMaxVisible | integer | optional | The maxVisible budget on mobile viewports (renderer default 1). |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: PageHeaderProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
PageTabsProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| tabStyle | Enum<'line' | 'card' | 'pill'> | optional (default: "line") | Tab-strip visual style: 'line' underlines the active tab, 'card' frames each tab, 'pill' renders rounded pills |
| type | never | optional | [REMOVED] page:tabs property type was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — a props key named type collides with the page component's own dispatch key, so it is unauthorable in the flat and JSX carriers and was never validated in them. Rename the key to tabStyle; the value (line | card | pill) is unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| position | Enum<'top' | 'left'> | optional (default: "top") | |
| alwaysShowStrip | boolean | optional | Render the tab strip even when only one tab is visible (renderer default: a one-tab strip is hidden). |
| items | { label: string | Record<string, string>; icon?: string; visibleWhen?: string | object; value?: string; … }[] | ✅ | |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: PageTabsProps.items[number]
| Property | Type | Required | Description |
|---|---|---|---|
| label | string | Record<string, string> | ✅ | Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time |
| icon | string | optional | Lucide icon name rendered in the tab trigger, left of the label. Read on this component — the renderer draws it via LazyIcon; contrast the item key beside it, which no read point takes and which the alias table answers with value. |
| visibleWhen | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Visibility predicate (CEL) — the whole tab (header + panel) is omitted when FALSE; the renderer falls back to the first visible tab when the active one is hidden. Contract-bound roots: record, current_user (ADR-0068 aliases user / ctx.user), page.<var>. ⚠️ NOT the same environment as page-component visibleWhen: this surface's own evaluator binds data to the record ROW (the component-node evaluator mounts no data root) and also spreads the row's bare fields — renderer behaviour, NOT contract-guaranteed. ADR-0089 canonical name — visible/showWhen/visibility/visibleOn are all rejected here (not folded in), each with a pointer at this key. |
| value | string | optional | Stable ?tab= URL token for this tab (default: index-derived tab-<i>, which is not durable across item-list changes) |
| count | integer | optional | Badge count shown next to the tab label (default: derived from record:related_list descendants) |
| children | any[] | ✅ | Child components |
Nested Shape: PageTabsProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
RecordActivityProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| types | (Enum<'comment' | 'field_change' | 'task' | 'event' | 'email' | 'call' | 'note' | 'file' | 'record_create' | 'record_delete' | 'approval' | 'sharing' | 'system'> | string)[] | optional | Feed item kinds to show (default: all). Open vocabulary: the FeedItemType members are the platform built-in kinds, and author-contributed activity kinds (open sys_activity.type vocabulary, ADR-0052 §5b.2) are equally legal — entries are never validated against the built-in set. |
| filterMode | Enum<'all' | 'comments_only' | 'changes_only' | 'tasks_only'> | optional (default: "all") | Default activity filter |
| showFilterToggle | boolean | optional (default: true) | Show filter dropdown in panel header |
| limit | integer | optional (default: 20) | Number of items to load per page |
| showCompleted | boolean | optional (default: false) | Include completed activities |
| unifiedTimeline | boolean | optional (default: true) | Mix field changes and comments in one timeline (Airtable style) |
| showCommentInput | boolean | optional (default: true) | Show "Leave a comment" input at the bottom |
| enableMentions | boolean | optional (default: true) | Enable @mentions in comments |
| enableReactions | boolean | optional (default: false) | Enable emoji reactions on feed items |
| enableThreading | boolean | optional (default: false) | Enable threaded replies on comments |
| showSubscriptionToggle | boolean | optional (default: true) | Show bell icon for record-level notification subscription |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: RecordActivityProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
RecordAlertAction
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| actionName | string | ✅ | Name of an action declared on this object (actions[]) — resolved from object metadata and run through the shared action engine, so confirm/param dialogs, toast and reload behave exactly as in record:quick_actions. |
| label | string | Record<string, string> | optional | CTA button label — a string or an inline locale map, resolved with the same pickLocalized chain as title/body (default: the action's own label). |
| variant | Enum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'> | optional | Button variant — the Button primitive's own vocabulary (renderer default: destructive when severity is error, else default). |
RecordAlertProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| severity | Enum<'info' | 'warning' | 'error' | 'success'> | optional | Banner severity — styling, default icon, and the a11y role (error renders role="alert"/assertive; the rest role="status"/polite). Renderer default: info. |
| title | string | Record<string, string> | optional | Banner title — a string or an inline locale map ({ en, "zh-CN", … }), resolved to the current language at render (pickLocalized). |
| body | string | Record<string, string> | optional | Banner body — a string or an inline locale map, resolved like title. |
| visible | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Visibility predicate evaluated against the record page scope (record, user + ctx.* mirror, objectName, features) — a boolean literal, a CEL string, or a { dialect, source } envelope. Omit for always-visible; the banner is hidden while the record is still loading either way. |
| icon | string | optional | Lucide icon name (renderer default: the severity's own icon). Read on this component — contrast the rail's refused icon, which no render path reads. |
| action | { actionName: string; label?: string | Record<string, string>; variant?: Enum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'> } | optional | Optional call-to-action button rendered under the body — { actionName, label?, variant? }, resolved from the object's declared actions. |
| dismissible | boolean | optional | Render an X control; dismissal is remembered per object/record in localStorage (renderer default: off). |
| dismissKey | string | optional | Stable key the dismissal is remembered under, so reworded titles do not resurrect a dismissed banner (renderer default: the English resolution of title, else the severity). |
Nested Shape: RecordAlertProps.action
| Property | Type | Required | Description |
|---|---|---|---|
| actionName | string | ✅ | Name of an action declared on this object (actions[]) — resolved from object metadata and run through the shared action engine, so confirm/param dialogs, toast and reload behave exactly as in record:quick_actions. |
| label | string | Record<string, string> | optional | CTA button label — a string or an inline locale map, resolved with the same pickLocalized chain as title/body (default: the action's own label). |
| variant | Enum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'> | optional | Button variant — the Button primitive's own vocabulary (renderer default: destructive when severity is error, else default). |
RecordChatterProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| position | Enum<'bottom' | 'right' | 'left'> | optional | Where the panel docks relative to the record body — right/left dock a side panel, bottom renders in flow under the record body (renderer default: bottom). |
| width | string | number | optional | Panel width (e.g., "350px", "30%") — side positions (right/left) only. |
| collapsible | boolean | optional | Whether the panel can be collapsed (renderer default: off). |
| defaultCollapsed | boolean | optional | Whether the panel starts collapsed (renderer default: off; only meaningful with collapsible). |
| feed | { types?: (Enum<'comment' | 'field_change' | 'task' | 'event' | 'email' | 'call' | 'note' | …> | string)[]; filterMode?: Enum<'all' | 'comments_only' | 'changes_only' | 'tasks_only'>; showFilterToggle?: boolean; limit?: integer; … } | optional | Embedded activity feed configuration |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: RecordChatterProps.feed
| Property | Type | Required | Description |
|---|---|---|---|
| types | (Enum<'comment' | 'field_change' | 'task' | 'event' | 'email' | 'call' | 'note' | …> | string)[] | optional | Feed item kinds to show (default: all). Open vocabulary: the FeedItemType members are the platform built-in kinds, and author-contributed activity kinds (open sys_activity.type vocabulary, ADR-0052 §5b.2) are equally legal — entries are never validated against the built-in set. |
| filterMode | Enum<'all' | 'comments_only' | 'changes_only' | 'tasks_only'> | optional (default: "all") | Default activity filter |
| showFilterToggle | boolean | optional (default: true) | Show filter dropdown in panel header |
| limit | integer | optional (default: 20) | Number of items to load per page |
| showCompleted | boolean | optional (default: false) | Include completed activities |
| unifiedTimeline | boolean | optional (default: true) | Mix field changes and comments in one timeline (Airtable style) |
| showCommentInput | boolean | optional (default: true) | Show "Leave a comment" input at the bottom |
| enableMentions | boolean | optional (default: true) | Enable @mentions in comments |
| enableReactions | boolean | optional (default: false) | Enable emoji reactions on feed items |
| enableThreading | boolean | optional (default: false) | Enable threaded replies on comments |
| showSubscriptionToggle | boolean | optional (default: true) | Show bell icon for record-level notification subscription |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: RecordChatterProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
RecordDetailsProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| columns | Enum<'1' | '2' | '3' | '4'> | optional (default: "2") | Number of columns for field layout (1-4) |
| layout | never | optional | [REMOVED] record:details property layout was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — its declared auto | custom semantics were never implemented: the renderer tests layout only against inline | compact, two values the schema never permitted, so both legal values took the same branch and the key selected nothing. Delete the key — the body is already chosen by what you author: sections renders the explicit groups (the old custom), and omitting it falls back to the object's highlightFields (the old auto). Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| sections | { name?: string; label?: string | Record<string, string>; columns?: integer; group?: string; … }[] | optional | Field groups rendered as the detail body, in order. Object form: { name?, label?, columns?, fields, hideEmpty?, collapsible?, showBorder?, defaultCollapsed?, icon?, description?, headerColor?, count? } — or the group-reference form { group, columns?, hideEmpty?, showBorder?, headerColor? }, which inherits members and presentation from the object's fieldGroups entry (ADR-0085 §5). count: { field } shows how many values a multi-value field holds in the heading, through its {{ count }} hole. |
| fields | string[] | optional | Explicit field list to display (optional, overrides highlightFields) |
| hideFields | string[] | optional | Field names to omit from the body — applied to fields and to every section's fields (used to dedupe fields already shown in record:highlights or as the page title) |
| inlineEdit | boolean | optional | Allow inline field editing in the detail body (renderer default: on, where the object itself is editable — set false to force it off). |
| showHeader | boolean | optional | Render the detail body's own heading (renderer default: off). |
| enforceFieldSecurity | boolean | optional | Fold this block's field list through the caller's FIELD-read permissions before rendering, so a field the permission set denies leaves no empty row behind (renderer default: off). Presentation only: it re-applies the same field-read answer the server already enforced (ADR-0066 D3) and never widens access — with it off a denied field still arrives masked or stripped, and with it on the server still decides every value. |
| redactFields | string[] | optional | Field names this block never renders, whatever the permission answer (renderer default: render everything authored). Presentation only, evaluated in the browser after the record is fetched — the values are still in the page, so this is NOT a data-access control and NOT the object's publicSharing.redactFields, which removes them server-side. To keep a value from the caller, gate the field itself (requiredPermissions / maskingRule, ADR-0066 D3) or the permission set. Neighbours hideFields, which is the dedupe channel the renderer also writes to. |
| requiredPermissions | string[] | optional | [ADR-0066] Capabilities the user must ALL hold — names that permission sets grant through systemPermissions, not object actions: read or update here is an ordinary capability name, not the object's read or edit permission. When the client has resolved the user's capabilities and any of these is missing, this block does not render its content; wherever it would otherwise render, an insufficient-permissions notice takes its place. Presentation only: it authorises nothing, and the data API still serves the same data to the same user. A client that cannot resolve the user's capabilities (no permission provider, or one that does not report systemPermissions) renders this block as if they were held — it fails open. |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: RecordDetailsProps.sections[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Stable section identifier for i18n lookup (snake_case) — resolves objects.<object>._sections.<name>.label; a nameless section renders its authored label in every locale |
| label | string | Record<string, string> | optional | Section heading (omit for an untitled, borderless section) |
| columns | integer | optional | Field-grid columns for this section (1-4). Omitted → the renderer derives the width. |
| group | string | optional | Field group key (snake_case) whose members and presentation this section inherits, from the object's fieldGroups (ADR-0085 §5 deriveFieldGroupLayout). Mutually exclusive with fields, and with every key the group itself declares (name, label, icon, description, collapsible, defaultCollapsed). Must name a declared group — checked by reference diagnostics. |
| fields | string[] | optional | Field names rendered in this section, in order. Omit only when group supplies the members instead. |
| hideEmpty | boolean | optional | Hide this section's empty fields (renderer default: on — and a section whose fields are ALL empty then renders nothing at all: no heading, no skeleton). Set false to render empty rows, keeping the section's label skeleton on an all-empty record (e.g. a brand-new one). |
| collapsible | boolean | optional | Render this section as a collapsible card — the heading becomes a chevron toggle, initially expanded (renderer default: off). |
| showBorder | boolean | optional | Draw this section's card chrome (renderer default: derived — on for a titled section, off for an untitled one). Set false for a borderless titled section, or true for a bordered untitled one. |
| defaultCollapsed | boolean | optional | Start a collapsible: true section collapsed (renderer default: expanded). Consulted only when collapsible is on — a non-collapsible section never reads its collapse state. |
| icon | string | optional | Heading icon, as a lucide icon name (kebab-case, e.g. building-2). A value that is not an ASCII identifier (emoji, CJK text) renders as literal text beside the heading instead. Shown where the section heading renders: a titled section, or any collapsible section. |
| description | string | optional | Sub-heading text rendered under the section heading (plain string — the renderer applies no translation to it, unlike label). Renders on a titled or collapsible section; a collapsible section hides it while collapsed. |
| headerColor | Enum<'muted' | 'muted/50' | 'accent' | 'primary/10' | 'secondary/10' | 'destructive/10'> | optional | Section-header background tint, from the closed six-token vocabulary rendered by objectui's record:details header (muted | muted/50 | accent | primary/10 | secondary/10 | destructive/10). A value outside the enum is refused at authoring time rather than silently not painting. Omit for an untinted header. |
| count | { field: string } | optional | Show how many values a multi-value field holds on the current record in this section's heading: { field: 'tags' }, with label writing the {{ count }} hole where the number goes ('{{ count }} tags'). The field must be a multi-value field the bound object declares — multiselect, checkboxes, tags, or a select / lookup / user / file / image field with multiple: true — and an absent or empty value counts 0. Every locale of an inline label map, and a translated objects.<object>._sections.<name>.label, writes the same {{ count }} hole. Not allowed beside group. The record detail page fills every hole in the heading after translating it. |
Nested Shape: RecordDetailsProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
RecordHighlightsField
Highlight field: bare name, or {name,label?,type?,readonly?}
Union Options
This schema accepts one of the following structures:
Option 1
Type: string
Option 2
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Field name on the record |
| label | string | optional | Display label (overrides schema label) |
| type | string | optional | Override cell renderer type (rare) |
| readonly | boolean | optional | Render this chip read-only — suppresses inline editing on the highlight card. Use for hook/automation-maintained columns that must not be hand-edited from the record header. |
RecordHighlightsProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| fields | (string | { name: string; label?: string; type?: string; readonly?: boolean })[] | ✅ | Key fields to highlight (1-7 fields max, typically displayed as prominent cards). Each item may be a bare field name or {name, label?, type?, readonly?} for inline overrides. |
| layout | Enum<'horizontal' | 'vertical'> | optional (default: "horizontal") | Layout orientation for highlight fields |
| enforceFieldSecurity | boolean | optional | Fold this block's highlight chips through the caller's FIELD-read permissions before rendering, so a field the permission set denies leaves no empty chip behind (renderer default: off). Presentation only: it re-applies the same field-read answer the server already enforced (ADR-0066 D3) and never widens access — the record is fetched either way and the server still decides every value. |
| redactFields | string[] | optional | Field names this block never renders as a chip, whatever the permission answer (renderer default: render every field authored). Presentation only, evaluated in the browser after the record is fetched — the values are still in the page, so this is NOT a data-access control and NOT the object's publicSharing.redactFields, which removes them server-side. To keep a value from the caller, gate the field itself (requiredPermissions / maskingRule, ADR-0066 D3) or the permission set. |
| requiredPermissions | string[] | optional | [ADR-0066] Capabilities the user must ALL hold — names that permission sets grant through systemPermissions, not object actions: read or update here is an ordinary capability name, not the object's read or edit permission. When the client has resolved the user's capabilities and any of these is missing, this block does not render its content; wherever it would otherwise render, an insufficient-permissions notice takes its place. Presentation only: it authorises nothing, and the data API still serves the same data to the same user. A client that cannot resolve the user's capabilities (no permission provider, or one that does not report systemPermissions) renders this block as if they were held — it fails open. |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: RecordHighlightsProps.fields[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Field name on the record |
| label | string | optional | Display label (overrides schema label) |
| type | string | optional | Override cell renderer type (rare) |
| readonly | boolean | optional | Render this chip read-only — suppresses inline editing on the highlight card. Use for hook/automation-maintained columns that must not be hand-edited from the record header. |
Nested Shape: RecordHighlightsProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
RecordHistoryProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| limit | integer | optional | Maximum history entries displayed, and the $top of the self-fetch query (renderer default: 50). |
| emptyText | string | optional | Copy shown when the record has no history. Literal string rendered as-is in EVERY locale (no inline locale map — the timeline renders it as a raw React child; renderer default: "No history yet"). |
| unknownUserText | string | optional | Copy substituted when an entry has no resolvable actor. Literal string, every locale (renderer default: "Unknown user"). |
RecordLineItemsProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| childObject | string | optional | Child object whose records this grid lists, edits and saves. Optional because the component-level dataSource binding can supply the object instead; with neither, the panel shows a configuration hint and fetches nothing |
| relationshipField | string | ✅ | FK on the child object pointing back to the parent record — the rows are queried as { [relationshipField]: parentId }. Required: this panel does not auto-detect it |
| columns | { name: string; label?: string; type?: Enum<'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' | 'select' | 'lookup' | 'file'>; width?: number; … }[] | ✅ | Editable grid columns, drawn exactly as declared. Each entry is the strict, name-keyed inline grid column a relationship field's inlineColumns takes ({ name, label?, type?, options?, … } — objectui GridColumn); unknown keys and the retired field spelling are refused. Unlike the master-detail carriers, this block does NOT hydrate a column from the child object's field: declare label, type and options yourself — an identity-only { name } column is a text cell headed by its name. Required, with at least one column: nothing derives them, and an empty list draws a grid with no cells |
| parentObject | string | optional | Parent object the totalField rollup is written to (default: the object of the record on the page) |
| parentId | string | optional | Parent record id the rows belong to (default: the record on the page). Wins over recordId |
| recordId | string | optional | Alternate spelling of parentId the renderer also reads; parentId wins when both are set |
| amountField | string | optional | Numeric child column summed for the running total and for the totalField rollup (default amount when only totalField is set) |
| totalField | string | optional | Parent field to receive the rolled-up sum on save |
| title | string | optional | Panel title. A literal string rendered as-is in every locale (no inline locale map — the panel renders it as a React child); renderer default: the localized "Line Items" |
| readonly | boolean | optional | Render the grid read-only: cells locked, no Save button, and no adding or removing lines (renderer default: editable) |
| minRows | number | optional | Minimum number of rows |
| maxRows | number | optional | Maximum number of rows |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Additional criteria for the child rows — the ViewFilterRule array form [{ field, operator, value }, ...], AND-combined with the parent relationship condition, never substituted for it: it can only narrow this record's lines |
| sort | { field: string; order: Enum<'asc' | 'desc'> }[] | optional | Load order for the child rows — the SortItem array form [{ field, order }, ...] (renderer default: storage order) |
| limit | integer | optional | Row cap for the child fetch (renderer default: 500). The grid has no pagination, so this is the window of rows that are editable and saved together |
Nested Shape: RecordLineItemsProps.columns[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Child field this column shows — the key the grid reads and writes on each row object (objectui GridColumn.name). The retired field spelling is refused. |
| label | string | optional | Column header; defaults to the child field's label via hydration. |
| type | Enum<'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' | 'select' | 'lookup' | 'file'> | optional | Cell control, derived from the child field's type when omitted. Declaring it opts the column out of schema hydration — supply the extras (options / reference / …) yourself. |
| width | number | optional | Fixed column width in px; omitted columns use type-based role sizing (text flexes, numeric/date/select stay fixed). |
| required | boolean | optional | Cell is flagged inline-invalid while empty. Computed columns are never required. |
| options | { label: string; value: string }[] | optional | Select-cell options for type: 'select'; derived from the child field's options when the column declares no type. |
| prefix | string | optional | Symbol shown in a currency cell in place of the resolved currency's own symbol. No default: when omitted, the cell shows the symbol of the currency it resolves. It replaces the symbol only — the amount's decimal places stay the currency's. |
| step | number | optional | Input step for numeric cells. |
| reference | string | optional | Referenced object for type: 'lookup' cells; derived from the child lookup field when the column declares no type. |
| displayField | string | optional | Label field shown for a picked lookup record. |
| idField | string | optional | Id field stored for a picked lookup record. |
| multiple | boolean | optional | Multi-value column: multi-record lookup, or multi-file upload cell. |
| accept | string[] | optional | Accepted MIME types / extensions for a file cell's picker (e.g. ['image/*', '.pdf']); omit to accept anything. |
| defaultHidden | boolean | optional | Collapsed into the grid's column chooser by default (not dropped); required columns are never default-hidden. |
| computed | boolean | optional | Read-only computed column, recomputed live from sibling cells via expr and written back into the row. |
| expr | string | optional | Arithmetic expression for a computed column — a BARE string over + - * / %, parentheses, numeric literals and field refs (record.qty or qty), evaluated by the grid's own safe evaluator. Deliberately NOT a CEL Expression envelope; { dialect, source } is refused here. |
| scale | integer | optional | Decimal places to round a computed numeric result to (integer 0-100). REFUSED on a column declaring type: 'currency' — delete it there: the currency's ISO 4217 minor unit decides. The upper bound is the renderer's: the grid rounds with toFixed, which throws a RangeError above 100. |
| autofill | boolean | optional | For lookup columns: picking a record copies its same-named fields into sibling columns (a product's unit_price/description). On by default; set false to disable. |
| readonlyWhen | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Predicate (CEL) — the cell is read-only when TRUE, evaluated per row against the row as record plus the header as parent (e.g. Pparent.status == 'paid'). |
| requiredWhen | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } | optional | Predicate (CEL) — the cell is required when TRUE. Same record + parent scope as readonlyWhen. PRESENTATION ONLY: this flags the cell inline-invalid in the grid; nothing on the write path reads it. The server-enforced contract is the child FIELD's own requiredWhen — a transition gate, see Field.requiredWhen — which hydration copies onto an identity-only column, so declaring the requirement here alone enforces nothing. |
Nested Shape: RecordLineItemsProps.filter[number]
View filter rule
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to filter on |
| operator | Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …> | ✅ | Filter operator |
| value | string | number | boolean | null | (string | number)[] | optional | Filter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: icontains takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
Nested Shape: RecordLineItemsProps.sort[number]
Sort field and direction pair
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to sort by |
| order | Enum<'asc' | 'desc'> | ✅ | Sort direction |
RecordPathProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| statusField | string | ✅ | Field name representing the current status/stage |
| stages | { value: string; label: string | Record<string, string>; terminal?: Enum<'won' | 'lost'> }[] | optional | Explicit stage definitions (if not using field metadata) |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: RecordPathProps.stages[number]
| Property | Type | Required | Description |
|---|---|---|---|
| value | string | ✅ | |
| label | string | Record<string, string> | ✅ | Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time |
| terminal | Enum<'won' | 'lost'> | optional | Mark this stage a terminus and its kind — overrides the renderer's value/label token heuristic |
Nested Shape: RecordPathProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
RecordQuickActionsProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| actionNames | string[] | optional | Names of actions declared on this object (actions[]), in display order. The engine still location-filters named actions. Measured: when omitted (and the host supplies nothing) the bar resolves NO actions and renders its empty placeholder — it does not fall back to "every action at this location", whatever the registration's input list claims. |
| requiredPermissions | string[] | optional | [ADR-0066] Capabilities the user must ALL hold — names that permission sets grant through systemPermissions, not object actions: read or update here is an ordinary capability name, not the object's read or edit permission. When the client has resolved the user's capabilities and any of these is missing, this block does not render its content; wherever it would otherwise render, an insufficient-permissions notice takes its place. Presentation only: it authorises nothing, and the data API still serves the same data to the same user. A client that cannot resolve the user's capabilities (no permission provider, or one that does not report systemPermissions) renders this block as if they were held — it fails open. |
| location | Enum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | 'record_related' | 'record_section'> | optional | Which declared action location this bar renders (renderer default: record_header). |
| align | Enum<'start' | 'center' | 'end'> | optional | Horizontal alignment of the button row (renderer default: end). |
| inline | boolean | optional | Render in the flow instead of pulling up into the record-header band. The page header sets this itself when it hosts the bar in its own action slot. |
| variant | Enum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'> | optional | Button variant for every action — the Button primitive's own vocabulary; a per-action variant on the resolved def wins (renderer default: default). |
| size | Enum<'default' | 'sm' | 'lg' | 'icon'> | optional | Button size for every action — the Button primitive's own vocabulary; a per-action size wins (renderer default: sm). |
RecordReferenceRailProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| entries | { objectName: string; relationshipField: string; title?: string; limit?: integer; … }[] | ✅ | Related collections to summarize — one compact card per entry (icon-less title, total-count badge, top-N preview rows). An empty rail renders nothing, so at least one entry is required. |
| hideEmpty | boolean | optional | Fold entries whose related count is 0 into a single "+ N empty" expander chip (renderer default: on; set false to always render every card). |
Nested Shape: RecordReferenceRailProps.entries[number]
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | ✅ | Related object name whose records this card summarizes (e.g. "task", "opportunity_quote") |
| relationshipField | string | ✅ | Field on the related object that points back to this record (e.g. "account_id") |
| title | string | optional | Literal card title. Rendered as-is in EVERY locale (no inline locale map — the rail renders it as a raw React child); omit to use the related object's localized label. |
| limit | integer | optional | Preview rows per card, and the $top of the one query this entry issues (renderer default: 3). |
| displayField | string | optional | Field of the related record rendered in each preview row (renderer fallback when omitted: name / title / subject / label / … / id). |
RecordRelatedListProps
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | ✅ | Related object name (e.g., "task", "opportunity") |
| relationshipField | string | ✅ | Field on related object that points to this record (e.g., "account_id") |
| relationshipValueField | string | optional (default: "id") | Parent-record field whose value relationshipField stores (default 'id'; e.g. 'name' for name-keyed junctions). |
| parentDataSource | { object: string; view?: string; filter?: object[]; sort?: object[]; … } | optional | [PLANNED — not yet rendered] The PARENT record this related list hangs off, bound by a query of its own: an object binding { object, view?, filter?, sort?, limit? }, the shape dataSource takes. Read by the single-record rule: the FIRST row of the bound set by its sort is the parent (declare limit: 1 to say one row is meant), and when no row answers, the block renders a loud absence, never an empty list, which would read as "this record has no related records". relationshipValueField is read on that row. When this key is absent, the record page's record context remains the parent. The node-level dataSource on this block keeps its published meaning, the CHILD rows the list draws (it fills objectName, columns, filter, sort and limit), and is never read as the parent. |
| columns | string[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[] | optional | Fields to display in the related list — either plain field-name strings, or the same per-column entries a saved list view declares (ListColumn: field, plus label, width, align, hidden, sortable, summary, …). A view-supplied list may arrive in the ListColumn spelling: objectui composes a saved view's columns onto this block verbatim, and this key declares the SAME union as listViews[].columns. One spelling per list — the two arms are exclusive, so an array mixing strings and column objects is refused. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list is just another surface that lists that object). Override chain: child highlightFields → field-level relatedListColumns (field-name strings only) → this inline list. |
| sort | string | { field: string; order: Enum<'asc' | 'desc'> }[] | optional | Sort order for related records |
| limit | integer | optional (default: 5) | Number of records to display initially |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Additional filter criteria for related records |
| title | string | Record<string, string> | optional | Custom title for the related list |
| showViewAll | boolean | optional (default: true) | Show "View All" link to see all related records |
| actions | string[] | optional | Action IDs available for related records |
| add | { picker: object; linkField?: string; label?: string | Record<string, string> } | optional | Add-existing-via-picker config (generic m2m/junction assignment). |
| enforceFieldSecurity | boolean | optional | Fold this list's columns through the caller's FIELD-read permissions on the RELATED object before rendering (renderer default: off). Presentation only: it re-applies the same field-read answer the server already enforced (ADR-0066 D3) and never widens access — the rows are fetched either way and the server still decides every value. |
| redactFields | string[] | optional | Field names this list never renders, whatever the permission answer (renderer default: render every column authored or derived). Applies to the authored columns AND to the columns the list derives for itself when none are authored. Presentation only, evaluated in the browser after the rows are fetched — the values are still in the page, so this is NOT a data-access control and NOT the object's publicSharing.redactFields, which removes them server-side. To keep a value from the caller, gate the field itself (requiredPermissions / maskingRule, ADR-0066 D3) or the permission set. |
| requiredPermissions | string[] | optional | [ADR-0066] Capabilities the user must ALL hold — names that permission sets grant through systemPermissions, not object actions: read or update here is an ordinary capability name, not the object's read or edit permission. When the client has resolved the user's capabilities and any of these is missing, this block does not render its content; wherever it would otherwise render, an insufficient-permissions notice takes its place. Presentation only: it authorises nothing, and the data API still serves the same data to the same user. A client that cannot resolve the user's capabilities (no permission provider, or one that does not report systemPermissions) renders this block as if they were held — it fails open. |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
Nested Shape: RecordRelatedListProps.parentDataSource
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object to query |
| view | string | optional | Named view to apply |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Additional filter criteria — the ViewFilterRule array form [{ field, operator, value }, ...], the one filter orthography every filter door in ComponentPropsMap shares; AND-combined with the filter of the named view. The MongoDB-style record form is refused — see migration element-data-source-and-object-block-filter-rule-array |
| sort | { field: string; order: Enum<'asc' | 'desc'> }[] | optional | Sort order |
| limit | integer | optional | Max records to display |
Nested Shape: RecordRelatedListProps.columns[number]
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name (snake_case) |
| label | string | Record<string, string> | optional | Display label override |
| width | number | optional | Column width in pixels |
| align | Enum<'left' | 'center' | 'right'> | optional | Text alignment |
| hidden | boolean | optional | Hide column by default |
| sortable | boolean | optional | Allow sorting by this column |
| resizable | boolean | optional | Allow resizing this column |
| wrap | boolean | optional | Allow text wrapping |
| type | string | optional | Renderer type override (e.g., "currency", "date") |
| pinned | Enum<'left' | 'right'> | optional | Pin/freeze column to left or right side |
| summary | Enum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …> | { type: Enum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …>; field?: string } | optional | Footer aggregation for this column — the function alone, or { type, field } to aggregate another field |
| prefix | { field: string; type?: Enum<'badge' | 'text'> } | optional | Field rendered inline before this cell value |
| link | boolean | optional | Functions as the primary navigation link (triggers View navigation) |
| action | string | optional | Registered Action ID to execute when clicked |
Nested Shape: RecordRelatedListProps.filter[number]
View filter rule
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field name to filter on |
| operator | Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …> | ✅ | Filter operator |
| value | string | number | boolean | null | (string | number)[] | optional | Filter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: icontains takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
Nested Shape: RecordRelatedListProps.add
| Property | Type | Required | Description |
|---|---|---|---|
| picker | { object: string; valueField?: string; labelField?: string; filter?: object[] } | ✅ | Where the Add affordance sources records from. |
| linkField | string | optional | Field on objectName that stores the picked record id (junction case). Omit for a 1:m re-parent. |
| label | string | Record<string, string> | optional | Label for the Add button (default "Add"). |
Nested Shape: RecordRelatedListProps.aria
| Property | Type | Required | Description |
|---|---|---|---|
| ariaLabel | string | Record<string, string> | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. |
| ariaDescribedBy | string | optional | ID of element providing additional description (WAI-ARIA aria-describedby) |
| role | string | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") |
ReferenceRailEntry
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objectName | string | ✅ | Related object name whose records this card summarizes (e.g. "task", "opportunity_quote") |
| relationshipField | string | ✅ | Field on the related object that points back to this record (e.g. "account_id") |
| title | string | optional | Literal card title. Rendered as-is in EVERY locale (no inline locale map — the rail renders it as a raw React child); omit to use the related object's localized label. |
| limit | integer | optional | Preview rows per card, and the $top of the one query this entry issues (renderer default: 3). |
| displayField | string | optional | Field of the related record rendered in each preview row (renderer fallback when omitted: name / title / subject / label / … / id). |