Object
Object protocol schemas
API Operations Enum
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
getlistcreateupdatedeletebulk
ApiOperation
Allowed Values
getlistcreateupdatedeleteupsertbulkaggregatehistorysearchrestorepurgeimportexport
Index
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Index name (auto-generated if not provided) |
| fields | string[] | ✅ | Fields included in the index |
| unique | boolean | 'global' | 'organization' | ✅ | 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, #5082) — state the scope. 'tenant'/'org' are rejected — the word is 'organization' |
| type | never | optional | [REMOVED] indexes[].type was removed in @objectstack/spec 17.0.0 (#5248, 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 rewrite existing sources automatically. |
| partial | never | optional | [REMOVED] indexes[].partial was removed in @objectstack/spec 17.0.0 (#5248, #4943, 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 rewrite existing sources automatically. |
Lifecycle
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| class | Enum<'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> } | optional | Age-based retention window enforced by the LifecycleService Reaper. |
| ttl | { field: string; expireAfter: string } | optional | Per-row TTL auto-expiry (transient/event classes). |
| storage | { strategy: 'rotation'; shards: integer; unit: Enum<'day' | 'week' | 'month'> } | optional | Physical storage strategy for high-frequency telemetry (LifecycleService Rotator). |
| archive | { after: string; to: string; keep?: string } | optional | Cold-store archival (LifecycleService Archiver) — audit-class hot→cold hand-off. |
| reclaim | boolean | optional | Run driver space reclamation (SQLite incremental_vacuum) after sweeping this object. Default true for non-record classes. |
LifecycleClass
Allowed Values
recordaudittelemetrytransientevent
Object
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Machine unique key (snake_case). Immutable. |
| label | string | optional | Human readable singular label (e.g. "Account") |
| pluralLabel | string | optional | Human readable plural label (e.g. "Accounts") |
| description | string | optional | Developer documentation / description |
| icon | string | optional | Icon name (Lucide/Material) for UI representation |
| isSystem | boolean | optional | Is system object (protected from deletion; defaults its org-wide sharing to public when no sharingModel is set — plugin-sharing) |
| managedBy | Enum<'platform' | 'config' | 'system-data' | 'engine-owned' | 'append-only' | 'better-auth'> | optional | Lifecycle 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. |
| ownership | Enum<'user' | 'business_unit' | 'org' | 'none'> | optional | Record-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; … } | optional | Per-object override of the resolved CRUD affordance matrix. |
| systemFields | false | { tenant?: boolean; audit?: boolean } | optional | Opt out of, or selectively disable, registry-level system-field auto-injection. |
| datasource | string | optional | Target Datasource ID. "default" is the primary DB. |
| external | { remoteName?: string; remoteSchema?: string; writable?: boolean; columnMap?: Record<string, string>; … } | optional | Remote table binding for federated (external) objects. |
| fields | Record<string, { name?: string; label?: string; type: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | … +42 more>; description?: string; … }> | ✅ | Field definitions map. Keys must be snake_case identifiers. |
| indexes | { name?: string; fields: string[]; unique?: boolean | 'global' | 'organization' }[] | optional | Database performance indexes |
| fieldGroups | { key: string; label: string; icon?: string; description?: string; … }[] | optional | Ordered list of field groups (array order = display order). See ObjectFieldGroupSchema. |
| tenancy | { enabled: boolean; tenantField?: string } | optional | Multi-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). |
| requiredPermissions | string[] | { 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; … } | optional | Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService. |
| fileAccessDelegate | string | optional | Kernel 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. |
| validations | any[] | optional | Object-level validation rules |
| activityMilestones | { field: string; value: string; summary: string; type?: string }[] | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
| nameField | string | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
| displayNameField | string | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
| titleFormat | string | { 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. |
| highlightFields | string[] | 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. |
| stageField | string | false | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
| listViews | Record<string, { name?: string; label?: string | Record<string, string>; type?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>; data?: object | … +3 more; … }> | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) |
| searchableFields | string[] | optional | Fields 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 (#6674) — 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'>[]; … } | optional | Enabled system features modules |
| sharingModel | Enum<'private' | 'public_read' | 'public_read_write' | 'controlled_by_parent'> | optional | Org-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). |
| externalSharingModel | Enum<'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; … } | optional | Public share-link policy (Notion/Figma-style link sharing) |
| actions | { name: string; label: string | Record<string, string>; description?: string | Record<string, string>; objectName?: string; … }[] | optional | Actions 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 } | optional | Package author protection block — lock policy for this object. |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
ObjectAccessConfig
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| default | Enum<'public' | 'private'> | ✅ | Default exposure posture: public (covered by wildcard grants) | private (needs explicit grant; exempt from wildcard RLS). |
ObjectCapabilities
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| trackHistory | boolean | ✅ | 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 |
| searchable | boolean | ✅ | Index records for global search |
| apiEnabled | boolean | ✅ | Expose object via automatic APIs |
| apiMethods | Enum<'get' | 'list' | 'create' | 'update' | 'delete' | 'bulk'>[] | optional | Whitelist of allowed API operations (six primitives; undefined = all, [] = none) |
| files | boolean | ✅ | Generic record Attachments panel (sys_attachment). Opt-in: true surfaces the panel and permits attachments targeting this object; otherwise creation is rejected. Field.file/Field.image are independent |
| feeds | boolean | ✅ | Record comments/collaboration feed. Default on; explicit false hides the feed UI and rejects new comments for this object |
| activities | boolean | ✅ | Record activity timeline (sys_activity mirror of CRUD). Default on; explicit false stops mirroring and hides the timeline |
| clone | boolean | ✅ | Allow record deep cloning |
ObjectExtension
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| extend | string | ✅ | Target object name (FQN) to extend |
| fields | Record<string, { name?: string; label?: string; type: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | … +42 more>; description?: string; … }> | optional | Fields to add/override |
| label | string | optional | Override label for the extended object |
| pluralLabel | string | optional | Override plural label for the extended object |
| description | string | optional | Override description for the extended object |
| validations | any[] | optional | Additional validation rules to merge into the target object |
| indexes | { name?: string; fields: string[]; unique?: boolean | 'global' | 'organization' }[] | optional | Additional indexes to merge into the target object |
| priority | integer | optional | Merge priority (higher = applied later) |
ObjectExternalBinding
External datasource binding (ADR-0015)
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| remoteName | string | optional | Remote table/view name. Defaults to object.name. |
| remoteSchema | string | optional | Remote schema/database qualifier. |
| writable | boolean | ✅ | Per-object write opt-in (also requires datasource.external.allowWrites). |
| columnMap | Record<string, string> | optional | Remote column name → local field name. |
| introspectedAt | string | optional | Set by os datasource introspect; informational. |
| ignoreColumns | string[] | optional | Remote columns to skip during validation (dev convenience). |
ObjectFieldGroup
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| key | string | ✅ | Group machine key (snake_case). Referenced by Field.group. |
| label | string | ✅ | Group display label |
| icon | string | optional | Icon name (Lucide/Material) for the group header |
| description | string | optional | Optional description shown under the group header |
| collapse | Enum<'none' | 'expanded' | 'collapsed'> | ✅ | [ADR-0085] Section collapse behaviour: 'none' (always open, no toggle), 'expanded' (collapsible, starts open), 'collapsed' (collapsible, starts closed). |
| defaultExpanded | boolean | optional | [DEPRECATED → collapse] true → 'expanded', false → 'collapsed'. |
| collapsible | boolean | optional | [DEPRECATED → collapse] Boolean pair with collapsed; use the collapse enum. |
| collapsed | boolean | optional | [DEPRECATED → collapse] Boolean pair with collapsible; use the collapse enum. |
ObjectOwnershipEnum
Allowed Values
ownextendoverlay
ObjectRequiredPermissions
Union Options
This schema accepts one of the following structures:
Option 1
Type: string[]
Option 2
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| read | string[] | optional | Capabilities required to read (find/findOne/count/aggregate). |
| create | string[] | optional | Capabilities required to create (insert). |
| update | string[] | optional | Capabilities required to update (update/transfer/restore). |
| delete | string[] | optional | Capabilities required to delete (delete/purge). |
PerOperationRequiredPermissions
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| read | string[] | optional | Capabilities required to read (find/findOne/count/aggregate). |
| create | string[] | optional | Capabilities required to create (insert). |
| update | string[] | optional | Capabilities required to update (update/transfer/restore). |
| delete | string[] | optional | Capabilities required to delete (delete/purge). |
RowCrudActionOverride
Boolean-or-predicates override for a built-in CRUD affordance.
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | optional | Object-level on/off for the generic affordance; same meaning as the bare boolean form. Omitted → managedBy bucket default. |
| visibleWhen | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | optional | CEL 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. |
| disabledWhen | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | optional | CEL 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
| Property | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | ✅ | Enable multi-tenancy for this object |
| tenantField | string | optional | Column 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 (#5315). |