ObjectStackObjectStack

View

View protocol schemas

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 configViewItemNameSchemaQUALIFIED_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, FormButtonConfigSchema, FormFieldSchema, FormFieldPublicPickerSchema, 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, FormButtonConfig, FormField, FormFieldPublicPicker, 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
startDateFieldstringField providing the event start date/time
endDateFieldstringoptionalField providing the event end date/time (defaults to a single-day event)
titleFieldstringField displayed as the event title
colorFieldstringoptionalField whose value determines the event color

ColumnPrefix

Compound-cell prefix configuration

Properties

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

ColumnSummary

Aggregation function for column footer summary

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)

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
fieldstringField 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; color?: string; visibleWhen?: string | object }[]optionalOptions for select/multiselect/radio/checkboxes fields (per-option default is not accepted here — declare the pre-selected choice on the object definition)
referencestringoptionalTarget object name for lookup/master_detail fields
publicPicker{ displayFields?: string[]; maxResults?: integer; filter?: object[]; object?: string }optionalOpt this field into the anonymous public-form lookup picker (GET /forms/:slug/lookup/:field). Without it the route answers 403 LOOKUP_NOT_PUBLIC and the field is stripped from the rendered public form.
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 (non-negative integer)
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
colSpanintegeroptional[legacy — prefer span] Absolute column span (1-4). Fragile when the column count is derived per surface (mobile 1 / modal 2 / page 3-4): a fixed span only lines up at the width the author imagined. The renderer clamps it to the current column count. Prefer span.
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 (wide widgets like textarea/richtext/json/file/subform take the whole row). 'full': whole row at any column count. Prefer this over the absolute colSpan.
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 (ruled 2026-08-27): 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
labelstringDisplay label (human-readable, any case allowed)
valuestringStored value (lowercase machine identifier)
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 — so a user-gated CHOICE belongs here. e.g. Precord.country == 'cn' or P'admin' in current_user.positions

Nested Shape: FormField.publicPicker

