ObjectStackObjectStack

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

PropertyTypeRequiredDescription
namestringoptionalAction 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
labelstringoptionalButton text. A literal string, placed as-is — localize through the translation bundle entry for this component id, not an inline locale map
iconstringoptionalLucide icon name drawn left of the label, resolved through the shared action-icon resolver (an unknown name draws no icon)
actionTypestringoptionalExecutor 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
variantEnum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'primary'>optionalButton variant — the Button primitive's vocabulary, plus primary, which renders as default (renderer default: default)
sizeEnum<'default' | 'sm' | 'lg' | 'icon' | 'md'>optionalButton size — the Button primitive's vocabulary, plus md, which renders as default (renderer default: default)
visibleboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility 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
disabledboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalDisabled 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
paramsanyoptionalAction 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
descriptionstringoptionalAction description, forwarded to the runner — the parameter dialog shows it under its title
targetstringoptionalExecutor 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
openInEnum<'self' | 'new-tab'>optionalFor a url action: self navigates in place, new-tab opens a new browser tab
methodstringoptionalHTTP method for an api action, forwarded to the runner
bodyExtraanyoptionalStatic request-body fields for an api action, forwarded to the runner
bodyShapeanyoptionalHow an api action shapes its request body, forwarded to the runner
operationEnum<'update'>optionalDeclarative 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
patchRecord<string, any>optionalStatic 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)
confirmTextstringoptionalConfirmation question asked before the action runs
successMessagestringoptionalToast shown when the action succeeds
errorMessagestringoptionalToast shown when the action fails, in place of the raw error
refreshAfterbooleanoptionalRefresh the surrounding data after the action runs
undoablebooleanoptionalOffer an Undo affordance after an update action
recordIdFieldstringoptionalRow field whose value identifies the record the action acts on
locationsEnum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | 'record_related' | 'record_section'>[]optionalAction locations, forwarded to the runner — the console uses them to tell a record-scoped action from an object-level one
toastanyoptionalToast behaviour, forwarded to the runner
resultDialoganyoptionalOne-shot result dialog for a value the response shows exactly once, forwarded to the runner
onSuccess{ navigate: string; openIn?: Enum<'self' | 'newTab'> }optionalPost-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
objectNamestringoptionalObject 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

PropertyTypeRequiredDescription
navigatestring✅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.
openInEnum<'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

PropertyTypeRequiredDescription
actions{ name?: string; label?: string; icon?: string; type?: string; … }[]optionalThe 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
displayEnum<'inline' | 'dropdown'>optionalDisplay mode: inline renders every action as a button row; dropdown renders one trigger button and lists the actions in its menu (renderer default: inline)
locationEnum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | 'record_related' | 'record_section'>optionalRender only the members whose locations include this location. Omit to render every member
labelstringoptionalDropdown trigger text (renderer default: Actions). Inline mode renders no group label. A literal string — localize through the translation bundle entry for this component id
iconstringoptionalLucide icon name on the dropdown trigger. Inline mode renders no group icon
variantEnum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'>optionalButton variant for the dropdown trigger and for every inline member that sets none — the Button primitive's vocabulary (renderer default: outline)
sizeEnum<'default' | 'sm' | 'lg' | 'icon'>optionalButton size for the dropdown trigger and for every inline member that sets none — the Button primitive's vocabulary
visibleboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility 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]

PropertyTypeRequiredDescription
namestringoptionalAction 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
labelstringoptionalThe item's text. A literal string, placed as-is — localize through the translation bundle entry for this component id
iconstringoptionalLucide icon name drawn before the label, resolved through the shared action-icon resolver (an unknown name draws no icon)
typestringoptionalExecutor 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)
variantEnum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'primary'>optionalItem 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
visibleboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility 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
disabledboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalDisabled 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
tagsEnum<'separator-before'>[]optionalItem tags — separator-before draws a divider above the item in a dropdown or menu (not above the first item); no other tag is drawn
paramsanyoptionalAction 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
descriptionstringoptionalAction description, forwarded to the runner — the parameter dialog shows it under its title
targetstringoptionalExecutor 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
openInEnum<'self' | 'new-tab'>optionalFor a url action: self navigates in place, new-tab opens a new browser tab
methodstringoptionalHTTP method for an api action, forwarded to the runner
bodyExtraanyoptionalStatic request-body fields for an api action, forwarded to the runner
bodyShapeanyoptionalHow an api action shapes its request body, forwarded to the runner
operationEnum<'update'>optionalDeclarative 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
patchRecord<string, any>optionalStatic 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)
confirmTextstringoptionalConfirmation question asked before the action runs
successMessagestringoptionalToast shown when the action succeeds
errorMessagestringoptionalToast shown when the action fails, in place of the raw error
refreshAfterbooleanoptionalRefresh the surrounding data after the action runs
locationsEnum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | …>[]optionalAction 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
toastanyoptionalToast behaviour, forwarded to the runner
resultDialoganyoptionalOne-shot result dialog for a value the response shows exactly once, forwarded to the runner
onSuccess{ navigate: string; openIn?: Enum<'self' | 'newTab'> }optionalPost-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
objectNamestringoptionalObject 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
sizeEnum<'default' | 'sm' | 'lg' | 'icon' | 'md'>optionalInline 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

PropertyTypeRequiredDescription
namestringoptionalAction 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
labelstringoptionalAccessible 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
iconstringoptionalLucide icon name, resolved through the shared action-icon resolver (renderer registration default play)
actionTypestringoptionalExecutor 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
variantEnum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'primary'>optionalButton variant — the Button primitive's vocabulary, plus primary, which renders as default (renderer default: ghost)
visibleboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility 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
disabledboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalDisabled 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
descriptionstringoptionalAction description — the tooltip text when there is no label, and forwarded to the runner for the parameter dialog
paramsanyoptionalAction 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
targetstringoptionalExecutor 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
openInEnum<'self' | 'new-tab'>optionalFor a url action: self navigates in place, new-tab opens a new browser tab
methodstringoptionalHTTP method for an api action, forwarded to the runner
bodyExtraanyoptionalStatic request-body fields for an api action, forwarded to the runner
bodyShapeanyoptionalHow an api action shapes its request body, forwarded to the runner
operationEnum<'update'>optionalDeclarative 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
patchRecord<string, any>optionalStatic 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)
confirmTextstringoptionalConfirmation question asked before the action runs
successMessagestringoptionalToast shown when the action succeeds
errorMessagestringoptionalToast shown when the action fails, in place of the raw error
refreshAfterbooleanoptionalRefresh the surrounding data after the action runs
locationsEnum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | 'record_related' | 'record_section'>[]optionalAction locations, forwarded to the runner — the console uses them to tell a record-scoped action from an object-level one
toastanyoptionalToast behaviour, forwarded to the runner
resultDialoganyoptionalOne-shot result dialog for a value the response shows exactly once, forwarded to the runner
onSuccess{ navigate: string; openIn?: Enum<'self' | 'newTab'> }optionalPost-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
objectNamestringoptionalObject 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

PropertyTypeRequiredDescription
navigatestring✅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.
openInEnum<'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

PropertyTypeRequiredDescription
actions{ name?: string; label?: string; icon?: string; type?: string; … }[]optionalThe 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
labelstringoptionalTrigger 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
iconstringoptionalLucide icon name on the trigger (renderer default: the horizontal ellipsis)
variantEnum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'>optionalTrigger button variant — the Button primitive's vocabulary (renderer default: ghost)
sizeEnum<'default' | 'sm' | 'lg' | 'icon'>optionalTrigger button size — the Button primitive's vocabulary (renderer default: icon)
visibleboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility 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]

PropertyTypeRequiredDescription
namestringoptionalAction 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
labelstringoptionalThe item's text. A literal string, placed as-is — localize through the translation bundle entry for this component id
iconstringoptionalLucide icon name drawn before the label, resolved through the shared action-icon resolver (an unknown name draws no icon)
typestringoptionalExecutor 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)
variantEnum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'primary'>optionalItem 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
visibleboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility 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
disabledboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalDisabled 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
tagsEnum<'separator-before'>[]optionalItem tags — separator-before draws a divider above the item in a dropdown or menu (not above the first item); no other tag is drawn
paramsanyoptionalAction 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
descriptionstringoptionalAction description, forwarded to the runner — the parameter dialog shows it under its title
targetstringoptionalExecutor 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
openInEnum<'self' | 'new-tab'>optionalFor a url action: self navigates in place, new-tab opens a new browser tab
methodstringoptionalHTTP method for an api action, forwarded to the runner
bodyExtraanyoptionalStatic request-body fields for an api action, forwarded to the runner
bodyShapeanyoptionalHow an api action shapes its request body, forwarded to the runner
operationEnum<'update'>optionalDeclarative 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
patchRecord<string, any>optionalStatic 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)
confirmTextstringoptionalConfirmation question asked before the action runs
successMessagestringoptionalToast shown when the action succeeds
errorMessagestringoptionalToast shown when the action fails, in place of the raw error
refreshAfterbooleanoptionalRefresh the surrounding data after the action runs
locationsEnum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | …>[]optionalAction 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
toastanyoptionalToast behaviour, forwarded to the runner
resultDialoganyoptionalOne-shot result dialog for a value the response shows exactly once, forwarded to the runner
onSuccess{ navigate: string; openIn?: Enum<'self' | 'newTab'> }optionalPost-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
objectNamestringoptionalObject 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

