ObjectStackObjectStack

Page

Page protocol schemas

Page Region Schema A named region in the template where components are dropped.

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

TypeScript Usage

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

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

ElementDataSource

Properties

PropertyTypeRequiredDescription
objectstringObject to query
viewstringoptionalNamed view to apply
filteranyoptionalAdditional filter criteria
sort{ field: string; order: Enum<'asc' | 'desc'> }[]optionalSort order
limitintegeroptionalMax records to display

Nested Shape: ElementDataSource.sort[number]

Sort field and direction pair

PropertyTypeRequiredDescription
fieldstringField 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
allowPrintingbooleanoptionalAllow users to print the page

Nested Shape: InterfacePageConfig.columns[number]

PropertyTypeRequiredDescription
fieldstringField 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
fieldstringField name to sort by
orderEnum<'asc' | 'desc'>Sort direction

Nested Shape: InterfacePageConfig.filterBy[number]

View filter rule

PropertyTypeRequiredDescription
fieldstringField 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.

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: false)Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in.
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"

Page

Properties

PropertyTypeRequiredDescription
namestringPage 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)
assignedProfilesstring[]optional
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
kindEnum<'full' | 'slotted' | 'html' | 'react' | 'jsx'>optional (default: "full")Page override mode. full | slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; the legacy value 'jsx' is a deprecated alias), 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 amendment 2026-06-30).
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' (alias 'jsx') 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 amendment 2026-06-30). Authoritative over regions in both.
requiresstring[]optionalPlugin namespaces the JSX source references (validated at save and load)
_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
namestringVariable 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
namestringRegion 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
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")

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' | 'app:launcher' | 'nav:menu' | 'nav:breadcrumb' | 'global:search' | 'global:notifications' | 'user:profile' | 'ai:chat_window' | 'ai:suggestion' | 'element:text' | 'element:number' | 'element:image' | 'element:divider' | 'element:button' | 'element:record_picker' | 'element:text_input'> | stringComponent 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).
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 app, features, os.user and binds data to the data-source ADAPTER here — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). ⚠️ data is surface-dependent: on a page:tabs item visibleWhen it is the record ROW instead. 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?: any; sort?: object[]; … }optionalPer-element data binding for multi-object pages
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; apply them 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

PropertyTypeRequiredDescription
objectstringObject to query
viewstringoptionalNamed view to apply
filteranyoptionalAdditional filter criteria
sort{ field: string; order: Enum<'asc' | 'desc'> }[]optionalSort order
limitintegeroptionalMax records to display

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
  • app:launcher
  • nav:menu
  • nav:breadcrumb
  • global:search
  • global:notifications
  • user:profile
  • ai:chat_window
  • ai:suggestion
  • element:text
  • element:number
  • element:image
  • element:divider
  • element:button
  • element:record_picker
  • element:text_input

PageRegion

Properties

PropertyTypeRequiredDescription
namestringRegion 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' | …> | stringComponent 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).
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 app, features, os.user and binds data to the data-source ADAPTER here — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). ⚠️ data is surface-dependent: on a page:tabs item visibleWhen it is the record ROW instead. 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?: any; sort?: object[]; … }optionalPer-element data binding for multi-object pages
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; apply them 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
namestringVariable 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