ObjectStackObjectStack

Page schema — UI Protocol property reference

Page schemas of the ObjectStack UI Protocol: ElementApiDataSource, ElementDataSource and 8 more — each property with its type, default and a TypeScript example.

Source: packages/spec/src/ui/page.zod.ts

TypeScript Usage

import { ElementApiDataSourceSchema, ElementDataSourceSchema, InterfacePageConfigSchema, PageSchema, PageComponentSchema, PageComponentType, PagePrintSchema, PageRegionSchema, PageTypeSchema, PageVariableSchema } from '@objectstack/spec/ui';
import type { ElementApiDataSource, ElementDataSource, InterfacePageConfig, Page, PageComponent, PageComponentType, PagePrint, PageRegion, PageType, PageVariable } from '@objectstack/spec/ui';

// Validate data
const result = ElementApiDataSourceSchema.parse(data);

ElementApiDataSource

[PLANNED — not yet rendered] API binding of a page element: { provider: 'api', read: { url }, path? } reads one same-origin endpoint with the console's signed-in fetch and binds the selected value, which the element reads as data in its expression holes and its visibleWhen. The console's generic stat, usage-bar and checklist blocks are the planned readers; until they ship, the binding parses and no block reads it.

Properties

PropertyTypeRequiredDescription
provider'api'✅Binds the element to one same-origin API endpoint instead of an object
read{ url: string; method?: 'GET' }✅The read request — the same-origin endpoint the element binds
pathstringoptionalDotted path selecting the sub-tree of the response the element binds, e.g. data.environments in a { success, data } envelope; omitted, the element binds the whole response

Nested Shape: ElementApiDataSource.read

PropertyTypeRequiredDescription
urlstring✅Same-origin API path the element reads — it opens with a single /, e.g. /api/v1/cloud/home. An absolute URL of any scheme, a protocol-relative //host, a backslash read as a slash, a space or control character, and a relative path are refused at parse. No credential is written here: the console reads the endpoint with its own signed-in fetch.
method'GET'optionalHTTP method of the read — GET, the only method a page element's read is sent with

ElementDataSource

Properties

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: ElementDataSource.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: ElementDataSource.sort[number]

Sort field and direction pair

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

InterfacePageConfig

Interface-level page configuration (Airtable parity)

Properties