PropertyTypeRequiredDescription
labelstring | Record<string, string>✅Button display label
variantEnum<'primary' | 'secondary' | 'danger' | 'ghost' | 'link'>optional (default: "primary")Button visual variant
sizeEnum<'small' | 'medium' | 'large'>optional (default: "medium")Button size
iconstringoptionalLucide 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.
iconPositionEnum<'left' | 'right'>optional (default: "left")Icon position relative to label
disabledbooleanoptional (default: false)Disable the button
action{ type?: Enum<'script' | 'url' | 'modal' | 'flow' | 'api' | 'form'>; name?: string; label?: string | Record<string, string>; target?: string; … }optionalInline action executed on click
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes

Nested Shape: ElementButtonProps.action

PropertyTypeRequiredDescription
typeEnum<'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.
namestringoptionalMachine name (lowercase snake_case)
labelstring | Record<string, string>optionalDisplay label
targetstringoptionalURL, 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.
openInEnum<'self' | 'new-tab'>optionalFor 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).
methodEnum<'POST' | 'PATCH' | 'PUT' | 'DELETE'>optionalHTTP method for type:"api" actions. Defaults to POST.
params{ name?: string; field?: string; objectOverride?: string; label?: string | Record<string, string>; … }[]optionalInput parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in bodyExtra).
bodyExtraRecord<string, any>optionalStatic 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.
confirmTextstring | Record<string, string>optionalConfirmation 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.
successMessagestring | Record<string, string>optionalSuccess 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.
errorMessagestring | Record<string, string>optionalError message to show when the action fails (overrides the raw error).
refreshAfterbooleanoptional (default: false)Refresh view after execution
opensInNewTabbooleanoptionalOpen 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

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

ElementDefinitionListProps

Properties

PropertyTypeRequiredDescription
items{ term: string; description?: any }[]optionalTerm/description pairs, in order. Omitted or empty renders the "No details" empty state
columnsEnum<1 | 2>optionalGrid columns from the small breakpoint up — the NUMBER 1 or 2 (renderer default: 1)
inlinebooleanoptionalPut each term and its description on one baseline-aligned row instead of stacking them

Nested Shape: ElementDefinitionListProps.items[number]

PropertyTypeRequiredDescription
termstring✅The term — rendered as the row's label, as-is
descriptionanyoptionalThe value shown under (or beside) the term — a string or number as-is, an object as JSON; omitted renders an em dash

ElementFilterProps

Properties