PropertyTypeRequiredDescription
displayFieldsstring[]optionalFields projected into each picker result (with id); the visitor's search matches contains on the first entry. At most 5 (the route projects no more); omitted → ['name'].
maxResultsintegeroptionalMaximum rows a lookup returns (default 20, hard ceiling 50 — the route clamps; anonymous visitors cannot paginate past it).
filter{ field: string; operator?: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalStatic pre-filter rows ANDed ahead of the visitor's search (e.g. only active records are searchable). Same { field, operator, value } dialect as list-view filters.
objectstringoptionalReferenced-object override for the picker search; omitted → resolved from the reference key on the field definition.

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

FormFieldPublicPicker

Public-lookup opt-in: enables GET /forms/:slug/lookup/:field for this field on an anonymous public form (without it the route answers 403 LOOKUP_NOT_PUBLIC).

Properties

PropertyTypeRequiredDescription
displayFieldsstring[]optionalFields projected into each picker result (with id); the visitor's search matches contains on the first entry. At most 5 (the route projects no more); omitted → ['name'].
maxResultsintegeroptionalMaximum rows a lookup returns (default 20, hard ceiling 50 — the route clamps; anonymous visitors cannot paginate past it).
filter{ field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalStatic pre-filter rows ANDed ahead of the visitor's search (e.g. only active records are searchable). Same { field, operator, value } dialect as list-view filters.
objectstringoptionalReferenced-object override for the picker search; omitted → resolved from the reference key on the field definition.

Nested Shape: FormFieldPublicPicker.filter[number]

View filter rule

PropertyTypeRequiredDescription
fieldstringField name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key.

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)
collapsedbooleanoptional (default: false)
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 (ruled 2026-08-27): 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'.
fields(string | { field: string; type?: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; options?: object[]; reference?: string; … })[]

Nested Shape: FormSection.fields[number]

PropertyTypeRequiredDescription
fieldstringField name (snake_case)
typeEnum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>optionalField type (auto-infers widget if omitted)
options{ label: string; value: string; color?: string; visibleWhen?: string | object }[]optionalOptions for select/multiselect/radio/checkboxes fields (per-option default is not accepted here — declare the pre-selected choice on the object definition)
referencestringoptionalTarget object name for lookup/master_detail fields
publicPicker{ displayFields?: string[]; maxResults?: integer; filter?: object[]; object?: string }optionalOpt this field into the anonymous public-form lookup picker (GET /forms/:slug/lookup/:field). Without it the route answers 403 LOOKUP_NOT_PUBLIC and the field is stripped from the rendered public form.
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 (non-negative integer)
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
colSpanintegeroptional[legacy — prefer span] Absolute column span (1-4). Fragile when the column count is derived per surface (mobile 1 / modal 2 / page 3-4): a fixed span only lines up at the width the author imagined. The renderer clamps it to the current column count. Prefer span.
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 (wide widgets like textarea/richtext/json/file/subform take the whole row). 'full': whole row at any column count. Prefer this over the absolute colSpan.
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 (ruled 2026-08-27): 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
labelstringDisplay label (human-readable, any case allowed)
valuestringStored value (lowercase machine identifier)
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 — so a user-gated CHOICE belongs here. e.g. Precord.country == 'cn' or P'admin' in current_user.positions

FormView

Properties

PropertyTypeRequiredDescription
typeEnum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>optional (default: "simple")
layoutEnum<'vertical' | 'horizontal' | 'inline' | 'grid'>optionalField layout direction
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)
allowSkipbooleanoptionalAllow skipping steps (wizard forms)
showStepIndicatorbooleanoptionalShow the step indicator (wizard forms)
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: 'api'; read?: object; write?: object } | { 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?: any[]; 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; apply them 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 (ruled 2026-08-11).
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; apply them by hand.

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

PropertyTypeRequiredDescription
provider'object'
objectstringTarget object name

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

PropertyTypeRequiredDescription
provider'api'
read{ url: string; method?: Enum<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>; headers?: Record<string, string>; params?: Record<string, any>; … }optionalConfiguration for fetching data
write{ url: string; method?: Enum<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>; headers?: Record<string, string>; params?: Record<string, any>; … }optionalConfiguration for submitting data (for forms/editable tables)

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

PropertyTypeRequiredDescription
provider'value'
itemsany[]Static data array

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

PropertyTypeRequiredDescription
provider'schema'
schemaIdstringSchema 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)
collapsedbooleanoptional (default: false)
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 (ruled 2026-08-27): 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'.
fields(string | { field: string; type?: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; options?: object[]; reference?: string; … })[]

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)
collapsedbooleanoptional (default: false)
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 (ruled 2026-08-27): 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'.
fields(string | { field: string; type?: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; options?: object[]; reference?: string; … })[]

Nested Shape: FormView.subforms[number]

PropertyTypeRequiredDescription
childObjectstringChild object whose records are entered inline
relationshipFieldstringoptionalFK on the child pointing back to the parent (auto-detected when omitted)
columnsany[]optionalEditable grid columns (derived from the child object when omitted)
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
publicLinkstringoptionalGenerated public share URL
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'
urlstringWhere the browser goes after a successful submit. Ruled 2026-08-11: (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
startDateFieldstringField providing the task start date
endDateFieldstringField providing the task end date
titleFieldstringField displayed as the task title
progressFieldstringoptionalField providing the task completion percentage
dependenciesFieldstringoptionalField listing the task's predecessor (dependency) record ids
colorFieldstringoptionalField that drives the bar color
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)
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')

Nested Shape: GanttConfig.quickFilters[number]

PropertyTypeRequiredDescription
fieldstringRecord 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

GanttQuickFilter

Properties

PropertyTypeRequiredDescription
fieldstringRecord 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

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)

Nested Shape: GroupingConfig.fields[number]

PropertyTypeRequiredDescription
fieldstringField name to group by
orderEnum<'asc' | 'desc'>optional (default: "asc")Group sort order
collapsedbooleanoptional (default: false)Collapse groups by default

