ObjectStackObjectStack

Object

Object protocol schemas

Source: packages/spec/src/data/object.zod.ts

TypeScript Usage

import { ApiMethod, ApiOperationSchema, IndexSchema, LifecycleSchema, LifecycleClassSchema, ObjectSchema, ObjectAccessConfigSchema, ObjectCapabilities, ObjectExtensionSchema, ObjectExternalBindingSchema, ObjectFieldGroupSchema, ObjectOwnershipEnum, ObjectRequiredPermissionsSchema, PerOperationRequiredPermissionsSchema, RowCrudActionOverrideSchema, TenancyConfigSchema } from '@objectstack/spec/data';
import type { ApiMethod, ApiOperation, Lifecycle, LifecycleClass, ObjectAccessConfig, ObjectCapabilities, ObjectExtension, ObjectExternalBinding, ObjectFieldGroup, ObjectRequiredPermissions, PerOperationRequiredPermissions, RowCrudActionOverride, TenancyConfig } from '@objectstack/spec/data';

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

ApiMethod

Allowed Values

  • get
  • list
  • create
  • update
  • delete
  • bulk

ApiOperation

Allowed Values

  • get
  • list
  • create
  • update
  • delete
  • upsert
  • bulk
  • aggregate
  • history
  • search
  • restore
  • purge
  • import
  • export

Index

Properties