PropertyTypeRequiredDescription
sourcestringoptionalSource object name for the page
columnsstring[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[]optionalColumns shown by the page. Blank = all object fields. Defined directly on the page (no view inheritance).
sort{ field: string; order: Enum<'asc' | 'desc'> }[]optionalDefault sort order for the page, defined directly on the page.
filterBy{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalAlways-on page filter (base filter).
levelsintegeroptionalNumber of hierarchy levels to display
sourceViewstringoptional@deprecated Legacy named-view inheritance. Define columns/sort/filterBy on the page instead.
appearance{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[] }optionalAppearance and visualization configuration
userFilters{ element?: Enum<'dropdown' | 'tabs' | 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }optionalEnd-user quick-filter bar for this page (overrides the source view's userFilters)
userActions{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }optionalUser action toggles
addRecord{ enabled?: boolean; position?: Enum<'top' | 'bottom' | 'both'>; mode?: Enum<'inline' | 'form' | 'modal'>; formView?: string }optionalAdd record entry point configuration
buttonsstring[]optionalToolbar buttons — names of the source object's actions to surface in the page toolbar
recordActionEnum<'drawer' | 'page' | 'modal' | 'none'>optionalHow clicking a record opens its detail (drawer | page | modal | none). Default: drawer
showRecordCountbooleanoptionalShow record count at page bottom
emptyState{ title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object }optionalWhat the page draws in place of rows when its list has none to show: { title, message, icon, link }, the list view's own empty-state shape. The console's list page prefers it, whole, over its source view's empty state; with none declared, the page draws the source view's, if any.
allowPrintingbooleanoptionalAllow users to print the page

Nested Shape: InterfacePageConfig.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: InterfacePageConfig.sort[number]

Sort field and direction pair

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

Nested Shape: InterfacePageConfig.filterBy[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: InterfacePageConfig.appearance

PropertyTypeRequiredDescription
showDescriptionbooleanoptional (default: true)Show the view description text
allowedVisualizationsEnum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[]optionalWhitelist of visualization types users can switch between (e.g. ["grid", "gallery", "kanban"])

Nested Shape: InterfacePageConfig.userFilters

PropertyTypeRequiredDescription
elementEnum<'dropdown' | 'tabs' | 'toggle'>optional (default: "dropdown")Filter control style: "dropdown" (per-field value selectors) or "tabs" (named presets). "toggle" is deprecated.
fields{ field: string; label?: string | Record<string, string>; type?: Enum<'select' | 'multi-select' | 'boolean' | 'date-range' | 'text'>; options?: object[]; … }[]optionalFields exposed as quick filters (dropdown/toggle elements)
tabs{ name: string; label?: string | Record<string, string>; icon?: string; view?: string; … }[]optionalNamed filter presets rendered as tabs (tabs element). Reuses ViewTabSchema
showAllRecordsbooleanoptionalShow an "All records" tab before the presets (tabs element)
allowAddTabbooleanoptionalLet end users add their own tab after the presets (tabs element): the affordance asks for a name and snapshots the filters currently applied as a new tab. SESSION-SCOPED — an added tab lives only for the current mount, is never written back as metadata (ADR-0047), and carries a remove control the authored presets do not. Page lists only — object views use listViews for named presets

Nested Shape: InterfacePageConfig.userActions

PropertyTypeRequiredDescription
sortbooleanoptional (default: true)Allow users to sort records
searchbooleanoptional (default: true)Allow users to search records
filterbooleanoptional (default: true)Allow users to filter records
refreshbooleanoptional (default: true)Allow users to reload the view data from the backend without a full page reload
rowHeightbooleanoptional (default: true)Allow users to toggle row height/density
groupbooleanoptional (default: true)Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level grouping block.
addRecordFormbooleanoptional (default: false)Add records through a form instead of inline
editInlinebooleanoptional (default: true)Allow users to edit records inline. On by default: a user who may update the object edits a cell in place with the field's type-aware widget (the same control the form uses), under the permission gate that already decides whether the object is editable at all. Declare editInline: false to make the list read-only in place.
hideFieldsbooleanoptional (default: false)Allow users to hide/show fields from the toolbar (the affordance behind the view-level hiddenFields list). Boolean toggle — distinct from the record-details component's hideFields, which is an array of field names to omit. Off by default: column hiding is opt-in.
rowColorbooleanoptional (default: false)Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level rowColor block. Off by default: row colouring is opt-in.
buttonsstring[]optionalCustom action button IDs to show in the toolbar

Nested Shape: InterfacePageConfig.addRecord

PropertyTypeRequiredDescription
enabledbooleanoptional (default: true)Show the add record entry point
positionEnum<'top' | 'bottom' | 'both'>optional (default: "bottom")Position of the add record button
modeEnum<'inline' | 'form' | 'modal'>optional (default: "inline")How to add a new record
formViewstringoptionalNamed form view to use when mode is "form" or "modal"

Nested Shape: InterfacePageConfig.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.

Page

Properties

PropertyTypeRequiredDescription
namestring✅Page unique name (lowercase snake_case)
labelstring | Record<string, string>✅Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
descriptionstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
iconstringoptionalPage icon name
typeEnum<'record' | 'home' | 'app' | 'utility' | 'list'>optional (default: "record")Page type
variables{ name: string; type?: Enum<'string' | 'number' | 'boolean' | 'object' | 'array' | 'record_id'>; defaultValue?: any; source?: string }[]optionalLocal page state, exposed to expressions as page.<name> and writable by interactive elements via source (master/detail, filtered dashboards).
objectstringoptionalBound object (for Record pages)
templatestringoptional (default: "default")Layout template name (e.g. "header-sidebar-main")
regions{ name: string; width?: Enum<'small' | 'medium' | 'large' | 'full'>; components: object[] }[]optionalLayout regions (header, main, sidebar, footer) with their components. Optional — list pages use interfaceConfig, slotted pages use slots, and an empty full page falls back to the synthesized default layout.
isDefaultbooleanoptional (default: false)Whether this record page is the default record page of its object — the page the record view opens. At most one isDefault: true record page may exist for an object in a deployment: a second is refused (by defineStack / os validate, at package registration and at metadata save), naming both pages. Saving a page under the existing default's own name is that page, not a second default. Outside this rule for a page that is not a record page bound to an object.
assignedProfilesneveroptional[REMOVED] page.assignedProfiles was removed in @objectstack/spec 17.5.0 (ADR-0090 D2, ADR-0049 enforce-or-remove) — it was named for the Profile concept ADR-0090 D2 deleted, and it gated nothing: no renderer, route or metadata read door ever read the key, so a page that "assigned profiles" stayed open to every caller who could reach it. Delete the key. Page audience is the permission set's: gate the DATA the page shows with the object's permission sets, and bind those sets to people through positions (named in a position's permissionSets) — those are the checks the runtime actually runs. 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.
interfaceConfig{ source?: string; columns?: string[] | object[]; sort?: object[]; filterBy?: object[]; … }optionalInterface-level page configuration (for Airtable-style interface pages)
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes
print{ paperSize?: Enum<'A4' | 'A5' | 'Letter' | 'Legal'>; orientation?: Enum<'portrait' | 'landscape'>; margins?: object; repeatHeader?: boolean; … }optionalPrint declaration — makes this page a print page, a document authored in the page's own blocks: paper size and orientation, margins in millimetres, the running header and footer (the page's own header / footer regions), page numbers and page-break hints, each mapped to print CSS. A print page is a kind: 'full' page of type: 'record', 'home' or 'app' with its blocks in regions, and every block in it must come from the printable block subset: containers that draw every child (page:section, page:card, page:footer), field blocks (record:details, record:highlights), the child-record table record:line_items, element:text, element:image, element:divider, element:definition-list, element:repeater, element:number and object-metric; os validate / os build / os lint and the metadata save door refuse any other block inside it. Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, the declaration is validated but nothing applies it. A list view's print button is allowPrinting, not this.
kindEnum<'full' | 'slotted' | 'html' | 'react'>optional (default: "full")Page override mode. full | slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; ADR-0081 named it html), styled by the registered components' structured props plus a JSON style object with hsl(var(--token)) theme colors; react = real-React source executed at render by the runtime (ADR-0081), styled by inline style with the same token colors; it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via the OS_PAGE_REACT=off env toggle. Do not author Tailwind classes in page source in either tier: source is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080).
slots{ header?: object | object[]; actions?: object | object[]; alerts?: object | object[]; highlights?: object | object[]; … }optionalSlot override map for slotted pages
sourcestringoptionalPage source text. For kind==='html' it is constrained JSX compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute), styled by the registered components' structured props plus a JSON style object with hsl(var(--token)) theme colors. For kind==='react' it is real React/JSX executed at render by @object-ui/react-runtime (trusted tier), styled by inline style with the same token colors. Do not author Tailwind classes in page source in either tier: source is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). Authoritative over regions in both.
requiresstring[]optionalPlugin namespaces the page's source uses, derived from the source at save — omit it. The key exists only on a kind==='html' page, the kind whose source is compiled at save; on a 'react', 'full' or 'slotted' page — and a page that omits kind, which is 'full' — it is refused at parse. On a server that has the deployment's SDUI component manifest, saving an html page compiles its source and stores the namespaces it uses here; a written list that disagrees with the source is refused (422 INVALID_METADATA, page-requires-disagrees-with-source) — on a draft save it is kept until the draft's publish, which refuses it. At load, a stored page whose list names a plugin no component in that manifest carries is reported, page and plugin named, and is still served. A server with no manifest checks neither and says so once at boot.
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.

Nested Shape: Page.variables[number]

PropertyTypeRequiredDescription
namestring✅Variable name. Exposed to expressions as page.<name>.
typeEnum<'string' | 'number' | 'boolean' | 'object' | 'array' | 'record_id'>optional (default: "string")
defaultValueanyoptionalInitial value. Defaults to a type-appropriate empty value when omitted.
sourcestringoptionalComponent id that writes this variable (e.g. an element:record_picker whose id matches).

Nested Shape: Page.regions[number]

PropertyTypeRequiredDescription
namestring✅Region name (e.g. "sidebar", "main", "header")
widthEnum<'small' | 'medium' | 'large' | 'full'>optional
components{ type: Enum<'page:header' | 'page:footer' | 'page:sidebar' | 'page:tabs' | 'page:accordion' | …> | string; id?: string; label?: string | Record<string, string>; properties?: Record<string, any>; … }[]✅Components in this region

Nested Shape: Page.interfaceConfig

PropertyTypeRequiredDescription
sourcestringoptionalSource object name for the page
columnsstring[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[]optionalColumns shown by the page. Blank = all object fields. Defined directly on the page (no view inheritance).
sort{ field: string; order: Enum<'asc' | 'desc'> }[]optionalDefault sort order for the page, defined directly on the page.
filterBy{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalAlways-on page filter (base filter).
levelsintegeroptionalNumber of hierarchy levels to display
sourceViewstringoptional@deprecated Legacy named-view inheritance. Define columns/sort/filterBy on the page instead.
appearance{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[] }optionalAppearance and visualization configuration
userFilters{ element?: Enum<'dropdown' | 'tabs' | 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }optionalEnd-user quick-filter bar for this page (overrides the source view's userFilters)
userActions{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }optionalUser action toggles
addRecord{ enabled?: boolean; position?: Enum<'top' | 'bottom' | 'both'>; mode?: Enum<'inline' | 'form' | 'modal'>; formView?: string }optionalAdd record entry point configuration
buttonsstring[]optionalToolbar buttons — names of the source object's actions to surface in the page toolbar
recordActionEnum<'drawer' | 'page' | 'modal' | 'none'>optionalHow clicking a record opens its detail (drawer | page | modal | none). Default: drawer
showRecordCountbooleanoptionalShow record count at page bottom
emptyState{ title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object }optionalWhat the page draws in place of rows when its list has none to show: { title, message, icon, link }, the list view's own empty-state shape. The console's list page prefers it, whole, over its source view's empty state; with none declared, the page draws the source view's, if any.
allowPrintingbooleanoptionalAllow users to print the page

Nested Shape: Page.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")

Nested Shape: Page.print

PropertyTypeRequiredDescription
paperSizeEnum<'A4' | 'A5' | 'Letter' | 'Legal'>optionalPaper size — the size keyword of the print CSS @page { size } rule (A4 when omitted). Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it.
orientationEnum<'portrait' | 'landscape'>optionalSheet orientation — the orientation keyword of the @page { size } rule (portrait when omitted). Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it.
margins{ top?: number; right?: number; bottom?: number; left?: number }optionalSheet margins in MILLIMETRES, one number per side — the @page { margin } rule; a side left out takes the renderer's default. Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it.
repeatHeaderbooleanoptionalRepeat the page's header region (its regions entry named header) at the top of every printed sheet — the running header. The parse refuses it on a page with no header region. Off when omitted: the region prints once, at the top of the first sheet. Consumer: the console's browser print rendering; until it ships, nothing applies it.
repeatFooterbooleanoptionalRepeat the page's footer region (its regions entry named footer) at the bottom of every printed sheet — the running footer. The parse refuses it on a page with no footer region. Off when omitted: the region prints once, after the body. Consumer: the console's browser print rendering; until it ships, nothing applies it.
pageNumbersbooleanoptionalPrint the sheet number and the sheet count in the bottom margin of every sheet — the @page margin box with counter(page) and counter(pages). Off when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it.
repeatTableHeadersbooleanoptionalPage-break hint: repeat a table's column headings at the top of every sheet the table runs onto (thead { display: table-header-group }). On when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it.
avoidBreakInsidebooleanoptionalPage-break hint: keep each block whole on one sheet when it fits, moving it to the next sheet instead of splitting it (break-inside: avoid on every block). Off when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it.

PageComponent

Properties

PropertyTypeRequiredDescription
typeEnum<'page:header' | 'page:footer' | 'page:sidebar' | 'page:tabs' | 'page:accordion' | 'page:card' | 'page:section' | 'record:details' | 'record:highlights' | 'record:related_list' | 'record:activity' | 'record:chatter' | 'record:discussion' | 'record:path' | 'record:alert' | 'record:quick_actions' | 'record:reference_rail' | 'record:history' | 'record:approval_decision' | 'record:approvals' | 'record:attachments' | 'app:launcher' | 'nav:menu' | 'nav:breadcrumb' | 'global:search' | 'global:notifications' | 'ai:suggestion' | 'element:text' | 'element:number' | 'element:image' | 'element:divider' | 'element:button' | 'element:record_picker' | 'element:text_input'> | string✅Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. object-grid, mcp:connect-agent). The spec's own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by os validate / os build / os lint (rule component-type-unknown); a type the vocabulary RETIRED by name (user:profile — shell chrome, not author-placeable; ai:chat_window — no renderer by design, the floating chat overlay is the AI chat entry point; element:filter and element:form — retired whole, no renderer for either ever shipped) is refused at the parse itself, with the retirement prescription.
idstringoptionalUnique instance ID
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
propertiesRecord<string, any>optional (default: {})Component props passed to the widget. See component.zod.ts for schemas.
eventsRecord<string, string>optionalEvent handlers map
styleRecord<string, string>optionalInline styles or utility classes
classNamestringoptionalCSS class names
responsiveStyles{ large?: Record<string, string | number>; medium?: Record<string, string | number>; small?: Record<string, string | number>; xsmall?: Record<string, string | number> }optionalPer-breakpoint scoped style maps (ADR-0065)
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: record, current_user (ADR-0068 aliases user / ctx.user — one object, three spellings), and page state as page.<var>. The shipping renderer additionally mounts features and os.user — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). It mounts NO data root here: a data.* read faults and a faulting predicate renders the component, so such a gate never gates; the page's row is record. data is declared only for an element whose dataSource is the PLANNED API binding (provider: 'api'), which reads its bound value as data once that binding's renderer ships. ⚠️ A page:tabs item visibleWhen is a different evaluator: it binds data to the record ROW. e.g. "page.selectedProjectId != ''"
visibilitystring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optional[DEPRECATED → visibleWhen] Visibility predicate (CEL). Normalized to visibleWhen at parse.
dataSource{ object: string; view?: string; filter?: object[]; sort?: object[]; … } | { provider: 'api'; read: object; path?: string }optionalPer-element data binding: an object binding { object, view?, filter?, sort?, limit? } that overrides the page object, or an API binding { provider: 'api', read: { url }, path? } that reads one same-origin endpoint
responsiveneveroptional[REMOVED] page.components[].responsive was removed in @objectstack/spec 17 (ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling responsiveStyles (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. responsiveStyles: { xsmall: { display: 'none' } } to hide a component on the narrowest screens. 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: PageComponent.responsiveStyles

PropertyTypeRequiredDescription
largeRecord<string, string | number>optionalUnconditional base (desktop-first)
mediumRecord<string, string | number>optionalApplied at ≤ medium breakpoint
smallRecord<string, string | number>optionalApplied at ≤ small breakpoint
xsmallRecord<string, string | number>optionalApplied at ≤ xsmall breakpoint

Nested Shape: PageComponent.dataSource[option 1]

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: PageComponent.dataSource[option 2]

[PLANNED — not yet rendered] API binding of a page element: { provider: 'api', read: { url }, path? } reads one same-origin endpoint with the console's signed-in fetch and binds the selected value, which the element reads as data in its expression holes and its visibleWhen. The console's generic stat, usage-bar and checklist blocks are the planned readers; until they ship, the binding parses and no block reads it.

PropertyTypeRequiredDescription
provider'api'✅Binds the element to one same-origin API endpoint instead of an object
read{ url: string; method?: 'GET' }✅The read request — the same-origin endpoint the element binds
pathstringoptionalDotted path selecting the sub-tree of the response the element binds, e.g. data.environments in a { success, data } envelope; omitted, the element binds the whole response

Nested Shape: PageComponent.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")

PageComponentType

Allowed Values

  • page:header
  • page:footer
  • page:sidebar
  • page:tabs
  • page:accordion
  • page:card
  • page:section
  • record:details
  • record:highlights
  • record:related_list
  • record:activity
  • record:chatter
  • record:discussion
  • record:path
  • record:alert
  • record:quick_actions
  • record:reference_rail
  • record:history
  • record:approval_decision
  • record:approvals
  • record:attachments
  • app:launcher
  • nav:menu
  • nav:breadcrumb
  • global:search
  • global:notifications
  • ai:suggestion
  • element:text
  • element:number
  • element:image
  • element:divider
  • element:button
  • element:record_picker
  • element:text_input

PagePrint

Print declaration: paper size, orientation, margins, the running header and footer, page numbering and page-break hints. Its presence makes the page a print page, whose blocks must come from the printable block subset.

Properties

PropertyTypeRequiredDescription
paperSizeEnum<'A4' | 'A5' | 'Letter' | 'Legal'>optionalPaper size — the size keyword of the print CSS @page { size } rule (A4 when omitted). Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it.
orientationEnum<'portrait' | 'landscape'>optionalSheet orientation — the orientation keyword of the @page { size } rule (portrait when omitted). Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it.
margins{ top?: number; right?: number; bottom?: number; left?: number }optionalSheet margins in MILLIMETRES, one number per side — the @page { margin } rule; a side left out takes the renderer's default. Consumer: the console's browser print rendering, and later the server-side PDF renderer; until it ships, nothing applies it.
repeatHeaderbooleanoptionalRepeat the page's header region (its regions entry named header) at the top of every printed sheet — the running header. The parse refuses it on a page with no header region. Off when omitted: the region prints once, at the top of the first sheet. Consumer: the console's browser print rendering; until it ships, nothing applies it.
repeatFooterbooleanoptionalRepeat the page's footer region (its regions entry named footer) at the bottom of every printed sheet — the running footer. The parse refuses it on a page with no footer region. Off when omitted: the region prints once, after the body. Consumer: the console's browser print rendering; until it ships, nothing applies it.
pageNumbersbooleanoptionalPrint the sheet number and the sheet count in the bottom margin of every sheet — the @page margin box with counter(page) and counter(pages). Off when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it.
repeatTableHeadersbooleanoptionalPage-break hint: repeat a table's column headings at the top of every sheet the table runs onto (thead { display: table-header-group }). On when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it.
avoidBreakInsidebooleanoptionalPage-break hint: keep each block whole on one sheet when it fits, moving it to the next sheet instead of splitting it (break-inside: avoid on every block). Off when omitted. Consumer: the console's browser print rendering; until it ships, nothing applies it.

Nested Shape: PagePrint.margins

PropertyTypeRequiredDescription
topnumberoptionalTop margin, in millimetres
rightnumberoptionalRight margin, in millimetres
bottomnumberoptionalBottom margin, in millimetres
leftnumberoptionalLeft margin, in millimetres

PageRegion

Properties

PropertyTypeRequiredDescription
namestring✅Region name (e.g. "sidebar", "main", "header")
widthEnum<'small' | 'medium' | 'large' | 'full'>optional
components{ type: Enum<'page:header' | 'page:footer' | 'page:sidebar' | 'page:tabs' | 'page:accordion' | …> | string; id?: string; label?: string | Record<string, string>; properties?: Record<string, any>; … }[]✅Components in this region

Nested Shape: PageRegion.components[number]

PropertyTypeRequiredDescription
typeEnum<'page:header' | 'page:footer' | 'page:sidebar' | 'page:tabs' | 'page:accordion' | …> | string✅Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. object-grid, mcp:connect-agent). The spec's own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by os validate / os build / os lint (rule component-type-unknown); a type the vocabulary RETIRED by name (user:profile — shell chrome, not author-placeable; ai:chat_window — no renderer by design, the floating chat overlay is the AI chat entry point; element:filter and element:form — retired whole, no renderer for either ever shipped) is refused at the parse itself, with the retirement prescription.
idstringoptionalUnique instance ID
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
propertiesRecord<string, any>optional (default: {})Component props passed to the widget. See component.zod.ts for schemas.
eventsRecord<string, string>optionalEvent handlers map
styleRecord<string, string>optionalInline styles or utility classes
classNamestringoptionalCSS class names
responsiveStyles{ large?: Record<string, string | number>; medium?: Record<string, string | number>; small?: Record<string, string | number>; xsmall?: Record<string, string | number> }optionalPer-breakpoint scoped style maps (ADR-0065)
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: record, current_user (ADR-0068 aliases user / ctx.user — one object, three spellings), and page state as page.<var>. The shipping renderer additionally mounts features and os.user — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). It mounts NO data root here: a data.* read faults and a faulting predicate renders the component, so such a gate never gates; the page's row is record. data is declared only for an element whose dataSource is the PLANNED API binding (provider: 'api'), which reads its bound value as data once that binding's renderer ships. ⚠️ A page:tabs item visibleWhen is a different evaluator: it binds data to the record ROW. e.g. "page.selectedProjectId != ''"
visibilitystring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optional[DEPRECATED → visibleWhen] Visibility predicate (CEL). Normalized to visibleWhen at parse.
dataSource{ object: string; view?: string; filter?: object[]; sort?: object[]; … } | { provider: 'api'; read: object; path?: string }optionalPer-element data binding: an object binding { object, view?, filter?, sort?, limit? } that overrides the page object, or an API binding { provider: 'api', read: { url }, path? } that reads one same-origin endpoint
responsiveneveroptional[REMOVED] page.components[].responsive was removed in @objectstack/spec 17 (ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling responsiveStyles (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. responsiveStyles: { xsmall: { display: 'none' } } to hide a component on the narrowest screens. 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

PageType

Page type — the page KIND. Only types with a dedicated renderer are authorizable; visualizations of a list page live in interfaceConfig, not here.

Allowed Values

  • record
  • home
  • app
  • utility
  • list

PageVariable

Properties

PropertyTypeRequiredDescription
namestring✅Variable name. Exposed to expressions as page.<name>.
typeEnum<'string' | 'number' | 'boolean' | 'object' | 'array' | 'record_id'>optional (default: "string")
defaultValueanyoptionalInitial value. Defaults to a type-appropriate empty value when omitted.
sourcestringoptionalComponent id that writes this variable (e.g. an element:record_picker whose id matches).

On this page