PropertyTypeRequiredDescription
objectneveroptional[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.
fieldsneveroptional[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.
targetVariableneveroptional[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.
layoutneveroptional[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.
showSearchneveroptional[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.
arianeveroptional[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

PropertyTypeRequiredDescription
objectneveroptional[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.
fieldsneveroptional[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.
modeneveroptional[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.
submitLabelneveroptional[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.
onSubmitneveroptional[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.
arianeveroptional[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

PropertyTypeRequiredDescription
srcstring✅Image URL or attachment field
altstringoptionalAlt text for accessibility
fitEnum<'cover' | 'contain' | 'fill'>optional (default: "cover")Image object-fit mode
heightnumberoptionalFixed height in pixels
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes

Nested Shape: ElementImageProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

ElementMetadataViewerProps

Properties

PropertyTypeRequiredDescription
typeEnum<'state_machine' | 'flow' | 'permission'>✅Metadata view kind (ADR-0051): state_machine | flow | permission
namestring✅Target metadata item name; resolved package-scoped (ADR-0048), then dependencies (ADR-0046 §3.3)
objectstringoptionalOwning 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
modeEnum<'diagram' | 'matrix' | 'summary'>optionalRender form; defaults per type (diagram for flow/state_machine, matrix for permission)
detailEnum<'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 }optionalARIA accessibility attributes

Nested Shape: ElementMetadataViewerProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

ElementNumberProps

Properties

PropertyTypeRequiredDescription
objectneveroptional[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.
fieldstringoptionalField to aggregate
aggregateEnum<'count' | 'sum' | 'avg' | 'min' | 'max'>✅Aggregation function
filterneveroptional[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.
formatEnum<'number' | 'currency' | 'percent'>optionalNumber display format
prefixstringoptionalPrefix text (e.g. "$")
suffixstringoptionalSuffix text (e.g. "%")
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes

Nested Shape: ElementNumberProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

ElementRecordPickerProps

Properties

PropertyTypeRequiredDescription
objectneveroptional[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.
labelFieldstringoptionalField rendered as each row's text (default name)
valueFieldstringoptionalField whose value is written into the bound page variable (default id)
labelstring | Record<string, string>optionalControl label rendered above the select
filterneveroptional[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.
sortneveroptional[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.
limitneveroptional[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.
targetVariableneveroptional[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.
placeholderstring | Record<string, string>optionalPlaceholder text
emptyTextstring | Record<string, string>optionalText shown when the query returns no records (default "No records")
displayFieldneveroptional[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.
searchFieldsneveroptional[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.
multipleneveroptional[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 }optionalARIA accessibility attributes

Nested Shape: ElementRecordPickerProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

ElementRepeaterProps

Properties

PropertyTypeRequiredDescription
objectneveroptional[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.
titleFieldstringoptionalField shown first on each line, emphasized
fields(string | { field: string })[]optionalFields shown after the title on each line, in order — a bare field name, or { field }
filterneveroptional[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.
sortneveroptional[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.
limitneveroptional[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.
emptyTextstringoptionalCopy shown when the query returns no records (renderer default: "No records"). A literal string — localize through the translation bundle entry for this component id
dividedbooleanoptionalDraw a separator between lines (renderer default: true)

Nested Shape: ElementRepeaterProps.fields[number]

PropertyTypeRequiredDescription
fieldstring✅Field name

ElementTextInputProps

Properties

PropertyTypeRequiredDescription
inputTypeEnum<'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).
labelstring | Record<string, string>optionalField label shown above the input
placeholderstring | Record<string, string>optionalPlaceholder text shown when empty
defaultValuestring | numberoptionalInitial value; seeds the bound page variable on mount
requiredbooleanoptional (default: false)Mark the field as required
disabledbooleanoptional (default: false)Disable the input
descriptionstring | Record<string, string>optionalHelper text shown below the input
targetVariableneveroptional[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 }optionalARIA accessibility attributes

Nested Shape: ElementTextInputProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

ElementTextProps

Properties

PropertyTypeRequiredDescription
contentstring | Record<string, string>✅Text or Markdown content — a plain string, or an inline locale map
variantEnum<'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'body' | 'caption' | 'overline'>optional (default: "body")Text style variant
alignEnum<'left' | 'center' | 'right'>optional (default: "left")Text alignment
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes

Nested Shape: ElementTextProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

ObjectCalendarProps

Properties

PropertyTypeRequiredDescription
objectNamestringoptionalObject 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; … }optionalCalendar field config { startDateField, endDateField?, titleField?, colorField?, allDayField? } — the same block a list view's calendar declares; startDateField is required
defaultViewEnum<'month' | 'week' | 'day'>optionalInitial view mode
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalBase 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'> }[]optionalRow 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
dataany[]optionalPre-fetched records — skips the internal fetch
staticDataany[]optionalStatic inline records
localestringoptionalLocale override for the calendar chrome
loadingbooleanoptionalExternal 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'>; … }optionalEvent-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

PropertyTypeRequiredDescription
startDateFieldstring✅Field providing the event start date/time
endDateFieldstringoptionalField providing the event end date/time (defaults to a single-day event)
titleFieldstringoptionalField displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain)
colorFieldstringoptionalField 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
allDayFieldstringoptionalField 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

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>✅Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter 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

PropertyTypeRequiredDescription
fieldstring✅Field name to sort by
orderEnum<'asc' | 'desc'>✅Sort direction

Nested Shape: ObjectCalendarProps.navigation

PropertyTypeRequiredDescription
modeEnum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>optional (default: "page")
viewneveroptional[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).
preventNavigationbooleanoptional (default: false)Disable standard navigation entirely
openNewTabbooleanoptional (default: false)Force open in new tab (applies to page mode)
sizeEnum<'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.
widthstring | numberoptional[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

PropertyTypeRequiredDescription
objectNamestringoptionalObject this form creates/edits. Optional because the component-level dataSource binding can supply the object instead
recordIdstring | numberoptionalRecord to load (edit/view modes)
modeEnum<'create' | 'edit' | 'view'>optionalForm mode
formTypeEnum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>optionalForm presentation
layoutEnum<'vertical' | 'horizontal'>optionalField layout — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set columns
columnsnumberoptionalNumber of field columns (multi-column forms), honoured under either layout
fieldsstring[]optionalField 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; … }[]optionalField 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; … }[]optionalForm 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, … }
titlestring | Record<string, string>optionalForm title
descriptionstring | Record<string, string>optionalForm description (rendered by the drawer/modal presentations)
defaultTabstringoptionalInitially active tab (tabbed)
tabPositionEnum<'top' | 'bottom' | 'left' | 'right'>optionalTab strip position (tabbed)
allowSkipbooleanoptionalAllow skipping steps (wizard)
showStepIndicatorbooleanoptionalShow the step indicator (wizard)
splitDirectionEnum<'horizontal' | 'vertical'>optionalSplit direction (split)
splitSizenumberoptionalSplit panel size in percent (split)
splitResizablebooleanoptionalAllow resizing the split (split)
drawerSideEnum<'top' | 'bottom' | 'left' | 'right'>optionalDrawer side (drawer)
drawerWidthstring | numberoptionalDrawer width (drawer)
modalSizeEnum<'sm' | 'default' | 'lg' | 'xl' | 'full'>optionalModal size (modal)
modalCloseButtonbooleanoptionalShow the modal close button (modal)
contentLayoutEnum<'simple' | 'tabbed'>optionalHow 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
confirmOnDiscardbooleanoptionalConfirm before discarding edits (drawer/modal)
submitTextstring | Record<string, string>optionalSubmit button label
cancelTextstring | Record<string, string>optionalCancel button label
nextTextstring | Record<string, string>optionalNext-step button label (wizard)
prevTextstring | Record<string, string>optionalPrevious-step button label (wizard)
showSubmitbooleanoptionalShow the submit button
showCancelbooleanoptionalShow the cancel button
showResetbooleanoptionalShow the reset button
submitBehavior{ kind: 'thank-you'; title?: string; message?: string } | { kind: 'redirect'; url: string; delayMs?: integer } | { kind: 'continue' } | { kind: 'next-record' }optionalWhat 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
successMessagestring | Record<string, string>optionalToast message on successful submit
resetOnSuccessbooleanoptionalReset the form after a successful submit
navigateOnSuccessstringoptionalRelative 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
readOnlybooleanoptionalRender every field read-only
initialValuesRecord<string, any>optionalPrefill values (create mode)
initialDataRecord<string, any>optionalAlternate spelling of initialValues the renderer also reads
mobile{ stickyActions?: boolean; stepper?: boolean | 'auto'; stepperMinFields?: integer; stepperFieldsPerStep?: integer; … }optionalPhone presentation options, each opt-in — { stickyActions?, stepper?, stepperMinFields?, stepperFieldsPerStep?, fullscreenLongText? }. Read by the flat (simple, sectionless) form

Nested Shape: ObjectFormProps.customFields[number]

PropertyTypeRequiredDescription
namestring✅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
labelstringoptionalThe label drawn beside the control (a plain string)
descriptionstringoptionalHelp text drawn under the control
typestringoptionalThe 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
inputTypestringoptionalThe HTML type of the built-in input (email, tel, number, date, …)
widgetstringoptionalA widget to render instead of the one type resolves to
requiredbooleanoptionalRefuse the submit while the field is empty
disabledbooleanoptionalDraw the control greyed out and not interactive
readonlybooleanoptionalDraw the value plainly, not editable
hiddenbooleanoptionalDo not draw the field (its value still submits)
placeholderstringoptionalPlaceholder text in the empty control
options{ label: string; value: string | number | boolean; description?: string; visibleWhen?: string | object }[]optionalThe 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; … }optionalExtra rules checked at submit — { required?, minLength?, maxLength?, min?, max? }, each bound rule a { value, message }, and required the message a required field shows
dependsOnstring | (string | { field: string; param?: string })[]optionalThe 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
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalPredicate (CEL) — the field is drawn only when TRUE
readonlyWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalPredicate (CEL) — the field is read-only when TRUE
requiredWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalPredicate (CEL) — the field is required when TRUE
colSpanintegeroptionalAbsolute column span (1-4), clamped to the form grid's column count
spanEnum<'auto' | 'full'>optionalRelative width: 'auto' (the default) sizes the field from its widget and the column count; 'full' takes the whole row
groupstringoptionalThe object field group (fieldGroups[].key) this field is drawn in when the form derives its sections from the object's groups
multiplebooleanoptionalHold several values instead of one (file, image, lookup, user and select fields)
rowsintegeroptionalHeight of the textarea / markdown editor, in text rows
acceptstring[]optionalUpload types a file field's picker offers, as MIME types or extensions (e.g. ["image/*", ".pdf"])
dimensionsintegeroptionalVector dimensionality a vector field prints beside its value
referencestringoptionalThe object a lookup / user field's picker queries
minnumberoptionalMinimum value — the native control's min, enforced by the browser at submit
maxnumberoptionalMaximum value — the native control's max, enforced by the browser at submit
minLengthintegeroptionalMinimum character count — the native control's minlength
maxLengthintegeroptionalMaximum character count — the control's ceiling (a textarea also draws its counter)
patternstringoptionalRegular expression the value must match, as a string — the native control's pattern, enforced by the browser at submit
returnTypeEnum<'number' | 'text' | 'boolean' | 'date' | 'currency'>optionalThe 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; … }optionalThe 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; … }[]optionalThe columns of a grid field — the strict, name-keyed inline grid column a relationship field's inlineColumns takes
minRowsnumberoptionalA 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
maxRowsnumberoptionalA 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
allowAddbooleanoptionalWhether 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
allowDeletebooleanoptionalWhether 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
allowReorderbooleanoptionalWhether 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
totalFieldstringoptionalThe 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
addLabelstringoptionalLabel 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
sortFieldstringoptionalA 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]

PropertyTypeRequiredDescription
namestringoptionalStable section identifier (snake_case) — the heading resolves through objects.<object>._sections.<name>.label
labelstringoptionalSection heading (a plain string)
descriptionstringoptionalText drawn under the heading
collapsiblebooleanoptionalDraw a disclosure control on the heading, so a reader can close the section and open it again. collapsed: true implies it
collapsedbooleanoptionalStart the section closed (implies collapsible)
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalPredicate (CEL) — the whole section, heading and fields, is drawn only when TRUE
columnsintegeroptionalField-grid columns for this section (1-4), a number
paneEnum<'primary' | 'secondary'>optionalThe split form's panel this section renders in; omitted → the first section 'primary', the others 'secondary'
groupstringoptionalField 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; … })[]optionalThe 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']

PropertyTypeRequiredDescription
kind'redirect'✅
urlstring✅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.
delayMsintegeroptional

Nested Shape: ObjectFormProps.mobile

PropertyTypeRequiredDescription
stickyActionsbooleanoptionalPin the submit / cancel bar to the bottom of a phone-width viewport
stepperboolean | 'auto'optionalOne-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
stepperMinFieldsintegeroptionalThe field count at which stepper: 'auto' steps up (default 8)
stepperFieldsPerStepintegeroptionalFields shown per stepper step (default 1)
fullscreenLongTextbooleanoptionalOffer a fullscreen editor on textarea and rich-text fields

ObjectGanttProps

Properties

PropertyTypeRequiredDescription
objectNamestringoptionalObject 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> }optionalData 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
staticDataany[]optionalInline 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)[] }[]optionalBase 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'> }[]optionalTask 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; … }optionalGantt-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'>; … }optionalTask-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
labelstring | Record<string, string>optionalGantt label — the second link of the exported PNG/PDF file-name chain, after gantt.exportFileName and before the bound object's own label
skipWeekendsbooleanoptionalMeasure duration and auto-schedule math in WORKING days, skipping Saturdays and Sundays
holidaysstring[]optionalAdditional non-working dates for the working calendar, ISO yyyy-mm-dd strings; folded into a Set for the duration math
persistLayoutbooleanoptionalOpt OUT of layout and filter-chip persistence — only an explicit false disables it; the storage key is objectName:viewName
viewNamestringoptionalLayout-persistence scope, the second half of the objectName:viewName storage key (renderer default 'default')
markers{ date: string; label?: string; color?: string }[]optionalExtra 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
criticalPathbooleanoptionalSeed the critical-path highlight ON; the toolbar toggle stays available either way
showBaselinesbooleanoptionalRender the planned-vs-actual baseline bars — ON unless an explicit false disables it
readOnlybooleanoptionalDisable every write path on this gantt and lock the record drawer
mobileReadOnlybooleanoptionalAuto read-only on narrow viewports — ON unless an explicit false disables it

Nested Shape: ObjectGanttProps.data[provider='object']

PropertyTypeRequiredDescription
provider'object'✅
objectstring✅Target object name

Nested Shape: ObjectGanttProps.data[provider='value']

PropertyTypeRequiredDescription
provider'value'✅
itemsany[]✅Static data array

Nested Shape: ObjectGanttProps.data[provider='schema']

PropertyTypeRequiredDescription
provider'schema'✅
schemaIdstring✅Schema identifier — typically the metadata type name
schemaRecord<string, any>optionalInline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable.

Nested Shape: ObjectGanttProps.filter[number]

View filter rule

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>✅Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter 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

PropertyTypeRequiredDescription
fieldstring✅Field name to sort by
orderEnum<'asc' | 'desc'>✅Sort direction

Nested Shape: ObjectGanttProps.gantt

PropertyTypeRequiredDescription
startDateFieldstring✅Field providing the task start date
endDateFieldstring✅Field providing the task end date
titleFieldstring✅Field displayed as the task title
progressFieldstringoptionalField providing the task completion percentage
dependenciesFieldstringoptionalField listing the task's predecessor (dependency) record ids
colorFieldstringoptionalField 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
parentFieldstringoptionalField holding the parent task id (builds the summary → step tree)
typeFieldstringoptionalField whose value maps to task/summary/milestone
baselineStartFieldstringoptionalBaseline (planned) start field
baselineEndFieldstringoptionalBaseline (planned) end field
groupByFieldstringoptionalField 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.
resourceViewbooleanoptionalRender a per-resource workload histogram instead of the timeline
assigneeFieldstringoptionalResource field to bucket load by (resource view)
effortFieldstringoptionalPer-task load units (resource view; default 1)
capacitynumberoptionalPer-resource capacity ceiling; loads above this flag overload
tooltipFields(string | { field: string; label?: string })[]optionalFields to surface in the hover tooltip, in display order
quickFilters{ field: string; label?: string; options?: (string | object)[] }[]optionalMulti-select filter dropdowns rendered above the chart
autoZoomToFilterbooleanoptionalWhen true (default), filtering zooms the range to the filtered tasks
viewModeEnum<'day' | 'week' | 'month' | 'quarter' | 'year'>optionalTimeline granularity — one column per day/week/month/quarter/year (also the resource-view column granularity; renderer default 'day')
borderColorFieldstringoptionalField 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
lockFieldstringoptionalField 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
objectFieldstringoptionalField 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
summaryExtentEnum<'children' | 'self'>optionalHow 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)
defaultCollapsedDepthintegeroptionalAuto-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
dependencyTypesbooleanoptionalWhether 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
timeZonestringoptionalBusiness 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
exportFileNamestringoptionalBase 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 }optionalPer-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 }optionalShift 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

PropertyTypeRequiredDescription
modeEnum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>optional (default: "page")
viewneveroptional[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).
preventNavigationbooleanoptional (default: false)Disable standard navigation entirely
openNewTabbooleanoptional (default: false)Force open in new tab (applies to page mode)
sizeEnum<'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.
widthstring | numberoptional[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]

PropertyTypeRequiredDescription
datestring✅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
labelstringoptionalText drawn against the line
colorstringoptionalLine colour, any CSS colour (renderer default: the theme's primary colour)

ObjectGridProps

Properties

PropertyTypeRequiredDescription
objectNamestringoptionalObject this grid binds to. Optional because the component-level dataSource binding can supply the object instead
labelstring | Record<string, string>optionalGrid label — used as the table caption and export file title
titlestring | Record<string, string>optionalFallback for label (the renderer reads label || title)
descriptionstring | Record<string, string>optionalOne 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 }optionalWhat the grid draws instead of an empty table: { title, message, icon, link } — the list view's own empty-state shape
columnsstring[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[]optionalColumns — 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
fieldsstring[]optionalField-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)[] }[]optionalBase 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
defaultFiltersneveroptional[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'> }[]optionalInitial 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
defaultSortneveroptional[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>optionalPagination 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
pageSizeintegeroptionalFlat page-size shorthand, a positive integer; pagination.pageSize wins when both are set
showPaginationbooleanoptionalShow the pager (read only when pagination is absent)
searchableFieldsstring[]optionalFields the toolbar search queries; a non-empty list enables search
showSearchbooleanoptionalShow the search box (read only when searchableFields is absent)
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height — one of compact, short, medium, tall, extra_tall; the same values a list view's rowHeight takes
grouping{ fields: object[] }optionalRow grouping config
aggregations{ field: string; type: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'> }[]optionalPer-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> }[]optionalConditional 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> }optionalRow colour by field value — { field, colors }, the same block a list view's rowColor declares
selection{ type?: Enum<'none' | 'single' | 'multiple'> }optionalSelection config { type } — none, single or multiple, the same block a list view's selection declares
selectableboolean | Enum<'single' | 'multiple'>optionalLegacy selection shorthand, read only when selection is absent — true (multiple), false (off), 'single' or 'multiple'. Prefer selection
rowActionsstring[]optionalPer-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
bulkActionsstring[]optionalBulk action names shown on selection — the same list a list view's bulkActions declares; a full def goes on bulkActionDefs
batchActionsstring[]optionalAlternate 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'>; … }[]optionalInline 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'>; … }optionalRow-click navigation config — the same block ListViewSchema.navigation declares ({ mode, size, openNewTab, preventNavigation })
editablebooleanoptionalEnable inline cell editing
singleClickEditbooleanoptionalEnter cell edit on single click (default true when editable)
keyboardNavigationbooleanoptionalArrow-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
resizablebooleanoptionalAllow column resize (the renderer default is on)
resizableColumnsneveroptional[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.
reorderableColumnsbooleanoptionalAllow column drag-reorder
frozenColumnsnumberoptionalHow many leading columns stay frozen (default 1)
showColumnTypeIconsbooleanoptionalShow field-type icons in column headers
exportOptions{ formats?: Enum<'csv' | 'xlsx' | 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }optionalExport 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 }optionalBuilt-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> }optionalData 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
staticDataany[]optionalDeprecated bare-array static-rows shortcut the renderer still reads. Prefer data: { provider: 'value', items: [...] }

Nested Shape: ObjectGridProps.emptyState

PropertyTypeRequiredDescription
titlestring | Record<string, string>optionalHeading of the empty state
messagestring | Record<string, string>optionalLine of text below the heading
iconstringoptionalIcon 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 }optionalThe 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]

PropertyTypeRequiredDescription
fieldstring✅Field name (snake_case)
labelstring | Record<string, string>optionalDisplay label override
widthnumberoptionalColumn width in pixels
alignEnum<'left' | 'center' | 'right'>optionalText alignment
hiddenbooleanoptionalHide column by default
sortablebooleanoptionalAllow sorting by this column
resizablebooleanoptionalAllow resizing this column
wrapbooleanoptionalAllow text wrapping
typestringoptionalRenderer type override (e.g., "currency", "date")
pinnedEnum<'left' | 'right'>optionalPin/freeze column to left or right side
summaryEnum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …> | { type: Enum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …>; field?: string }optionalFooter aggregation for this column — the function alone, or { type, field } to aggregate another field
prefix{ field: string; type?: Enum<'badge' | 'text'> }optionalField rendered inline before this cell value
linkbooleanoptionalFunctions as the primary navigation link (triggers View navigation)
actionstringoptionalRegistered Action ID to execute when clicked

Nested Shape: ObjectGridProps.filter[number]

View filter rule

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>✅Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter 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

PropertyTypeRequiredDescription
fieldstring✅Field name to sort by
orderEnum<'asc' | 'desc'>✅Sort direction

Nested Shape: ObjectGridProps.grouping

PropertyTypeRequiredDescription
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]

PropertyTypeRequiredDescription
fieldstring✅Field whose values the function aggregates within each group (ignored by count)
typeEnum<'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]

PropertyTypeRequiredDescription
conditionstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }✅Predicate (CEL) to evaluate.
styleRecord<string, string>✅CSS styles to apply when condition is true

Nested Shape: ObjectGridProps.rowColor

PropertyTypeRequiredDescription
fieldstring✅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.
colorsRecord<string, string>optionalMap 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

PropertyTypeRequiredDescription
typeEnum<'none' | 'single' | 'multiple'>optional (default: "none")Selection mode

Nested Shape: ObjectGridProps.bulkActionDefs[number]

PropertyTypeRequiredDescription
namestring✅Stable identifier — the audit-log action key, and (for an aggregate def) the name of the object action to dispatch.
labelstringoptionalButton + dialog-header text. Plain string: an authored def is not i18n-resolved (declare a real action and name it in bulkActions to get localization).
iconstringoptionalLucide icon name (e.g. "user-check", "trash").
variantEnum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'>optionalVisual treatment of the button.
operationEnum<'update' | 'delete' | 'custom'>✅What the executor does: 'update'/'delete' are data-plane mass mutations; 'custom' dispatches an object action (see execution).
executionEnum<'perRecord' | 'aggregate'>optionalFor 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.
patchRecord<string, any>optionalFor 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' | …>; … }[]optionalInputs collected once before the run. Omit to skip the params step and go straight to confirm.
confirmTextstringoptionalConfirmation text shown above the affected-record summary.
confirmLabelstringoptionalCustom Confirm button label (default: "Run").
visiblestring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalEligibility 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.
requiredPermissionsstring[]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.
maxRecordsintegeroptionalSelection 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.
batchSizeintegeroptionalRecords per executor batch (default 200). Data-plane operations only — an aggregate run is a single call by definition.

Nested Shape: ObjectGridProps.navigation

PropertyTypeRequiredDescription
modeEnum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>optional (default: "page")
viewneveroptional[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).
preventNavigationbooleanoptional (default: false)Disable standard navigation entirely
openNewTabbooleanoptional (default: false)Force open in new tab (applies to page mode)
sizeEnum<'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.
widthstring | numberoptional[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

PropertyTypeRequiredDescription
formatsEnum<'csv' | 'xlsx' | 'json'>[]optionalFormats offered in the export menu (default: ['csv', 'json']). XLSX is delivered by the server stream only.
maxRecordsintegeroptionalMaximum number of records to export; 0 or absent = unlimited
includeHeadersbooleanoptionalInclude column headers in the exported file (default true)
fileNamePrefixstringoptionalDownload file name prefix — replaces the object label and suppresses the view label in the generated file name
streamingbooleanoptionalSet false to force the client-side export path (csv/json only) instead of the server stream

Nested Shape: ObjectGridProps.operations

PropertyTypeRequiredDescription
createbooleanoptionalShow the add-row affordance (also gated by the user's create permission)
updatebooleanoptionalOffer the row menu's generic Edit entry
deletebooleanoptionalOffer the row menu's generic Delete entry
exportbooleanoptionalfalse hides the export menu that exportOptions enables

Nested Shape: ObjectGridProps.data[provider='object']

PropertyTypeRequiredDescription
provider'object'✅
objectstring✅Target object name

Nested Shape: ObjectGridProps.data[provider='value']

PropertyTypeRequiredDescription
provider'value'✅
itemsany[]✅Static data array

Nested Shape: ObjectGridProps.data[provider='schema']

PropertyTypeRequiredDescription
provider'schema'✅
schemaIdstring✅Schema identifier — typically the metadata type name
schemaRecord<string, any>optionalInline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable.

ObjectKanbanProps

Properties

PropertyTypeRequiredDescription
objectNamestringoptionalObject this board binds to. Optional because the component-level dataSource binding can supply the object instead
groupBystringoptionalField whose values become the board columns
columnsstring[] | { id: string; title: string; cards?: object[]; limit?: integer; … }[]optionalSwimlane 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)[] }[]optionalBase 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
limitintegeroptionalMaximum 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
dataany[]optionalStatic inline cards — bypasses the object query
cardTitlestringoptionalField rendered as each card title
titleFieldstringoptionalLegacy fallback for cardTitle (the board reads cardTitle || titleField). Prefer cardTitle
cardFieldsstring[]optionalFields rendered on each card
swimlaneFieldstringoptionalField for horizontal swimlanes (in addition to columns)
grouping{ fields: object[] }optionalView grouping config; its first field is the swimlane fallback
quickAddneveroptional[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.
coverImageFieldstringoptionalImage field rendered as the card cover
conditionalFormatting{ condition: string | object; style: Record<string, string> }[]optionalCard 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'>; … }optionalCard-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]

PropertyTypeRequiredDescription
idstring✅Lane id — the groupBy value whose records land in this lane (matched as a string)
titlestring✅Lane heading, localized against the groupBy picklist's option labels
cards({ id: string; title: string } & Record<string, any>)[]optionalA 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
limitintegeroptionalWIP limit — the card count at which the lane warns; never reaches the query
classNamestringoptionalCSS class names applied to the lane
collapsedbooleanoptionalWhether 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

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>✅Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter 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

PropertyTypeRequiredDescription
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]

PropertyTypeRequiredDescription
conditionstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }✅Predicate (CEL) to evaluate.
styleRecord<string, string>✅CSS styles to apply when condition is true

Nested Shape: ObjectKanbanProps.navigation

PropertyTypeRequiredDescription
modeEnum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>optional (default: "page")
viewneveroptional[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).
preventNavigationbooleanoptional (default: false)Disable standard navigation entirely
openNewTabbooleanoptional (default: false)Force open in new tab (applies to page mode)
sizeEnum<'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.
widthstring | numberoptional[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

PropertyTypeRequiredDescription
objectNamestringoptionalObject 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> }optionalData 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
staticDataany[]optionalInline 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)[] }[]optionalBase 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'> }[]optionalMarker 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; … }optionalMap 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
mapStylestringoptionalMapLibre 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'>; … }optionalMarker-click navigation config — the same block ListViewSchema.navigation declares ({ mode, size, openNewTab, preventNavigation }), mode one of the seven NavigationModeSchema values
enableClusteringbooleanoptionalGroup nearby markers into clusters. Absent, the renderer clusters only above 100 markers

Nested Shape: ObjectMapProps.data[provider='object']

PropertyTypeRequiredDescription
provider'object'✅
objectstring✅Target object name

Nested Shape: ObjectMapProps.data[provider='value']

PropertyTypeRequiredDescription
provider'value'✅
itemsany[]✅Static data array

Nested Shape: ObjectMapProps.data[provider='schema']

PropertyTypeRequiredDescription
provider'schema'✅
schemaIdstring✅Schema identifier — typically the metadata type name
schemaRecord<string, any>optionalInline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable.

Nested Shape: ObjectMapProps.filter[number]

View filter rule

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>✅Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter 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

PropertyTypeRequiredDescription
fieldstring✅Field name to sort by
orderEnum<'asc' | 'desc'>✅Sort direction

Nested Shape: ObjectMapProps.map

PropertyTypeRequiredDescription
latitudeFieldstringoptionalField providing the marker latitude (used with longitudeField)
longitudeFieldstringoptionalField providing the marker longitude (used with latitudeField)
locationFieldstringoptionalField providing a combined location — a "lat,lng" string or a { lat, lng } object — as the alternative to the latitudeField/longitudeField pair
titleFieldstringoptionalField displayed as the marker title (popup heading, mobile record card, and what the map search box matches on)
descriptionFieldstringoptionalField displayed as the marker description
zoomnumberoptionalInitial zoom level (1-20). Omit to let the renderer fit the camera to the queried records
center[number, number]optionalInitial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records
stylestringoptionalMap 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

PropertyTypeRequiredDescription
modeEnum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>optional (default: "page")
viewneveroptional[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).
preventNavigationbooleanoptional (default: false)Disable standard navigation entirely
openNewTabbooleanoptional (default: false)Force open in new tab (applies to page mode)
sizeEnum<'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.
widthstring | numberoptional[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

PropertyTypeRequiredDescription
objectNamestringoptionalPARENT object. Optional because the component-level dataSource binding can supply the object instead
recordIdstring | numberoptionalParent record to load (edit mode)
modeEnum<'create' | 'edit'>optionalForm mode
formTypeEnum<'simple' | 'tabbed'>optionalParent form presentation — the two variants the renderer honours for the parent half
sections{ name?: string; label?: string; description?: string; collapsible?: boolean; … }[]optionalParent form sections — the same section shape object-form takes: { name?, label?, description?, collapsible?, collapsed?, visibleWhen?, columns?, pane?, fields } or { group, columns?, pane? }
fieldsstring[]optionalParent 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[]; … }[]optionalDetail 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
titlestring | Record<string, string>optionalForm title
submitTextstring | Record<string, string>optionalSubmit button label
cancelTextstring | Record<string, string>optionalCancel button label
showSubmitbooleanoptionalShow the submit button
initialValuesRecord<string, any>optionalPrefill values for the parent (create mode)
initialDataRecord<string, any>optionalAlternate spelling of initialValues the renderer also reads
taxRateFieldstringoptionalChild field holding the per-line tax rate (line-items totals)

Nested Shape: ObjectMasterDetailFormProps.sections[number]

PropertyTypeRequiredDescription
namestringoptionalStable section identifier (snake_case) — the heading resolves through objects.<object>._sections.<name>.label
labelstringoptionalSection heading (a plain string)
descriptionstringoptionalText drawn under the heading
collapsiblebooleanoptionalDraw a disclosure control on the heading, so a reader can close the section and open it again. collapsed: true implies it
collapsedbooleanoptionalStart the section closed (implies collapsible)
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalPredicate (CEL) — the whole section, heading and fields, is drawn only when TRUE
columnsintegeroptionalField-grid columns for this section (1-4), a number
paneEnum<'primary' | 'secondary'>optionalThe split form's panel this section renders in; omitted → the first section 'primary', the others 'secondary'
groupstringoptionalField 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; … })[]optionalThe 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]

