ObjectStackObjectStack

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

PropertyTypeRequiredDescription
namestringMachine name (snake_case)
labelstringDisplay label
descriptionstringoptionalPage description
typeenumoptionalPage type (see below; default 'record')
objectstringoptionalAssociated object (for record type)
templatestringoptionalLayout template name (default: 'default')
kindenumoptionalPage 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
regionsPageRegion[]optionalLayout regions with components (default []list pages render via interfaceConfig, and an empty record/home/app page falls back to the synthesized default layout)
sourcestringoptionalPage 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
variablesPageVariable[]optionalLocal state variables
isDefaultbooleanoptionalIs default page for its type
assignedProfilesstring[]optionalProfiles that can access this page

Page Types

TypeDescriptionUse Case
recordTied to a specific object recordCustom detail pages
homeLanding/home pageApp entry points
appGeneral application pageCustom layouts
utilityUtility/helper panelTools, settings, wizards
listRecord list/interface surfaceData-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:

kindYou writeExecuted?Reach for it when
full (default) / slottedStructured regions / slots of components — the rest of this pageRecord, home and app layouts assembled from the component catalogue
htmlA source string of constrained JSX: registered components plus safe native HTMLNo — parsed into the same component tree at save time by @objectstack/sdui-parserFree-form layout, landing pages and composed dashboards, including author- or AI-generated ones you have not reviewed
reactA source string of real React — hooks, handlers, arbitrary JSYes — in the app's own React tree, no sandboxInteractive 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: [/* ... */],
  },
]
PropertyTypeRequiredDescription
namestringRegion identifier
widthenumoptional'small', 'medium', 'large', 'full'
componentsPageComponent[]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' },
}
PropertyTypeRequiredDescription
typestringComponent type (standard PageComponentType enum or custom string)
idstringoptionalUnique component instance identifier
labelstringoptionalDisplay label
propertiesRecord<string, unknown>optionalComponent-specific configuration (default {} — many components carry no props)
eventsRecord<string, string>optionalEvent handlers (action expressions)
styleobjectoptionalCSS styles
classNamestringoptionalCSS class names
responsiveStylesobjectoptionalPreferred 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.
visibleWhenstringoptionalVisibility 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 a type:'record' page. A kind:'react' page mounts no such context and os validate rejects 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:filter and element:form were retired in v17.x — no renderer ever shipped for either. List surfaces own their filtering via a view's userFilters quick-filter bar or the list toolbar's filter builder; for forms use the object-bound object-form block, 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' },
        },
      ],
    },
  ],
};

On this page