Page Metadata
Build custom pages with component-based layouts, variables, and event handling
A Page defines a custom UI layout using components, regions, and variables. Unlike Views which are bound to a single Object, Pages are flexible containers that can combine multiple components, embed views, and manage local state.
Basic Structure
const homePage = {
name: 'sales_home',
label: 'Sales Home',
type: 'home',
regions: [
{
name: 'header',
width: 'full',
components: [
{
type: 'metric_card',
id: 'total_revenue',
label: 'Total Revenue',
properties: {
object: 'opportunity',
field: 'amount',
aggregate: 'sum',
format: 'currency',
},
},
],
},
{
name: 'main',
width: 'large',
components: [
{
type: 'list_view',
id: 'recent_deals',
label: 'Recent Deals',
properties: {
object: 'opportunity',
view: 'recent_open',
limit: 10,
},
},
],
},
],
};Page Properties
| Property | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Machine name (snake_case) |
label | string | ✅ | Display label |
description | string | optional | Page description |
type | enum | optional | Page type (see below; default 'record') |
object | string | optional | Associated object (for record type) |
template | string | optional | Layout template name (default: 'default') |
kind | enum | optional | Page override mode (default 'full'). full | slotted = structured authoring; html = author-written constrained JSX/HTML compiled (parsed, never executed) to the tree (ADR-0080; the legacy value 'jsx' is a deprecated alias); react = real-React source executed at render by the runtime (ADR-0081) — it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via OS_PAGE_REACT=off. See Authoring modes |
regions | PageRegion[] | optional | Layout regions with components (default [] — list pages render via interfaceConfig, and an empty record/home/app page falls back to the synthesized default layout) |
source | string | optional | Page source text — required (and authoritative over regions) when kind is 'html' or 'react'. For kind: 'html' it is constrained JSX/HTML compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute). For kind: 'react' it is real React/JSX executed at render by @object-ui/react-runtime (trusted tier). See React Pages |
variables | PageVariable[] | optional | Local state variables |
isDefault | boolean | optional | Is default page for its type |
assignedProfiles | string[] | optional | Profiles that can access this page |
Page Types
| Type | Description | Use Case |
|---|---|---|
record | Tied to a specific object record | Custom detail pages |
home | Landing/home page | App entry points |
app | General application page | Custom layouts |
utility | Utility/helper panel | Tools, settings, wizards |
list | Record list/interface surface | Data-driven interface pages |
Earlier roadmap types (dashboard, form, record_detail, record_review, overview, blank) were removed from the schema because they never shipped a renderer (ADR-0049 enforce-or-remove); only the five types above are valid.
Authoring modes
A page's kind selects how the body is authored. Three modes ship, and the rest of
this page describes the first one:
kind | You write | Executed? | Reach for it when |
|---|---|---|---|
full (default) / slotted | Structured regions / slots of components — the rest of this page | — | Record, home and app layouts assembled from the component catalogue |
html | A source string of constrained JSX: registered components plus safe native HTML | No — parsed into the same component tree at save time by @objectstack/sdui-parser | Free-form layout, landing pages and composed dashboards, including author- or AI-generated ones you have not reviewed |
react | A source string of real React — hooks, handlers, arbitrary JS | Yes — in the app's own React tree, no sandbox | Interactive business UIs — master/detail, wizards, state-driven filters — written by authors you trust |
The two source tiers set source instead of regions. source is authoritative over
regions: when both are present the source wins, and regions holds at most a derived
cache of it. A page whose kind is html, react or jsx with no non-empty source
is rejected by the schema rather than rendering empty.
'jsx' is a deprecated alias for 'html', still accepted and converted at load time.
Because kind: 'react' executes author JavaScript, it is gated by a host capability that
defaults ON and is disabled server-side per deployment with OS_PAGE_REACT=off. The
html tier is unaffected by that switch — it is never executed.
Neither source tier is styled with Tailwind utility classes. A page's source is
runtime metadata, and the console's build-time Tailwind scans only the console's own
source — so a utility class name in page source silently produces no CSS, with no error
(ADR-0065). Style an html page with its components' structured props and a JSON style
object; style a react page with inline style={{ … }} and hsl(var(--token)) theme
colors.
Both tiers are checked at author time — block props, field bindings and source syntax —
by os validate, os lint and os build alike. See
React Pages for the full authoring guide and
Validating metadata for the rules.
Regions
Regions define layout zones on the page. Each region contains components.
regions: [
{
name: 'sidebar',
width: 'small',
components: [/* ... */],
},
{
name: 'content',
width: 'large',
components: [/* ... */],
},
]| Property | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Region identifier |
width | enum | optional | 'small', 'medium', 'large', 'full' |
components | PageComponent[] | ✅ | Components in this region |
Components
Components are the building blocks placed inside regions.
{
type: 'chart',
id: 'revenue_chart',
label: 'Revenue Trend',
properties: {
chartType: 'line',
object: 'opportunity',
categoryField: 'close_date',
valueField: 'amount',
},
events: {
onClick: "navigate_to('opportunity_detail', { id: $event.id })",
},
visibleWhen: "'sales_manager' in current_user.positions",
style: { height: '400px' },
}| Property | Type | Required | Description |
|---|---|---|---|
type | string | ✅ | Component type (standard PageComponentType enum or custom string) |
id | string | optional | Unique component instance identifier |
label | string | optional | Display label |
properties | Record<string, unknown> | optional | Component-specific configuration (default {} — many components carry no props) |
events | Record<string, string> | optional | Event handlers (action expressions) |
style | object | optional | CSS styles |
className | string | optional | CSS class names |
responsiveStyles | object | optional | Preferred SDUI styling channel (ADR-0065): desktop-first per-breakpoint scoped style maps, compiled to id-scoped CSS at render. Keys are large (the unconditional base), then medium / small / xsmall as max-width overrides. Prefer design tokens, e.g. { large: { padding: 'var(--space-8)' }, small: { padding: 'var(--space-4)' } }. Use over ad-hoc style / className for metadata-authored pages. |
visibleWhen | string | optional | Visibility predicate (CEL) — component renders only when the expression is true. Binds record, current_user, and page state as page.<var>. (Legacy alias visibility is accepted but deprecated per ADR-0089.) |
The type field is a union of the standard PageComponentType enum and any custom string. The standard (namespaced) component types include:
- Structure:
page:header,page:footer,page:sidebar,page:tabs,page:accordion,page:card,page:section - Record context:
record:details,record:highlights,record:related_list,record:activity,record:chatter,record:path,record:alert,record:quick_actions,record:reference_rail,record:history— each renders from the record context a record page mounts, so they belong on atype:'record'page. Akind:'react'page mounts no such context andos validaterejects them there (see Validating metadata §10b) - Navigation:
app:launcher,nav:menu,nav:breadcrumb - Utility:
global:search,global:notifications,user:profile - AI:
ai:chat_window,ai:suggestion - Elements:
element:text,element:number,element:image,element:divider,element:button,element:record_picker,element:text_input(element:filterandelement:formwere retired in v17.x — no renderer ever shipped for either. List surfaces own their filtering via a view'suserFiltersquick-filter bar or the list toolbar's filter builder; for forms use the object-boundobject-formblock, which is rendered and designer-publishable)
Components may also carry dataSource (per-element object binding for multi-object pages), responsiveStyles (per-breakpoint scoped CSS, ADR-0065), and aria configuration. Custom string types are also accepted for project-specific widgets. (The former responsive layout block was retired in v17.x — no renderer ever applied it; see the upgrade guide.)
Variables
Pages can define local variables for managing state across components.
variables: [
{
name: 'selected_tab',
type: 'string',
defaultValue: 'overview',
},
{
name: 'date_range',
type: 'object',
defaultValue: { start: null, end: null },
},
]Variable type accepts 'string' (default), 'number', 'boolean', 'object', 'array', or 'record_id'.
Complete Example
const accountRecordPage = {
name: 'account_detail',
label: 'Account Detail',
type: 'record',
object: 'account',
variables: [
{ name: 'active_tab', type: 'string', defaultValue: 'details' },
],
regions: [
{
name: 'header',
width: 'full',
components: [
{
type: 'record:highlights',
id: 'header',
properties: {
fields: ['name', 'type', 'industry', 'owner'],
},
},
// Record actions are their own component — `record:highlights` renders
// field chips and nothing else.
{
type: 'record:quick_actions',
properties: {
actionNames: ['edit', 'delete', 'clone'],
},
},
],
},
{
name: 'sidebar',
width: 'small',
components: [
{
type: 'record:details',
id: 'key_fields',
label: 'Key Fields',
properties: {
fields: ['annual_revenue', 'employees', 'website', 'phone'],
},
},
{
type: 'record:activity',
id: 'activities',
label: 'Activity',
},
],
},
{
name: 'main',
width: 'large',
components: [
{
type: 'record:related_list',
id: 'contacts',
label: 'Contacts',
// `objectName` is the RELATED (child) object being listed;
// `relationshipField` is the child's field pointing back at this record.
properties: { objectName: 'contact', relationshipField: 'account' },
},
{
type: 'record:related_list',
id: 'opportunities',
label: 'Opportunities',
properties: { objectName: 'opportunity', relationshipField: 'account' },
},
],
},
],
};Related
- View Metadata — List views and form views for record display
- Dashboard Metadata — Analytics-focused page layout
- App Metadata — Organize pages into applications
- React Pages — The
htmlandreactsource-authoring tiers in full - Validating metadata — Every author-time rule a page is held to