GroupingField

Properties

PropertyTypeRequiredDescription
fieldstringField name to group by
orderEnum<'asc' | 'desc'>optional (default: "asc")Group sort order
collapsedbooleanoptional (default: false)Collapse groups by default

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
urlstringAPI 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
groupByFieldstringField to group columns by (usually status/select)
summarizeFieldstringoptionalField to sum at top of column (e.g. amount)
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
datasetstringDataset name to bind (ADR-0021)
dimensionsstring[]optionalDimension names — X/group/split
valuesstring[]Measure names — Y (at least one)

ListColumn

Properties

PropertyTypeRequiredDescription
fieldstringField name (snake_case)
labelstring | Record<string, string>optionalDisplay label override
widthnumberoptionalColumn width in pixels
alignEnum<'left' | 'center' | 'right'>optionalText alignment
hiddenbooleanoptionalHide column by default
sortablebooleanoptionalAllow sorting by this column
resizablebooleanoptionalAllow resizing this column
wrapbooleanoptionalAllow text wrapping
typestringoptionalRenderer type override (e.g., "currency", "date")
pinnedEnum<'left' | 'right'>optionalPin/freeze column to left or right side
summaryEnum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | '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
fieldstringField 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
centerany[]optionalInitial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records

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' | 'page'>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'api'; read?: object; write?: object } | { 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
filter{ field: string; operator?: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sortstring | { 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'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: 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; … } & Record<string, any>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[] }optionalList chart view configuration
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 } & Record<string, any>optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNamestringoptionalPublished page this view mounts — required when type: 'page', and refused on every other view type. Rendering is delegated to the existing page renderer; the page keeps its own assignedProfiles audience.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view
fieldOrderstring[]optionalExplicit field display order for this view
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
tabs{ name: string; label?: string | Record<string, string>; icon?: string; view?: string; … }[]optionalTab definitions for multi-tab view interface
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 }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; apply them 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; apply them 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; apply them 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; apply them 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; apply them by hand.

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

PropertyTypeRequiredDescription
provider'object'
objectstringTarget object name

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

PropertyTypeRequiredDescription
provider'api'
read{ url: string; method?: Enum<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>; headers?: Record<string, string>; params?: Record<string, any>; … }optionalConfiguration for fetching data
write{ url: string; method?: Enum<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>; headers?: Record<string, string>; params?: Record<string, any>; … }optionalConfiguration for submitting data (for forms/editable tables)

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

PropertyTypeRequiredDescription
provider'value'
itemsany[]Static data array

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

PropertyTypeRequiredDescription
provider'schema'
schemaIdstringSchema 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
fieldstringField name (snake_case)
labelstring | Record<string, string>optionalDisplay label override
widthnumberoptionalColumn width in pixels
alignEnum<'left' | 'center' | 'right'>optionalText alignment
hiddenbooleanoptionalHide column by default
sortablebooleanoptionalAllow sorting by this column
resizablebooleanoptionalAllow resizing this column
wrapbooleanoptionalAllow text wrapping
typestringoptionalRenderer type override (e.g., "currency", "date")
pinnedEnum<'left' | 'right'>optionalPin/freeze column to left or right side
summaryEnum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …> | { type: Enum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …>; field?: string }optionalFooter aggregation for this column — the function alone, or { type, field } to aggregate another field
prefix{ field: string; type?: Enum<'badge' | 'text'> }optionalField rendered inline before this cell value
linkbooleanoptionalFunctions as the primary navigation link (triggers View navigation)
actionstringoptionalRegistered Action ID to execute when clicked

Nested Shape: ListView.filter[number]

View filter rule

PropertyTypeRequiredDescription
fieldstringField name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>optionalFilter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key.

Nested Shape: 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")
viewstringoptionalName of the form view to use for details (e.g. "summary_view", "edit_form")
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: 25)Number of records per page
pageSizeOptionsinteger[]optionalAvailable page size options

Nested Shape: ListView.kanban

