ObjectStackObjectStack

View schema — UI Protocol property reference

View protocol schemas — the view metadata type and its three persisted body spellings.

View protocol schemas — the view metadata type and its three persisted body spellings.

Covers the authoring surfaces (defineView, defineViewItem), the wire doors Studio and the REST layer write through, and ViewMetadataSchema — the union every persisted view body is judged by.

Name grammar depends on the body spelling

ViewMetadataSchema is a union over three persisted body shapes, and they do not share one name grammar. Which grammar applies is decided by the shape of the body — something an author never names explicitly — so neither failure direction below is discoverable from the key being written:

body spellingrecognised byname is declared asflat (undotted) name
standalone ViewItem recorda nested configViewItemNameSchema — QUALIFIED_ITEM_NAME_PATTERN, dot REQUIREDrejected, located at ["name"]
flattened runtime overlayan inline view config; no config, no container slotz.string().optional() — no grammar at allaccepted
defineView containera container slot (list / form / listViews / formViews)z.string().optional() on ViewSchema — no grammar at allaccepted, and normally IS flat

The two permissive rows are deliberate, not gaps left to tighten later:

  • A container's own name is the bare object key. ADR-0017 §3.2's dual-read loader registers the aggregated container under <object> and each expanded item under <object>.<viewKey>, so an object-scoped container is named crm_lead — a name with no dot to carry.
  • An overlay's name is stamped by the write path, not authored: normalizeViewMetadata puts it on every view body at the single write chokepoint, and a personalization PUT inherits the identity of the entry it shadows.

So both of the readings an author naturally forms are wrong:

  • "the dot is mandatory on every view row" — read off ViewItemNameSchema alone. It is not: overlay and container rows accept flat names, and rows in this repo legitimately use them (case_grid, cases, the container row crm_lead). Those rows are correct as written, not defects awaiting a dotted rewrite.
  • "flat names are fine generally" — read off one of those flat-named rows. It is not: put the same name on a standalone ViewItem record and it is refused, on the one field the flat-named row told you to fill.

This describes what the three shapes already do; it widens and narrows nothing. The one item-name grammar itself lives in shared/identifiers.zod.ts — grammar changes belong there, not here.

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

TypeScript Usage

import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, EmptyStateSchema, FormButtonConfigSchema, FormFieldSchema, FormSectionSchema, FormSelectOptionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, HttpMethodSubsetSchema, HttpRequestSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListMapConfigSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewItemWireSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui';
import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, EmptyState, FormButtonConfig, FormField, FormSection, FormSelectOption, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, HttpMethodSubset, HttpRequest, KanbanConfig, ListChartConfig, ListColumn, ListMapConfig, ListView, NavigationConfig, NavigationMode, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemName, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';

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

AddRecordConfig

Add record entry point configuration

Properties

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"

AppearanceConfig

Appearance and visualization configuration

Properties

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"])

CalendarConfig

Properties

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

ColumnPrefix

Compound-cell prefix configuration

Properties

PropertyTypeRequiredDescription
fieldstring✅Field whose value renders before the cell value
typeEnum<'badge' | 'text'>optional (default: "text")How the prefix value is rendered

ColumnSummary

Aggregation function for the column footer summary — and, on a grouped list view, the per-group header summary (server-side): count (COUNT(*), the group count), count_unique (count_distinct), sum, avg, min, max map onto the query AST's AggregationFunction; count_filled, count_empty, percent_filled, percent_empty derive from one COUNT(field) node (the non-null count) and the group count — count_filled = COUNT(field), count_empty = count − COUNT(field), percent_filled = COUNT(field) / count (0 when count is 0), percent_empty = 1 − percent_filled. Server-side "empty" is null on every face; the footer's client-side reading of empty strings and empty arrays as empty is the renderer's to converge

Allowed Values

  • none
  • count
  • count_empty
  • count_filled
  • count_unique
  • percent_empty
  • percent_filled
  • sum
  • avg
  • min
  • max

ColumnSummaryConfig

Column footer summary configuration

Properties

PropertyTypeRequiredDescription
typeEnum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | 'percent_empty' | 'percent_filled' | 'sum' | 'avg' | 'min' | 'max'>✅Aggregation function
fieldstringoptionalField to aggregate (defaults to the column field)

EmptyState

Properties

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.
PropertyTypeRequiredDescription
labelstring | Record<string, string>✅Text of the link — a string, or an inline locale map resolved against the display locale
urlstring✅Where the link goes — a target a type: 'url' action opens: an in-app path that starts with / or ./ (such as /docs/connect), or an address that opens with http://, https:// or mailto:. Any other scheme, a protocol-relative //host, a bare relative path and a tab, line feed or carriage return are refused at parse; a template that opens with ${…}, such as ${ctx.origin}/docs, is judged when it is clicked, after interpolation.

FormButtonConfig

Properties

PropertyTypeRequiredDescription
showbooleanoptionalWhether the button is rendered (renderer default applies when omitted)
labelstring | Record<string, string>optionalButton label (i18n-capable; renderer default when omitted)

FormField

Properties