PropertyTypeRequiredDescription
childObjectstring✅Child object whose records are entered inline
relationshipFieldstringoptionalFK 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; … }[]optionalEditable 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.
formFieldsstring[]optionalChild 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
inlineModeEnum<'grid' | 'form'>optionalInline-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
amountFieldstringoptionalNumeric 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
sortFieldneveroptional[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.
totalFieldstringoptionalParent field to receive the rolled-up sum
titlestringoptionalSection title
minRowsnumberoptionalMinimum number of rows
maxRowsnumberoptionalMaximum number of rows
addLabelstringoptionalAdd-row button label

ObjectMetricProps

Properties

PropertyTypeRequiredDescription
objectNamestringoptionalObject this metric aggregates. Optional because the component-level dataSource binding can supply the object instead
labelstring | Record<string, string>optionalMetric label
descriptionstring | Record<string, string>optionalHelper text under the value
titlestring | Record<string, string>optionalDrill-down panel title; defaults to the metric label
iconstringoptionalLucide 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).
colorVariantEnum<'default' | 'blue' | 'teal' | 'orange' | 'purple' | 'success' | 'warning' | 'danger'>optionalIcon container color variant
aggregate{ field?: string; function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; groupBy?: string | object }optionalAggregation 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)[] }[]optionalFilter 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
formatstringoptionalNumber format pattern (e.g. '0,0', '$0,0', '0%')
currencystringoptionalISO currency code (e.g. 'USD') — enables currency formatting
prefixstringoptionalStatic prefix before the formatted value
suffixstringoptionalStatic suffix after the formatted value
invertbooleanoptionalDisplay 1 - value for opposite-signal gauges (compliance/uptime)
variantEnum<'card' | 'bare'>optionalLayout variant
fallbackValuestring | numberoptionalStatic value shown when no data source is available
trend{ value: number; label?: string | Record<string, string>; direction?: Enum<'up' | 'down' | 'neutral'> }optionalStatic 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[]; … }optionalClick-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'> }optionalPeriod-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