PropertyTypeRequiredDescription
groupByFieldstringField to group columns by (usually status/select)
summarizeFieldstringoptionalField to sum at top of column (e.g. amount)
columnsstring[]Fields to show on cards

Nested Shape: ListView.calendar

PropertyTypeRequiredDescription
startDateFieldstringField providing the event start date/time
endDateFieldstringoptionalField providing the event end date/time (defaults to a single-day event)
titleFieldstringField displayed as the event title
colorFieldstringoptionalField whose value determines the event color

Nested Shape: ListView.gantt

PropertyTypeRequiredDescription
startDateFieldstringField providing the task start date
endDateFieldstringField providing the task end date
titleFieldstringField displayed as the task title
progressFieldstringoptionalField providing the task completion percentage
dependenciesFieldstringoptionalField listing the task's predecessor (dependency) record ids
colorFieldstringoptionalField that drives the bar color
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)
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')

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
startDateFieldstringField for timeline item start date
endDateFieldstringoptionalField for timeline item end date
titleFieldstringField to display as timeline item title
groupByFieldstringoptionalField to group timeline rows
colorFieldstringoptionalField to determine item 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
datasetstringDataset 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
centerany[]optionalInitial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records

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)

Nested Shape: ListView.rowColor

PropertyTypeRequiredDescription
fieldstringField to derive color from (typically a select/status field)
colorsRecord<string, string>optionalMap of field value to color (hex/token)

Nested Shape: ListView.bulkActionDefs[number]

PropertyTypeRequiredDescription
namestringStable 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-2").
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' | …>; … } & Record<string, any>)[]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: false)Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in.
hideFieldsbooleanoptional (default: false)Allow users to hide/show fields from the toolbar (the affordance behind the view-level hiddenFields list). Boolean toggle — distinct from the record-details component's hideFields, which is an array of field names to omit. Off by default: column hiding is opt-in.
rowColorbooleanoptional (default: false)Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level rowColor block. Off by default: row colouring is opt-in.
buttonsstring[]optionalCustom action button IDs to show in the toolbar

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

Tab configuration for multi-tab view interface

PropertyTypeRequiredDescription
namestringTab 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: 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>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
messagestring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
iconstringoptional

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")
viewstringoptionalName of the form view to use for details (e.g. "summary_view", "edit_form")
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' | 'page'>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'api'; read?: object; write?: object } | { 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
filter{ field: string; operator?: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sortstring | { 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'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: 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; … } & Record<string, any>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[] }optionalList chart view configuration
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 } & Record<string, any>optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNamestringoptionalPublished page this view mounts — required when type: 'page', and refused on every other view type. Rendering is delegated to the existing page renderer; the page keeps its own assignedProfiles audience.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view
fieldOrderstring[]optionalExplicit field display order for this view
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
tabs{ name: string; label?: string | Record<string, string>; icon?: string; view?: string; … }[]optionalTab definitions for multi-tab view interface
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 }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; apply them 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; apply them 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; apply them 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; apply them 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; apply them by hand.
userFilters{ element?: Enum<'dropdown' | 'toggle'>; fields?: object[] }optional

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

PropertyTypeRequiredDescription
provider'object'
objectstringTarget object name

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

PropertyTypeRequiredDescription
provider'api'
read{ url: string; method?: Enum<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>; headers?: Record<string, string>; params?: Record<string, any>; … }optionalConfiguration for fetching data
write{ url: string; method?: Enum<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>; headers?: Record<string, string>; params?: Record<string, any>; … }optionalConfiguration for submitting data (for forms/editable tables)

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

PropertyTypeRequiredDescription
provider'value'
itemsany[]Static data array

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