PropertyTypeRequiredDescription
fieldstring✅Field name (snake_case)
typeEnum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | 'markdown' | 'html' | 'richtext' | 'number' | 'currency' | 'percent' | 'date' | … +35 more>optionalField type (auto-infers widget if omitted)
options{ label: string; value: string; description?: string; color?: string; … }[]optionalOptions for select/multiselect/radio/checkboxes fields (per-option default is not accepted here — declare the pre-selected choice on the object definition). On a metadata form (schema-bound, built by defineForm), an enum-typed row may list its members here, to give them human labels or to offer a deliberate subset. An option value is a lowercase system identifier, so a row whose members cannot be spelled as option values (a hyphen, a capital) omits options: the control derives the members from the served JSON Schema, and their meanings go in helpText.
referencestringoptionalTarget object name for lookup/master_detail fields
publicPickerneveroptional[REMOVED] view.form.sections[].fields[].publicPicker was removed in @objectstack/spec 17.6.0 (ADR-0087 D2) — an anonymous public form no longer offers record search: lookup, master_detail and user fields are always left off the anonymous rendering, and the anonymous record-search route (GET /forms/:slug/lookup/:field) no longer exists. Delete the key (the whole publicPicker block). To let a visitor choose from a fixed list, use a select field with static options; to let them pick an existing record, put the form behind sign-in. 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.
maxLengthintegeroptionalMaximum character length (positive integer; for text/textarea/email/url/phone)
minLengthintegeroptionalMinimum character length (positive integer; minLength: 0 is refused — express "no minimum" by omitting the key)
minnumberoptionalMinimum value (for number/currency/percent/slider)
maxnumberoptionalMaximum value
precisionintegeroptionalTotal digits (non-negative integer; for number/currency)
scaleintegeroptionalDecimal places (integer 0-100). The upper bound is the platform's, not a policy: renderers turn scale into fraction digits through toFixed and Intl.NumberFormat's maximumFractionDigits, both of which throw a RangeError above 100 — so a larger declaration is unrenderable by any conforming consumer.
multiplebooleanoptionalAllow multiple values (for select/lookup/file/image)
labelstring | Record<string, string>optionalDisplay label override
placeholderstring | Record<string, string>optionalPlaceholder text
helpTextstring | Record<string, string>optionalHelp/hint text
readonlybooleanoptionalRead-only override
immutablebooleanoptionalEditable on create, locked once the record exists (e.g. machine names).
requiredbooleanoptionalRequired override
hiddenbooleanoptionalHidden override
colSpanintegeroptionalAbsolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (colSpan: 4 in a 3-column grid renders as 3); a colSpan within the column count renders as authored, and colSpan: 1 emits no span class at all.
spanEnum<'auto' | 'full'>optional (default: "auto")Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide field:grid widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: the renderer emits one clamped col-span class per multi-column tier (@md:col-span-2 @2xl:col-span-3 for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest.
widgetstringoptionalCustom widget/component name (overrides type-based inference)
languagestringoptionalCode editor language (for type=code)
keyField{ field?: string; label?: string | Record<string, string>; placeholder?: string | Record<string, string>; helpText?: string | Record<string, string>; … }optionalKey column config for record-typed fields
dependsOnstringoptionalParent field name for cascading
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility predicate (CEL) — field shown only when TRUE. Root: record (+ previous, parent) in runtime forms, or data in metadata forms. current_user (and the ADR-0068 aliases user / ctx.user / os.user) resolves here — CLIENT-SIDE only: nothing server-side evaluates a form-view field visibleWhen, so a role test here hides the control and protects no data (declare permission-set field-level security for that), and on the public /f/:slug route no host publishes a scope, so the root is unbound and the predicate faults open. No features.* on ANY form-view predicate — refused at parse: the root is unbound on the standalone form routes (/forms/:name, /f/:slug) and the predicate would fault open there. Inside a repeater data is the ROW, but it is still spelled data — a bare identifier is unbound and faults open too. e.g. Precord.priority == 'urgent'
visibleOnstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optional[DEPRECATED → visibleWhen] Visibility predicate (CEL). Normalized to visibleWhen at parse.
disclosureEnum<'inline' | 'popover'>optionalComposite rendering: inline bordered box (default) or a summary line + gear popover (progressive disclosure).
fields[FormField](#formfield)[]optionalSub-fields for composite/repeater/record types

Allowed Values: FormField.type

  • text
  • textarea
  • email
  • url
  • phone
  • password
  • secret
  • markdown
  • html
  • richtext
  • number
  • currency
  • percent
  • date
  • datetime
  • time
  • boolean
  • toggle
  • select
  • multiselect
  • radio
  • checkboxes
  • lookup
  • master_detail
  • tree
  • user
  • image
  • file
  • avatar
  • video
  • audio
  • formula
  • summary
  • autonumber
  • composite
  • repeater
  • record
  • location
  • address
  • code
  • json
  • color
  • rating
  • slider
  • signature
  • qrcode
  • progress
  • tags
  • vector

Nested Shape: FormField.options[number]

Form-view select option — the object-field option shape minus the per-option default key (declare the pre-selected choice on the object definition: field-level defaultValue, or default: true on that field's own options entry).

PropertyTypeRequiredDescription
labelstring✅Display label (human-readable, any case allowed)
valuestring✅Stored value (lowercase machine identifier)
descriptionstringoptionalOptional secondary/help text for this option. Lookup option search matches it in addition to the label; renderers may show it as supporting text.
colorstringoptionalColor code for badges/charts
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalPer-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live record plus the host predicate scope, which binds current_user. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false, or cannot be evaluated — so a user-gated CHOICE belongs here. e.g. Precord.country == 'cn' or P'admin' in current_user.positions. On an OBJECT field's option it reads the record's OWN columns: the server never reads a related record there, so a read THROUGH a reference field (record.account.tier) would fault and every write picking the option would be refused, and objectstack validate refuses it — enforce such a restriction with a validations[] script rule, whose condition is read one hop through a reference.

Nested Shape: FormField.keyField

PropertyTypeRequiredDescription
fieldstringoptional (default: "name")Property name that holds the key inside each item (defaults to "name")
labelstring | Record<string, string>optionalDisplay label for the key column
placeholderstring | Record<string, string>optionalPlaceholder when entering a new key
helpTextstring | Record<string, string>optionalHelp text under the key input
regexstringoptionalJS regex source string the key must match (no flags)
immutablebooleanoptional (default: true)If true, the key is read-only after creation

FormSection

Properties

PropertyTypeRequiredDescription
namestringoptionalStable section identifier for i18n lookup (snake_case)
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
descriptionstringoptionalOptional description rendered under the section header.
collapsiblebooleanoptional (default: false)Whether the section renders a disclosure control, so a reader can close it and open it again. Default false: a section declaring neither collapse key is always open and shows no control. ⚠️ collapsed: true IMPLIES this key — an explicit collapsible: false beside it does NOT take the control away. The renderer resolves that from the DECLARATION; parse never rewrites the pair, so a parsed section still reports the false that was authored. Only true is refused on a wizard step and beside group; false is accepted in both, because it declares exactly what those surfaces already deliver.
collapsedbooleanoptional (default: false)Whether the section starts closed. Default false. ⚠️ collapsed: true IMPLIES collapsible and is sufficient ON ITS OWN — a section that starts closed always carries the disclosure control that reopens it, and it outranks an explicit collapsible: false, so writing collapsed: true alone is a correct way to say "collapsed by default". The implication is a renderer rule, never a parse-time rewrite: { collapsed: true } still parses to collapsible: false, collapsed: true, so the parsed collapsible must never be read as "a control renders". Only true is refused on a wizard step (steps do not collapse) and beside group, whose field group declares the pair.
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility predicate (CEL) — section shown only when TRUE. Root: record (+ previous, parent) in runtime forms, or data in metadata forms. current_user (and the ADR-0068 aliases user / ctx.user / os.user) resolves here too — CLIENT-SIDE only: nothing server-side evaluates a form-view section visibleWhen, so a role test here hides the controls and protects no data (declare permission-set field-level security for that), and on the public /f/:slug route no host publishes a scope, so the root is unbound and the predicate faults open. No features.* on ANY form-view predicate — refused at parse: unbound on the standalone form routes, where the predicate would fault open.
visibleOnstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optional[DEPRECATED → visibleWhen] Visibility predicate (CEL). Hides the whole section when false. Normalized to visibleWhen at parse.
columnsEnum<'1' | '2' | '3' | '4'> | 1 | 2 | 3 | 4optional (default: 1)
paneEnum<'primary' | 'secondary'>optionalSplit pane this section renders in (split forms only; a parse error elsewhere). Omitted → first section 'primary', others 'secondary'.
groupstringoptionalField group key (snake_case) whose members and presentation this section inherits, from the bound object's fieldGroups (ADR-0085 §5 deriveFieldGroupLayout). Mutually exclusive with fields, and with every key the group itself declares (name, label, description, collapsible, collapsed, visibleWhen/visibleOn). Not valid on a wizard step. Must name a declared group — checked by reference diagnostics.
fields(string | { field: string; type?: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; options?: object[]; reference?: string; … })[]optional

Nested Shape: FormSection.fields[number]

PropertyTypeRequiredDescription
fieldstring✅Field name (snake_case)
typeEnum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>optionalField type (auto-infers widget if omitted)
options{ label: string; value: string; description?: string; color?: string; … }[]optionalOptions for select/multiselect/radio/checkboxes fields (per-option default is not accepted here — declare the pre-selected choice on the object definition). On a metadata form (schema-bound, built by defineForm), an enum-typed row may list its members here, to give them human labels or to offer a deliberate subset. An option value is a lowercase system identifier, so a row whose members cannot be spelled as option values (a hyphen, a capital) omits options: the control derives the members from the served JSON Schema, and their meanings go in helpText.
referencestringoptionalTarget object name for lookup/master_detail fields
publicPickerneveroptional[REMOVED] view.form.sections[].fields[].publicPicker was removed in @objectstack/spec 17.6.0 (ADR-0087 D2) — an anonymous public form no longer offers record search: lookup, master_detail and user fields are always left off the anonymous rendering, and the anonymous record-search route (GET /forms/:slug/lookup/:field) no longer exists. Delete the key (the whole publicPicker block). To let a visitor choose from a fixed list, use a select field with static options; to let them pick an existing record, put the form behind sign-in. 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.
maxLengthintegeroptionalMaximum character length (positive integer; for text/textarea/email/url/phone)
minLengthintegeroptionalMinimum character length (positive integer; minLength: 0 is refused — express "no minimum" by omitting the key)
minnumberoptionalMinimum value (for number/currency/percent/slider)
maxnumberoptionalMaximum value
precisionintegeroptionalTotal digits (non-negative integer; for number/currency)
scaleintegeroptionalDecimal places (integer 0-100). The upper bound is the platform's, not a policy: renderers turn scale into fraction digits through toFixed and Intl.NumberFormat's maximumFractionDigits, both of which throw a RangeError above 100 — so a larger declaration is unrenderable by any conforming consumer.
multiplebooleanoptionalAllow multiple values (for select/lookup/file/image)
labelstring | Record<string, string>optionalDisplay label override
placeholderstring | Record<string, string>optionalPlaceholder text
helpTextstring | Record<string, string>optionalHelp/hint text
readonlybooleanoptionalRead-only override
immutablebooleanoptionalEditable on create, locked once the record exists (e.g. machine names).
requiredbooleanoptionalRequired override
hiddenbooleanoptionalHidden override
colSpanintegeroptionalAbsolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (colSpan: 4 in a 3-column grid renders as 3); a colSpan within the column count renders as authored, and colSpan: 1 emits no span class at all.
spanEnum<'auto' | 'full'>optional (default: "auto")Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide field:grid widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: the renderer emits one clamped col-span class per multi-column tier (@md:col-span-2 @2xl:col-span-3 for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest.
widgetstringoptionalCustom widget/component name (overrides type-based inference)
languagestringoptionalCode editor language (for type=code)
keyField{ field?: string; label?: string | Record<string, string>; placeholder?: string | Record<string, string>; helpText?: string | Record<string, string>; … }optionalKey column config for record-typed fields
dependsOnstringoptionalParent field name for cascading
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility predicate (CEL) — field shown only when TRUE. Root: record (+ previous, parent) in runtime forms, or data in metadata forms. current_user (and the ADR-0068 aliases user / ctx.user / os.user) resolves here — CLIENT-SIDE only: nothing server-side evaluates a form-view field visibleWhen, so a role test here hides the control and protects no data (declare permission-set field-level security for that), and on the public /f/:slug route no host publishes a scope, so the root is unbound and the predicate faults open. No features.* on ANY form-view predicate — refused at parse: the root is unbound on the standalone form routes (/forms/:name, /f/:slug) and the predicate would fault open there. Inside a repeater data is the ROW, but it is still spelled data — a bare identifier is unbound and faults open too. e.g. Precord.priority == 'urgent'
visibleOnstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optional[DEPRECATED → visibleWhen] Visibility predicate (CEL). Normalized to visibleWhen at parse.
disclosureEnum<'inline' | 'popover'>optionalComposite rendering: inline bordered box (default) or a summary line + gear popover (progressive disclosure).
fields{ field: string; type?: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; options?: object[]; reference?: string; … }[]optionalSub-fields for composite/repeater/record types

FormSelectOption

Form-view select option — the object-field option shape minus the per-option default key (declare the pre-selected choice on the object definition: field-level defaultValue, or default: true on that field's own options entry).

Properties

PropertyTypeRequiredDescription
labelstring✅Display label (human-readable, any case allowed)
valuestring✅Stored value (lowercase machine identifier)
descriptionstringoptionalOptional secondary/help text for this option. Lookup option search matches it in addition to the label; renderers may show it as supporting text.
colorstringoptionalColor code for badges/charts
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalPer-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live record plus the host predicate scope, which binds current_user. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false, or cannot be evaluated — so a user-gated CHOICE belongs here. e.g. Precord.country == 'cn' or P'admin' in current_user.positions. On an OBJECT field's option it reads the record's OWN columns: the server never reads a related record there, so a read THROUGH a reference field (record.account.tier) would fault and every write picking the option would be refused, and objectstack validate refuses it — enforce such a restriction with a validations[] script rule, whose condition is read one hop through a reference.

FormView

Properties

PropertyTypeRequiredDescription
typeEnum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>optional (default: "simple")
layoutEnum<'vertical' | 'horizontal'>optionalField layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set columns
columnsintegeroptionalNumber of columns for the form body
titlestringoptionalForm title
descriptionstringoptionalForm description
defaultTabstringoptionalInitially active tab (tabbed forms)
tabPositionEnum<'top' | 'bottom' | 'left' | 'right'>optionalTab strip position (tabbed forms)
allowSkipbooleanoptionalWizard step-gate opt-out: allow entering a later step without submitting the one before it. Navigation freedom, NOT a validation exemption — the final submit still re-validates every step and returns to the first failing one. Default (absent/false): steps unlock in array order as each prior step submits validly. UI admission only, never authorization.
showStepIndicatorbooleanoptionalShow the wizard step indicator (renderer default: shown). Step progress (completed/current/upcoming/invalid) is derived from the step gate — this boolean is the only authorable progress surface.
splitDirectionEnum<'horizontal' | 'vertical'>optionalSplit orientation (split forms)
splitSizenumberoptionalPrimary split panel size, % (split forms)
splitResizablebooleanoptionalWhether the split is resizable (split forms)
drawerSideEnum<'top' | 'bottom' | 'left' | 'right'>optionalDrawer side (drawer forms)
drawerWidthstringoptional[DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it.
modalSizeEnum<'sm' | 'default' | 'lg' | 'xl' | 'full'>optionalModal size (modal forms)
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
sections{ name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[]optional
groups{ name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[]optional[LEGACY ALIAS → sections] Accepted for back-compat and folded onto sections at parse; sections wins when both are present. Prefer sections.
subforms{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]optionalInline master-detail child collections
defaultSortneveroptional[REMOVED] form.defaultSort was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's sort. Delete the key and set the sort on the related list view instead. 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.
sharing{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }optionalPublic sharing configuration for this form
submitBehavior{ kind: 'thank-you'; title?: string; message?: string } | { kind: 'redirect'; url: string; delayMs?: integer } | { kind: 'continue' } | { kind: 'next-record' }optionalPost-submit behavior. On the redirect arm, url is relative-only and interpolates only declared record fields as {{record.field_name}}, URL-escaped.
buttons{ submit?: object; cancel?: object; reset?: object }optionalForm action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm.
defaultsRecord<string, any>optionalInitial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;).
arianeveroptional[REMOVED] form.aria was removed in @objectstack/spec 17.0.0 (audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. 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.

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

PropertyTypeRequiredDescription
provider'object'✅
objectstring✅Target object name

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

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

Nested Shape: FormView.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: FormView.sections[number]

PropertyTypeRequiredDescription
namestringoptionalStable section identifier for i18n lookup (snake_case)
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
descriptionstringoptionalOptional description rendered under the section header.
collapsiblebooleanoptional (default: false)Whether the section renders a disclosure control, so a reader can close it and open it again. Default false: a section declaring neither collapse key is always open and shows no control. ⚠️ collapsed: true IMPLIES this key — an explicit collapsible: false beside it does NOT take the control away. The renderer resolves that from the DECLARATION; parse never rewrites the pair, so a parsed section still reports the false that was authored. Only true is refused on a wizard step and beside group; false is accepted in both, because it declares exactly what those surfaces already deliver.
collapsedbooleanoptional (default: false)Whether the section starts closed. Default false. ⚠️ collapsed: true IMPLIES collapsible and is sufficient ON ITS OWN — a section that starts closed always carries the disclosure control that reopens it, and it outranks an explicit collapsible: false, so writing collapsed: true alone is a correct way to say "collapsed by default". The implication is a renderer rule, never a parse-time rewrite: { collapsed: true } still parses to collapsible: false, collapsed: true, so the parsed collapsible must never be read as "a control renders". Only true is refused on a wizard step (steps do not collapse) and beside group, whose field group declares the pair.
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility predicate (CEL) — section shown only when TRUE. Root: record (+ previous, parent) in runtime forms, or data in metadata forms. current_user (and the ADR-0068 aliases user / ctx.user / os.user) resolves here too — CLIENT-SIDE only: nothing server-side evaluates a form-view section visibleWhen, so a role test here hides the controls and protects no data (declare permission-set field-level security for that), and on the public /f/:slug route no host publishes a scope, so the root is unbound and the predicate faults open. No features.* on ANY form-view predicate — refused at parse: unbound on the standalone form routes, where the predicate would fault open.
visibleOnstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optional[DEPRECATED → visibleWhen] Visibility predicate (CEL). Hides the whole section when false. Normalized to visibleWhen at parse.
columnsEnum<'1' | '2' | '3' | '4'> | 1 | 2 | 3 | 4optional (default: 1)
paneEnum<'primary' | 'secondary'>optionalSplit pane this section renders in (split forms only; a parse error elsewhere). Omitted → first section 'primary', others 'secondary'.
groupstringoptionalField group key (snake_case) whose members and presentation this section inherits, from the bound object's fieldGroups (ADR-0085 §5 deriveFieldGroupLayout). Mutually exclusive with fields, and with every key the group itself declares (name, label, description, collapsible, collapsed, visibleWhen/visibleOn). Not valid on a wizard step. Must name a declared group — checked by reference diagnostics.
fields(string | { field: string; type?: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; options?: object[]; reference?: string; … })[]optional

Nested Shape: FormView.groups[number]

PropertyTypeRequiredDescription
namestringoptionalStable section identifier for i18n lookup (snake_case)
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
descriptionstringoptionalOptional description rendered under the section header.
collapsiblebooleanoptional (default: false)Whether the section renders a disclosure control, so a reader can close it and open it again. Default false: a section declaring neither collapse key is always open and shows no control. ⚠️ collapsed: true IMPLIES this key — an explicit collapsible: false beside it does NOT take the control away. The renderer resolves that from the DECLARATION; parse never rewrites the pair, so a parsed section still reports the false that was authored. Only true is refused on a wizard step and beside group; false is accepted in both, because it declares exactly what those surfaces already deliver.
collapsedbooleanoptional (default: false)Whether the section starts closed. Default false. ⚠️ collapsed: true IMPLIES collapsible and is sufficient ON ITS OWN — a section that starts closed always carries the disclosure control that reopens it, and it outranks an explicit collapsible: false, so writing collapsed: true alone is a correct way to say "collapsed by default". The implication is a renderer rule, never a parse-time rewrite: { collapsed: true } still parses to collapsible: false, collapsed: true, so the parsed collapsible must never be read as "a control renders". Only true is refused on a wizard step (steps do not collapse) and beside group, whose field group declares the pair.
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optionalVisibility predicate (CEL) — section shown only when TRUE. Root: record (+ previous, parent) in runtime forms, or data in metadata forms. current_user (and the ADR-0068 aliases user / ctx.user / os.user) resolves here too — CLIENT-SIDE only: nothing server-side evaluates a form-view section visibleWhen, so a role test here hides the controls and protects no data (declare permission-set field-level security for that), and on the public /f/:slug route no host publishes a scope, so the root is unbound and the predicate faults open. No features.* on ANY form-view predicate — refused at parse: unbound on the standalone form routes, where the predicate would fault open.
visibleOnstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object }optional[DEPRECATED → visibleWhen] Visibility predicate (CEL). Hides the whole section when false. Normalized to visibleWhen at parse.
columnsEnum<'1' | '2' | '3' | '4'> | 1 | 2 | 3 | 4optional (default: 1)
paneEnum<'primary' | 'secondary'>optionalSplit pane this section renders in (split forms only; a parse error elsewhere). Omitted → first section 'primary', others 'secondary'.
groupstringoptionalField group key (snake_case) whose members and presentation this section inherits, from the bound object's fieldGroups (ADR-0085 §5 deriveFieldGroupLayout). Mutually exclusive with fields, and with every key the group itself declares (name, label, description, collapsible, collapsed, visibleWhen/visibleOn). Not valid on a wizard step. Must name a declared group — checked by reference diagnostics.
fields(string | { field: string; type?: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; options?: object[]; reference?: string; … })[]optional

Nested Shape: FormView.subforms[number]

PropertyTypeRequiredDescription
childObjectstring✅Child object whose records are entered inline
relationshipFieldstringoptionalFK on the child pointing back to the parent (auto-detected 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.
amountFieldstringoptionalNumeric child column summed for the running total
totalFieldstringoptionalParent field to receive the rolled-up sum
titlestringoptionalSection title
addLabelstringoptionalAdd-row button label
minRowsnumberoptional
maxRowsnumberoptional

Nested Shape: FormView.sharing

PropertyTypeRequiredDescription
enabledbooleanoptional (default: false)Enable public sharing
publicLinkstringoptionalPublic slug of the form, chosen by the author: not a URL, and never generated. /forms/x, forms/x and x name one slug, x. It is served to anonymous visitors only while enabled and allowAnonymous are both true: the REST form door answers GET /api/v1/forms/:slug (default API base), and a host that serves the console shows the form at /_console/f/:slug and redirects /forms/:slug there, query string carried.
passwordstringoptionalPassword required to access shared link
allowedDomainsstring[]optionalRestrict access to specific email domains (e.g. ["example.com"])
expiresAtstringoptionalExpiration date/time in ISO 8601 format
allowAnonymousbooleanoptional (default: false)Allow access without authentication

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

PropertyTypeRequiredDescription
submit{ show?: boolean; label?: string | Record<string, string> }optionalSubmit button
cancel{ show?: boolean; label?: string | Record<string, string> }optionalCancel button
reset{ show?: boolean; label?: string | Record<string, string> }optionalReset button

GalleryConfig

Gallery/card view configuration

Properties

PropertyTypeRequiredDescription
coverFieldstringoptionalAttachment/image field to display as card cover
coverFitEnum<'cover' | 'contain'>optional (default: "cover")Image fit mode for card cover
cardSizeEnum<'small' | 'medium' | 'large'>optional (default: "medium")Card size in gallery view
titleFieldstringoptionalField to display as card title
visibleFieldsstring[]optionalFields to display on card body

GanttConfig

Properties

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: GanttConfig.quickFilters[number]

PropertyTypeRequiredDescription
fieldstring✅Record field / dot-path the dimension filters on
labelstringoptionalTrigger label (falls back to the field label)
options(string | { value: string | number; label?: string })[]optionalExplicit option override for fixed enums

Nested Shape: GanttConfig.interactions

PropertyTypeRequiredDescription
movebooleanoptionalBar and subtree dragging along the timeline (default true)
resizebooleanoptionalEdge resize grips that change a duration (default true)
progressbooleanoptionalThe progress drag handle (default true)
linkbooleanoptionalDependency UI — the drag-to-link dots and the create/delete menu entries (default true)

Nested Shape: GanttConfig.timeSegments

PropertyTypeRequiredDescription
dayStartstringoptionalClock time the shift-day begins, 'HH:mm' (24h); the day column starts here and runs 24h. Renderer default '00:00' (calendar day); for an 08:00 handover set '08:00'
bands{ key?: string; label: string; start: string; end: string; … }[]✅Ordered bands covering the 24h shift-day, beginning at dayStart
showMidnightbooleanoptionalDraw the dashed calendar-midnight (00:00) cue inside cross-midnight bands; renderer default true

GanttQuickFilter

Properties

PropertyTypeRequiredDescription
fieldstring✅Record field / dot-path the dimension filters on
labelstringoptionalTrigger label (falls back to the field label)
options(string | { value: string | number; label?: string })[]optionalExplicit option override for fixed enums

GroupingConfig

Record grouping configuration — SERVER-SIDE: the set of groups and every number in a group header (the count and the per-column summaries) are properties of the query, not of the fetched page, answered by one aggregate query (groupBy = the fields in nesting order, count + the mapped column summaries, the view filter); rows inside a group are paged by the existing find with the group key AND-ed into the view filter. Compiled by compileListViewGroupQuery / compileListViewGroupRowsQuery

Properties

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: GroupingConfig.fields[number]

PropertyTypeRequiredDescription
fieldstring✅Field name to group by — one groupBy column of the group header query; the header row carries its raw stored value (null for the empty group). NO leading or trailing whitespace: the name is compiled into the aggregate query verbatim, so a padded spelling buckets every row into one empty group instead of grouping them.
orderEnum<'asc' | 'desc'>optional (default: "asc")Group sort order — applied by the consumer over the header rows (the aggregate query carries no orderBy)
collapsedbooleanoptional (default: false)Collapse groups by default (presentation only)

GroupingField

Properties

PropertyTypeRequiredDescription
fieldstring✅Field name to group by — one groupBy column of the group header query; the header row carries its raw stored value (null for the empty group). NO leading or trailing whitespace: the name is compiled into the aggregate query verbatim, so a padded spelling buckets every row into one empty group instead of grouping them.
orderEnum<'asc' | 'desc'>optional (default: "asc")Group sort order — applied by the consumer over the header rows (the aggregate query carries no orderBy)
collapsedbooleanoptional (default: false)Collapse groups by default (presentation only)

HttpMethodSubset

HTTP methods a view data source may request — the subset of HttpMethod without HEAD/OPTIONS.

Allowed Values

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE

HttpRequest

Properties

PropertyTypeRequiredDescription
urlstring✅API endpoint URL
methodEnum<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>optional (default: "GET")HTTP method
headersRecord<string, string>optionalCustom HTTP headers
paramsRecord<string, any>optionalQuery parameters
bodyanyoptionalRequest body for POST/PUT/PATCH

KanbanConfig

Properties

PropertyTypeRequiredDescription
groupByFieldstring✅Field to group columns by (usually status/select). NO leading or trailing whitespace: the board reads this name off every card verbatim, so a padded spelling collapses the whole board into one Uncategorized lane.
summarizeFieldstringoptionalField to sum at top of column (e.g. amount)
titleFieldstringoptionalField displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain)
columnsstring[]✅Fields to show on cards

ListChartConfig

List chart view configuration

Properties

PropertyTypeRequiredDescription
chartTypeEnum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>optional (default: "bar")Chart visualisation type
datasetstring✅Dataset name to bind (ADR-0021)
dimensionsstring[]optionalDimension names — X/group/split
valuesstring[]✅Measure names — Y (at least one)

ListColumn

Properties

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' | 'percent_empty' | 'percent_filled' | 'sum' | 'avg' | 'min' | 'max'> | { 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: ListColumn.summary

Column footer summary configuration

PropertyTypeRequiredDescription
typeEnum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …>✅Aggregation function
fieldstringoptionalField to aggregate (defaults to the column field)

Nested Shape: ListColumn.prefix

PropertyTypeRequiredDescription
fieldstring✅Field whose value renders before the cell value
typeEnum<'badge' | 'text'>optional (default: "text")How the prefix value is rendered

ListMapConfig

Map view configuration

Properties

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

ListView

Properties

PropertyTypeRequiredDescription
namestringoptionalInternal view name (lowercase snake_case)
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
typeEnum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
columnsstring[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[]✅Fields to display as columns — the PROJECTION of the per-view field composition columns x hiddenFields x fieldOrder: this list is the candidate set AND the baseline order; hiddenFields subtracts from it and fieldOrder orders what survives. hiddenFields and fieldOrder cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's ListView grid derives the object's default columns).
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sort{ field: string; order: Enum<'asc' | 'desc'> }[]optional
searchableFieldsstring[]optionalFields enabled for search
filterableFieldsstring[]optionalLegacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters
userFilters{ element?: Enum<'dropdown' | 'tabs' | 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }optionalEnd-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields
resizablebooleanoptionalEnable column resizing
compactToolbarbooleanoptionalCollapse Group/Color/Density/Hide-fields into a single View settings popover
selection{ type?: Enum<'none' | 'single' | 'multiple'> }optionalRow selection configuration
navigation{ mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }optionalKanban-board configuration — applies when the view renders as a kanban layout
calendar{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }optionalCalendar configuration — applies when the view renders as a calendar layout
gantt{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }optionalGantt-timeline configuration — applies when the view renders as a gantt layout
gallery{ coverField?: string; coverFit?: Enum<'cover' | 'contain'>; cardSize?: Enum<'small' | 'medium' | 'large'>; titleField?: string; … }optionalGallery/card view configuration
timeline{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }optionalTimeline view configuration
chart{ chartType?: Enum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }optionalChart binding — applies when the view renders as a chart. A type: 'chart' view must bind one: it names the ADR-0021 dataset and the measures (values) the chart plots, and there is no default binding
map{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }optionalMap configuration — applies when the view renders as a map layout
tree{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNameneveroptional[REMOVED] view.pageName was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a type: 'page' view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — { type: 'page', pageName: '<page_name>' } under the app's navigation — which is a different key on a different surface and is the page mount that has always rendered. 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.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
requiredPermissionsstring[]optional[ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through systemPermissions, the same list as requiredPermissions on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object.
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema)
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view — the SUBTRACTION of the per-view field composition columns x hiddenFields x fieldOrder: each name listed here is removed from the columns projection before fieldOrder orders the remainder. A name columns never projected subtracts nothing.
fieldOrderstring[]optionalExplicit field display order for this view — the ORDERING of the per-view field composition columns x hiddenFields x fieldOrder: it sorts what survives columns minus hiddenFields and never adds a field. A surviving column absent from fieldOrder sorts LAST, after every listed one, keeping its columns-relative order; a name listed here that did not survive orders nothing.
rowActionsstring[]optionalActions available for individual row items
bulkActionsstring[]optionalActions available when multiple rows are selected
bulkActionDefs{ name: string; label?: string; icon?: string; variant?: Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'>; … }[]optionalRich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a patch / 'delete') that no action expresses, or for an operation: 'custom' + execution: 'aggregate' entry that dispatches the action it NAMES once for the whole selection — the renderer injects params._selectedIds: string[] (read that on the server, not recordId) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. batchSize does not apply (the call is never chunked); set maxRecords on defs whose server work is expensive. For the PER-RECORD dispatch use bulkActions: ['<name>'] instead — the bare-string form, promoted with the action's own label, params and visible; a 'custom' def without execution: 'aggregate' has no dispatcher and is refused at parse time. Toolbar url/api actions can also interpolate the current selection via ${ctx.selection.ids} / ${ctx.selection.count}.
conditionalFormatting{ condition: string | object; style: Record<string, string> }[]optionalConditional formatting rules for list rows
inlineEditbooleanoptionalAllow inline editing of records directly in the list view
exportOptionsEnum<'csv' | 'xlsx' | 'json'>[] | { formats?: Enum<'csv' | 'xlsx' | 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }optionalExport configuration for the list toolbar export menu: { formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }. A bare format array is the legacy spelling and lifts to { formats: [...] } at parse.
userActions{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }optionalUser action toggles for the view toolbar
appearance{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[] }optionalAppearance and visualization configuration
tabsneveroptional[REMOVED] view.list.tabs was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's listViews instead: the tab's name becomes the entry's key, its label the entry's label, and its filter rules join the view's own filter on that entry (copy the view's columns too); a tab whose view already named a list view needs nothing more. Every listViews entry renders as a tab in the switcher. 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.
addRecord{ enabled?: boolean; position?: Enum<'top' | 'bottom' | 'both'>; mode?: Enum<'inline' | 'form' | 'modal'>; formView?: string }optionalAdd record entry point configuration
showRecordCountbooleanoptionalShow record count at the bottom of the list
allowPrintingbooleanoptionalAllow users to print the view
emptyState{ title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object }optionalEmpty state configuration when no records found
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes for the list view
responsiveneveroptional[REMOVED] view.responsive was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. 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.
performanceneveroptional[REMOVED] view.performance was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. 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.
stripedneveroptional[REMOVED] view.striped was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. 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.
borderedneveroptional[REMOVED] view.bordered was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. 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.
virtualScrollneveroptional[REMOVED] view.virtualScroll was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via pagination. 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.

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

PropertyTypeRequiredDescription
provider'object'✅
objectstring✅Target object name

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

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

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

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

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

PropertyTypeRequiredDescription
pageSizeintegeroptional (default: 50)Number of records per page. On a view with no pager (kanban, gallery, timeline) it is the fetch ceiling, and the renderer owes two things: bound its fetch at this number, and, when the filtered set is larger than it, show a visible truncation signal saying what is on screen is not the whole set
pageSizeOptionsinteger[]optionalAvailable page size options

Nested Shape: ListView.kanban

PropertyTypeRequiredDescription
groupByFieldstring✅Field to group columns by (usually status/select). NO leading or trailing whitespace: the board reads this name off every card verbatim, so a padded spelling collapses the whole board into one Uncategorized lane.
summarizeFieldstringoptionalField to sum at top of column (e.g. amount)
titleFieldstringoptionalField displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain)
columnsstring[]✅Fields to show on cards

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

PropertyTypeRequiredDescription
coverFieldstringoptionalAttachment/image field to display as card cover
coverFitEnum<'cover' | 'contain'>optional (default: "cover")Image fit mode for card cover
cardSizeEnum<'small' | 'medium' | 'large'>optional (default: "medium")Card size in gallery view
titleFieldstringoptionalField to display as card title
visibleFieldsstring[]optionalFields to display on card body

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

PropertyTypeRequiredDescription
chartTypeEnum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>optional (default: "bar")Chart visualisation type
datasetstring✅Dataset name to bind (ADR-0021)
dimensionsstring[]optionalDimension names — X/group/split
valuesstring[]✅Measure names — Y (at least one)

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

PropertyTypeRequiredDescription
typeEnum<'personal' | 'collaborative'>optional (default: "collaborative")View ownership type
lockedBystringoptionalUser who locked the view configuration

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

Properties

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.

Allowed Values

  • page
  • drawer
  • modal
  • split
  • popover
  • new_window
  • none

ObjectListView

Properties

PropertyTypeRequiredDescription
namestringoptionalInternal view name (lowercase snake_case)
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
typeEnum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
columnsstring[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[]✅Fields to display as columns — the PROJECTION of the per-view field composition columns x hiddenFields x fieldOrder: this list is the candidate set AND the baseline order; hiddenFields subtracts from it and fieldOrder orders what survives. hiddenFields and fieldOrder cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's ListView grid derives the object's default columns).
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sort{ field: string; order: Enum<'asc' | 'desc'> }[]optional
searchableFieldsstring[]optionalFields enabled for search
filterableFieldsstring[]optionalLegacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters
resizablebooleanoptionalEnable column resizing
compactToolbarbooleanoptionalCollapse Group/Color/Density/Hide-fields into a single View settings popover
selection{ type?: Enum<'none' | 'single' | 'multiple'> }optionalRow selection configuration
navigation{ mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }optionalKanban-board configuration — applies when the view renders as a kanban layout
calendar{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }optionalCalendar configuration — applies when the view renders as a calendar layout
gantt{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }optionalGantt-timeline configuration — applies when the view renders as a gantt layout
gallery{ coverField?: string; coverFit?: Enum<'cover' | 'contain'>; cardSize?: Enum<'small' | 'medium' | 'large'>; titleField?: string; … }optionalGallery/card view configuration
timeline{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }optionalTimeline view configuration
chart{ chartType?: Enum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }optionalChart binding — applies when the view renders as a chart. A type: 'chart' view must bind one: it names the ADR-0021 dataset and the measures (values) the chart plots, and there is no default binding
map{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }optionalMap configuration — applies when the view renders as a map layout
tree{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNameneveroptional[REMOVED] view.pageName was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a type: 'page' view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — { type: 'page', pageName: '<page_name>' } under the app's navigation — which is a different key on a different surface and is the page mount that has always rendered. 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.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
requiredPermissionsstring[]optional[ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through systemPermissions, the same list as requiredPermissions on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object.
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema)
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view — the SUBTRACTION of the per-view field composition columns x hiddenFields x fieldOrder: each name listed here is removed from the columns projection before fieldOrder orders the remainder. A name columns never projected subtracts nothing.
fieldOrderstring[]optionalExplicit field display order for this view — the ORDERING of the per-view field composition columns x hiddenFields x fieldOrder: it sorts what survives columns minus hiddenFields and never adds a field. A surviving column absent from fieldOrder sorts LAST, after every listed one, keeping its columns-relative order; a name listed here that did not survive orders nothing.
rowActionsstring[]optionalActions available for individual row items
bulkActionsstring[]optionalActions available when multiple rows are selected
bulkActionDefs{ name: string; label?: string; icon?: string; variant?: Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'>; … }[]optionalRich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a patch / 'delete') that no action expresses, or for an operation: 'custom' + execution: 'aggregate' entry that dispatches the action it NAMES once for the whole selection — the renderer injects params._selectedIds: string[] (read that on the server, not recordId) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. batchSize does not apply (the call is never chunked); set maxRecords on defs whose server work is expensive. For the PER-RECORD dispatch use bulkActions: ['<name>'] instead — the bare-string form, promoted with the action's own label, params and visible; a 'custom' def without execution: 'aggregate' has no dispatcher and is refused at parse time. Toolbar url/api actions can also interpolate the current selection via ${ctx.selection.ids} / ${ctx.selection.count}.
conditionalFormatting{ condition: string | object; style: Record<string, string> }[]optionalConditional formatting rules for list rows
inlineEditbooleanoptionalAllow inline editing of records directly in the list view
exportOptionsEnum<'csv' | 'xlsx' | 'json'>[] | { formats?: Enum<'csv' | 'xlsx' | 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }optionalExport configuration for the list toolbar export menu: { formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }. A bare format array is the legacy spelling and lifts to { formats: [...] } at parse.
userActions{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }optionalUser action toggles for the view toolbar
appearance{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[] }optionalAppearance and visualization configuration
tabsneveroptional[REMOVED] view.list.tabs was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's listViews instead: the tab's name becomes the entry's key, its label the entry's label, and its filter rules join the view's own filter on that entry (copy the view's columns too); a tab whose view already named a list view needs nothing more. Every listViews entry renders as a tab in the switcher. 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.
addRecord{ enabled?: boolean; position?: Enum<'top' | 'bottom' | 'both'>; mode?: Enum<'inline' | 'form' | 'modal'>; formView?: string }optionalAdd record entry point configuration
showRecordCountbooleanoptionalShow record count at the bottom of the list
allowPrintingbooleanoptionalAllow users to print the view
emptyState{ title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object }optionalEmpty state configuration when no records found
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes for the list view
responsiveneveroptional[REMOVED] view.responsive was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. 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.
performanceneveroptional[REMOVED] view.performance was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. 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.
stripedneveroptional[REMOVED] view.striped was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. 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.
borderedneveroptional[REMOVED] view.bordered was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. 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.
virtualScrollneveroptional[REMOVED] view.virtualScroll was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via pagination. 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.
userFilters{ element?: Enum<'dropdown' | 'toggle'>; fields?: object[] }optional

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

PropertyTypeRequiredDescription
provider'object'✅
objectstring✅Target object name

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

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

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

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

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

PropertyTypeRequiredDescription
pageSizeintegeroptional (default: 50)Number of records per page. On a view with no pager (kanban, gallery, timeline) it is the fetch ceiling, and the renderer owes two things: bound its fetch at this number, and, when the filtered set is larger than it, show a visible truncation signal saying what is on screen is not the whole set
pageSizeOptionsinteger[]optionalAvailable page size options

Nested Shape: ObjectListView.kanban

PropertyTypeRequiredDescription
groupByFieldstring✅Field to group columns by (usually status/select). NO leading or trailing whitespace: the board reads this name off every card verbatim, so a padded spelling collapses the whole board into one Uncategorized lane.
summarizeFieldstringoptionalField to sum at top of column (e.g. amount)
titleFieldstringoptionalField displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain)
columnsstring[]✅Fields to show on cards

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

PropertyTypeRequiredDescription
coverFieldstringoptionalAttachment/image field to display as card cover
coverFitEnum<'cover' | 'contain'>optional (default: "cover")Image fit mode for card cover
cardSizeEnum<'small' | 'medium' | 'large'>optional (default: "medium")Card size in gallery view
titleFieldstringoptionalField to display as card title
visibleFieldsstring[]optionalFields to display on card body

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

PropertyTypeRequiredDescription
chartTypeEnum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>optional (default: "bar")Chart visualisation type
datasetstring✅Dataset name to bind (ADR-0021)
dimensionsstring[]optionalDimension names — X/group/split
valuesstring[]✅Measure names — Y (at least one)

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

PropertyTypeRequiredDescription
typeEnum<'personal' | 'collaborative'>optional (default: "collaborative")View ownership type
lockedBystringoptionalUser who locked the view configuration

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

PropertyTypeRequiredDescription
elementEnum<'dropdown' | 'toggle'>optional (default: "dropdown")Filter control style on object views: "dropdown" (per-field value chips). "toggle" is deprecated. "tabs" is page-only — use listViews for named presets.
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)

ObjectUserFilters

Properties

PropertyTypeRequiredDescription
elementEnum<'dropdown' | 'toggle'>optional (default: "dropdown")Filter control style on object views: "dropdown" (per-field value chips). "toggle" is deprecated. "tabs" is page-only — use listViews for named presets.
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)

Nested Shape: ObjectUserFilters.fields[number]

Quick-filter field configuration

PropertyTypeRequiredDescription
fieldstring✅Field name on the source object (must exist — checked by reference diagnostics)
labelstring | Record<string, string>optionalDisplay label override (defaults to the field label)
typeEnum<'select' | 'multi-select' | 'boolean' | 'date-range' | 'text'>optionalFilter control type. Omit to infer from the field definition
options{ value: string | number | boolean; label: string | Record<string, string>; color?: string }[]optionalStatic options. Omit to derive from the field definition (select options / lookup records)
showCountbooleanoptionalShow per-option record counts
defaultValues(string | number | boolean)[]optionalPre-selected values when the view loads

PaginationConfig

Properties

PropertyTypeRequiredDescription
pageSizeintegeroptional (default: 50)Number of records per page. On a view with no pager (kanban, gallery, timeline) it is the fetch ceiling, and the renderer owes two things: bound its fetch at this number, and, when the filtered set is larger than it, show a visible truncation signal saying what is on screen is not the whole set
pageSizeOptionsinteger[]optionalAvailable page size options

RowColorConfig

Row color configuration based on field values

Properties

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.

RowHeight

Row height / density setting for list view

Allowed Values

  • compact
  • short
  • medium
  • tall
  • extra_tall

SelectionConfig

Properties

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

TimelineConfig

Timeline view configuration

Properties

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

TreeConfig

Properties

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)

UserActionsConfig

User action toggles for the view toolbar

Properties

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

UserFilterField

Quick-filter field configuration

Properties

PropertyTypeRequiredDescription
fieldstring✅Field name on the source object (must exist — checked by reference diagnostics)
labelstring | Record<string, string>optionalDisplay label override (defaults to the field label)
typeEnum<'select' | 'multi-select' | 'boolean' | 'date-range' | 'text'>optionalFilter control type. Omit to infer from the field definition
options{ value: string | number | boolean; label: string | Record<string, string>; color?: string }[]optionalStatic options. Omit to derive from the field definition (select options / lookup records)
showCountbooleanoptionalShow per-option record counts
defaultValues(string | number | boolean)[]optionalPre-selected values when the view loads

Nested Shape: UserFilterField.options[number]

PropertyTypeRequiredDescription
valuestring | number | boolean✅Option value
labelstring | Record<string, string>✅Option label
colorstringoptionalOption color token/hex

UserFilters

End-user quick-filter configuration (Airtable "User filters" parity)

Properties

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: UserFilters.fields[number]

Quick-filter field configuration

PropertyTypeRequiredDescription
fieldstring✅Field name on the source object (must exist — checked by reference diagnostics)
labelstring | Record<string, string>optionalDisplay label override (defaults to the field label)
typeEnum<'select' | 'multi-select' | 'boolean' | 'date-range' | 'text'>optionalFilter control type. Omit to infer from the field definition
options{ value: string | number | boolean; label: string | Record<string, string>; color?: string }[]optionalStatic options. Omit to derive from the field definition (select options / lookup records)
showCountbooleanoptionalShow per-option record counts
defaultValues(string | number | boolean)[]optionalPre-selected values when the view loads

Nested Shape: UserFilters.tabs[number]

Tab configuration for multi-tab view interface

PropertyTypeRequiredDescription
namestring✅Tab identifier (snake_case)
labelstring | Record<string, string>optionalDisplay label
iconstringoptionalTab icon name
viewstringoptionalReferenced list view name from listViews
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalTab-specific filter criteria
orderintegeroptionalTab display order
pinnedbooleanoptional (default: false)Pin tab (cannot be removed by users)
isDefaultbooleanoptional (default: false)Set as the default active tab
visiblebooleanoptional (default: true)Tab visibility

View

Properties

PropertyTypeRequiredDescription
namestringoptionalItem name — supplied by the metadata door; for an object-scoped container it is the object name.
labelstring | Record<string, string>optionalHuman-readable label shown in metadata lists.
objectstringoptionalObject this container binds to — how a stack-level views: [...] entry says which object its views belong to; read by getViewsByObject() / GET /meta/view?object=.
list{ name?: string; label?: string | Record<string, string>; type?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>; data?: object | object | object; … }optional
form{ type?: Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>; layout?: Enum<'vertical' | 'horizontal'>; columns?: integer; title?: string; … }optional
listViewsRecord<string, { name?: string; label?: string | Record<string, string>; type?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>; data?: object | object | object; … }>optionalAdditional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047)
formViewsRecord<string, { type?: Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>; layout?: Enum<'vertical' | 'horizontal'>; columns?: integer; title?: string; … }>optionalAdditional named form views
protection{ lock: Enum<'none' | 'no-overlay' | 'no-delete' | 'full'>; reason: string; docsUrl?: string }optionalPackage author protection block — lock policy for this view.
_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: View.list

PropertyTypeRequiredDescription
namestringoptionalInternal view name (lowercase snake_case)
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
typeEnum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
columnsstring[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[]✅Fields to display as columns — the PROJECTION of the per-view field composition columns x hiddenFields x fieldOrder: this list is the candidate set AND the baseline order; hiddenFields subtracts from it and fieldOrder orders what survives. hiddenFields and fieldOrder cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's ListView grid derives the object's default columns).
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sort{ field: string; order: Enum<'asc' | 'desc'> }[]optional
searchableFieldsstring[]optionalFields enabled for search
filterableFieldsstring[]optionalLegacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters
resizablebooleanoptionalEnable column resizing
compactToolbarbooleanoptionalCollapse Group/Color/Density/Hide-fields into a single View settings popover
selection{ type?: Enum<'none' | 'single' | 'multiple'> }optionalRow selection configuration
navigation{ mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }optionalKanban-board configuration — applies when the view renders as a kanban layout
calendar{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }optionalCalendar configuration — applies when the view renders as a calendar layout
gantt{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }optionalGantt-timeline configuration — applies when the view renders as a gantt layout
gallery{ coverField?: string; coverFit?: Enum<'cover' | 'contain'>; cardSize?: Enum<'small' | 'medium' | 'large'>; titleField?: string; … }optionalGallery/card view configuration
timeline{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }optionalTimeline view configuration
chart{ chartType?: Enum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }optionalChart binding — applies when the view renders as a chart. A type: 'chart' view must bind one: it names the ADR-0021 dataset and the measures (values) the chart plots, and there is no default binding
map{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }optionalMap configuration — applies when the view renders as a map layout
tree{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNameneveroptional[REMOVED] view.pageName was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a type: 'page' view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — { type: 'page', pageName: '<page_name>' } under the app's navigation — which is a different key on a different surface and is the page mount that has always rendered. 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.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
requiredPermissionsstring[]optional[ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through systemPermissions, the same list as requiredPermissions on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object.
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema)
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view — the SUBTRACTION of the per-view field composition columns x hiddenFields x fieldOrder: each name listed here is removed from the columns projection before fieldOrder orders the remainder. A name columns never projected subtracts nothing.
fieldOrderstring[]optionalExplicit field display order for this view — the ORDERING of the per-view field composition columns x hiddenFields x fieldOrder: it sorts what survives columns minus hiddenFields and never adds a field. A surviving column absent from fieldOrder sorts LAST, after every listed one, keeping its columns-relative order; a name listed here that did not survive orders nothing.
rowActionsstring[]optionalActions available for individual row items
bulkActionsstring[]optionalActions available when multiple rows are selected
bulkActionDefs{ name: string; label?: string; icon?: string; variant?: Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'>; … }[]optionalRich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a patch / 'delete') that no action expresses, or for an operation: 'custom' + execution: 'aggregate' entry that dispatches the action it NAMES once for the whole selection — the renderer injects params._selectedIds: string[] (read that on the server, not recordId) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. batchSize does not apply (the call is never chunked); set maxRecords on defs whose server work is expensive. For the PER-RECORD dispatch use bulkActions: ['<name>'] instead — the bare-string form, promoted with the action's own label, params and visible; a 'custom' def without execution: 'aggregate' has no dispatcher and is refused at parse time. Toolbar url/api actions can also interpolate the current selection via ${ctx.selection.ids} / ${ctx.selection.count}.
conditionalFormatting{ condition: string | object; style: Record<string, string> }[]optionalConditional formatting rules for list rows
inlineEditbooleanoptionalAllow inline editing of records directly in the list view
exportOptionsEnum<'csv' | 'xlsx' | 'json'>[] | { formats?: Enum<'csv' | 'xlsx' | 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }optionalExport configuration for the list toolbar export menu: { formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }. A bare format array is the legacy spelling and lifts to { formats: [...] } at parse.
userActions{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }optionalUser action toggles for the view toolbar
appearance{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[] }optionalAppearance and visualization configuration
tabsneveroptional[REMOVED] view.list.tabs was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's listViews instead: the tab's name becomes the entry's key, its label the entry's label, and its filter rules join the view's own filter on that entry (copy the view's columns too); a tab whose view already named a list view needs nothing more. Every listViews entry renders as a tab in the switcher. 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.
addRecord{ enabled?: boolean; position?: Enum<'top' | 'bottom' | 'both'>; mode?: Enum<'inline' | 'form' | 'modal'>; formView?: string }optionalAdd record entry point configuration
showRecordCountbooleanoptionalShow record count at the bottom of the list
allowPrintingbooleanoptionalAllow users to print the view
emptyState{ title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object }optionalEmpty state configuration when no records found
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes for the list view
responsiveneveroptional[REMOVED] view.responsive was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. 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.
performanceneveroptional[REMOVED] view.performance was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. 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.
stripedneveroptional[REMOVED] view.striped was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. 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.
borderedneveroptional[REMOVED] view.bordered was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. 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.
virtualScrollneveroptional[REMOVED] view.virtualScroll was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via pagination. 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.
userFilters{ element?: Enum<'dropdown' | 'toggle'>; fields?: object[] }optional

Nested Shape: View.form

PropertyTypeRequiredDescription
typeEnum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>optional (default: "simple")
layoutEnum<'vertical' | 'horizontal'>optionalField layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set columns
columnsintegeroptionalNumber of columns for the form body
titlestringoptionalForm title
descriptionstringoptionalForm description
defaultTabstringoptionalInitially active tab (tabbed forms)
tabPositionEnum<'top' | 'bottom' | 'left' | 'right'>optionalTab strip position (tabbed forms)
allowSkipbooleanoptionalWizard step-gate opt-out: allow entering a later step without submitting the one before it. Navigation freedom, NOT a validation exemption — the final submit still re-validates every step and returns to the first failing one. Default (absent/false): steps unlock in array order as each prior step submits validly. UI admission only, never authorization.
showStepIndicatorbooleanoptionalShow the wizard step indicator (renderer default: shown). Step progress (completed/current/upcoming/invalid) is derived from the step gate — this boolean is the only authorable progress surface.
splitDirectionEnum<'horizontal' | 'vertical'>optionalSplit orientation (split forms)
splitSizenumberoptionalPrimary split panel size, % (split forms)
splitResizablebooleanoptionalWhether the split is resizable (split forms)
drawerSideEnum<'top' | 'bottom' | 'left' | 'right'>optionalDrawer side (drawer forms)
drawerWidthstringoptional[DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it.
modalSizeEnum<'sm' | 'default' | 'lg' | 'xl' | 'full'>optionalModal size (modal forms)
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
sections{ name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[]optional
groups{ name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[]optional[LEGACY ALIAS → sections] Accepted for back-compat and folded onto sections at parse; sections wins when both are present. Prefer sections.
subforms{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]optionalInline master-detail child collections
defaultSortneveroptional[REMOVED] form.defaultSort was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's sort. Delete the key and set the sort on the related list view instead. 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.
sharing{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }optionalPublic sharing configuration for this form
submitBehavior{ kind: 'thank-you'; title?: string; message?: string } | { kind: 'redirect'; url: string; delayMs?: integer } | { kind: 'continue' } | { kind: 'next-record' }optionalPost-submit behavior. On the redirect arm, url is relative-only and interpolates only declared record fields as {{record.field_name}}, URL-escaped.
buttons{ submit?: object; cancel?: object; reset?: object }optionalForm action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm.
defaultsRecord<string, any>optionalInitial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;).
arianeveroptional[REMOVED] form.aria was removed in @objectstack/spec 17.0.0 (audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. 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.

Nested Shape: View.listViews[string]

PropertyTypeRequiredDescription
namestringoptionalInternal view name (lowercase snake_case)
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
typeEnum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
columnsstring[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[]✅Fields to display as columns — the PROJECTION of the per-view field composition columns x hiddenFields x fieldOrder: this list is the candidate set AND the baseline order; hiddenFields subtracts from it and fieldOrder orders what survives. hiddenFields and fieldOrder cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's ListView grid derives the object's default columns).
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sort{ field: string; order: Enum<'asc' | 'desc'> }[]optional
searchableFieldsstring[]optionalFields enabled for search
filterableFieldsstring[]optionalLegacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters
resizablebooleanoptionalEnable column resizing
compactToolbarbooleanoptionalCollapse Group/Color/Density/Hide-fields into a single View settings popover
selection{ type?: Enum<'none' | 'single' | 'multiple'> }optionalRow selection configuration
navigation{ mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }optionalKanban-board configuration — applies when the view renders as a kanban layout
calendar{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }optionalCalendar configuration — applies when the view renders as a calendar layout
gantt{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }optionalGantt-timeline configuration — applies when the view renders as a gantt layout
gallery{ coverField?: string; coverFit?: Enum<'cover' | 'contain'>; cardSize?: Enum<'small' | 'medium' | 'large'>; titleField?: string; … }optionalGallery/card view configuration
timeline{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }optionalTimeline view configuration
chart{ chartType?: Enum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }optionalChart binding — applies when the view renders as a chart. A type: 'chart' view must bind one: it names the ADR-0021 dataset and the measures (values) the chart plots, and there is no default binding
map{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }optionalMap configuration — applies when the view renders as a map layout
tree{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNameneveroptional[REMOVED] view.pageName was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a type: 'page' view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — { type: 'page', pageName: '<page_name>' } under the app's navigation — which is a different key on a different surface and is the page mount that has always rendered. 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.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
requiredPermissionsstring[]optional[ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through systemPermissions, the same list as requiredPermissions on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object.
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema)
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view — the SUBTRACTION of the per-view field composition columns x hiddenFields x fieldOrder: each name listed here is removed from the columns projection before fieldOrder orders the remainder. A name columns never projected subtracts nothing.
fieldOrderstring[]optionalExplicit field display order for this view — the ORDERING of the per-view field composition columns x hiddenFields x fieldOrder: it sorts what survives columns minus hiddenFields and never adds a field. A surviving column absent from fieldOrder sorts LAST, after every listed one, keeping its columns-relative order; a name listed here that did not survive orders nothing.
rowActionsstring[]optionalActions available for individual row items
bulkActionsstring[]optionalActions available when multiple rows are selected
bulkActionDefs{ name: string; label?: string; icon?: string; variant?: Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'>; … }[]optionalRich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a patch / 'delete') that no action expresses, or for an operation: 'custom' + execution: 'aggregate' entry that dispatches the action it NAMES once for the whole selection — the renderer injects params._selectedIds: string[] (read that on the server, not recordId) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. batchSize does not apply (the call is never chunked); set maxRecords on defs whose server work is expensive. For the PER-RECORD dispatch use bulkActions: ['<name>'] instead — the bare-string form, promoted with the action's own label, params and visible; a 'custom' def without execution: 'aggregate' has no dispatcher and is refused at parse time. Toolbar url/api actions can also interpolate the current selection via ${ctx.selection.ids} / ${ctx.selection.count}.
conditionalFormatting{ condition: string | object; style: Record<string, string> }[]optionalConditional formatting rules for list rows
inlineEditbooleanoptionalAllow inline editing of records directly in the list view
exportOptionsEnum<'csv' | 'xlsx' | 'json'>[] | { formats?: Enum<'csv' | 'xlsx' | 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }optionalExport configuration for the list toolbar export menu: { formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }. A bare format array is the legacy spelling and lifts to { formats: [...] } at parse.
userActions{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }optionalUser action toggles for the view toolbar
appearance{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[] }optionalAppearance and visualization configuration
tabsneveroptional[REMOVED] view.list.tabs was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's listViews instead: the tab's name becomes the entry's key, its label the entry's label, and its filter rules join the view's own filter on that entry (copy the view's columns too); a tab whose view already named a list view needs nothing more. Every listViews entry renders as a tab in the switcher. 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.
addRecord{ enabled?: boolean; position?: Enum<'top' | 'bottom' | 'both'>; mode?: Enum<'inline' | 'form' | 'modal'>; formView?: string }optionalAdd record entry point configuration
showRecordCountbooleanoptionalShow record count at the bottom of the list
allowPrintingbooleanoptionalAllow users to print the view
emptyState{ title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object }optionalEmpty state configuration when no records found
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes for the list view
responsiveneveroptional[REMOVED] view.responsive was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. 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.
performanceneveroptional[REMOVED] view.performance was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. 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.
stripedneveroptional[REMOVED] view.striped was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. 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.
borderedneveroptional[REMOVED] view.bordered was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. 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.
virtualScrollneveroptional[REMOVED] view.virtualScroll was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via pagination. 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.
userFilters{ element?: Enum<'dropdown' | 'toggle'>; fields?: object[] }optional

Nested Shape: View.formViews[string]

PropertyTypeRequiredDescription
typeEnum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>optional (default: "simple")
layoutEnum<'vertical' | 'horizontal'>optionalField layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set columns
columnsintegeroptionalNumber of columns for the form body
titlestringoptionalForm title
descriptionstringoptionalForm description
defaultTabstringoptionalInitially active tab (tabbed forms)
tabPositionEnum<'top' | 'bottom' | 'left' | 'right'>optionalTab strip position (tabbed forms)
allowSkipbooleanoptionalWizard step-gate opt-out: allow entering a later step without submitting the one before it. Navigation freedom, NOT a validation exemption — the final submit still re-validates every step and returns to the first failing one. Default (absent/false): steps unlock in array order as each prior step submits validly. UI admission only, never authorization.
showStepIndicatorbooleanoptionalShow the wizard step indicator (renderer default: shown). Step progress (completed/current/upcoming/invalid) is derived from the step gate — this boolean is the only authorable progress surface.
splitDirectionEnum<'horizontal' | 'vertical'>optionalSplit orientation (split forms)
splitSizenumberoptionalPrimary split panel size, % (split forms)
splitResizablebooleanoptionalWhether the split is resizable (split forms)
drawerSideEnum<'top' | 'bottom' | 'left' | 'right'>optionalDrawer side (drawer forms)
drawerWidthstringoptional[DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it.
modalSizeEnum<'sm' | 'default' | 'lg' | 'xl' | 'full'>optionalModal size (modal forms)
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
sections{ name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[]optional
groups{ name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[]optional[LEGACY ALIAS → sections] Accepted for back-compat and folded onto sections at parse; sections wins when both are present. Prefer sections.
subforms{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]optionalInline master-detail child collections
defaultSortneveroptional[REMOVED] form.defaultSort was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's sort. Delete the key and set the sort on the related list view instead. 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.
sharing{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }optionalPublic sharing configuration for this form
submitBehavior{ kind: 'thank-you'; title?: string; message?: string } | { kind: 'redirect'; url: string; delayMs?: integer } | { kind: 'continue' } | { kind: 'next-record' }optionalPost-submit behavior. On the redirect arm, url is relative-only and interpolates only declared record fields as {{record.field_name}}, URL-escaped.
buttons{ submit?: object; cancel?: object; reset?: object }optionalForm action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm.
defaultsRecord<string, any>optionalInitial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;).
arianeveroptional[REMOVED] form.aria was removed in @objectstack/spec 17.0.0 (audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. 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.

Nested Shape: View.protection

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>✅Lock policy — none | no-overlay | no-delete | full.
reasonstring✅User-visible reason shown when the lock blocks an action.
docsUrlstringoptionalOptional URL the Studio banner links to for more context.

ViewData

Union Options

This schema accepts one of the following structures:

Option 1

Properties

PropertyTypeRequiredDescription
provider'object'✅
objectstring✅Target object name

Option 2

Properties

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

Option 3

Properties

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.


ViewFilterRule

View filter rule

Properties

PropertyTypeRequiredDescription
fieldstring✅Field name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | 'starts_with' | 'ends_with' | 'greater_than' | 'less_than' | 'greater_than_or_equal' | … +10 more>✅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.

Allowed Values: ViewFilterRule.operator

  • equals
  • not_equals
  • contains
  • not_contains
  • icontains
  • starts_with
  • ends_with
  • greater_than
  • less_than
  • greater_than_or_equal
  • less_than_or_equal
  • in
  • not_in
  • is_empty
  • is_not_empty
  • is_null
  • is_not_null
  • before
  • after
  • between

ViewItem

Union Options

This schema accepts one of the following structures:

Option 1

Properties

PropertyTypeRequiredDescription
viewKind'list'✅
config{ name?: string; label?: string | Record<string, string>; type?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>; data?: object | object | object; … }✅List-family view configuration.
namestring✅Globally-unique view id, <object>.<viewKey>.
objectstring✅Bound object name — the foreign key used to aggregate views.
labelstring | Record<string, string>optionalDisplay label (supports i18n).
isDefaultbooleanoptionalWhether this is the object's default view in the switcher.
orderintegeroptionalSort order within the object's view switcher / left rail.
scopeEnum<'package' | 'shared' | 'personal'>optionalIdentity layer (defaults to package for source-loaded views).
ownerneveroptional[REMOVED] view.owner was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a personal view item belonged to, and nothing ever read it: the view switcher (GET /meta/view?object=) serves every item bound to the object without looking at owner, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. 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.
hiddenneveroptional[REMOVED] view.hidden was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: GET /meta/view?object= and the console's view switcher list every item bound to the object, hidden: true included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). 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.
protection{ lock: Enum<'none' | 'no-overlay' | 'no-delete' | 'full'>; reason: string; docsUrl?: string }optionalPackage author protection block — lock policy for this view.
_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: ViewItem[viewKind='list'].config

PropertyTypeRequiredDescription
namestringoptionalInternal view name (lowercase snake_case)
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
typeEnum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
columnsstring[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[]✅Fields to display as columns — the PROJECTION of the per-view field composition columns x hiddenFields x fieldOrder: this list is the candidate set AND the baseline order; hiddenFields subtracts from it and fieldOrder orders what survives. hiddenFields and fieldOrder cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's ListView grid derives the object's default columns).
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sort{ field: string; order: Enum<'asc' | 'desc'> }[]optional
searchableFieldsstring[]optionalFields enabled for search
filterableFieldsstring[]optionalLegacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters
userFilters{ element?: Enum<'dropdown' | 'tabs' | 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }optionalEnd-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields
resizablebooleanoptionalEnable column resizing
compactToolbarbooleanoptionalCollapse Group/Color/Density/Hide-fields into a single View settings popover
selection{ type?: Enum<'none' | 'single' | 'multiple'> }optionalRow selection configuration
navigation{ mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }optionalKanban-board configuration — applies when the view renders as a kanban layout
calendar{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }optionalCalendar configuration — applies when the view renders as a calendar layout
gantt{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }optionalGantt-timeline configuration — applies when the view renders as a gantt layout
gallery{ coverField?: string; coverFit?: Enum<'cover' | 'contain'>; cardSize?: Enum<'small' | 'medium' | 'large'>; titleField?: string; … }optionalGallery/card view configuration
timeline{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }optionalTimeline view configuration
chart{ chartType?: Enum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }optionalChart binding — applies when the view renders as a chart. A type: 'chart' view must bind one: it names the ADR-0021 dataset and the measures (values) the chart plots, and there is no default binding
map{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }optionalMap configuration — applies when the view renders as a map layout
tree{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNameneveroptional[REMOVED] view.pageName was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a type: 'page' view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — { type: 'page', pageName: '<page_name>' } under the app's navigation — which is a different key on a different surface and is the page mount that has always rendered. 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.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
requiredPermissionsstring[]optional[ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through systemPermissions, the same list as requiredPermissions on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object.
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema)
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view — the SUBTRACTION of the per-view field composition columns x hiddenFields x fieldOrder: each name listed here is removed from the columns projection before fieldOrder orders the remainder. A name columns never projected subtracts nothing.
fieldOrderstring[]optionalExplicit field display order for this view — the ORDERING of the per-view field composition columns x hiddenFields x fieldOrder: it sorts what survives columns minus hiddenFields and never adds a field. A surviving column absent from fieldOrder sorts LAST, after every listed one, keeping its columns-relative order; a name listed here that did not survive orders nothing.
rowActionsstring[]optionalActions available for individual row items
bulkActionsstring[]optionalActions available when multiple rows are selected
bulkActionDefs{ name: string; label?: string; icon?: string; variant?: Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'>; … }[]optionalRich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a patch / 'delete') that no action expresses, or for an operation: 'custom' + execution: 'aggregate' entry that dispatches the action it NAMES once for the whole selection — the renderer injects params._selectedIds: string[] (read that on the server, not recordId) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. batchSize does not apply (the call is never chunked); set maxRecords on defs whose server work is expensive. For the PER-RECORD dispatch use bulkActions: ['<name>'] instead — the bare-string form, promoted with the action's own label, params and visible; a 'custom' def without execution: 'aggregate' has no dispatcher and is refused at parse time. Toolbar url/api actions can also interpolate the current selection via ${ctx.selection.ids} / ${ctx.selection.count}.
conditionalFormatting{ condition: string | object; style: Record<string, string> }[]optionalConditional formatting rules for list rows
inlineEditbooleanoptionalAllow inline editing of records directly in the list view
exportOptionsEnum<'csv' | 'xlsx' | 'json'>[] | { formats?: Enum<'csv' | 'xlsx' | 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }optionalExport configuration for the list toolbar export menu: { formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }. A bare format array is the legacy spelling and lifts to { formats: [...] } at parse.
userActions{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }optionalUser action toggles for the view toolbar
appearance{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[] }optionalAppearance and visualization configuration
tabsneveroptional[REMOVED] view.list.tabs was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's listViews instead: the tab's name becomes the entry's key, its label the entry's label, and its filter rules join the view's own filter on that entry (copy the view's columns too); a tab whose view already named a list view needs nothing more. Every listViews entry renders as a tab in the switcher. 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.
addRecord{ enabled?: boolean; position?: Enum<'top' | 'bottom' | 'both'>; mode?: Enum<'inline' | 'form' | 'modal'>; formView?: string }optionalAdd record entry point configuration
showRecordCountbooleanoptionalShow record count at the bottom of the list
allowPrintingbooleanoptionalAllow users to print the view
emptyState{ title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object }optionalEmpty state configuration when no records found
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes for the list view
responsiveneveroptional[REMOVED] view.responsive was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. 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.
performanceneveroptional[REMOVED] view.performance was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. 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.
stripedneveroptional[REMOVED] view.striped was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. 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.
borderedneveroptional[REMOVED] view.bordered was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. 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.
virtualScrollneveroptional[REMOVED] view.virtualScroll was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via pagination. 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.

Nested Shape: ViewItem[viewKind='list'].protection

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>✅Lock policy — none | no-overlay | no-delete | full.
reasonstring✅User-visible reason shown when the lock blocks an action.
docsUrlstringoptionalOptional URL the Studio banner links to for more context.

Option 2

Properties

PropertyTypeRequiredDescription
viewKind'form'✅
config{ type?: Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>; layout?: Enum<'vertical' | 'horizontal'>; columns?: integer; title?: string; … }✅Form view configuration.
namestring✅Globally-unique view id, <object>.<viewKey>.
objectstring✅Bound object name — the foreign key used to aggregate views.
labelstring | Record<string, string>optionalDisplay label (supports i18n).
isDefaultbooleanoptionalWhether this is the object's default view in the switcher.
orderintegeroptionalSort order within the object's view switcher / left rail.
scopeEnum<'package' | 'shared' | 'personal'>optionalIdentity layer (defaults to package for source-loaded views).
ownerneveroptional[REMOVED] view.owner was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a personal view item belonged to, and nothing ever read it: the view switcher (GET /meta/view?object=) serves every item bound to the object without looking at owner, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. 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.
hiddenneveroptional[REMOVED] view.hidden was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: GET /meta/view?object= and the console's view switcher list every item bound to the object, hidden: true included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). 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.
protection{ lock: Enum<'none' | 'no-overlay' | 'no-delete' | 'full'>; reason: string; docsUrl?: string }optionalPackage author protection block — lock policy for this view.
_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: ViewItem[viewKind='form'].config

PropertyTypeRequiredDescription
typeEnum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>optional (default: "simple")
layoutEnum<'vertical' | 'horizontal'>optionalField layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set columns
columnsintegeroptionalNumber of columns for the form body
titlestringoptionalForm title
descriptionstringoptionalForm description
defaultTabstringoptionalInitially active tab (tabbed forms)
tabPositionEnum<'top' | 'bottom' | 'left' | 'right'>optionalTab strip position (tabbed forms)
allowSkipbooleanoptionalWizard step-gate opt-out: allow entering a later step without submitting the one before it. Navigation freedom, NOT a validation exemption — the final submit still re-validates every step and returns to the first failing one. Default (absent/false): steps unlock in array order as each prior step submits validly. UI admission only, never authorization.
showStepIndicatorbooleanoptionalShow the wizard step indicator (renderer default: shown). Step progress (completed/current/upcoming/invalid) is derived from the step gate — this boolean is the only authorable progress surface.
splitDirectionEnum<'horizontal' | 'vertical'>optionalSplit orientation (split forms)
splitSizenumberoptionalPrimary split panel size, % (split forms)
splitResizablebooleanoptionalWhether the split is resizable (split forms)
drawerSideEnum<'top' | 'bottom' | 'left' | 'right'>optionalDrawer side (drawer forms)
drawerWidthstringoptional[DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it.
modalSizeEnum<'sm' | 'default' | 'lg' | 'xl' | 'full'>optionalModal size (modal forms)
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
sections{ name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[]optional
groups{ name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[]optional[LEGACY ALIAS → sections] Accepted for back-compat and folded onto sections at parse; sections wins when both are present. Prefer sections.
subforms{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]optionalInline master-detail child collections
defaultSortneveroptional[REMOVED] form.defaultSort was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's sort. Delete the key and set the sort on the related list view instead. 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.
sharing{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }optionalPublic sharing configuration for this form
submitBehavior{ kind: 'thank-you'; title?: string; message?: string } | { kind: 'redirect'; url: string; delayMs?: integer } | { kind: 'continue' } | { kind: 'next-record' }optionalPost-submit behavior. On the redirect arm, url is relative-only and interpolates only declared record fields as {{record.field_name}}, URL-escaped.
buttons{ submit?: object; cancel?: object; reset?: object }optionalForm action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm.
defaultsRecord<string, any>optionalInitial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;).
arianeveroptional[REMOVED] form.aria was removed in @objectstack/spec 17.0.0 (audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. 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.

Nested Shape: ViewItem[viewKind='form'].protection

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>✅Lock policy — none | no-overlay | no-delete | full.
reasonstring✅User-visible reason shown when the lock blocks an action.
docsUrlstringoptionalOptional URL the Studio banner links to for more context.


ViewItemName

Globally-unique view id, <object>.<viewKey>.

Type: string


ViewItemWire

Union Options

This schema accepts one of the following structures:

Option 1

Properties

PropertyTypeRequiredDescription
viewKind'list'✅
config{ name?: string; label?: string | Record<string, string>; type?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>; data?: object | object | object; … }✅List-family view configuration.
namestring✅Globally-unique view id, <object>.<viewKey>.
objectstring✅Bound object name — the foreign key used to aggregate views.
labelstring | Record<string, string>optionalDisplay label (supports i18n).
isDefaultbooleanoptionalWhether this is the object's default view in the switcher.
orderintegeroptionalSort order within the object's view switcher / left rail.
scopeEnum<'package' | 'shared' | 'personal'>optionalIdentity layer (defaults to package for source-loaded views).
ownerneveroptional[REMOVED] view.owner was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a personal view item belonged to, and nothing ever read it: the view switcher (GET /meta/view?object=) serves every item bound to the object without looking at owner, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. 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.
hiddenneveroptional[REMOVED] view.hidden was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: GET /meta/view?object= and the console's view switcher list every item bound to the object, hidden: true included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). 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.
protection{ lock: Enum<'none' | 'no-overlay' | 'no-delete' | 'full'>; reason: string; docsUrl?: string }optionalPackage author protection block — lock policy for this view.
_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.
isPinnedbooleanoptionalConsole round-trip: the view is pinned in the object's view switcher. Written by the console's pin toggle through the view metadata API and read back to draw the pinned group. Stored on the view's row, which has no per-user scope. Not authored.
sortOrderintegeroptionalConsole round-trip: the view's position among the object's saved views in the switcher, 0-based and counted over saved views only (a code-defined view carries none). Written by the console's drag-reorder and read back to order the tabs. Not authored: order is the authored default position.
visibilityEnum<'private' | 'team' | 'organization' | 'public'>optionalConsole round-trip: the group the switcher files this view's tab under (private, team, organization or public). Display grouping only, NOT access control: nothing restricts who can list or open the view by this value. No console control sets it; the console carries a stored value forward when it re-saves the row. Not authored.
columnState{ order?: string[]; widths?: Record<string, number> }optionalStudio round-trip: column order/widths (runtime-only state, written by the console grid and stored on the view's row, which has no per-user scope — not authored)
optionsneveroptionalRefused: a view item record carries no top-level options bag. Its per-kind blocks live under config (config.kanban, config.timeline, …); the legacy options.KIND bag belongs to the flattened list overlay only.

Nested Shape: ViewItemWire[viewKind='list'].config

PropertyTypeRequiredDescription
namestringoptionalInternal view name (lowercase snake_case)
labelstring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
typeEnum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
columnsstring[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[]✅Fields to display as columns — the PROJECTION of the per-view field composition columns x hiddenFields x fieldOrder: this list is the candidate set AND the baseline order; hiddenFields subtracts from it and fieldOrder orders what survives. hiddenFields and fieldOrder cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's ListView grid derives the object's default columns).
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sort{ field: string; order: Enum<'asc' | 'desc'> }[]optional
searchableFieldsstring[]optionalFields enabled for search
filterableFieldsstring[]optionalLegacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters
userFilters{ element?: Enum<'dropdown' | 'tabs' | 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }optionalEnd-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields
resizablebooleanoptionalEnable column resizing
compactToolbarbooleanoptionalCollapse Group/Color/Density/Hide-fields into a single View settings popover
selection{ type?: Enum<'none' | 'single' | 'multiple'> }optionalRow selection configuration
navigation{ mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }optionalKanban-board configuration — applies when the view renders as a kanban layout
calendar{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }optionalCalendar configuration — applies when the view renders as a calendar layout
gantt{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }optionalGantt-timeline configuration — applies when the view renders as a gantt layout
gallery{ coverField?: string; coverFit?: Enum<'cover' | 'contain'>; cardSize?: Enum<'small' | 'medium' | 'large'>; titleField?: string; … }optionalGallery/card view configuration
timeline{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }optionalTimeline view configuration
chart{ chartType?: Enum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }optionalChart binding — applies when the view renders as a chart. A type: 'chart' view must bind one: it names the ADR-0021 dataset and the measures (values) the chart plots, and there is no default binding
map{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }optionalMap configuration — applies when the view renders as a map layout
tree{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNameneveroptional[REMOVED] view.pageName was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a type: 'page' view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — { type: 'page', pageName: '<page_name>' } under the app's navigation — which is a different key on a different surface and is the page mount that has always rendered. 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.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
requiredPermissionsstring[]optional[ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through systemPermissions, the same list as requiredPermissions on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object.
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema)
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view — the SUBTRACTION of the per-view field composition columns x hiddenFields x fieldOrder: each name listed here is removed from the columns projection before fieldOrder orders the remainder. A name columns never projected subtracts nothing.
fieldOrderstring[]optionalExplicit field display order for this view — the ORDERING of the per-view field composition columns x hiddenFields x fieldOrder: it sorts what survives columns minus hiddenFields and never adds a field. A surviving column absent from fieldOrder sorts LAST, after every listed one, keeping its columns-relative order; a name listed here that did not survive orders nothing.
rowActionsstring[]optionalActions available for individual row items
bulkActionsstring[]optionalActions available when multiple rows are selected
bulkActionDefs{ name: string; label?: string; icon?: string; variant?: Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'>; … }[]optionalRich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a patch / 'delete') that no action expresses, or for an operation: 'custom' + execution: 'aggregate' entry that dispatches the action it NAMES once for the whole selection — the renderer injects params._selectedIds: string[] (read that on the server, not recordId) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. batchSize does not apply (the call is never chunked); set maxRecords on defs whose server work is expensive. For the PER-RECORD dispatch use bulkActions: ['<name>'] instead — the bare-string form, promoted with the action's own label, params and visible; a 'custom' def without execution: 'aggregate' has no dispatcher and is refused at parse time. Toolbar url/api actions can also interpolate the current selection via ${ctx.selection.ids} / ${ctx.selection.count}.
conditionalFormatting{ condition: string | object; style: Record<string, string> }[]optionalConditional formatting rules for list rows
inlineEditbooleanoptionalAllow inline editing of records directly in the list view
exportOptionsEnum<'csv' | 'xlsx' | 'json'>[] | { formats?: Enum<'csv' | 'xlsx' | 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }optionalExport configuration for the list toolbar export menu: { formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }. A bare format array is the legacy spelling and lifts to { formats: [...] } at parse.
userActions{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }optionalUser action toggles for the view toolbar
appearance{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[] }optionalAppearance and visualization configuration
tabsneveroptional[REMOVED] view.list.tabs was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's listViews instead: the tab's name becomes the entry's key, its label the entry's label, and its filter rules join the view's own filter on that entry (copy the view's columns too); a tab whose view already named a list view needs nothing more. Every listViews entry renders as a tab in the switcher. 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.
addRecord{ enabled?: boolean; position?: Enum<'top' | 'bottom' | 'both'>; mode?: Enum<'inline' | 'form' | 'modal'>; formView?: string }optionalAdd record entry point configuration
showRecordCountbooleanoptionalShow record count at the bottom of the list
allowPrintingbooleanoptionalAllow users to print the view
emptyState{ title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object }optionalEmpty state configuration when no records found
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes for the list view
responsiveneveroptional[REMOVED] view.responsive was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. 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.
performanceneveroptional[REMOVED] view.performance was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. 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.
stripedneveroptional[REMOVED] view.striped was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. 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.
borderedneveroptional[REMOVED] view.bordered was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. 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.
virtualScrollneveroptional[REMOVED] view.virtualScroll was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via pagination. 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.

Nested Shape: ViewItemWire[viewKind='list'].protection

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>✅Lock policy — none | no-overlay | no-delete | full.
reasonstring✅User-visible reason shown when the lock blocks an action.
docsUrlstringoptionalOptional URL the Studio banner links to for more context.

Nested Shape: ViewItemWire[viewKind='list'].columnState

PropertyTypeRequiredDescription
orderstring[]optionalColumn order as field names, leftmost first (runtime-only state — written by the console grid, never authored).
widthsRecord<string, number>optionalColumn widths in pixels, keyed by field name (runtime-only state — written by the console grid, never authored).

Option 2

Properties

PropertyTypeRequiredDescription
viewKind'form'✅
config{ type?: Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>; layout?: Enum<'vertical' | 'horizontal'>; columns?: integer; title?: string; … }✅Form view configuration.
namestring✅Globally-unique view id, <object>.<viewKey>.
objectstring✅Bound object name — the foreign key used to aggregate views.
labelstring | Record<string, string>optionalDisplay label (supports i18n).
isDefaultbooleanoptionalWhether this is the object's default view in the switcher.
orderintegeroptionalSort order within the object's view switcher / left rail.
scopeEnum<'package' | 'shared' | 'personal'>optionalIdentity layer (defaults to package for source-loaded views).
ownerneveroptional[REMOVED] view.owner was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the user a personal view item belonged to, and nothing ever read it: the view switcher (GET /meta/view?object=) serves every item bound to the object without looking at owner, so a view marked as one user's was listed for every user who can read the object. Delete the key. Nothing restricts a view item to one user today — per-user view scoping is a parked direction (ADR-0017), not a shipped mechanism — so a view item is visible to everyone who can read its object. 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.
hiddenneveroptional[REMOVED] view.hidden was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it promised to hide a view item from the switcher, and nothing ever read it: GET /meta/view?object= and the console's view switcher list every item bound to the object, hidden: true included. Delete the key; to take a view out of the switcher, delete the view item itself (or stop shipping it from source). 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.
protection{ lock: Enum<'none' | 'no-overlay' | 'no-delete' | 'full'>; reason: string; docsUrl?: string }optionalPackage author protection block — lock policy for this view.
_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.
isPinnedbooleanoptionalConsole round-trip: the view is pinned in the object's view switcher. Written by the console's pin toggle through the view metadata API and read back to draw the pinned group. Stored on the view's row, which has no per-user scope. Not authored.
sortOrderintegeroptionalConsole round-trip: the view's position among the object's saved views in the switcher, 0-based and counted over saved views only (a code-defined view carries none). Written by the console's drag-reorder and read back to order the tabs. Not authored: order is the authored default position.
visibilityEnum<'private' | 'team' | 'organization' | 'public'>optionalConsole round-trip: the group the switcher files this view's tab under (private, team, organization or public). Display grouping only, NOT access control: nothing restricts who can list or open the view by this value. No console control sets it; the console carries a stored value forward when it re-saves the row. Not authored.
columnState{ order?: string[]; widths?: Record<string, number> }optionalStudio round-trip: column order/widths (runtime-only state, written by the console grid and stored on the view's row, which has no per-user scope — not authored)
optionsneveroptionalRefused: a view item record carries no top-level options bag. Its per-kind blocks live under config (config.kanban, config.timeline, …); the legacy options.KIND bag belongs to the flattened list overlay only.

Nested Shape: ViewItemWire[viewKind='form'].config

PropertyTypeRequiredDescription
typeEnum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>optional (default: "simple")
layoutEnum<'vertical' | 'horizontal'>optionalField layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set columns
columnsintegeroptionalNumber of columns for the form body
titlestringoptionalForm title
descriptionstringoptionalForm description
defaultTabstringoptionalInitially active tab (tabbed forms)
tabPositionEnum<'top' | 'bottom' | 'left' | 'right'>optionalTab strip position (tabbed forms)
allowSkipbooleanoptionalWizard step-gate opt-out: allow entering a later step without submitting the one before it. Navigation freedom, NOT a validation exemption — the final submit still re-validates every step and returns to the first failing one. Default (absent/false): steps unlock in array order as each prior step submits validly. UI admission only, never authorization.
showStepIndicatorbooleanoptionalShow the wizard step indicator (renderer default: shown). Step progress (completed/current/upcoming/invalid) is derived from the step gate — this boolean is the only authorable progress surface.
splitDirectionEnum<'horizontal' | 'vertical'>optionalSplit orientation (split forms)
splitSizenumberoptionalPrimary split panel size, % (split forms)
splitResizablebooleanoptionalWhether the split is resizable (split forms)
drawerSideEnum<'top' | 'bottom' | 'left' | 'right'>optionalDrawer side (drawer forms)
drawerWidthstringoptional[DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it.
modalSizeEnum<'sm' | 'default' | 'lg' | 'xl' | 'full'>optionalModal size (modal forms)
data{ provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> }optionalData source configuration (defaults to "object" provider)
sections{ name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[]optional
groups{ name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[]optional[LEGACY ALIAS → sections] Accepted for back-compat and folded onto sections at parse; sections wins when both are present. Prefer sections.
subforms{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]optionalInline master-detail child collections
defaultSortneveroptional[REMOVED] form.defaultSort was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's sort. Delete the key and set the sort on the related list view instead. 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.
sharing{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }optionalPublic sharing configuration for this form
submitBehavior{ kind: 'thank-you'; title?: string; message?: string } | { kind: 'redirect'; url: string; delayMs?: integer } | { kind: 'continue' } | { kind: 'next-record' }optionalPost-submit behavior. On the redirect arm, url is relative-only and interpolates only declared record fields as {{record.field_name}}, URL-escaped.
buttons{ submit?: object; cancel?: object; reset?: object }optionalForm action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm.
defaultsRecord<string, any>optionalInitial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;).
arianeveroptional[REMOVED] form.aria was removed in @objectstack/spec 17.0.0 (audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. 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.

Nested Shape: ViewItemWire[viewKind='form'].protection

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>✅Lock policy — none | no-overlay | no-delete | full.
reasonstring✅User-visible reason shown when the lock blocks an action.
docsUrlstringoptionalOptional URL the Studio banner links to for more context.

Nested Shape: ViewItemWire[viewKind='form'].columnState

PropertyTypeRequiredDescription
orderstring[]optionalColumn order as field names, leftmost first (runtime-only state — written by the console grid, never authored).
widthsRecord<string, number>optionalColumn widths in pixels, keyed by field name (runtime-only state — written by the console grid, never authored).


ViewKind

Whether config is a ListView (list family) or a FormView.

Allowed Values

  • list
  • form

ViewScope

View identity layer: package | shared | personal.

Allowed Values

  • package
  • shared
  • personal

ViewSharing

View sharing and access configuration

Properties

PropertyTypeRequiredDescription
typeEnum<'personal' | 'collaborative'>optional (default: "collaborative")View ownership type
lockedBystringoptionalUser who locked the view configuration

ViewTab

Tab configuration for multi-tab view interface

Properties

PropertyTypeRequiredDescription
namestring✅Tab identifier (snake_case)
labelstring | Record<string, string>optionalDisplay label
iconstringoptionalTab icon name
viewstringoptionalReferenced list view name from listViews
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalTab-specific filter criteria
orderintegeroptionalTab display order
pinnedbooleanoptional (default: false)Pin tab (cannot be removed by users)
isDefaultbooleanoptional (default: false)Set as the default active tab
visiblebooleanoptional (default: true)Tab visibility

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

VisualizationType

Visualization type that users can switch to

Allowed Values

  • grid
  • kanban
  • gallery
  • calendar
  • timeline
  • gantt
  • map
  • chart
  • tree

On this page

Name grammar depends on the body spellingTypeScript UsageAddRecordConfigPropertiesAppearanceConfigPropertiesCalendarConfigPropertiesColumnPrefixPropertiesColumnSummaryAllowed ValuesColumnSummaryConfigPropertiesEmptyStatePropertiesNested Shape: EmptyState.linkFormButtonConfigPropertiesFormFieldPropertiesAllowed Values: FormField.typeNested Shape: FormField.options[number]Nested Shape: FormField.keyFieldFormSectionPropertiesNested Shape: FormSection.fields[number]FormSelectOptionPropertiesFormViewPropertiesNested Shape: FormView.data[provider='object']Nested Shape: FormView.data[provider='value']Nested Shape: FormView.data[provider='schema']Nested Shape: FormView.sections[number]Nested Shape: FormView.groups[number]Nested Shape: FormView.subforms[number]Nested Shape: FormView.sharingNested Shape: FormView.submitBehavior[kind='redirect']Nested Shape: FormView.buttonsGalleryConfigPropertiesGanttConfigPropertiesNested Shape: GanttConfig.quickFilters[number]Nested Shape: GanttConfig.interactionsNested Shape: GanttConfig.timeSegmentsGanttQuickFilterPropertiesGroupingConfigPropertiesNested Shape: GroupingConfig.fields[number]GroupingFieldPropertiesHttpMethodSubsetAllowed ValuesHttpRequestPropertiesKanbanConfigPropertiesListChartConfigPropertiesListColumnPropertiesNested Shape: ListColumn.summaryNested Shape: ListColumn.prefixListMapConfigPropertiesListViewPropertiesNested Shape: ListView.data[provider='object']Nested Shape: ListView.data[provider='value']Nested Shape: ListView.data[provider='schema']Nested Shape: ListView.columns[number]Nested Shape: ListView.filter[number]Nested Shape: ListView.userFiltersNested Shape: ListView.selectionNested Shape: ListView.navigationNested Shape: ListView.paginationNested Shape: ListView.kanbanNested Shape: ListView.calendarNested Shape: ListView.ganttNested Shape: ListView.galleryNested Shape: ListView.timelineNested Shape: ListView.chartNested Shape: ListView.mapNested Shape: ListView.treeNested Shape: ListView.sharingNested Shape: ListView.groupingNested Shape: ListView.rowColorNested Shape: ListView.bulkActionDefs[number]Nested Shape: ListView.conditionalFormatting[number]Nested Shape: ListView.exportOptionsNested Shape: ListView.userActionsNested Shape: ListView.appearanceNested Shape: ListView.addRecordNested Shape: ListView.emptyStateNested Shape: ListView.ariaNavigationConfigPropertiesNavigationModeAllowed ValuesObjectListViewPropertiesNested Shape: ObjectListView.data[provider='object']Nested Shape: ObjectListView.data[provider='value']Nested Shape: ObjectListView.data[provider='schema']Nested Shape: ObjectListView.columns[number]Nested Shape: ObjectListView.filter[number]Nested Shape: ObjectListView.selectionNested Shape: ObjectListView.navigationNested Shape: ObjectListView.paginationNested Shape: ObjectListView.kanbanNested Shape: ObjectListView.calendarNested Shape: ObjectListView.ganttNested Shape: ObjectListView.galleryNested Shape: ObjectListView.timelineNested Shape: ObjectListView.chartNested Shape: ObjectListView.mapNested Shape: ObjectListView.treeNested Shape: ObjectListView.sharingNested Shape: ObjectListView.groupingNested Shape: ObjectListView.rowColorNested Shape: ObjectListView.bulkActionDefs[number]Nested Shape: ObjectListView.conditionalFormatting[number]Nested Shape: ObjectListView.exportOptionsNested Shape: ObjectListView.userActionsNested Shape: ObjectListView.appearanceNested Shape: ObjectListView.addRecordNested Shape: ObjectListView.emptyStateNested Shape: ObjectListView.ariaNested Shape: ObjectListView.userFiltersObjectUserFiltersPropertiesNested Shape: ObjectUserFilters.fields[number]PaginationConfigPropertiesRowColorConfigPropertiesRowHeightAllowed ValuesSelectionConfigPropertiesTimelineConfigPropertiesTreeConfigPropertiesUserActionsConfigPropertiesUserFilterFieldPropertiesNested Shape: UserFilterField.options[number]UserFiltersPropertiesNested Shape: UserFilters.fields[number]Nested Shape: UserFilters.tabs[number]ViewPropertiesNested Shape: View.listNested Shape: View.formNested Shape: View.listViews[string]Nested Shape: View.formViews[string]Nested Shape: View.protectionViewDataUnion OptionsOption 1PropertiesOption 2PropertiesOption 3PropertiesViewFilterRulePropertiesAllowed Values: ViewFilterRule.operatorViewItemUnion OptionsOption 1PropertiesNested Shape: ViewItem[viewKind='list'].configNested Shape: ViewItem[viewKind='list'].protectionOption 2PropertiesNested Shape: ViewItem[viewKind='form'].configNested Shape: ViewItem[viewKind='form'].protectionViewItemNameViewItemWireUnion OptionsOption 1PropertiesNested Shape: ViewItemWire[viewKind='list'].configNested Shape: ViewItemWire[viewKind='list'].protectionNested Shape: ViewItemWire[viewKind='list'].columnStateOption 2PropertiesNested Shape: ViewItemWire[viewKind='form'].configNested Shape: ViewItemWire[viewKind='form'].protectionNested Shape: ViewItemWire[viewKind='form'].columnStateViewKindAllowed ValuesViewScopeAllowed ValuesViewSharingPropertiesViewTabPropertiesNested Shape: ViewTab.filter[number]VisualizationTypeAllowed Values