PropertyTypeRequiredDescription
fieldstringoptionalField to aggregate — required for every function but count, which counts rows
functionEnum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>✅Aggregation function — count, sum, avg, min, max or count_distinct
groupBystring | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string }optionalField 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

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>✅Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter 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

PropertyTypeRequiredDescription
valuenumber✅The change shown on the badge, painted as a percentage (12 reads 12%)
labelstring | Record<string, string>optionalBadge caption — a string or an inline locale map. The tile-level description outranks it in the one caption slot they share
directionEnum<'up' | 'down' | 'neutral'>optionalArrow beside the value; omit it for no arrow

Nested Shape: ObjectMetricProps.drillDown

PropertyTypeRequiredDescription
enabledbooleanoptionalTurn the tile's drill on or off; the block being present already means on, so this is only needed to force it off
titlestringoptionalDrill 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
targetEnum<'drawer' | 'dialog' | 'navigate'>optionalWhere 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')
columnsstring[]optionalField names to show as columns in the drilled list (default: the table's own columns)
maxRowsintegeroptionalRows 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'>; … }optionalDrill 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

PropertyTypeRequiredDescription
kindEnum<'previousPeriod' | 'previousYear'>✅Comparison window: previousPeriod (equal-length, immediately before) or previousYear (−1 calendar year)

ObjectTimelineProps

Properties

PropertyTypeRequiredDescription
objectNamestringoptionalObject 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; … }optionalTimeline 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)[] }[]optionalBase 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'> }[]optionalRow 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
limitintegeroptionalMaximum 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
dataany[]optionalPre-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'>; … }[]optionalStatic 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
variantEnum<'vertical' | 'horizontal' | 'gantt'>optionalRail 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
dateFormatEnum<'short' | 'long' | 'iso'>optionalHow each entry's date is rendered (renderer default short): short / long are locale-formatted, iso is the locale-free machine form
rowLabelstringoptionalHeader label for the gantt row column — read by the gantt branch only, which on this block needs authored items
minDatestringoptionalPin the gantt axis start (ISO yyyy-mm-dd) instead of deriving it from the rows; only a non-empty value is honoured
maxDatestringoptionalPin the gantt axis end (ISO yyyy-mm-dd) instead of deriving it from the rows; only a non-empty value is honoured
descriptionFieldstringoptionalField 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 }optionalRecord-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'>; … }optionalEntry-click navigation config — the same block ListViewSchema.navigation declares ({ mode, size, openNewTab, preventNavigation })