PropertyTypeRequiredDescription
provider'schema'
schemaIdstringSchema 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
fieldstringField name (snake_case)
labelstring | Record<string, string>optionalDisplay label override
widthnumberoptionalColumn width in pixels
alignEnum<'left' | 'center' | 'right'>optionalText alignment
hiddenbooleanoptionalHide column by default
sortablebooleanoptionalAllow sorting by this column
resizablebooleanoptionalAllow resizing this column
wrapbooleanoptionalAllow text wrapping
typestringoptionalRenderer type override (e.g., "currency", "date")
pinnedEnum<'left' | 'right'>optionalPin/freeze column to left or right side
summaryEnum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …> | { type: Enum<'none' | 'count' | 'count_empty' | 'count_filled' | 'count_unique' | …>; field?: string }optionalFooter aggregation for this column — the function alone, or { type, field } to aggregate another field
prefix{ field: string; type?: Enum<'badge' | 'text'> }optionalField rendered inline before this cell value
linkbooleanoptionalFunctions as the primary navigation link (triggers View navigation)
actionstringoptionalRegistered Action ID to execute when clicked

Nested Shape: ObjectListView.filter[number]

View filter rule

PropertyTypeRequiredDescription
fieldstringField name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>optionalFilter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key.

Nested Shape: 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")
viewstringoptionalName of the form view to use for details (e.g. "summary_view", "edit_form")
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: 25)Number of records per page
pageSizeOptionsinteger[]optionalAvailable page size options

Nested Shape: ObjectListView.kanban

PropertyTypeRequiredDescription
groupByFieldstringField to group columns by (usually status/select)
summarizeFieldstringoptionalField to sum at top of column (e.g. amount)
columnsstring[]Fields to show on cards

Nested Shape: ObjectListView.calendar

PropertyTypeRequiredDescription
startDateFieldstringField providing the event start date/time
endDateFieldstringoptionalField providing the event end date/time (defaults to a single-day event)
titleFieldstringField displayed as the event title
colorFieldstringoptionalField whose value determines the event color

Nested Shape: ObjectListView.gantt

PropertyTypeRequiredDescription
startDateFieldstringField providing the task start date
endDateFieldstringField providing the task end date
titleFieldstringField displayed as the task title
progressFieldstringoptionalField providing the task completion percentage
dependenciesFieldstringoptionalField listing the task's predecessor (dependency) record ids
colorFieldstringoptionalField that drives the bar color
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)
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')

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
startDateFieldstringField for timeline item start date
endDateFieldstringoptionalField for timeline item end date
titleFieldstringField to display as timeline item title
groupByFieldstringoptionalField to group timeline rows
colorFieldstringoptionalField to determine item 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
datasetstringDataset 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
centerany[]optionalInitial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records

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)

Nested Shape: ObjectListView.rowColor

PropertyTypeRequiredDescription
fieldstringField to derive color from (typically a select/status field)
colorsRecord<string, string>optionalMap of field value to color (hex/token)

Nested Shape: ObjectListView.bulkActionDefs[number]

PropertyTypeRequiredDescription
namestringStable 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-2").
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' | …>; … } & Record<string, any>)[]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: false)Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in.
hideFieldsbooleanoptional (default: false)Allow users to hide/show fields from the toolbar (the affordance behind the view-level hiddenFields list). Boolean toggle — distinct from the record-details component's hideFields, which is an array of field names to omit. Off by default: column hiding is opt-in.
rowColorbooleanoptional (default: false)Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level rowColor block. Off by default: row colouring is opt-in.
buttonsstring[]optionalCustom action button IDs to show in the toolbar

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

Tab configuration for multi-tab view interface

PropertyTypeRequiredDescription
namestringTab 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: 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>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
messagestring | Record<string, string>optionalDisplay label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time
iconstringoptional

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
fieldstringField 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: 25)Number of records per page
pageSizeOptionsinteger[]optionalAvailable page size options

RowColorConfig

Row color configuration based on field values

Properties