PropertyTypeRequiredDescription
namestringoptionalIndex name (auto-generated if not provided)
fieldsstring[]Fields included in the index
uniqueboolean | 'global' | 'organization'optional (default: false)Whether the index enforces uniqueness, and at which scope (ADR-0120). 'global' = materialized over exactly fields, no organization column injected — one holder across the whole installation; 'organization' = the driver prepends the NULL-safe organization key part (COALESCE(organization_id, 'global')) at registration — one holder per organization; bare true = deprecated positional spelling of 'global' (warned in 17.x by lint unique/unscoped-declared-index, rejected at protocol 18) — state the scope. 'tenant'/'org' are rejected — the word is 'organization'
typeneveroptional[REMOVED] indexes[].type was removed in @objectstack/spec 17.0.0 (ADR-0049) — no driver ever read it. SqlDriver.syncDeclaredIndexes creates every declared index through knex's table.index() / table.unique(), which cannot express an access method, so the value changed no DDL; its .default('btree') merely made an inert knob show up in every parse output. Delete the key. The index method is the driver/dialect's decision (Postgres defaults to B-tree; gin/gist/fulltext are dialect-specific and are chosen by a database-layer migration when a workload actually needs one). Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.
partialneveroptional[REMOVED] indexes[].partial was removed in @objectstack/spec 17.0.0 (ADR-0049) — no driver ever emitted the WHERE clause, so a declared partial index was materialized as a FULL index and the predicate silently did nothing. Delete the key. Partial indexes are built at the database layer, not the declaration surface: issue CREATE [UNIQUE] INDEX … WHERE <predicate> from a runtime migration (this is what metadata-protocol's ensureOverlayIndex already does for sys_metadata). Drift detection is unaffected — it reads partiality back from the database's own DDL, never from this key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.

Lifecycle

Properties

PropertyTypeRequiredDescription
classEnum<'record' | 'audit' | 'telemetry' | 'transient' | 'event'>Persistence contract: record (business truth, permanent) | audit (compliance ledger) | telemetry (high-freq log) | transient (ephemeral state) | event (bus messages).
retention{ maxAge: string; onlyWhen?: Record<string, string | number | boolean | object | object> }optionalAge-based retention window enforced by the LifecycleService Reaper.
ttl{ field: string; expireAfter: string; onlyWhen?: Record<string, string | number | boolean | object | object> }optionalPer-row TTL auto-expiry (transient/event classes).
storage{ strategy: 'rotation'; shards: integer; unit: Enum<'day' | 'week' | 'month'> }optionalPhysical storage strategy for high-frequency telemetry (LifecycleService Rotator).
archive{ after: string; to: string; keep?: string }optionalCold-store archival (LifecycleService Archiver) — audit-class hot→cold hand-off.
reclaimbooleanoptionalRun driver space reclamation (SQLite incremental_vacuum) after sweeping this object. Default true for non-record classes.

Nested Shape: Lifecycle.retention

PropertyTypeRequiredDescription
maxAgestringRows older than this (by created_at) are deleted by the Reaper — or archived first when archive is set.
onlyWhenRecord<string, string | number | boolean | { $in: (string | number)[] } | { $null: boolean }>optionalRow filter the retention applies to — per-field equality, {$in: [...]} or the null predicate {$null: true|false} (e.g. { status: { $in: ["completed", "failed"] } }). Rows OUTSIDE the filter are retained regardless of age: for tables that interleave live workflow state with terminal history (sys_automation_run). Incompatible with rotation storage and archive, which act on whole shards / age alone.

Nested Shape: Lifecycle.ttl

PropertyTypeRequiredDescription
fieldstringTimestamp field the TTL is measured from (e.g. created_at, expires_at).
expireAfterstringRows expire this long after field and are deleted by the Reaper.
onlyWhenRecord<string, string | number | boolean | { $in: (string | number)[] } | { $null: boolean }>optionalRow filter the TTL reap applies to — per-field equality, {$in: [...]} or the null predicate {$null: true|false} (e.g. { revoked_at: { $null: true } }). Rows OUTSIDE the filter are retained regardless of expiry: for tables that interleave live rows with terminal history a TTL keyed on the same timestamp would otherwise destroy (a sys_session audit tombstone backdates expires_at, so a naive TTL reaps tombstones first). Incompatible with rotation storage, which DROPs whole shards, and with archive, which selects rows by the ttl cutoff alone and does not apply this filter.

Nested Shape: Lifecycle.storage

PropertyTypeRequiredDescription
strategy'rotation'Time-shard the table. The retained window (shards × unit) is the same on every dialect; the reclamation is not — SQLite DROPs the oldest shard whole (O(1) reclaim), other dialects reap that same window by age from created_at.
shardsintegerNumber of shards retained; total window = shards × unit.
unitEnum<'day' | 'week' | 'month'>Time width of one shard.

Nested Shape: Lifecycle.archive

PropertyTypeRequiredDescription
afterstringRows older than this are copied to the archive datasource before hot deletion.
tostringTarget datasource name for cold storage. When it is not registered, the Archiver skips (audit rows are then retained, never dropped unarchived).
keepstringoptionalHow long archived rows are kept in cold storage (undefined = forever).

LifecycleClass

Allowed Values

  • record
  • audit
  • telemetry
  • transient
  • event

Object

Properties

PropertyTypeRequiredDescription
namestringMachine unique key (snake_case). Immutable.
labelstringoptionalHuman readable singular label (e.g. "Account")
pluralLabelstringoptionalHuman readable plural label (e.g. "Accounts")
descriptionstringoptionalDeveloper documentation / description
iconstringoptionalIcon name (Lucide/Material) for UI representation
isSystembooleanoptional (default: false)Is system object (protected from deletion; defaults its org-wide sharing to public when no sharingModel is set — plugin-sharing)
managedByEnum<'platform' | 'config' | 'system-data' | 'engine-owned' | 'append-only' | 'better-auth'>optionalLifecycle bucket — platform (user CRUD) | config (admin authored) | system-data (platform-defined schema, admin/user-writable data) | engine-owned (engine owns the lifecycle, no user writes) | append-only (audit) | better-auth (identity). UI clients honour the resolved affordance matrix.
ownershipEnum<'user' | 'business_unit' | 'org' | 'none'>optionalRecord-ownership model: user (default — injects reassignable owner_id plus owning_business_unit_id) | business_unit (unit-owned: owning_business_unit_id only, no owner_id) | org | none (no per-record owner, neither anchor). Distinct from the package own/extend contribution kind.
userActions{ create?: boolean | object; import?: boolean | object; edit?: boolean | object; delete?: boolean | object; … }optionalPer-object override of the resolved CRUD affordance matrix.
systemFieldsfalse | { tenant?: boolean; audit?: boolean }optionalOpt out of, or selectively disable, registry-level system-field auto-injection.
datasourcestringoptional (default: "default")Target Datasource ID. "default" is the primary DB.
external{ remoteName?: string; remoteSchema?: string; writable?: boolean; columnMap?: Record<string, string>; … }optionalRemote table binding for federated (external) objects.
fieldsRecord<string, { name?: string; label?: string; type: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; description?: string; … }>Field definitions map. Keys must be snake_case identifiers.
indexes{ name?: string; fields: string[]; unique?: boolean | 'global' | 'organization' }[]optionalDatabase performance indexes
fieldGroups{ key: string; label: string; icon?: string; description?: string; … }[]optionalOrdered list of field groups (array order = display order). See ObjectFieldGroupSchema.
tenancy{ enabled: boolean; tenantField?: string; organizationField?: string }optionalMulti-tenancy configuration for SaaS applications
access{ default?: Enum<'public' | 'private'> }optional[ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default).
requiredPermissionsstring[] | { read?: string[]; create?: string[]; update?: string[]; delete?: string[] }optional[ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — string[] gates all CRUD, or a {read,create,update,delete} map gates per operation.
lifecycle{ class: Enum<'record' | 'audit' | 'telemetry' | 'transient' | 'event'>; retention?: object; ttl?: object; storage?: object; … }optionalData lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService.
fileAccessDelegatestringoptionalKernel service that authorizes downloads of files owned by this object's media fields, instead of testing whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed.
validationsany[]optionalObject-level validation rules
activityMilestones{ field: string; value: string; summary: string; type?: string }[]optionalDeclarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2).
nameFieldstringoptional[ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title").
displayNameFieldstringoptional[DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField.
titleFormatstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optional[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField.
highlightFieldsstring[]optional[ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout.
stageFieldstring | falseoptional[ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed.
editModeEnum<'modal' | 'page'>optionalEdit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family).
listViewsRecord<string, { name?: string; label?: string | Record<string, string>; type?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | …>; data?: object | … +3 more; … }>optionalBuilt-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047)
searchableFieldsstring[]optionalFields the $search query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. Entries must name a STORED column: a virtual formula field is computed on read and materializes no column, so searching it can never match and it is refused — mirror the value onto a stored text field and declare that.
enable{ trackHistory?: boolean; searchable?: boolean; apiEnabled?: boolean; apiMethods?: Enum<'get' | 'list' | 'create' | 'update' | 'delete' | 'bulk'>[]; … }optionalEnabled system features modules
sharingModelEnum<'private' | 'public_read' | 'public_read_write' | 'controlled_by_parent'>optionalOrg-Wide Default record visibility (OWD) for INTERNAL users. Canonical four only (legacy aliases removed, ADR-0090 D4): private (owner-only) | public_read (everyone reads, owner writes) | public_read_write (everyone reads+writes) | controlled_by_parent (derived from the master record). A CUSTOM object that omits this resolves to private at runtime (ADR-0090 D1).
externalSharingModelEnum<'private' | 'public_read' | 'public_read_write' | 'controlled_by_parent'>optional[ADR-0090 D11] OWD for external (portal/partner) principals. Defaults to private; must be <= sharingModel in openness.
publicSharing{ enabled?: boolean; allowedAudiences?: Enum<'public' | 'link_only' | 'signed_in' | 'email'>[]; allowedPermissions?: Enum<'view' | 'comment' | 'edit'>[]; maxExpiryDays?: integer; … }optionalPublic share-link policy (Notion/Figma-style link sharing)
actions{ name: string; label: string | Record<string, string>; description?: string | Record<string, string>; objectName?: string; … }[]optionalActions associated with this object (auto-populated from top-level actions via objectName)
protection{ lock: Enum<'none' | 'no-overlay' | 'no-delete' | 'full'>; reason: string; docsUrl?: string }optionalPackage author protection block — lock policy for this object.
_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: Object.userActions

PropertyTypeRequiredDescription
createboolean | { enabled?: boolean; visibleWhen?: string | object; disabledWhen?: string | object }optionalShow generic "New" button. Boolean, or an object adding visibleWhen/disabledWhen CEL predicates evaluated once per toolbar against the record in scope (the host record on a related list).
importboolean | { enabled?: boolean; visibleWhen?: string | object; disabledWhen?: string | object }optionalShow CSV import wizard entry. Boolean, or an object adding visibleWhen/disabledWhen CEL predicates evaluated once per toolbar against the record in scope (the host record on a related list).
editboolean | { enabled?: boolean; visibleWhen?: string | object; disabledWhen?: string | object }optionalAllow inline / form edit of existing rows. Boolean, or an object adding per-record visibleWhen/disabledWhen CEL predicates.
deleteboolean | { enabled?: boolean; visibleWhen?: string | object; disabledWhen?: string | object }optionalShow row-level delete + bulk delete. Boolean, or an object adding per-record visibleWhen/disabledWhen CEL predicates.
exportCsvbooleanoptionalShow CSV export entry.

Nested Shape: Object.systemFields

PropertyTypeRequiredDescription
tenantbooleanoptionalInject the organization_id column. Default true (the column is always provisioned; the multi-tenant flag governs only its index).
auditbooleanoptionalInject the audit columns (created_at/created_by/updated_at/updated_by). Default true.

Nested Shape: Object.external

PropertyTypeRequiredDescription
remoteNamestringoptionalRemote table/view name. Defaults to object.name.
remoteSchemastringoptionalRemote schema/database qualifier.
writablebooleanoptional (default: false)Per-object write opt-in (also requires datasource.external.allowWrites).
columnMapRecord<string, string>optionalRemote column name → local field name.
introspectedAtstringoptionalSet by os datasource introspect; informational.
ignoreColumnsstring[]optionalRemote columns to skip during validation (dev convenience).

Nested Shape: Object.fields[string]

PropertyTypeRequiredDescription
namestringoptionalMachine name (snake_case)
labelstringoptionalHuman readable label
typeEnum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>Field Data Type
descriptionstringoptionalTooltip/Help text
formatstringoptionalFormat string (e.g. email, phone)
requiredbooleanoptional (default: false)Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (multiple: true) required means NON-EMPTY array — an emptied required set fails validation loudly; [] does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (storage.notNull), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field.
storage{ notNull?: boolean }optionalPhysical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested.
searchablebooleanoptional (default: false)Is searchable
multiplebooleanoptional (default: false)Allow multiple values (Stores as Array/JSON). Applicable for select, lookup, file, image. An emptied multi-value lookup reads back as [], never null — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair (maintainer ruling 2026-08-18).
uniqueboolean | 'global' | 'organization'optional (default: false)Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization'
defaultValueanyoptionalDefault applied on INSERT when the field is omitted or null ('' is a real value, not absence). Three legal shapes, discriminated in the engine's own order: a CEL Expression envelope { dialect: 'cel', source: 'today()' } (accepted structurally; result type is a runtime concern); a runtime TOKEN — NOW() on datetime/date/time only, current_user on user or lookup with reference: 'sys_user' only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 valueSchemaFor). Anything else is refused at parse time with a prescriptive message.
maxLengthintegeroptionalMax character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode.
minLengthintegeroptionalMin character length (positive integer; minLength: 0 is refused — express "no minimum" by omitting the key). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode.
precisionintegeroptionalTotal digits (non-negative integer)
scaleintegeroptionalDecimal places (non-negative integer)
minnumberoptionalMinimum value
maxnumberoptionalMaximum value
useGroupingbooleanoptionalDigit-grouping presentation hint for number fields — maps to Intl.NumberFormat's useGrouping. Absent = renderer decides (interim heuristic today, locale default eventually); false = author opts out of grouping (e.g. a year or other ordinal/identifier integer); true = author pins grouping on.
acceptstring[]optionalPermitted upload types for media fields, as MIME types or extensions (e.g. ["image/*", ".pdf"]). Offered to the file picker AND enforced on write.
maxSizeintegeroptionalMaximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser.
options{ label: string; value: string; color?: string; default?: boolean; … }[]optionalStatic options for select/multiselect
referencestringoptionalTarget object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects.
referenceViastringoptionalDeclares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. record_id with referenceVia: 'object_name'. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with reference (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior.
deleteBehaviorEnum<'set_null' | 'cascade' | 'restrict'>optional (default: "set_null")What happens if referenced record is deleted
inlineEditboolean | Enum<'grid' | 'form'>optionalEdit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form.
inlineTitlestringoptionalTitle for the inline master-detail grid
inlineColumns{ name: string; label?: string; type?: Enum<'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' | 'select' | 'lookup' | 'file'>; width?: number; … }[]optionalExplicit columns for the inline grid (derived from the child object when omitted). Each entry is a strict, name-keyed column ({ name, label?, type?, … } — objectui GridColumn); identity-only entries ({ name }) hydrate everything else from the child object's fields. Unknown keys and the retired field spelling are refused at parse.
inlineAmountFieldstringoptionalNumeric child field summed for the inline grid total
relatedListboolean | 'primary'optionalShow this child collection as a related list on the parent's detail page (read-side mirror of inlineEdit). false = suppress; true/absent = shown (stacked under the shared "Related" tab); 'primary' = core relationship, promoted to its own tab. Prominence intent, not a layout switch (ADR-0085). A derived related list (relatedList: 'primary') inherits its row order from the child object's DEFAULT list view sort — the isDefault view, or the first declared list item when none is marked default; wire spelling is sort=<field> / sort=-<field>, never $orderby. A child object with no list-view sort emits no ordering parameter, and rows fall back to record-id order.
relatedListTitlestringoptionalTitle for the detail-page related list
relatedListColumnsstring[]optionalExplicit columns for the detail-page related list, as child field names (e.g. ['name', 'status']); derived from the child object (highlightFields → field walk) when omitted. Strings only — labels, cell types and formatting always derive from the child object's field definitions; column objects are refused at parse.
relatedListFilteranyoptionalDeclarative default filter for the detail-page related list: AND-composed with the parent-relationship condition { [referenceField]: parentId } — an authored constraint, never a user-editable suggestion. The related-list tab badge count honors the same composed filter, so counts match the visible rows. Canonical Query-DSL FilterCondition (the same dialect as a query where), e.g. { status: { $ne: 'deleted' } } to hide soft-deleted children.
displayFieldstringoptionalField shown as each candidate's label in the picker/popover (defaults to the referenced object's name/title).
descriptionFieldstringoptionalSecondary field shown under the label in the quick-select popover.
lookupColumns(string | { field: string; label?: string; width?: string; type?: string })[]optionalExplicit columns for the record-picker table; auto-derived from the referenced object when omitted.
lookupPageSizeintegeroptionalRows per page in the record-picker dialog (default 10).
lookupFilters{ field: string; operator: Enum<'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte' | 'contains' | 'in' | 'notIn'>; value: any }[]optionalBase filters restricting which records are selectable (e.g. only active). The structured, picker-honoured lookup filter.
dependsOn(string | { field: string; param?: string })[]optionalDeclares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For lookup/master_detail it scopes the candidate query (string = same local/remote key; {field,param} when the remote filter key differs — the {field,param} form is lookup-only). For select/multiselect/radio the actual per-option rule lives in each option's visibleWhen; list the referenced fields here (string form) so the option list gates and refreshes with the parent.
allowCreatebooleanoptionalAllow inline quick-create from the record picker: when no match exists the user can create a record from the typed text (optimistic dataSource.create with the display field). Best for simple objects whose only required field is the display field.
expressionstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalFormula expression (CEL). e.g. Frecord.amount * 0.1
returnTypeEnum<'number' | 'text' | 'boolean' | 'date'>optionalInferred value type of a formula field (number/text/boolean/date)
summaryOperations{ object: string; field: string; function: Enum<'count' | 'sum' | 'min' | 'max' | 'avg'>; relationshipField?: string; … }optionalRoll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted.
languagestringoptionalProgramming language for syntax highlighting (e.g., javascript, python, sql)
stepnumberoptionalStep increment for slider (default: 1)
currencyConfig{ precision?: integer; currencyMode?: Enum<'dynamic' | 'fixed'>; defaultCurrency?: string }optionalConfiguration for currency field type
dimensionsintegeroptionalVector dimensionality (e.g., 1536 for OpenAI embeddings)
trackHistorybooleanoptionalRender this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field.
groupstringoptionalField group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system")
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalPredicate (CEL) — field is shown only when TRUE (else hidden). e.g. Precord.type == 'invoice'
readonlyWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalPredicate (CEL) — field is read-only when TRUE. e.g. Precord.status == 'paid'
requiredWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalPredicate (CEL) — field is required when TRUE. The only slot; the conditionalRequired alias was removed in protocol 17.
conditionalRequiredneveroptional[REMOVED] conditionalRequired was removed in @objectstack/spec 17 — use requiredWhen. Rename the key; the value (a CEL predicate) is unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.
widgetstringoptionalForm widget override — names a registered field component (resolved as field:<widget>) to render this field instead of the type default. Degrades to the type renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker".
hiddenbooleanoptional (default: false)Hidden from default UI
internalbooleanoptionalNever return this field's value on the generic data path — the engine OMITS the key from find/findOne results, the 201 create body and the by-id update body, on the default projection AND when a client names the field in ?select=. Storage, filtering and indexing are untouched, so a server-side verifier can still match on the column and a purpose-built mint route can still return the value once at creation. The read protection for ADR-0100's third credential channel (auth-subsystem one-way hashes on text columns). Omission, not masking: a mask signals 'a value is set', which carries no information on a required column.
readonlybooleanoptional (default: false)Read-only — never editable in forms, AND server-enforced on BOTH write paths: a non-system write to this field is silently dropped from the payload on UPDATE and on INSERT (a create can no longer directly seed e.g. approval_status: "approved"), symmetric with readonlyWhen. A stripped INSERT field still falls back to its defaultValue. Exempt from the strip on BOTH paths: isSystem writes (seed replay, migration). Exempt on the UPDATE path ONLY: an opt-in "historical" import (preserveAudit) — which admits a whitelist (the audit/timestamp family plus author-declared business readonly fields). On INSERT the exemption does NOT apply: a non-system create that requests preserveAudit still has its readonly fields stripped, and is warned loudly that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. A normal (non-system) import is NOT system-context and still strips.
requiredPermissionsstring[]optional[ADR-0066 D3] Capabilities required to read/edit this field (mask on read, deny on write; AND-gate).
maskingRuleEnum<'phone' | 'id_card' | 'bank_account' | 'email' | 'name'> | { keepHead: integer; keepTail: integer }optionalPartial masking rule enforced by the runtime FieldMasker (single channel — API, UI, export and AI context all see the same masked value). A named preset ('phone' 138**5678, 'id_card' keep 6+4, 'bank_account' keep last 4, 'email' j*@example.com, 'name' keep first char) or { keepHead, keepTail }. Masked for every non-system caller unless the field's requiredPermissions are ALL held (that evaluation is the unmask gate); a permission set marking the field non-readable still deletes it entirely. Deterministic, length-preserving output; masked callers cannot filter/sort/group/aggregate on the field.
ackPlaintextMaskingbooleanoptional[ADR-0100] Affirm a generic password field's plaintext-at-rest / masked-on-read contract is intended, silencing the author-time warning. No effect on non-password fields.
systembooleanoptionalAuto-injected system/audit field (e.g. created_at, updated_by, organization_id). Tools that surface system fields separately from author-declared business fields should branch on this flag.
sortablebooleanoptional (default: true)Whether field is sortable in list views
inlineHelpTextstringoptionalHelp text displayed below the field in forms
placeholderstringoptionalPlaceholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from inlineHelpText (always-visible help rendered beside/under the input) and description (tooltip/developer documentation).
autonumberFormatstringoptional (default: "{0000}")Auto-number format: literal text + {0000} counter, {YYYY}/{MM}/{DD}/{YYYYMMDD} date tokens (business tz), and {field_name} interpolation. Counter resets per rendered prefix (e.g. AD{YYYYMMDD}``{0000} resets daily). Omitted on an autonumber field ⇒ the contract default {0000} (#6555).
externalIdbooleanoptional (default: false)Is external ID for upsert operations
_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: Object.indexes[number]

PropertyTypeRequiredDescription
namestringoptionalIndex name (auto-generated if not provided)
fieldsstring[]Fields included in the index
uniqueboolean | 'global' | 'organization'optional (default: false)Whether the index enforces uniqueness, and at which scope (ADR-0120). 'global' = materialized over exactly fields, no organization column injected — one holder across the whole installation; 'organization' = the driver prepends the NULL-safe organization key part (COALESCE(organization_id, 'global')) at registration — one holder per organization; bare true = deprecated positional spelling of 'global' (warned in 17.x by lint unique/unscoped-declared-index, rejected at protocol 18) — state the scope. 'tenant'/'org' are rejected — the word is 'organization'
typeneveroptional[REMOVED] indexes[].type was removed in @objectstack/spec 17.0.0 (ADR-0049) — no driver ever read it. SqlDriver.syncDeclaredIndexes creates every declared index through knex's table.index() / table.unique(), which cannot express an access method, so the value changed no DDL; its .default('btree') merely made an inert knob show up in every parse output. Delete the key. The index method is the driver/dialect's decision (Postgres defaults to B-tree; gin/gist/fulltext are dialect-specific and are chosen by a database-layer migration when a workload actually needs one). Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.
partialneveroptional[REMOVED] indexes[].partial was removed in @objectstack/spec 17.0.0 (ADR-0049) — no driver ever emitted the WHERE clause, so a declared partial index was materialized as a FULL index and the predicate silently did nothing. Delete the key. Partial indexes are built at the database layer, not the declaration surface: issue CREATE [UNIQUE] INDEX … WHERE <predicate> from a runtime migration (this is what metadata-protocol's ensureOverlayIndex already does for sys_metadata). Drift detection is unaffected — it reads partiality back from the database's own DDL, never from this key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.

Nested Shape: Object.fieldGroups[number]

PropertyTypeRequiredDescription
keystringGroup machine key (snake_case). Referenced by Field.group.
labelstringGroup display label
iconstringoptionalIcon name (Lucide/Material) for the group header
descriptionstringoptionalOptional description shown under the group header
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalSection visibility predicate (CEL) — the whole group (header included) is shown only when TRUE, else hidden (fail-closed). e.g. Precord.type == 'invoice'
collapseEnum<'none' | 'expanded' | 'collapsed'>optional (default: "none")[ADR-0085] Section collapse behaviour: 'none' (always open, no toggle), 'expanded' (collapsible, starts open), 'collapsed' (collapsible, starts closed).
defaultExpandedbooleanoptional[DEPRECATED → collapse] true → 'expanded', false → 'collapsed'.
collapsiblebooleanoptional[DEPRECATED → collapse] Boolean pair with collapsed; use the collapse enum.
collapsedbooleanoptional[DEPRECATED → collapse] Boolean pair with collapsible; use the collapse enum.

Nested Shape: Object.tenancy

PropertyTypeRequiredDescription
enabledbooleanEnable multi-tenancy for this object
tenantFieldstringoptionalColumn this object is tenant-scoped by. Omit it unless the tenant column genuinely is not the platform's: when undeclared the driver falls back to organization_id, the kernel-injected column the RLS predicates and tenantPolicy() also assume. A declared name is honoured only when the object really has that field — otherwise the same organization_id fallback applies. No default is materialized here on purpose.
organizationFieldstringoptionalSTAMP-ONLY: column carrying the organization a row is ABOUT, consulted by the three sanctioned platform-row writers — audit stamping, the approval-row writer (plugin-approvals), and the automation-run recorder (service-automation) — via the shared resolveRecordOrganizationField resolver in @objectstack/metadata-core. It does NOT tenant-scope anything — no read path (applyTenantScope, injectTenantOnInsert, computeTenantLayer0Filter) reads it, so declaring it never walls the object and never hides rows. Declare it only when the organization a row belongs to lives under a column that deliberately is NOT the tenant column: sys_api_key is the shipped example — a credential table that must stay unwalled (enabled: false) while history/revocation audit rows stamp the organization of the key they describe (active_organization_id). Ordinary tenant objects omit it; their stamp column is resolved from tenantField / organization_id already. Honoured only when the object really has the field, like tenantField.

Nested Shape: Object.access

PropertyTypeRequiredDescription
defaultEnum<'public' | 'private'>optional (default: "public")Default exposure posture: public (covered by wildcard grants) | private (needs explicit grant; exempt from wildcard RLS).

Nested Shape: Object.requiredPermissions

PropertyTypeRequiredDescription
readstring[]optionalCapabilities required to read (find/findOne/count/aggregate).
createstring[]optionalCapabilities required to create (insert).
updatestring[]optionalCapabilities required to update (update/transfer/restore).
deletestring[]optionalCapabilities required to delete (delete/purge).

Nested Shape: Object.lifecycle

PropertyTypeRequiredDescription
classEnum<'record' | 'audit' | 'telemetry' | 'transient' | 'event'>Persistence contract: record (business truth, permanent) | audit (compliance ledger) | telemetry (high-freq log) | transient (ephemeral state) | event (bus messages).
retention{ maxAge: string; onlyWhen?: Record<string, string | number | boolean | object | object> }optionalAge-based retention window enforced by the LifecycleService Reaper.
ttl{ field: string; expireAfter: string; onlyWhen?: Record<string, string | number | boolean | object | object> }optionalPer-row TTL auto-expiry (transient/event classes).
storage{ strategy: 'rotation'; shards: integer; unit: Enum<'day' | 'week' | 'month'> }optionalPhysical storage strategy for high-frequency telemetry (LifecycleService Rotator).
archive{ after: string; to: string; keep?: string }optionalCold-store archival (LifecycleService Archiver) — audit-class hot→cold hand-off.
reclaimbooleanoptionalRun driver space reclamation (SQLite incremental_vacuum) after sweeping this object. Default true for non-record classes.

Nested Shape: Object.activityMilestones[number]

PropertyTypeRequiredDescription
fieldstringField to watch (typically a status/stage select).
valuestringThe value the field must transition INTO to fire the milestone.
summarystringActivity summary template; {field} tokens interpolate the record value. e.g. "Deal won: {name}".
typestringoptionalActivity type for the emitted row (default "completed").

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

PropertyTypeRequiredDescription
trackHistorybooleanoptional (default: false)Show the record History tab (audit-trail UI). Pair with per-field trackHistory to pick which field diffs are summarized; audit capture itself is always on for compliance
searchablebooleanoptional (default: true)Index records for global search
apiEnabledbooleanoptional (default: true)Expose object via automatic APIs
apiMethodsEnum<'get' | 'list' | 'create' | 'update' | 'delete' | 'bulk'>[]optionalWhitelist of allowed API operations (six primitives; undefined = all, [] = none)
filesbooleanoptional (default: false)Generic record Attachments panel (sys_attachment). Opt-in: true surfaces the panel and permits attachments to target this object; otherwise any write that makes an attachment target it is rejected (403 FILES_DISABLED) — a create and an update that re-points an existing attachment alike. Field.file/Field.image are independent
feedsbooleanoptional (default: true)Record comments/collaboration feed. Default on; explicit false hides the feed UI and rejects any write that makes a comment target this object (403 FEEDS_DISABLED) — a new comment and an update that re-threads an existing one alike
activitiesbooleanoptional (default: true)Record activity timeline (sys_activity mirror of CRUD). Default on; explicit false stops mirroring and hides the timeline
clonebooleanoptional (default: true)Allow record deep cloning

Nested Shape: Object.publicSharing

PropertyTypeRequiredDescription
enabledbooleanoptional (default: false)Allow records of this object to be published via share link
allowedAudiencesEnum<'public' | 'link_only' | 'signed_in' | 'email'>[]optionalAudiences callers may select when creating a link
allowedPermissionsEnum<'view' | 'comment' | 'edit'>[]optionalPermission levels selectable on the share dialog
maxExpiryDaysintegeroptionalReject links with expiry beyond this many days
redactFieldsstring[]optionalField names removed from records served via a share token
eligibilitystringoptionalCEL expression that must evaluate to true on the target record

Nested Shape: Object.actions[number]

PropertyTypeRequiredDescription
namestringMachine name (lowercase snake_case)
labelstring | Record<string, string>Display label
descriptionstring | Record<string, string>optionalExplanatory line shown under the title in the action's param dialog. Carries the confirm question for an action that collects params (one dialog, not two —). Not the LLM-facing ai.description.
objectNamestringoptionalTarget object this action belongs to. When set, the action is auto-merged into the object's actions array by defineStack().
iconstringoptionalIcon name
locationsEnum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | …>[]optionalLocations where this action is visible
componentEnum<'action:button' | 'action:icon' | 'action:menu' | 'action:group'>optionalVisual component override
typeEnum<'script' | 'url' | 'modal' | 'flow' | 'api' | 'form'>optional (default: "script")Action functionality type
targetstringoptionalURL, Script Name, Flow ID, or API Endpoint. Supports ${param.X} and ${ctx.X} interpolation.
openInEnum<'self' | 'new-tab'>optionalFor type:'url' — where to open target. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of params (which is user-input-collection only).
body{ language: 'expression'; source: string } | { language: 'js'; source: string; capabilities?: Enum<'api.read' | 'api.write' | 'api.transaction' | 'crypto.uuid' | 'log'>[]; timeoutMs?: integer; … }optionalAction body — expression (L1) or sandboxed JS (L2). Only used when type is script.
executeneveroptional[REMOVED] execute was removed in @objectstack/spec 17 — use target. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.
params{ name?: string; field?: string; objectOverride?: string; label?: string | Record<string, string>; … }[]optionalInput parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in bodyExtra).
variantEnum<'primary' | 'secondary' | 'danger' | 'ghost' | 'link'>optionalButton visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent)
ordernumberoptionalSort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without order keep their registration order.
confirmTextstring | Record<string, string>optionalConfirmation message before execution. On a registered action, pairing this with a non-empty params is refused — that opens a second dialog for one decision; put the question on description instead. Correct on a param-LESS action, where the confirm is the only dialog there is.
successMessagestring | Record<string, string>optionalSuccess message to show after execution
errorMessagestring | Record<string, string>optionalError message to show when the action fails (overrides the raw error).
refreshAfterbooleanoptional (default: false)Refresh view after execution
undoablebooleanoptionalOffer an Undo affordance after this single-record update action succeeds.
resultDialog{ title?: string | Record<string, string>; description?: string | Record<string, string>; acknowledge?: string | Record<string, string>; format?: Enum<'qrcode' | 'code-list' | 'secret' | 'text' | 'json'>; … }optionalRender API response in a one-shot reveal dialog (suppresses successMessage when set).
visibleboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalVisibility predicate — true/false literal, CEL string, or {dialect, source} envelope. The action is offered when it evaluates TRUE. Omit = always visible.
requiresFeatureEnum<'twoFactor' | 'organization' | 'multiOrgEnabled' | 'degradedTenancy' | …>optionalPublic auth feature flag gating this action; lowered into visible at parse time.
disabledboolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalDisabled predicate — true/false literal, CEL string, or {dialect, source} envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled.
requiredPermissionsstring[]optional[ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a type: api action pointed at a custom endpoint must re-check it there.
shortcutneveroptional[REMOVED] action.shortcut was removed in @objectstack/spec 17.0.0 (audit close-out) — it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults action metadata. Delete the key. For a real shortcut, register the key in the Console keyboard stack and have its handler invoke the action by name. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.
bulkEnabledneveroptional[REMOVED] action.bulkEnabled was removed in @objectstack/spec 17.0.0 (audit close-out) — the multi-select toolbar is driven by the LIST VIEW's bulkActions / bulkActionDefs, never by this flag, so setting it changed nothing. Delete the key and declare the action in the view's bulkActions instead. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.
ai{ exposed?: boolean; description?: string; category?: Enum<'data' | 'action' | 'flow' | 'integration' | 'vector_search' | 'analytics' | 'utility'>; paramHints?: Record<string, object>; … }optionalAI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents.
recordIdParamstringoptionalBody key to inject the row id into when running from a list_item context.
recordIdFieldstringoptionalRow field whose value seeds recordIdParam. Defaults to "id".
bodyShape'flat' | { wrap: string }optionalBody wrapping: flat (default) or { wrap: key } to nest user-collected params under a key.
methodEnum<'POST' | 'PATCH' | 'PUT' | 'DELETE'>optionalHTTP method for type:"api" actions. Defaults to POST.
bodyExtraRecord<string, any>optionalStatic request-body fields for a type:"api" action, merged last (overrides user params). {{page.<var>}} tokens are resolved by the runtime. This — not params — is where a payload goes.
modeEnum<'create' | 'edit' | 'delete' | 'custom'>optionalSemantic mode of the action.
opensInNewTabbooleanoptionalOpen the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl.
newTabUrlstringoptionalDirect new-tab URL template ({recordId} placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself.
onSuccess{ navigate: string; openIn?: Enum<'self' | 'newTab'> }optionalPost-success navigation for type:'api' and type:'script' actions. navigate is a route/URL template interpolating ${param.*}, ${ctx.*} and ${result.*} (the server response); openIn defaults 'self'. The handler-return convention ({ redirectUrl } without openIn) keeps its 17.0.0 new-tab behavior.
aria{ ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string }optionalARIA accessibility attributes
_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: Object.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.

ObjectAccessConfig

Properties

PropertyTypeRequiredDescription
defaultEnum<'public' | 'private'>optional (default: "public")Default exposure posture: public (covered by wildcard grants) | private (needs explicit grant; exempt from wildcard RLS).

ObjectCapabilities

Properties

PropertyTypeRequiredDescription
trackHistorybooleanoptional (default: false)Show the record History tab (audit-trail UI). Pair with per-field trackHistory to pick which field diffs are summarized; audit capture itself is always on for compliance
searchablebooleanoptional (default: true)Index records for global search
apiEnabledbooleanoptional (default: true)Expose object via automatic APIs
apiMethodsEnum<'get' | 'list' | 'create' | 'update' | 'delete' | 'bulk'>[]optionalWhitelist of allowed API operations (six primitives; undefined = all, [] = none)
filesbooleanoptional (default: false)Generic record Attachments panel (sys_attachment). Opt-in: true surfaces the panel and permits attachments to target this object; otherwise any write that makes an attachment target it is rejected (403 FILES_DISABLED) — a create and an update that re-points an existing attachment alike. Field.file/Field.image are independent
feedsbooleanoptional (default: true)Record comments/collaboration feed. Default on; explicit false hides the feed UI and rejects any write that makes a comment target this object (403 FEEDS_DISABLED) — a new comment and an update that re-threads an existing one alike
activitiesbooleanoptional (default: true)Record activity timeline (sys_activity mirror of CRUD). Default on; explicit false stops mirroring and hides the timeline
clonebooleanoptional (default: true)Allow record deep cloning

ObjectExtension

Properties

PropertyTypeRequiredDescription
extendstringTarget object name (FQN) to extend
fieldsRecord<string, { name?: string; label?: string; type: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>; description?: string; … }>optionalFields to add/override
labelstringoptionalOverride label for the extended object
pluralLabelstringoptionalOverride plural label for the extended object
descriptionstringoptionalOverride description for the extended object
validationsany[]optionalAdditional validation rules to merge into the target object
indexes{ name?: string; fields: string[]; unique?: boolean | 'global' | 'organization' }[]optionalAdditional indexes to merge into the target object
priorityintegeroptional (default: 200)Merge priority (higher = applied later)

Nested Shape: ObjectExtension.fields[string]

PropertyTypeRequiredDescription
namestringoptionalMachine name (snake_case)
labelstringoptionalHuman readable label
typeEnum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | …>Field Data Type
descriptionstringoptionalTooltip/Help text
formatstringoptionalFormat string (e.g. email, phone)
requiredbooleanoptional (default: false)Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (multiple: true) required means NON-EMPTY array — an emptied required set fails validation loudly; [] does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (storage.notNull), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field.
storage{ notNull?: boolean }optionalPhysical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested.
searchablebooleanoptional (default: false)Is searchable
multiplebooleanoptional (default: false)Allow multiple values (Stores as Array/JSON). Applicable for select, lookup, file, image. An emptied multi-value lookup reads back as [], never null — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair (maintainer ruling 2026-08-18).
uniqueboolean | 'global' | 'organization'optional (default: false)Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization'
defaultValueanyoptionalDefault applied on INSERT when the field is omitted or null ('' is a real value, not absence). Three legal shapes, discriminated in the engine's own order: a CEL Expression envelope { dialect: 'cel', source: 'today()' } (accepted structurally; result type is a runtime concern); a runtime TOKEN — NOW() on datetime/date/time only, current_user on user or lookup with reference: 'sys_user' only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 valueSchemaFor). Anything else is refused at parse time with a prescriptive message.
maxLengthintegeroptionalMax character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode.
minLengthintegeroptionalMin character length (positive integer; minLength: 0 is refused — express "no minimum" by omitting the key). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode.
precisionintegeroptionalTotal digits (non-negative integer)
scaleintegeroptionalDecimal places (non-negative integer)
minnumberoptionalMinimum value
maxnumberoptionalMaximum value
useGroupingbooleanoptionalDigit-grouping presentation hint for number fields — maps to Intl.NumberFormat's useGrouping. Absent = renderer decides (interim heuristic today, locale default eventually); false = author opts out of grouping (e.g. a year or other ordinal/identifier integer); true = author pins grouping on.
acceptstring[]optionalPermitted upload types for media fields, as MIME types or extensions (e.g. ["image/*", ".pdf"]). Offered to the file picker AND enforced on write.
maxSizeintegeroptionalMaximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser.
options{ label: string; value: string; color?: string; default?: boolean; … }[]optionalStatic options for select/multiselect
referencestringoptionalTarget object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects.
referenceViastringoptionalDeclares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. record_id with referenceVia: 'object_name'. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with reference (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior.
deleteBehaviorEnum<'set_null' | 'cascade' | 'restrict'>optional (default: "set_null")What happens if referenced record is deleted
inlineEditboolean | Enum<'grid' | 'form'>optionalEdit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form.
inlineTitlestringoptionalTitle for the inline master-detail grid
inlineColumns{ name: string; label?: string; type?: Enum<'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' | 'select' | 'lookup' | 'file'>; width?: number; … }[]optionalExplicit columns for the inline grid (derived from the child object when omitted). Each entry is a strict, name-keyed column ({ name, label?, type?, … } — objectui GridColumn); identity-only entries ({ name }) hydrate everything else from the child object's fields. Unknown keys and the retired field spelling are refused at parse.
inlineAmountFieldstringoptionalNumeric child field summed for the inline grid total
relatedListboolean | 'primary'optionalShow this child collection as a related list on the parent's detail page (read-side mirror of inlineEdit). false = suppress; true/absent = shown (stacked under the shared "Related" tab); 'primary' = core relationship, promoted to its own tab. Prominence intent, not a layout switch (ADR-0085). A derived related list (relatedList: 'primary') inherits its row order from the child object's DEFAULT list view sort — the isDefault view, or the first declared list item when none is marked default; wire spelling is sort=<field> / sort=-<field>, never $orderby. A child object with no list-view sort emits no ordering parameter, and rows fall back to record-id order.
relatedListTitlestringoptionalTitle for the detail-page related list
relatedListColumnsstring[]optionalExplicit columns for the detail-page related list, as child field names (e.g. ['name', 'status']); derived from the child object (highlightFields → field walk) when omitted. Strings only — labels, cell types and formatting always derive from the child object's field definitions; column objects are refused at parse.
relatedListFilteranyoptionalDeclarative default filter for the detail-page related list: AND-composed with the parent-relationship condition { [referenceField]: parentId } — an authored constraint, never a user-editable suggestion. The related-list tab badge count honors the same composed filter, so counts match the visible rows. Canonical Query-DSL FilterCondition (the same dialect as a query where), e.g. { status: { $ne: 'deleted' } } to hide soft-deleted children.
displayFieldstringoptionalField shown as each candidate's label in the picker/popover (defaults to the referenced object's name/title).
descriptionFieldstringoptionalSecondary field shown under the label in the quick-select popover.
lookupColumns(string | { field: string; label?: string; width?: string; type?: string })[]optionalExplicit columns for the record-picker table; auto-derived from the referenced object when omitted.
lookupPageSizeintegeroptionalRows per page in the record-picker dialog (default 10).
lookupFilters{ field: string; operator: Enum<'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte' | 'contains' | 'in' | 'notIn'>; value: any }[]optionalBase filters restricting which records are selectable (e.g. only active). The structured, picker-honoured lookup filter.
dependsOn(string | { field: string; param?: string })[]optionalDeclares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For lookup/master_detail it scopes the candidate query (string = same local/remote key; {field,param} when the remote filter key differs — the {field,param} form is lookup-only). For select/multiselect/radio the actual per-option rule lives in each option's visibleWhen; list the referenced fields here (string form) so the option list gates and refreshes with the parent.
allowCreatebooleanoptionalAllow inline quick-create from the record picker: when no match exists the user can create a record from the typed text (optimistic dataSource.create with the display field). Best for simple objects whose only required field is the display field.
expressionstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalFormula expression (CEL). e.g. Frecord.amount * 0.1
returnTypeEnum<'number' | 'text' | 'boolean' | 'date'>optionalInferred value type of a formula field (number/text/boolean/date)
summaryOperations{ object: string; field: string; function: Enum<'count' | 'sum' | 'min' | 'max' | 'avg'>; relationshipField?: string; … }optionalRoll-up summary definition. The engine recomputes the value when child records are inserted/updated/deleted.
languagestringoptionalProgramming language for syntax highlighting (e.g., javascript, python, sql)
stepnumberoptionalStep increment for slider (default: 1)
currencyConfig{ precision?: integer; currencyMode?: Enum<'dynamic' | 'fixed'>; defaultCurrency?: string }optionalConfiguration for currency field type
dimensionsintegeroptionalVector dimensionality (e.g., 1536 for OpenAI embeddings)
trackHistorybooleanoptionalRender this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field.
groupstringoptionalField group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system")
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalPredicate (CEL) — field is shown only when TRUE (else hidden). e.g. Precord.type == 'invoice'
readonlyWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalPredicate (CEL) — field is read-only when TRUE. e.g. Precord.status == 'paid'
requiredWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalPredicate (CEL) — field is required when TRUE. The only slot; the conditionalRequired alias was removed in protocol 17.
conditionalRequiredneveroptional[REMOVED] conditionalRequired was removed in @objectstack/spec 17 — use requiredWhen. Rename the key; the value (a CEL predicate) is unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.
widgetstringoptionalForm widget override — names a registered field component (resolved as field:<widget>) to render this field instead of the type default. Degrades to the type renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker".
hiddenbooleanoptional (default: false)Hidden from default UI
internalbooleanoptionalNever return this field's value on the generic data path — the engine OMITS the key from find/findOne results, the 201 create body and the by-id update body, on the default projection AND when a client names the field in ?select=. Storage, filtering and indexing are untouched, so a server-side verifier can still match on the column and a purpose-built mint route can still return the value once at creation. The read protection for ADR-0100's third credential channel (auth-subsystem one-way hashes on text columns). Omission, not masking: a mask signals 'a value is set', which carries no information on a required column.
readonlybooleanoptional (default: false)Read-only — never editable in forms, AND server-enforced on BOTH write paths: a non-system write to this field is silently dropped from the payload on UPDATE and on INSERT (a create can no longer directly seed e.g. approval_status: "approved"), symmetric with readonlyWhen. A stripped INSERT field still falls back to its defaultValue. Exempt from the strip on BOTH paths: isSystem writes (seed replay, migration). Exempt on the UPDATE path ONLY: an opt-in "historical" import (preserveAudit) — which admits a whitelist (the audit/timestamp family plus author-declared business readonly fields). On INSERT the exemption does NOT apply: a non-system create that requests preserveAudit still has its readonly fields stripped, and is warned loudly that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. A normal (non-system) import is NOT system-context and still strips.
requiredPermissionsstring[]optional[ADR-0066 D3] Capabilities required to read/edit this field (mask on read, deny on write; AND-gate).
maskingRuleEnum<'phone' | 'id_card' | 'bank_account' | 'email' | 'name'> | { keepHead: integer; keepTail: integer }optionalPartial masking rule enforced by the runtime FieldMasker (single channel — API, UI, export and AI context all see the same masked value). A named preset ('phone' 138**5678, 'id_card' keep 6+4, 'bank_account' keep last 4, 'email' j*@example.com, 'name' keep first char) or { keepHead, keepTail }. Masked for every non-system caller unless the field's requiredPermissions are ALL held (that evaluation is the unmask gate); a permission set marking the field non-readable still deletes it entirely. Deterministic, length-preserving output; masked callers cannot filter/sort/group/aggregate on the field.
ackPlaintextMaskingbooleanoptional[ADR-0100] Affirm a generic password field's plaintext-at-rest / masked-on-read contract is intended, silencing the author-time warning. No effect on non-password fields.
systembooleanoptionalAuto-injected system/audit field (e.g. created_at, updated_by, organization_id). Tools that surface system fields separately from author-declared business fields should branch on this flag.
sortablebooleanoptional (default: true)Whether field is sortable in list views
inlineHelpTextstringoptionalHelp text displayed below the field in forms
placeholderstringoptionalPlaceholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from inlineHelpText (always-visible help rendered beside/under the input) and description (tooltip/developer documentation).
autonumberFormatstringoptional (default: "{0000}")Auto-number format: literal text + {0000} counter, {YYYY}/{MM}/{DD}/{YYYYMMDD} date tokens (business tz), and {field_name} interpolation. Counter resets per rendered prefix (e.g. AD{YYYYMMDD}``{0000} resets daily). Omitted on an autonumber field ⇒ the contract default {0000} (#6555).
externalIdbooleanoptional (default: false)Is external ID for upsert operations
_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: ObjectExtension.indexes[number]

PropertyTypeRequiredDescription
namestringoptionalIndex name (auto-generated if not provided)
fieldsstring[]Fields included in the index
uniqueboolean | 'global' | 'organization'optional (default: false)Whether the index enforces uniqueness, and at which scope (ADR-0120). 'global' = materialized over exactly fields, no organization column injected — one holder across the whole installation; 'organization' = the driver prepends the NULL-safe organization key part (COALESCE(organization_id, 'global')) at registration — one holder per organization; bare true = deprecated positional spelling of 'global' (warned in 17.x by lint unique/unscoped-declared-index, rejected at protocol 18) — state the scope. 'tenant'/'org' are rejected — the word is 'organization'
typeneveroptional[REMOVED] indexes[].type was removed in @objectstack/spec 17.0.0 (ADR-0049) — no driver ever read it. SqlDriver.syncDeclaredIndexes creates every declared index through knex's table.index() / table.unique(), which cannot express an access method, so the value changed no DDL; its .default('btree') merely made an inert knob show up in every parse output. Delete the key. The index method is the driver/dialect's decision (Postgres defaults to B-tree; gin/gist/fulltext are dialect-specific and are chosen by a database-layer migration when a workload actually needs one). Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.
partialneveroptional[REMOVED] indexes[].partial was removed in @objectstack/spec 17.0.0 (ADR-0049) — no driver ever emitted the WHERE clause, so a declared partial index was materialized as a FULL index and the predicate silently did nothing. Delete the key. Partial indexes are built at the database layer, not the declaration surface: issue CREATE [UNIQUE] INDEX … WHERE <predicate> from a runtime migration (this is what metadata-protocol's ensureOverlayIndex already does for sys_metadata). Drift detection is unaffected — it reads partiality back from the database's own DDL, never from this key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.

ObjectExternalBinding

External datasource binding (ADR-0015)

Properties

PropertyTypeRequiredDescription
remoteNamestringoptionalRemote table/view name. Defaults to object.name.
remoteSchemastringoptionalRemote schema/database qualifier.
writablebooleanoptional (default: false)Per-object write opt-in (also requires datasource.external.allowWrites).
columnMapRecord<string, string>optionalRemote column name → local field name.
introspectedAtstringoptionalSet by os datasource introspect; informational.
ignoreColumnsstring[]optionalRemote columns to skip during validation (dev convenience).

ObjectFieldGroup

Properties

PropertyTypeRequiredDescription
keystringGroup machine key (snake_case). Referenced by Field.group.
labelstringGroup display label
iconstringoptionalIcon name (Lucide/Material) for the group header
descriptionstringoptionalOptional description shown under the group header
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalSection visibility predicate (CEL) — the whole group (header included) is shown only when TRUE, else hidden (fail-closed). e.g. Precord.type == 'invoice'
collapseEnum<'none' | 'expanded' | 'collapsed'>optional (default: "none")[ADR-0085] Section collapse behaviour: 'none' (always open, no toggle), 'expanded' (collapsible, starts open), 'collapsed' (collapsible, starts closed).
defaultExpandedbooleanoptional[DEPRECATED → collapse] true → 'expanded', false → 'collapsed'.
collapsiblebooleanoptional[DEPRECATED → collapse] Boolean pair with collapsed; use the collapse enum.
collapsedbooleanoptional[DEPRECATED → collapse] Boolean pair with collapsible; use the collapse enum.

ObjectOwnershipEnum

Allowed Values

  • own
  • extend
  • overlay

ObjectRequiredPermissions

Union Options

This schema accepts one of the following structures:

Option 1

Type: string[]


Option 2

Properties

PropertyTypeRequiredDescription
readstring[]optionalCapabilities required to read (find/findOne/count/aggregate).
createstring[]optionalCapabilities required to create (insert).
updatestring[]optionalCapabilities required to update (update/transfer/restore).
deletestring[]optionalCapabilities required to delete (delete/purge).


PerOperationRequiredPermissions

Properties

PropertyTypeRequiredDescription
readstring[]optionalCapabilities required to read (find/findOne/count/aggregate).
createstring[]optionalCapabilities required to create (insert).
updatestring[]optionalCapabilities required to update (update/transfer/restore).
deletestring[]optionalCapabilities required to delete (delete/purge).

RowCrudActionOverride

Boolean-or-predicates override for a built-in CRUD affordance.

Properties

PropertyTypeRequiredDescription
enabledbooleanoptionalObject-level on/off for the generic affordance; same meaning as the bare boolean form. Omitted → managedBy bucket default.
visibleWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalCEL predicate over the record in scope (row record for edit/delete, host record for a related-list create/import toolbar); false → hide the button. Fail-closed.
disabledWhenstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalCEL predicate over the record in scope (row record for edit/delete, host record for a related-list create/import toolbar); true → render the button disabled. Fail-soft.

TenancyConfig

Properties

PropertyTypeRequiredDescription
enabledbooleanEnable multi-tenancy for this object
tenantFieldstringoptionalColumn this object is tenant-scoped by. Omit it unless the tenant column genuinely is not the platform's: when undeclared the driver falls back to organization_id, the kernel-injected column the RLS predicates and tenantPolicy() also assume. A declared name is honoured only when the object really has that field — otherwise the same organization_id fallback applies. No default is materialized here on purpose.
organizationFieldstringoptionalSTAMP-ONLY: column carrying the organization a row is ABOUT, consulted by the three sanctioned platform-row writers — audit stamping, the approval-row writer (plugin-approvals), and the automation-run recorder (service-automation) — via the shared resolveRecordOrganizationField resolver in @objectstack/metadata-core. It does NOT tenant-scope anything — no read path (applyTenantScope, injectTenantOnInsert, computeTenantLayer0Filter) reads it, so declaring it never walls the object and never hides rows. Declare it only when the organization a row belongs to lives under a column that deliberately is NOT the tenant column: sys_api_key is the shipped example — a credential table that must stay unwalled (enabled: false) while history/revocation audit rows stamp the organization of the key they describe (active_organization_id). Ordinary tenant objects omit it; their stamp column is resolved from tenantField / organization_id already. Honoured only when the object really has the field, like tenantField.

On this page