Nested Shape: ObjectTimelineProps.timeline

PropertyTypeRequiredDescription
startDateFieldstring✅Field for timeline item start date
endDateFieldstringoptionalField for timeline item end date
titleFieldstring✅Field to display as timeline item title
groupByFieldstringoptionalField 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.
colorFieldstringoptionalField 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
scaleEnum<'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'>optional (default: "week")Default timeline scale

Nested Shape: ObjectTimelineProps.filter[number]

View filter rule

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>✅Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter 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

PropertyTypeRequiredDescription
fieldstring✅Field name to sort by
orderEnum<'asc' | 'desc'>✅Sort direction

Nested Shape: ObjectTimelineProps.items[number]

PropertyTypeRequiredDescription
timestringoptionalWhen it happened — an ISO 8601 date (or date-time) string, formatted by the row's dateFormat
titlestringoptionalThe entry's heading — required on a feed entry (the arm a feed variant selects)
descriptionstringoptionalSecondary line under the title
variantEnum<'default' | 'success' | 'warning' | 'danger' | 'info'>optionalMarker colour (renderer default default)
iconstringoptionalEmoji or short text drawn inside the marker
contentanyoptionalChild page components drawn below the description — a component node or a list of them. Held opaque: not judged on this row
classNamestringoptionalTailwind classes for the entry
labelstringoptionalRow 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'> }[]optionalThe row's bars, each { title?, startDate?, endDate?, variant? }

Nested Shape: ObjectTimelineProps.mapping

PropertyTypeRequiredDescription
titlestringoptionalField whose value is each entry's title — read after timeline.titleField, ahead of the flat titleField (renderer default name)
datestringoptionalField whose value is each entry's date — read after timeline.startDateField / timeline.dateField, ahead of the flat spellings
descriptionstringoptionalField whose value is each entry's description — read ahead of descriptionField (renderer default description)
variantstringoptionalField whose value picks each entry's marker colour (renderer default variant) — the only spelling this binding has

Nested Shape: ObjectTimelineProps.navigation

PropertyTypeRequiredDescription
modeEnum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>optional (default: "page")
viewneveroptional[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).
preventNavigationbooleanoptional (default: false)Disable standard navigation entirely
openNewTabbooleanoptional (default: false)Force open in new tab (applies to page mode)
sizeEnum<'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.
widthstring | numberoptional[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

PropertyTypeRequiredDescription
objectNamestringoptionalObject 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> }optionalData 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
staticDataany[]optionalInline 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)[] }[]optionalBase 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 }optionalTree/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'>; … }optionalRow-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']

PropertyTypeRequiredDescription
provider'object'✅
objectstring✅Target object name

Nested Shape: ObjectTreeProps.data[provider='value']

PropertyTypeRequiredDescription
provider'value'✅
itemsany[]✅Static data array

Nested Shape: ObjectTreeProps.data[provider='schema']

PropertyTypeRequiredDescription
provider'schema'✅
schemaIdstring✅Schema identifier — typically the metadata type name
schemaRecord<string, any>optionalInline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable.

Nested Shape: ObjectTreeProps.filter[number]

View filter rule

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>✅Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter 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

PropertyTypeRequiredDescription
parentFieldstringoptionalSingle-parent pointer field (auto-detected from the object schema when omitted)
labelFieldstringoptionalField rendered indented in the first column (defaults to "name")
fieldsstring[]optionalAdditional fields rendered as flat columns alongside the label
defaultExpandedDepthintegeroptionalInitial expansion depth (0 = roots only; omit = expand all)

Nested Shape: ObjectTreeProps.navigation

PropertyTypeRequiredDescription
modeEnum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>optional (default: "page")
viewneveroptional[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).
preventNavigationbooleanoptional (default: false)Disable standard navigation entirely
openNewTabbooleanoptional (default: false)Force open in new tab (applies to page mode)
sizeEnum<'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.
widthstring | numberoptional[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

PropertyTypeRequiredDescription
items{ label: string | Record<string, string>; icon?: string; collapsed?: boolean; children: any[] }[]✅
allowMultiplebooleanoptional (default: false)Allow multiple panels to be expanded simultaneously
variantEnum<'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 }optionalARIA accessibility attributes

Nested Shape: PageAccordionProps.items[number]

PropertyTypeRequiredDescription
labelstring | Record<string, string>✅Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
iconstringoptionalLucide 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>.
collapsedbooleanoptional (default: false)
childrenany[]✅Child components

Nested Shape: PageAccordionProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

PageCardProps

Properties

PropertyTypeRequiredDescription
titlestring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
borderedbooleanoptional (default: true)
actionsneveroptional[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.
childrenany[]optionalCard content components, in order (the card body slot)
bodyneveroptional[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.
footerany[]optionalCard footer components (slot)
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes

Nested Shape: PageCardProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

PageContainerProps

Properties

PropertyTypeRequiredDescription
childrenany[]optionalChild components rendered inside this container, in order

PageHeaderProps

Properties

PropertyTypeRequiredDescription
titlestring | Record<string, string>optionalPage 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.
subtitlestring | Record<string, string>optionalPage subtitle
iconneveroptional[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.
breadcrumbneveroptional[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.
actionsstring[]optionalAction 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.
recordChromebooleanoptional (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.
showStarbooleanoptional (default: true)Show the follow (favourite) star beside the record title. Part of the record chrome — no effect when recordChrome is false.
showCopyIdbooleanoptional (default: true)Show the copy-record-id button beside the record title. Part of the record chrome — no effect when recordChrome is false.
maxVisibleintegeroptionalHow many header actions render as inline buttons before the rest fold into the overflow menu (renderer default 3).
mobileMaxVisibleintegeroptionalThe maxVisible budget on mobile viewports (renderer default 1).
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes

Nested Shape: PageHeaderProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

PageTabsProps

Properties

PropertyTypeRequiredDescription
tabStyleEnum<'line' | 'card' | 'pill'>optional (default: "line")Tab-strip visual style: 'line' underlines the active tab, 'card' frames each tab, 'pill' renders rounded pills
typeneveroptional[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.
positionEnum<'top' | 'left'>optional (default: "top")
alwaysShowStripbooleanoptionalRender 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 }optionalARIA accessibility attributes

Nested Shape: PageTabsProps.items[number]

PropertyTypeRequiredDescription
labelstring | Record<string, string>✅Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
iconstringoptionalLucide 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.
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility 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.
valuestringoptionalStable ?tab= URL token for this tab (default: index-derived tab-<i>, which is not durable across item-list changes)
countintegeroptionalBadge count shown next to the tab label (default: derived from record:related_list descendants)
childrenany[]✅Child components

Nested Shape: PageTabsProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

RecordActivityProps

Properties

PropertyTypeRequiredDescription
types(Enum<'comment' | 'field_change' | 'task' | 'event' | 'email' | 'call' | 'note' | 'file' | 'record_create' | 'record_delete' | 'approval' | 'sharing' | 'system'> | string)[]optionalFeed 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.
filterModeEnum<'all' | 'comments_only' | 'changes_only' | 'tasks_only'>optional (default: "all")Default activity filter
showFilterTogglebooleanoptional (default: true)Show filter dropdown in panel header
limitintegeroptional (default: 20)Number of items to load per page
showCompletedbooleanoptional (default: false)Include completed activities
unifiedTimelinebooleanoptional (default: true)Mix field changes and comments in one timeline (Airtable style)
showCommentInputbooleanoptional (default: true)Show "Leave a comment" input at the bottom
enableMentionsbooleanoptional (default: true)Enable @mentions in comments
enableReactionsbooleanoptional (default: false)Enable emoji reactions on feed items
enableThreadingbooleanoptional (default: false)Enable threaded replies on comments
showSubscriptionTogglebooleanoptional (default: true)Show bell icon for record-level notification subscription
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes

Nested Shape: RecordActivityProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

RecordAlertAction

Properties

PropertyTypeRequiredDescription
actionNamestring✅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.
labelstring | Record<string, string>optionalCTA button label — a string or an inline locale map, resolved with the same pickLocalized chain as title/body (default: the action's own label).
variantEnum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'>optionalButton variant — the Button primitive's own vocabulary (renderer default: destructive when severity is error, else default).

RecordAlertProps

Properties

PropertyTypeRequiredDescription
severityEnum<'info' | 'warning' | 'error' | 'success'>optionalBanner severity — styling, default icon, and the a11y role (error renders role="alert"/assertive; the rest role="status"/polite). Renderer default: info.
titlestring | Record<string, string>optionalBanner title — a string or an inline locale map ({ en, "zh-CN", … }), resolved to the current language at render (pickLocalized).
bodystring | Record<string, string>optionalBanner body — a string or an inline locale map, resolved like title.
visibleboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility 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.
iconstringoptionalLucide 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'> }optionalOptional call-to-action button rendered under the body — { actionName, label?, variant? }, resolved from the object's declared actions.
dismissiblebooleanoptionalRender an X control; dismissal is remembered per object/record in localStorage (renderer default: off).
dismissKeystringoptionalStable 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

PropertyTypeRequiredDescription
actionNamestring✅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.
labelstring | Record<string, string>optionalCTA button label — a string or an inline locale map, resolved with the same pickLocalized chain as title/body (default: the action's own label).
variantEnum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'>optionalButton variant — the Button primitive's own vocabulary (renderer default: destructive when severity is error, else default).

RecordChatterProps

Properties

PropertyTypeRequiredDescription
positionEnum<'bottom' | 'right' | 'left'>optionalWhere 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).
widthstring | numberoptionalPanel width (e.g., "350px", "30%") — side positions (right/left) only.
collapsiblebooleanoptionalWhether the panel can be collapsed (renderer default: off).
defaultCollapsedbooleanoptionalWhether 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; … }optionalEmbedded activity feed configuration
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes

Nested Shape: RecordChatterProps.feed

PropertyTypeRequiredDescription
types(Enum<'comment' | 'field_change' | 'task' | 'event' | 'email' | 'call' | 'note' | …> | string)[]optionalFeed 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.
filterModeEnum<'all' | 'comments_only' | 'changes_only' | 'tasks_only'>optional (default: "all")Default activity filter
showFilterTogglebooleanoptional (default: true)Show filter dropdown in panel header
limitintegeroptional (default: 20)Number of items to load per page
showCompletedbooleanoptional (default: false)Include completed activities
unifiedTimelinebooleanoptional (default: true)Mix field changes and comments in one timeline (Airtable style)
showCommentInputbooleanoptional (default: true)Show "Leave a comment" input at the bottom
enableMentionsbooleanoptional (default: true)Enable @mentions in comments
enableReactionsbooleanoptional (default: false)Enable emoji reactions on feed items
enableThreadingbooleanoptional (default: false)Enable threaded replies on comments
showSubscriptionTogglebooleanoptional (default: true)Show bell icon for record-level notification subscription
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes

Nested Shape: RecordChatterProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

RecordDetailsProps

Properties

PropertyTypeRequiredDescription
columnsEnum<'1' | '2' | '3' | '4'>optional (default: "2")Number of columns for field layout (1-4)
layoutneveroptional[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; … }[]optionalField 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.
fieldsstring[]optionalExplicit field list to display (optional, overrides highlightFields)
hideFieldsstring[]optionalField 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)
inlineEditbooleanoptionalAllow inline field editing in the detail body (renderer default: on, where the object itself is editable — set false to force it off).
showHeaderbooleanoptionalRender the detail body's own heading (renderer default: off).
enforceFieldSecuritybooleanoptionalFold 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.
redactFieldsstring[]optionalField 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.
requiredPermissionsstring[]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 }optionalARIA accessibility attributes

Nested Shape: RecordDetailsProps.sections[number]

PropertyTypeRequiredDescription
namestringoptionalStable section identifier for i18n lookup (snake_case) — resolves objects.<object>._sections.<name>.label; a nameless section renders its authored label in every locale
labelstring | Record<string, string>optionalSection heading (omit for an untitled, borderless section)
columnsintegeroptionalField-grid columns for this section (1-4). Omitted → the renderer derives the width.
groupstringoptionalField 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.
fieldsstring[]optionalField names rendered in this section, in order. Omit only when group supplies the members instead.
hideEmptybooleanoptionalHide 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).
collapsiblebooleanoptionalRender this section as a collapsible card — the heading becomes a chevron toggle, initially expanded (renderer default: off).
showBorderbooleanoptionalDraw 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.
defaultCollapsedbooleanoptionalStart a collapsible: true section collapsed (renderer default: expanded). Consulted only when collapsible is on — a non-collapsible section never reads its collapse state.
iconstringoptionalHeading 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.
descriptionstringoptionalSub-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.
headerColorEnum<'muted' | 'muted/50' | 'accent' | 'primary/10' | 'secondary/10' | 'destructive/10'>optionalSection-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 }optionalShow 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

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-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

PropertyTypeRequiredDescription
namestring✅Field name on the record
labelstringoptionalDisplay label (overrides schema label)
typestringoptionalOverride cell renderer type (rare)
readonlybooleanoptionalRender 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

PropertyTypeRequiredDescription
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.
layoutEnum<'horizontal' | 'vertical'>optional (default: "horizontal")Layout orientation for highlight fields
enforceFieldSecuritybooleanoptionalFold 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.
redactFieldsstring[]optionalField 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.
requiredPermissionsstring[]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 }optionalARIA accessibility attributes

Nested Shape: RecordHighlightsProps.fields[number]

PropertyTypeRequiredDescription
namestring✅Field name on the record
labelstringoptionalDisplay label (overrides schema label)
typestringoptionalOverride cell renderer type (rare)
readonlybooleanoptionalRender 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

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

RecordHistoryProps

Properties

PropertyTypeRequiredDescription
limitintegeroptionalMaximum history entries displayed, and the $top of the self-fetch query (renderer default: 50).
emptyTextstringoptionalCopy 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").
unknownUserTextstringoptionalCopy substituted when an entry has no resolvable actor. Literal string, every locale (renderer default: "Unknown user").

RecordLineItemsProps

Properties

PropertyTypeRequiredDescription
childObjectstringoptionalChild 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
relationshipFieldstring✅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
parentObjectstringoptionalParent object the totalField rollup is written to (default: the object of the record on the page)
parentIdstringoptionalParent record id the rows belong to (default: the record on the page). Wins over recordId
recordIdstringoptionalAlternate spelling of parentId the renderer also reads; parentId wins when both are set
amountFieldstringoptionalNumeric child column summed for the running total and for the totalField rollup (default amount when only totalField is set)
totalFieldstringoptionalParent field to receive the rolled-up sum on save
titlestringoptionalPanel 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"
readonlybooleanoptionalRender the grid read-only: cells locked, no Save button, and no adding or removing lines (renderer default: editable)
minRowsnumberoptionalMinimum number of rows
maxRowsnumberoptionalMaximum number of rows
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalAdditional 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'> }[]optionalLoad order for the child rows — the SortItem array form [{ field, order }, ...] (renderer default: storage order)
limitintegeroptionalRow 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]