PropertyTypeRequiredDescription
fieldstringField to derive color from (typically a select/status field)
colorsRecord<string, string>optionalMap of field value to color (hex/token)

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
startDateFieldstringField for timeline item start date
endDateFieldstringoptionalField for timeline item end date
titleFieldstringField to display as timeline item title
groupByFieldstringoptionalField to group timeline rows
colorFieldstringoptionalField to determine item 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: false)Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in.
hideFieldsbooleanoptional (default: false)Allow users to hide/show fields from the toolbar (the affordance behind the view-level hiddenFields list). Boolean toggle — distinct from the record-details component's hideFields, which is an array of field names to omit. Off by default: column hiding is opt-in.
rowColorbooleanoptional (default: false)Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level rowColor block. Off by default: row colouring is opt-in.
buttonsstring[]optionalCustom action button IDs to show in the toolbar

UserFilterField

Quick-filter field configuration

Properties

PropertyTypeRequiredDescription
fieldstringField 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 | booleanOption 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
fieldstringField 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
namestringTab 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' | …>; data?: object | … +3 more; … }optional
form{ type?: Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>; layout?: Enum<'vertical' | 'horizontal' | 'inline' | 'grid'>; columns?: integer; title?: string; … }optional
listViewsRecord<string, { name?: string; label?: string | Record<string, string>; type?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | …>; data?: object | … +3 more; … }>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' | 'inline' | 'grid'>; 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' | …>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'api'; read?: object; write?: object } | { 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
filter{ field: string; operator?: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sortstring | { 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'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: 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; … } & Record<string, any>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[] }optionalList chart view configuration
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 } & Record<string, any>optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNamestringoptionalPublished page this view mounts — required when type: 'page', and refused on every other view type. Rendering is delegated to the existing page renderer; the page keeps its own assignedProfiles audience.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view
fieldOrderstring[]optionalExplicit field display order for this view
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
tabs{ name: string; label?: string | Record<string, string>; icon?: string; view?: string; … }[]optionalTab definitions for multi-tab view interface
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 }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; apply them 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; apply them 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; apply them 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; apply them 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; apply them 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' | 'inline' | 'grid'>optionalField layout direction
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)
allowSkipbooleanoptionalAllow skipping steps (wizard forms)
showStepIndicatorbooleanoptionalShow the step indicator (wizard forms)
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: 'api'; read?: object; write?: object } | { 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?: any[]; 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; apply them 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 (ruled 2026-08-11).
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; apply them 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' | …>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'api'; read?: object; write?: object } | { 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
filter{ field: string; operator?: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sortstring | { 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'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: 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; … } & Record<string, any>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[] }optionalList chart view configuration
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 } & Record<string, any>optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNamestringoptionalPublished page this view mounts — required when type: 'page', and refused on every other view type. Rendering is delegated to the existing page renderer; the page keeps its own assignedProfiles audience.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view
fieldOrderstring[]optionalExplicit field display order for this view
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
tabs{ name: string; label?: string | Record<string, string>; icon?: string; view?: string; … }[]optionalTab definitions for multi-tab view interface
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 }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; apply them 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; apply them 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; apply them 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; apply them 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; apply them 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' | 'inline' | 'grid'>optionalField layout direction
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)
allowSkipbooleanoptionalAllow skipping steps (wizard forms)
showStepIndicatorbooleanoptionalShow the step indicator (wizard forms)
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: 'api'; read?: object; write?: object } | { 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?: any[]; 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; apply them 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 (ruled 2026-08-11).
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; apply them by hand.

Nested Shape: View.protection

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>Lock policy — none | no-overlay | no-delete | full.
reasonstringUser-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'
objectstringTarget object name

Option 2

Properties

PropertyTypeRequiredDescription
provider'api'
read{ url: string; method: Enum<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>; headers?: Record<string, string>; params?: Record<string, any>; … }optionalConfiguration for fetching data
write{ url: string; method: Enum<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>; headers?: Record<string, string>; params?: Record<string, any>; … }optionalConfiguration for submitting data (for forms/editable tables)

Nested Shape: ViewData[provider='api'].read

PropertyTypeRequiredDescription
urlstringAPI 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

Nested Shape: ViewData[provider='api'].write

PropertyTypeRequiredDescription
urlstringAPI 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

Option 3

Properties

PropertyTypeRequiredDescription
provider'value'
itemsany[]Static data array

Option 4

Properties

PropertyTypeRequiredDescription
provider'schema'
schemaIdstringSchema 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
fieldstringField 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.

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' | …>; data?: object | … +3 more; … }List-family view configuration.
namestringGlobally-unique view id, <object>.<viewKey>.
objectstringBound 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).
ownerstringoptionalOwner user id — set when scope is personal.
hiddenbooleanoptionalHidden from the switcher (per-user / per-org declutter).
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' | …>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'api'; read?: object; write?: object } | { 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
filter{ field: string; operator?: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sortstring | { 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'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: 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; … } & Record<string, any>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[] }optionalList chart view configuration
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 } & Record<string, any>optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNamestringoptionalPublished page this view mounts — required when type: 'page', and refused on every other view type. Rendering is delegated to the existing page renderer; the page keeps its own assignedProfiles audience.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view
fieldOrderstring[]optionalExplicit field display order for this view
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
tabs{ name: string; label?: string | Record<string, string>; icon?: string; view?: string; … }[]optionalTab definitions for multi-tab view interface
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 }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; apply them 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; apply them 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; apply them 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; apply them 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; apply them by hand.

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

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>Lock policy — none | no-overlay | no-delete | full.
reasonstringUser-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' | 'inline' | 'grid'>; columns?: integer; title?: string; … }Form view configuration.
namestringGlobally-unique view id, <object>.<viewKey>.
objectstringBound 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).
ownerstringoptionalOwner user id — set when scope is personal.
hiddenbooleanoptionalHidden from the switcher (per-user / per-org declutter).
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' | 'inline' | 'grid'>optionalField layout direction
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)
allowSkipbooleanoptionalAllow skipping steps (wizard forms)
showStepIndicatorbooleanoptionalShow the step indicator (wizard forms)
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: 'api'; read?: object; write?: object } | { 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?: any[]; 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; apply them 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 (ruled 2026-08-11).
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; apply them by hand.

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

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>Lock policy — none | no-overlay | no-delete | full.
reasonstringUser-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' | …>; data?: object | … +3 more; … }List-family view configuration.
namestringGlobally-unique view id, <object>.<viewKey>.
objectstringBound 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).
ownerstringoptionalOwner user id — set when scope is personal.
hiddenbooleanoptionalHidden from the switcher (per-user / per-org declutter).
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.
isPinnedbooleanoptionalStudio round-trip: view pinned in the switcher (per-user state, written by the console — not authored).
sortOrderintegeroptionalStudio round-trip: position within the switcher (per-user state, written by the console — not authored).
columnState{ order?: string[]; widths?: Record<string, number> }optionalStudio round-trip: per-user column order/widths (runtime-only state, written by the console grid — not authored)

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' | …>optional (default: "grid")
data{ provider: 'object'; object: string } | { provider: 'api'; read?: object; write?: object } | { 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
filter{ field: string; operator?: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[]optionalFilter criteria (JSON Rules)
sortstring | { 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'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }optionalConfiguration for item click navigation (page, drawer, modal, etc.)
pagination{ pageSize?: integer; pageSizeOptions?: integer[] }optionalPagination configuration
kanban{ groupByField: string; summarizeField?: 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; … } & Record<string, any>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[] }optionalList chart view configuration
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 } & Record<string, any>optionalTree/hierarchy configuration — applies when the view renders as a tree layout
pageNamestringoptionalPublished page this view mounts — required when type: 'page', and refused on every other view type. Rendering is delegated to the existing page renderer; the page keeps its own assignedProfiles audience.
descriptionstring | Record<string, string>optionalView description for documentation/tooltips
sharing{ type?: Enum<'personal' | 'collaborative'>; lockedBy?: string }optionalView sharing and access configuration
rowHeightEnum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>optionalRow height / density setting
grouping{ fields: object[] }optionalGroup records by one or more fields
rowColor{ field: string; colors?: Record<string, string> }optionalColor rows based on field value
hiddenFieldsstring[]optionalFields to hide in this specific view
fieldOrderstring[]optionalExplicit field display order for this view
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
tabs{ name: string; label?: string | Record<string, string>; icon?: string; view?: string; … }[]optionalTab definitions for multi-tab view interface
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 }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; apply them 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; apply them 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; apply them 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; apply them 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; apply them by hand.

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

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>Lock policy — none | no-overlay | no-delete | full.
reasonstringUser-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 per-user state — written by the console grid, never authored).
widthsRecord<string, number>optionalColumn widths in pixels, keyed by field name (runtime-only per-user 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' | 'inline' | 'grid'>; columns?: integer; title?: string; … }Form view configuration.
namestringGlobally-unique view id, <object>.<viewKey>.
objectstringBound 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).
ownerstringoptionalOwner user id — set when scope is personal.
hiddenbooleanoptionalHidden from the switcher (per-user / per-org declutter).
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.
isPinnedbooleanoptionalStudio round-trip: view pinned in the switcher (per-user state, written by the console — not authored).
sortOrderintegeroptionalStudio round-trip: position within the switcher (per-user state, written by the console — not authored).
columnState{ order?: string[]; widths?: Record<string, number> }optionalStudio round-trip: per-user column order/widths (runtime-only state, written by the console grid — not authored)

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

PropertyTypeRequiredDescription
typeEnum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>optional (default: "simple")
layoutEnum<'vertical' | 'horizontal' | 'inline' | 'grid'>optionalField layout direction
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)
allowSkipbooleanoptionalAllow skipping steps (wizard forms)
showStepIndicatorbooleanoptionalShow the step indicator (wizard forms)
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: 'api'; read?: object; write?: object } | { 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?: any[]; 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; apply them 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 (ruled 2026-08-11).
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; apply them by hand.

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

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>Lock policy — none | no-overlay | no-delete | full.
reasonstringUser-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 per-user state — written by the console grid, never authored).
widthsRecord<string, number>optionalColumn widths in pixels, keyed by field name (runtime-only per-user 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
namestringTab 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
fieldstringField name to filter on
operatorEnum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>Filter operator
valuestring | number | boolean | null | (string | number)[]optionalFilter value. The accepted SHAPE depends on the operator: in / not_in take an array (any length, including []), between takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key.

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 ValuesColumnSummaryConfigPropertiesFormButtonConfigPropertiesFormFieldPropertiesAllowed Values: FormField.typeNested Shape: FormField.options[number]Nested Shape: FormField.publicPickerNested Shape: FormField.keyFieldFormFieldPublicPickerPropertiesNested Shape: FormFieldPublicPicker.filter[number]FormSectionPropertiesNested Shape: FormSection.fields[number]FormSelectOptionPropertiesFormViewPropertiesNested Shape: FormView.data[provider='object']Nested Shape: FormView.data[provider='api']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]GanttQuickFilterPropertiesGroupingConfigPropertiesNested Shape: GroupingConfig.fields[number]GroupingFieldPropertiesHttpMethodSubsetAllowed ValuesHttpRequestPropertiesKanbanConfigPropertiesListChartConfigPropertiesListColumnPropertiesNested Shape: ListColumn.summaryNested Shape: ListColumn.prefixListMapConfigPropertiesListViewPropertiesNested Shape: ListView.data[provider='object']Nested Shape: ListView.data[provider='api']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.tabs[number]Nested Shape: ListView.addRecordNested Shape: ListView.emptyStateNested Shape: ListView.ariaNavigationConfigPropertiesNavigationModeAllowed ValuesObjectListViewPropertiesNested Shape: ObjectListView.data[provider='object']Nested Shape: ObjectListView.data[provider='api']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.tabs[number]Nested 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 2PropertiesNested Shape: ViewData[provider='api'].readNested Shape: ViewData[provider='api'].writeOption 3PropertiesOption 4PropertiesViewFilterRulePropertiesAllowed 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