PropertyTypeRequiredDescription
namestring✅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.
labelstringoptionalColumn header; defaults to the child field's label via hydration.
typeEnum<'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' | 'select' | 'lookup' | 'file'>optionalCell 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.
widthnumberoptionalFixed column width in px; omitted columns use type-based role sizing (text flexes, numeric/date/select stay fixed).
requiredbooleanoptionalCell is flagged inline-invalid while empty. Computed columns are never required.
options{ label: string; value: string }[]optionalSelect-cell options for type: 'select'; derived from the child field's options when the column declares no type.
prefixstringoptionalSymbol 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.
stepnumberoptionalInput step for numeric cells.
referencestringoptionalReferenced object for type: 'lookup' cells; derived from the child lookup field when the column declares no type.
displayFieldstringoptionalLabel field shown for a picked lookup record.
idFieldstringoptionalId field stored for a picked lookup record.
multiplebooleanoptionalMulti-value column: multi-record lookup, or multi-file upload cell.
acceptstring[]optionalAccepted MIME types / extensions for a file cell's picker (e.g. ['image/*', '.pdf']); omit to accept anything.
defaultHiddenbooleanoptionalCollapsed into the grid's column chooser by default (not dropped); required columns are never default-hidden.
computedbooleanoptionalRead-only computed column, recomputed live from sibling cells via expr and written back into the row.
exprstringoptionalArithmetic 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.
scaleintegeroptionalDecimal 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.
autofillbooleanoptionalFor 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.
readonlyWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalPredicate (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').
requiredWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalPredicate (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

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>✅Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter 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

PropertyTypeRequiredDescription
fieldstring✅Field name to sort by
orderEnum<'asc' | 'desc'>✅Sort direction

RecordPathProps

Properties

PropertyTypeRequiredDescription
statusFieldstring✅Field name representing the current status/stage
stages{ value: string; label: string | Record<string, string>; terminal?: Enum<'won' | 'lost'> }[]optionalExplicit stage definitions (if not using field metadata)
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes

Nested Shape: RecordPathProps.stages[number]

PropertyTypeRequiredDescription
valuestring✅
labelstring | Record<string, string>✅Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
terminalEnum<'won' | 'lost'>optionalMark this stage a terminus and its kind — overrides the renderer's value/label token heuristic

Nested Shape: RecordPathProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

RecordQuickActionsProps

Properties

PropertyTypeRequiredDescription
actionNamesstring[]optionalNames 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.
requiredPermissionsstring[]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.
locationEnum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | 'record_related' | 'record_section'>optionalWhich declared action location this bar renders (renderer default: record_header).
alignEnum<'start' | 'center' | 'end'>optionalHorizontal alignment of the button row (renderer default: end).
inlinebooleanoptionalRender 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.
variantEnum<'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'>optionalButton variant for every action — the Button primitive's own vocabulary; a per-action variant on the resolved def wins (renderer default: default).
sizeEnum<'default' | 'sm' | 'lg' | 'icon'>optionalButton size for every action — the Button primitive's own vocabulary; a per-action size wins (renderer default: sm).

RecordReferenceRailProps

Properties

PropertyTypeRequiredDescription
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.
hideEmptybooleanoptionalFold 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]

PropertyTypeRequiredDescription
objectNamestring✅Related object name whose records this card summarizes (e.g. "task", "opportunity_quote")
relationshipFieldstring✅Field on the related object that points back to this record (e.g. "account_id")
titlestringoptionalLiteral 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.
limitintegeroptionalPreview rows per card, and the $top of the one query this entry issues (renderer default: 3).
displayFieldstringoptionalField of the related record rendered in each preview row (renderer fallback when omitted: name / title / subject / label / … / id).

RecordRelatedListProps

Properties

PropertyTypeRequiredDescription
objectNamestring✅Related object name (e.g., "task", "opportunity")
relationshipFieldstring✅Field on related object that points to this record (e.g., "account_id")
relationshipValueFieldstringoptional (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.
columnsstring[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[]optionalFields 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.
sortstring | { field: string; order: Enum<'asc' | 'desc'> }[]optionalSort order for related records
limitintegeroptional (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)[] }[]optionalAdditional filter criteria for related records
titlestring | Record<string, string>optionalCustom title for the related list
showViewAllbooleanoptional (default: true)Show "View All" link to see all related records
actionsstring[]optionalAction IDs available for related records
add{ picker: object; linkField?: string; label?: string | Record<string, string> }optionalAdd-existing-via-picker config (generic m2m/junction assignment).
enforceFieldSecuritybooleanoptionalFold 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.
redactFieldsstring[]optionalField 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.
requiredPermissionsstring[]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 }optionalARIA accessibility attributes

Nested Shape: RecordRelatedListProps.parentDataSource

PropertyTypeRequiredDescription
objectstring✅Object to query
viewstringoptionalNamed view to apply
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalAdditional 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'> }[]optionalSort order
limitintegeroptionalMax records to display

Nested Shape: RecordRelatedListProps.columns[number]

PropertyTypeRequiredDescription
fieldstring✅Field name (snake_case)
labelstring | Record<string, string>optionalDisplay label override
widthnumberoptionalColumn width in pixels
alignEnum<'left' | 'center' | 'right'>optionalText alignment
hiddenbooleanoptionalHide column by default
sortablebooleanoptionalAllow sorting by this column
resizablebooleanoptionalAllow resizing this column
wrapbooleanoptionalAllow text wrapping
typestringoptionalRenderer type override (e.g., "currency", "date")
pinnedEnum<'left' | 'right'>optionalPin/freeze column to left or right side
summaryEnum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …> | { type: Enum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …>; field?: string }optionalFooter aggregation for this column — the function alone, or { type, field } to aggregate another field
prefix{ field: string; type?: Enum<'badge' | 'text'> }optionalField rendered inline before this cell value
linkbooleanoptionalFunctions as the primary navigation link (triggers View navigation)
actionstringoptionalRegistered Action ID to execute when clicked

Nested Shape: RecordRelatedListProps.filter[number]

View filter rule

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>✅Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter 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

PropertyTypeRequiredDescription
picker{ object: string; valueField?: string; labelField?: string; filter?: object[] }✅Where the Add affordance sources records from.
linkFieldstringoptionalField on objectName that stores the picked record id (junction case). Omit for a 1:m re-parent.
labelstring | Record<string, string>optionalLabel for the Add button (default "Add").

Nested Shape: RecordRelatedListProps.aria

PropertyTypeRequiredDescription
ariaLabelstring | Record<string, string>optionalAccessible 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.
ariaDescribedBystringoptionalID of element providing additional description (WAI-ARIA aria-describedby)
rolestringoptionalWAI-ARIA role attribute (e.g., "dialog", "navigation", "alert")

ReferenceRailEntry

Properties

PropertyTypeRequiredDescription
objectNamestring✅Related object name whose records this card summarizes (e.g. "task", "opportunity_quote")
relationshipFieldstring✅Field on the related object that points back to this record (e.g. "account_id")
titlestringoptionalLiteral 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.
limitintegeroptionalPreview rows per card, and the $top of the one query this entry issues (renderer default: 3).
displayFieldstringoptionalField of the related record rendered in each preview row (renderer fallback when omitted: name / title / subject / label / … / id).

On this page

TypeScript UsageActionButtonPropsPropertiesNested Shape: ActionButtonProps.onSuccessActionGroupPropsPropertiesNested Shape: ActionGroupProps.actions[number]ActionIconPropsPropertiesNested Shape: ActionIconProps.onSuccessActionMenuPropsPropertiesNested Shape: ActionMenuProps.actions[number]ElementButtonPropsPropertiesNested Shape: ElementButtonProps.actionNested Shape: ElementButtonProps.ariaElementDefinitionListPropsPropertiesNested Shape: ElementDefinitionListProps.items[number]ElementFilterPropsPropertiesElementFormPropsPropertiesElementImagePropsPropertiesNested Shape: ElementImageProps.ariaElementMetadataViewerPropsPropertiesNested Shape: ElementMetadataViewerProps.ariaElementNumberPropsPropertiesNested Shape: ElementNumberProps.ariaElementRecordPickerPropsPropertiesNested Shape: ElementRecordPickerProps.ariaElementRepeaterPropsPropertiesNested Shape: ElementRepeaterProps.fields[number]ElementTextInputPropsPropertiesNested Shape: ElementTextInputProps.ariaElementTextPropsPropertiesNested Shape: ElementTextProps.ariaObjectCalendarPropsPropertiesNested Shape: ObjectCalendarProps.calendarNested Shape: ObjectCalendarProps.filter[number]Nested Shape: ObjectCalendarProps.sort[number]Nested Shape: ObjectCalendarProps.navigationObjectFormPropsPropertiesNested Shape: ObjectFormProps.customFields[number]Nested Shape: ObjectFormProps.sections[number]Nested Shape: ObjectFormProps.submitBehavior[kind='redirect']Nested Shape: ObjectFormProps.mobileObjectGanttPropsPropertiesNested Shape: ObjectGanttProps.data[provider='object']Nested Shape: ObjectGanttProps.data[provider='value']Nested Shape: ObjectGanttProps.data[provider='schema']Nested Shape: ObjectGanttProps.filter[number]Nested Shape: ObjectGanttProps.sort[number]Nested Shape: ObjectGanttProps.ganttNested Shape: ObjectGanttProps.navigationNested Shape: ObjectGanttProps.markers[number]ObjectGridPropsPropertiesNested Shape: ObjectGridProps.emptyStateNested Shape: ObjectGridProps.columns[number]Nested Shape: ObjectGridProps.filter[number]Nested Shape: ObjectGridProps.sort[number]Nested Shape: ObjectGridProps.groupingNested Shape: ObjectGridProps.aggregations[number]Nested Shape: ObjectGridProps.conditionalFormatting[number]Nested Shape: ObjectGridProps.rowColorNested Shape: ObjectGridProps.selectionNested Shape: ObjectGridProps.bulkActionDefs[number]Nested Shape: ObjectGridProps.navigationNested Shape: ObjectGridProps.exportOptionsNested Shape: ObjectGridProps.operationsNested Shape: ObjectGridProps.data[provider='object']Nested Shape: ObjectGridProps.data[provider='value']Nested Shape: ObjectGridProps.data[provider='schema']ObjectKanbanPropsPropertiesNested Shape: ObjectKanbanProps.columns[number]Nested Shape: ObjectKanbanProps.filter[number]Nested Shape: ObjectKanbanProps.groupingNested Shape: ObjectKanbanProps.conditionalFormatting[number]Nested Shape: ObjectKanbanProps.navigationObjectMapPropsPropertiesNested Shape: ObjectMapProps.data[provider='object']Nested Shape: ObjectMapProps.data[provider='value']Nested Shape: ObjectMapProps.data[provider='schema']Nested Shape: ObjectMapProps.filter[number]Nested Shape: ObjectMapProps.sort[number]Nested Shape: ObjectMapProps.mapNested Shape: ObjectMapProps.navigationObjectMasterDetailFormPropsPropertiesNested Shape: ObjectMasterDetailFormProps.sections[number]Nested Shape: ObjectMasterDetailFormProps.details[number]ObjectMetricPropsPropertiesNested Shape: ObjectMetricProps.aggregateNested Shape: ObjectMetricProps.filter[number]Nested Shape: ObjectMetricProps.trendNested Shape: ObjectMetricProps.drillDownNested Shape: ObjectMetricProps.compareToObjectTimelinePropsPropertiesNested Shape: ObjectTimelineProps.timelineNested Shape: ObjectTimelineProps.filter[number]Nested Shape: ObjectTimelineProps.sort[number]Nested Shape: ObjectTimelineProps.items[number]Nested Shape: ObjectTimelineProps.mappingNested Shape: ObjectTimelineProps.navigationObjectTreePropsPropertiesNested Shape: ObjectTreeProps.data[provider='object']Nested Shape: ObjectTreeProps.data[provider='value']Nested Shape: ObjectTreeProps.data[provider='schema']Nested Shape: ObjectTreeProps.filter[number]Nested Shape: ObjectTreeProps.treeNested Shape: ObjectTreeProps.navigationPageAccordionPropsPropertiesNested Shape: PageAccordionProps.items[number]Nested Shape: PageAccordionProps.ariaPageCardPropsPropertiesNested Shape: PageCardProps.ariaPageContainerPropsPropertiesPageHeaderPropsPropertiesNested Shape: PageHeaderProps.ariaPageTabsPropsPropertiesNested Shape: PageTabsProps.items[number]Nested Shape: PageTabsProps.ariaRecordActivityPropsPropertiesNested Shape: RecordActivityProps.ariaRecordAlertActionPropertiesRecordAlertPropsPropertiesNested Shape: RecordAlertProps.actionRecordChatterPropsPropertiesNested Shape: RecordChatterProps.feedNested Shape: RecordChatterProps.ariaRecordDetailsPropsPropertiesNested Shape: RecordDetailsProps.sections[number]Nested Shape: RecordDetailsProps.ariaRecordHighlightsFieldUnion OptionsOption 1Option 2PropertiesRecordHighlightsPropsPropertiesNested Shape: RecordHighlightsProps.fields[number]Nested Shape: RecordHighlightsProps.ariaRecordHistoryPropsPropertiesRecordLineItemsPropsPropertiesNested Shape: RecordLineItemsProps.columns[number]Nested Shape: RecordLineItemsProps.filter[number]Nested Shape: RecordLineItemsProps.sort[number]RecordPathPropsPropertiesNested Shape: RecordPathProps.stages[number]Nested Shape: RecordPathProps.ariaRecordQuickActionsPropsPropertiesRecordReferenceRailPropsPropertiesNested Shape: RecordReferenceRailProps.entries[number]RecordRelatedListPropsPropertiesNested Shape: RecordRelatedListProps.parentDataSourceNested Shape: RecordRelatedListProps.columns[number]Nested Shape: RecordRelatedListProps.filter[number]Nested Shape: RecordRelatedListProps.addNested Shape: RecordRelatedListProps.ariaReferenceRailEntryProperties