Protocol schema — API Protocol reference
Protocol schemas of the ObjectStack API Protocol: AiAgentCapabilities and 136 more — each property with its type, default and a TypeScript example.
Source: packages/spec/src/api/protocol.zod.ts
TypeScript Usage
import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AuditMetaItemRequestSchema, AuditMetaItemResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CloneDataResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DiffMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, FindReferencesToMetaResponseSchema, FlowRegistrationSchema, FlowRegistrationRefusalSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaDiagnosticsResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetPublishedMetaItemResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HistoryMetaItemRequestSchema, HistoryMetaItemResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListDraftsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, MetadataConflictErrorSchema, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllPageHitSchema, SearchAllResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api';
import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AuditMetaItemRequest, AuditMetaItemResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CloneDataResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DiffMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, FindReferencesToMetaResponse, FlowRegistration, FlowRegistrationRefusal, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaDiagnosticsResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetPublishedMetaItemResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, HistoryMetaItemRequest, HistoryMetaItemResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListDraftsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, MetadataConflictError, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllPageHit, SearchAllResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api';
// Validate data
const result = AiAgentCapabilitiesSchema.parse(data);AiAgentCapabilities
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| authoring | boolean | ✅ | Authors app metadata (objects/views/flows) |
| canvas | boolean | ✅ | Drives the Live Canvas split view (ADR-0037) |
| debug | boolean | ✅ | Exposes the build-doctor debug drawer |
| resume | boolean | ✅ | Turns resume durable multi-step runs (ADR-0013) |
AiAgentChatRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| messages | ({ role: Enum<'system' | 'user' | 'assistant' | 'tool'>; content?: any; parts?: any[] } & Record<string, any>)[] | ✅ | Conversation messages (at least one) |
| context | Record<string, any> | optional | Agent context (app, object, record, …) |
| options | Record<string, any> | optional | Request options (model, temperature, …) |
Nested Shape: AiAgentChatRequest.messages[number]
| Property | Type | Required | Description |
|---|---|---|---|
| role | Enum<'system' | 'user' | 'assistant' | 'tool'> | ✅ | Message role |
| content | any | optional | Message content: a string, or an array of content parts |
| parts | any[] | optional | Vercel AI SDK v6 message parts (alternative to content) |
AiAgentSummary
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Agent name — the :agentName path segment |
| label | string | ✅ | Display label |
| role | string | ✅ | Agent role |
| capabilities | { authoring: boolean; canvas: boolean; debug: boolean; resume: boolean } | ✅ | Capability set implied by the agent surface |
Nested Shape: AiAgentSummary.capabilities
| Property | Type | Required | Description |
|---|---|---|---|
| authoring | boolean | ✅ | Authors app metadata (objects/views/flows) |
| canvas | boolean | ✅ | Drives the Live Canvas split view (ADR-0037) |
| debug | boolean | ✅ | Exposes the build-doctor debug drawer |
| resume | boolean | ✅ | Turns resume durable multi-step runs (ADR-0013) |
AiAgentsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| agents | { name: string; label: string; role: string; capabilities: object }[] | ✅ | Agents this caller may chat with |
Nested Shape: AiAgentsResponse.agents[number]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Agent name — the :agentName path segment |
| label | string | ✅ | Display label |
| role | string | ✅ | Agent role |
| capabilities | { authoring: boolean; canvas: boolean; debug: boolean; resume: boolean } | ✅ | Capability set implied by the agent surface |
AiChatRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| messages | ({ role: Enum<'system' | 'user' | 'assistant' | 'tool'>; content?: any; parts?: any[] } & Record<string, any>)[] | ✅ | Conversation messages (at least one) |
| system | string | optional | System prompt, prepended as a system message |
| model | string | optional | Model id override |
| temperature | number | optional | Sampling temperature |
| maxTokens | integer | optional | Maximum tokens to generate |
| stream | boolean | optional | false → JSON response; otherwise the UI Message Stream Protocol |
| conversationId | string | optional | Conversation to persist this turn into (auto-created when omitted) |
| turnId | string | optional | Stable per-turn idempotency key (ADR-0013 D1) |
| options | Record<string, any> | optional | Legacy nested request options |
Nested Shape: AiChatRequest.messages[number]
| Property | Type | Required | Description |
|---|---|---|---|
| role | Enum<'system' | 'user' | 'assistant' | 'tool'> | ✅ | Message role |
| content | any | optional | Message content: a string, or an array of content parts |
| parts | any[] | optional | Vercel AI SDK v6 message parts (alternative to content) |
AiChatResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| content | string | ✅ | Generated text |
| model | string | optional | Model that produced it |
| toolCalls | any[] | optional | Tool calls the model requested (Vercel ToolCallPart) |
| usage | { promptTokens: number; completionTokens: number; totalTokens: number } | optional | Token usage |
| conversationId | string | optional | Conversation the turn was persisted into |
Nested Shape: AiChatResponse.usage
| Property | Type | Required | Description |
|---|---|---|---|
| promptTokens | number | ✅ | Tokens consumed by the prompt |
| completionTokens | number | ✅ | Tokens generated |
| totalTokens | number | ✅ | prompt + completion |
AiCompleteRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| prompt | string | ✅ | Prompt text |
| options | Record<string, any> | optional | Request options (model, temperature, maxTokens, …) |
AiConversation
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Conversation id |
| title | string | optional | Title / summary |
| agentId | string | optional | Agent this conversation is bound to |
| userId | string | optional | Owning user |
| messages | ({ role: Enum<'system' | 'user' | 'assistant' | 'tool'>; content?: any; parts?: any[] } & Record<string, any>)[] | ✅ | Message history |
| createdAt | string | ✅ | Creation timestamp (ISO 8601) |
| updatedAt | string | ✅ | Last update timestamp (ISO 8601) |
| metadata | Record<string, any> | optional | Conversation metadata |
Nested Shape: AiConversation.messages[number]
| Property | Type | Required | Description |
|---|---|---|---|
| role | Enum<'system' | 'user' | 'assistant' | 'tool'> | ✅ | Message role |
| content | any | optional | Message content: a string, or an array of content parts |
| parts | any[] | optional | Vercel AI SDK v6 message parts (alternative to content) |
AiMessage
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| role | Enum<'system' | 'user' | 'assistant' | 'tool'> | ✅ | Message role |
| content | any | optional | Message content: a string, or an array of content parts |
| parts | any[] | optional | Vercel AI SDK v6 message parts (alternative to content) |
AiModelsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| models | (string | { id: string; label: string; default: boolean })[] | ✅ | Models this environment offers |
| defaultModel | string | optional | Default model id, when the service reports one |
Nested Shape: AiModelsResponse.models[number]
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Model id |
| label | string | ✅ | Display label for the picker |
| default | boolean | ✅ | Whether this is the environment default |
AiPendingAction
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Pending action id |
| object_name | string | ✅ | Object the action targets |
| action_name | string | ✅ | Action name |
| tool_name | string | ✅ | Tool that would execute it |
| tool_input | string | ✅ | Serialized tool input |
| status | Enum<'pending' | 'approved' | 'executed' | 'failed' | 'rejected'> | ✅ | Lifecycle status |
| result | string | optional | Serialized result, once executed |
| error | string | optional | Failure message, when status is failed |
| rejection_reason | string | optional | Reason given at rejection |
| conversation_id | string | optional | Conversation that proposed it |
| message_id | string | optional | Message that proposed it |
| proposed_by | string | optional | Actor that proposed it |
| decided_by | string | optional | Actor that approved or rejected it |
| proposed_at | string | ✅ | Proposal timestamp (ISO 8601) |
| decided_at | string | optional | Decision timestamp (ISO 8601) |
AiPendingActionStatus
Allowed Values
pendingapprovedexecutedfailedrejected
AiStreamChunk
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Frame type (text-delta, tool-input-available, finish, error, …) |
ApproveAiPendingActionResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| status | Enum<'executed' | 'failed'> | ✅ | Outcome of executing the approved action |
| result | any | optional | Tool result, when executed |
| error | string | optional | Failure reason, when failed |
AuditMetaItemRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name |
| name | string | ✅ | Item name |
| limit | number | optional | Maximum events to return, newest first. The implementation clamps to [1, 500] and defaults to 100 — out-of-range values are clamped, never refused. |
AuditMetaItemResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| events | { id: any; occurredAt: string; actor: string; source: string | null; … }[] | ✅ | Recent protection-audit events for the item, newest first. See the schema-level note for what an empty array means — and what it never means. |
Nested Shape: AuditMetaItemResponse.events[number]
| Property | Type | Required | Description |
|---|---|---|---|
| id | any | ✅ | Row id of the audit event. Opaque to callers. |
| occurredAt | string | ✅ | When the attempt happened (ISO-8601 string). |
| actor | string | ✅ | Who attempted the operation. system when the row recorded no actor. |
| source | string | null | ✅ | Which code path recorded the event (e.g. protocol.deleteMetaItem). null when the row recorded none. |
| operation | Enum<'save' | 'publish' | 'rollback' | 'delete' | 'reset'> | ✅ | Which metadata-protection door was attempted. |
| outcome | Enum<'allowed' | 'denied' | 'forced'> | ✅ | Whether the attempt went through, was refused, or overrode a lock (ADR-0010 §3.6). |
| code | string | ✅ | Machine-readable verdict code for the outcome (e.g. item_locked). Empty string when the row recorded none. |
| lockState | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | null | ✅ | The lock verdict in force at the time of the attempt (ADR-0010 §3.3). null when no lock applied. |
| lockOverridden | boolean | ✅ | True when the attempt went through by overriding a lock (outcome: "forced" rows). |
| requestId | string | null | ✅ | Correlation id of the originating request. null when the row recorded none. |
| note | string | null | ✅ | Free-text note recorded with the event. null when the row recorded none. |
AutomationActionsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| actions | { type: string; version: string; name: string; description?: string; … }[] | ✅ | Registered action descriptors (built-in + plugin) |
| total | integer | ✅ | Number of descriptors returned (after any filters) |
Nested Shape: AutomationActionsResponse.actions[number]
Canonical cross-paradigm action/node descriptor (ADR-0018)
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Registry action/node type (matches the executor type) |
| version | string | ✅ | Executor version (semver) |
| name | string | ✅ | Display label (or i18n key) |
| description | string | optional | Action description |
| icon | string | optional | Icon id resolved by the designer |
| category | Enum<'logic' | 'data' | 'io' | 'human' | 'control' | 'custom'> | optional (default: "custom") | Palette category |
| paradigms | Enum<'flow' | 'approval'>[] | optional (default: ["flow"]) | Authoring surfaces that may offer this action |
| configSchema | any | optional | JSON Schema for the node config (drives the designer form; undeclared keys are rejected at registration) |
| supportsPause | boolean | optional (default: false) | Supports async pause/resume |
| supportsCancellation | boolean | optional (default: false) | Supports cancellation |
| supportsRetry | boolean | optional (default: true) | Supports retry on failure |
| needsOutbox | boolean | optional (default: false) | Dispatch via service-messaging outbox (retry/idempotency/dead-letter) |
| isAsync | never | optional | [REMOVED] ActionDescriptor.isAsync was removed in @objectstack/spec 17 (ADR-0049) — no execution path ever read it, so declaring it never made a node suspend and omitting it never stopped one. Delete the key. The live mechanism is two-part: an executor suspends by RETURNING suspend: true from execute(), and its descriptor must declare supportsPause: true (plus the resumeAuthority its pauses need) or the engine refuses that suspension. Declaring isAsync: true alongside supportsPause: true was always redundant; declaring it alone was always inert. |
| handlerContract | Enum<'none' | 'pure'> | optional (default: "none") | Effect contract for author-supplied code this action invokes: 'none' (invokes none) or 'pure' (must not write — it returns a value and the flow graph persists it) |
| resumeAuthority | Enum<'any' | 'service'> | optional | Who may resume a run this node suspended: 'any' (the generic resume route) or 'service' (only the owning service, e.g. approvals). Carries no schema default so an omission stays observable — and an omission is fail-CLOSED at run time, equivalent to 'service': a pausing node whose pause is open to the generic route must declare 'any' explicitly |
| maturity | Enum<'ga' | 'beta' | 'reserved'> | optional (default: "ga") | Runtime maturity: ga (shipped), beta, or reserved (contract only — designers grey this out) |
| source | Enum<'builtin' | 'plugin'> | optional (default: "plugin") | builtin = platform baseline; plugin = third-party contributed |
| deprecated | boolean | optional (default: false) | Deprecated alias kept for back-compat |
| aliasOf | string | optional | Canonical type this alias forwards to |
AutomationTriggerRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| trigger | string | ✅ | |
| payload | Record<string, any> | ✅ |
AutomationTriggerResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | |
| jobId | string | optional | |
| result | any | optional |
BatchDataRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name |
| request | { operation: Enum<'create' | 'update' | 'upsert' | 'delete'>; records: object[]; options?: object } | ✅ | Batch operation request |
Nested Shape: BatchDataRequest.request
| Property | Type | Required | Description |
|---|---|---|---|
| operation | Enum<'create' | 'update' | 'upsert' | 'delete'> | ✅ | Type of batch operation |
| records | { id?: string; data?: Record<string, any>; externalId?: string }[] | ✅ | Array of records to process (server caps the count — see batch.maxBatchSize) |
| options | { atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean } | optional | Batch operation options |
BatchDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Operation success status |
| error | { code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … } | optional | Error details if success is false |
| meta | { timestamp: string; duration?: integer; requestId?: string; traceId?: string } | optional | Response metadata |
| operation | Enum<'create' | 'update' | 'upsert' | 'delete'> | optional | Operation type that was performed |
| total | number | ✅ | Total number of records in the batch |
| succeeded | number | ✅ | Number of records that succeeded |
| failed | number | ✅ | Number of records that failed |
| results | { id?: string; success: boolean; errors?: object[]; data?: Record<string, any>; … }[] | ✅ | Detailed results for each record |
Nested Shape: BatchDataResponse.error
| Property | Type | Required | Description |
|---|---|---|---|
| code | Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …> | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
| declaredCode | string | optional | The producer-declared code, verbatim, when it is not a member of the closed code vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
| message | string | ✅ | Readable error message |
| userMessage | string | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces message. |
| refusal | true | optional | Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value. |
| category | string | optional | Error category (e.g. validation, authorization) |
| httpStatus | integer | optional | HTTP status of the response carrying this error |
| details | any | optional | Additional error context (e.g. field validation errors) |
| requestId | string | optional | Request ID for tracking |
Nested Shape: BatchDataResponse.meta
| Property | Type | Required | Description |
|---|---|---|---|
| timestamp | string | ✅ | |
| duration | integer | optional | Server-side processing duration in milliseconds |
| requestId | string | optional | |
| traceId | string | optional |
Nested Shape: BatchDataResponse.results[number]
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | optional | Record ID if operation succeeded |
| success | boolean | ✅ | Whether this record was processed successfully |
| errors | { code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }[] | optional | Array of errors if operation failed. Branch on errors[0].code — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error. A NON-atomic batch that stopped (the continueOnError: false default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back. |
| data | Record<string, any> | optional | Full record data (if returnRecords=true) |
| index | number | optional | Index of the record in the request array |
| droppedFields | { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> }[] | optional | Write-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the in-engine static readonly strip on create. Per-row because a batch can drop different fields on different rows (readonlyWhen is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit X-ObjectStack-Dropped-Fields for batches. Optional — omit-when-empty keeps the shape backward-compatible. |
CheckPermissionRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name to check permissions for |
| action | Enum<'create' | 'read' | 'edit' | 'delete' | 'transfer' | 'restore' | 'purge'> | ✅ | Action to check |
| recordId | string | optional | Specific record ID (for record-level checks) |
| field | string | optional | Specific field name (for field-level checks) |
CheckPermissionResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| allowed | boolean | ✅ | Whether the action is permitted |
| reason | string | optional | Reason if denied |
CloneDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | The object name. |
| id | string | ✅ | The ID of the newly created clone. |
| sourceId | string | ✅ | The ID of the record the clone was copied from. |
| record | Record<string, any> | ✅ | The created clone, including server-generated fields. Engine-owned values (injected system/audit columns, autonumbers, computed formula/summary fields) are re-derived by the insert path rather than copied from the source; caller-supplied overrides win over copied values. |
| droppedFields | { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> }[] | optional | Write-observability: fields that were LEGALLY stripped before the clone was written — a non-system clone cannot seed a static readonly column, whether the value was COPIED from the source row or supplied through overrides (the strip runs inside engine.insert, after the beforeInsert hooks, isSystem-gated, exactly as on createData), so those keys are dropped and the field re-derives its default. Present ONLY when ≥1 field was dropped; the clone still succeeded without them (status/success semantics unchanged). Carried in the 201 body only — this route relays the producer verbatim and sets no X-ObjectStack-Dropped-Fields header. Optional — omit-when-empty keeps the shape backward-compatible for existing clients. |
| warnings | { field: string; code: string; message: string; rule?: string; … }[] | optional | The advisory validation-rule hits of THIS write: each severity: 'warning' or 'info' rule whose verdict on the written record was "violated", with its rule name, severity, field, code and author-written message — the same element the validate-only preview reports in a row's warnings. Advisory rules never block, so the write succeeded; this is the advisory to show the person who made it. Present ONLY when at least one advisory rule hit; a write with no hit omits the key. Hits of a nested write a hook or a flow made inside this one are not included, and a rule that could not be evaluated is not a hit. |
Nested Shape: CloneDataResponse.droppedFields[number]
A write-path strip event: caller-supplied fields legally dropped from the payload
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object the write targeted (resolved object name) |
| fields | string[] | ✅ | Caller-supplied field names the engine removed from the write payload |
| reason | Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> | ✅ | Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, the primary-key strip of a payload id the engine ruled is not an identifier, or a computed (formula) field no driver has a column for |
Nested Shape: CloneDataResponse.warnings[number]
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | The field the finding is about (_record for an object-level rule). |
| code | string | ✅ | Machine-readable finding code, e.g. required, invalid_type, rule_violation. |
| message | string | ✅ | Human-readable message — a validation rule's author-written text where one exists. |
| rule | string | optional | The name of the ADVISORY validation rule this finding reports. Present only on an advisory rule hit. |
| severity | Enum<'warning' | 'info'> | optional | The advisory rule's declared severity, warning or info. Present only on an advisory rule hit. |
CreateAiConversationRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| title | string | optional | Initial title |
| agentId | string | optional | Agent to bind the conversation to |
| metadata | Record<string, any> | optional | Conversation metadata |
CreateDataRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | The object name. |
| data | Record<string, any> | ✅ | The dictionary of field values to insert. |
CreateDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | The object name. |
| id | string | ✅ | The ID of the newly created record. |
| record | Record<string, any> | ✅ | The created record, including server-generated fields (created_at, owner). |
| droppedFields | { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> }[] | optional | Write-observability: caller-supplied fields that were LEGALLY stripped before the record was written — a non-system create cannot seed a static readonly column (the strip runs inside engine.insert, after the beforeInsert hooks, isSystem-gated), so those keys are dropped and the field re-derives its default. Present ONLY when ≥1 field was dropped; the create still succeeded without them (status/success semantics unchanged). REST additionally surfaces this as the X-ObjectStack-Dropped-Fields response header. Optional — omit-when-empty keeps the shape backward-compatible for existing clients. |
| warnings | { field: string; code: string; message: string; rule?: string; … }[] | optional | The advisory validation-rule hits of THIS write: each severity: 'warning' or 'info' rule whose verdict on the written record was "violated", with its rule name, severity, field, code and author-written message — the same element the validate-only preview reports in a row's warnings. Advisory rules never block, so the write succeeded; this is the advisory to show the person who made it. Present ONLY when at least one advisory rule hit; a write with no hit omits the key. Hits of a nested write a hook or a flow made inside this one are not included, and a rule that could not be evaluated is not a hit. |
Nested Shape: CreateDataResponse.droppedFields[number]
A write-path strip event: caller-supplied fields legally dropped from the payload
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object the write targeted (resolved object name) |
| fields | string[] | ✅ | Caller-supplied field names the engine removed from the write payload |
| reason | Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> | ✅ | Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, the primary-key strip of a payload id the engine ruled is not an identifier, or a computed (formula) field no driver has a column for |
Nested Shape: CreateDataResponse.warnings[number]
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | The field the finding is about (_record for an object-level rule). |
| code | string | ✅ | Machine-readable finding code, e.g. required, invalid_type, rule_violation. |
| message | string | ✅ | Human-readable message — a validation rule's author-written text where one exists. |
| rule | string | optional | The name of the ADVISORY validation rule this finding reports. Present only on an advisory rule hit. |
| severity | Enum<'warning' | 'info'> | optional | The advisory rule's declared severity, warning or info. Present only on an advisory rule hit. |
CreateManyDataRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name |
| records | Record<string, any>[] | ✅ | Array of records to create |
CreateManyDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name |
| records | Record<string, any>[] | ✅ | Created records |
| count | number | ✅ | Number of records created |
| droppedFields | { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> }[] | optional | Write-observability: caller-supplied readonly fields the in-engine create-side strip (engine.insert, isSystem-gated) removed before the rows were written. AGGREGATED across the batch (one event per object/reason with the UNION of dropped field names) rather than per-row, because this response is { object, records, count } and has no per-row slot to hang a drop set on — a union is the only view it can represent. So read a name here as "at least one row dropped this field", NOT "every row dropped the same set": the strip runs INSIDE engine.insert after the beforeInsert hooks and exempts keys a hook itself wrote, tracked per row: rows where a hook stamped a protected key drop a different set from rows where it did not. Present ONLY when ≥1 field was dropped; the creates still succeeded without them (count/success unchanged). Optional — omit-when-empty keeps the shape backward-compatible. (The same reading applies to the partial-success bulk create, whose batch-level droppedFields names no row for the same reason. The paths that DO carry per-row droppedFields on each result are the bulk UPDATE and the mixed batch, where each row is its own engine call — see BatchOperationResultSchema.) |
Nested Shape: CreateManyDataResponse.droppedFields[number]
A write-path strip event: caller-supplied fields legally dropped from the payload
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object the write targeted (resolved object name) |
| fields | string[] | ✅ | Caller-supplied field names the engine removed from the write payload |
| reason | Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> | ✅ | Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, the primary-key strip of a payload id the engine ruled is not an identifier, or a computed (formula) field no driver has a column for |
DeleteDataRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name |
| id | string | ✅ | Record ID to delete |
| expectedVersion | string | optional | Optimistic concurrency token (typically the updated_at value the client read). When provided, the server compares it against the current record version and returns 409 CONCURRENT_UPDATE if they differ. Optional — omit to skip the check. The quoted-empty entity-tag ("") is refused 400 VALIDATION_FAILED, not treated as omitted. |
DeleteDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name |
| id | string | ✅ | Deleted record ID |
| success | boolean | ✅ | Whether deletion succeeded |
DeleteManyDataRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| ids | string[] | ✅ | Array of record IDs to delete (server caps the count — see batch.maxBatchSize) |
| options | { atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean } | optional | Delete options |
| object | string | ✅ | Object name |
Nested Shape: DeleteManyDataRequest.options
| Property | Type | Required | Description |
|---|---|---|---|
| atomic | boolean | optional (default: false) | Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort. |
| returnRecords | boolean | optional (default: false) | If true, return full record data in response |
| continueOnError | boolean | optional (default: false) | If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total. |
| validateOnly | never | optional | [REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. |
DeleteManyDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Operation success status |
| error | { code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … } | optional | Error details if success is false |
| meta | { timestamp: string; duration?: integer; requestId?: string; traceId?: string } | optional | Response metadata |
| operation | Enum<'create' | 'update' | 'upsert' | 'delete'> | optional | Operation type that was performed |
| total | number | ✅ | Total number of records in the batch |
| succeeded | number | ✅ | Number of records that succeeded |
| failed | number | ✅ | Number of records that failed |
| results | { id?: string; success: boolean; errors?: object[]; data?: Record<string, any>; … }[] | ✅ | Detailed results for each record |
Nested Shape: DeleteManyDataResponse.error
| Property | Type | Required | Description |
|---|---|---|---|
| code | Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …> | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
| declaredCode | string | optional | The producer-declared code, verbatim, when it is not a member of the closed code vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
| message | string | ✅ | Readable error message |
| userMessage | string | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces message. |
| refusal | true | optional | Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value. |
| category | string | optional | Error category (e.g. validation, authorization) |
| httpStatus | integer | optional | HTTP status of the response carrying this error |
| details | any | optional | Additional error context (e.g. field validation errors) |
| requestId | string | optional | Request ID for tracking |
Nested Shape: DeleteManyDataResponse.meta
| Property | Type | Required | Description |
|---|---|---|---|
| timestamp | string | ✅ | |
| duration | integer | optional | Server-side processing duration in milliseconds |
| requestId | string | optional | |
| traceId | string | optional |
Nested Shape: DeleteManyDataResponse.results[number]
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | optional | Record ID if operation succeeded |
| success | boolean | ✅ | Whether this record was processed successfully |
| errors | { code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }[] | optional | Array of errors if operation failed. Branch on errors[0].code — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error. A NON-atomic batch that stopped (the continueOnError: false default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back. |
| data | Record<string, any> | optional | Full record data (if returnRecords=true) |
| index | number | optional | Index of the record in the request array |
| droppedFields | { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> }[] | optional | Write-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the in-engine static readonly strip on create. Per-row because a batch can drop different fields on different rows (readonlyWhen is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit X-ObjectStack-Dropped-Fields for batches. Optional — omit-when-empty keeps the shape backward-compatible. |
DeleteMetaItemRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name |
| name | string | ✅ | Item name |
| parentVersion | string | optional | ADR-0008 optimistic-concurrency pin: the version token the caller believes is current (on the REST door, the If-Match request header). Present, a concurrent edit is reported as a 409 conflict instead of silently reset; absent = last-write-wins against the current row (Studio's "Reset" button is unpinned). |
| actor | string | optional | Identity recorded on the delete's history tombstone row. On the REST door this is the request's authenticated identity (one producer) — never a caller-supplied header. Absent, the event is recorded actor-less (null), deliberately not attributed to "system". |
| state | Enum<'active' | 'draft'> | optional | Which lifecycle row to discard: draft discards the pending draft overlay only (the still-active overlay, if any, keeps serving); active or absent resets the live row. Absent defaults to active. |
| dropStorage | boolean | optional | Destructive opt-in, default false: also drop the object's physical table after the metadata row is removed (object type + active state only; never sys_ tables). Used by the "discard a previewed object" flow so a publish-to-preview leaves no orphan table. |
| member | string | optional | Bundle member selector, for a metadata type whose identity is a pair (today only email_template, identified by (name, locale)): the member this request addresses, spelled as its discriminator value (zh-CN). Absent, the canonical member (en-US for email_template), which is what a bare-name read answers; naming the canonical value is the same request. It is not the response translation locale, which ?locale= and Accept-Language choose. Refused with 400 VALIDATION_ERROR on a type that has no members, and for a blank value or one longer than a stored member can be (100 characters). A well-formed member with nothing stored or shipped answers as that door answers an absent item. |
| packageId | string | null | optional | ADR-0048 — the software package whose row this delete removes (sys_metadata.package_id; ?package=<id> on the REST door, where all and an empty value name no package), as a save names the row it writes. Named, the delete removes that package's row and no other; a package that holds no row removes nothing. Absent or null names no package: the delete of the active row then removes the row of the one package the item's active rows are bound to, is refused 409 RESOURCE_CONFLICT when they are bound to two or more, and with none removes the env-local row (no package stamp); the discard of the draft removes the draft a draft save naming no package writes, the one the per-item publish naming none promotes, and is refused where that publish is (see PublishMetaItemRequestSchema.packageId). That row is the one parentVersion is judged against. When another row of the item is still stored afterwards, the receipt message names it. |
DeleteMetaItemResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | |
| reset | boolean | optional | |
| seq | integer | optional | Monotonic sequence number of the metadata event this delete appended to the item history (sys_metadata_history.event_seq) — the ordering token the history/audit trail is read by, and the same key both write-verb siblings declare. Unlike version it is not an OCC token. Optional HERE, unlike on those siblings, because only the repository path's delete-ful branch appends an event: a no-op reset (no overlay row existed) and the control-plane legacy raw-engine path both answer without one. Absence means "this branch appended no history event", never "nothing was deleted" — read reset for that. |
| projectionApplied | { success: boolean; error?: string } | optional | Outcome of the awaited ADR-0094 mutation projector — the post-persist step that materializes this metadata into its derived data-plane read model. The same receipt {@link SaveMetaItemResponseSchema} and {@link PublishMetaItemResponseSchema} carry, because the projector runs on all three verbs of this door: on a delete it re-reads the layered state and either retires the derived record or resets it to the artifact baseline. Present ONLY when a projector is registered for this metadata type AND a row was actually deleted, which is why it is optional: its absence means "no projector ran", never "the projection failed". Best-effort by design — a projector failure is reported here and logged, never thrown, so a caller that needs the read model to be live must check projectionApplied.success rather than rely on the 200. |
| message | string | optional |
Nested Shape: DeleteMetaItemResponse.projectionApplied
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | False when the projector threw; the metadata delete itself still succeeded. |
| error | string | optional | Projector failure message, present only when success is false. |
DiffMetaItemResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type of the diffed item. |
| name | string | ✅ | Name of the diffed item. |
| fromVersion | number | null | ✅ | The older side's history version, null when that side is absent (e.g. the item had no earlier version). |
| toVersion | number | null | ✅ | The newer side's history version, null when that side is absent. |
| added | { path: string; value: any }[] | ✅ | Members present in to and absent in from. |
| removed | { path: string; value: any }[] | ✅ | Members present in from and absent in to. |
| changed | { path: string; from: any; to: any }[] | ✅ | Members present on both sides with different values. |
Nested Shape: DiffMetaItemResponse.added[number]
| Property | Type | Required | Description |
|---|---|---|---|
| path | string | ✅ | Dot path of the added member. |
| value | any | ✅ | The added value. |
Nested Shape: DiffMetaItemResponse.removed[number]
| Property | Type | Required | Description |
|---|---|---|---|
| path | string | ✅ | Dot path of the removed member. |
| value | any | ✅ | The removed value. |
Nested Shape: DiffMetaItemResponse.changed[number]
| Property | Type | Required | Description |
|---|---|---|---|
| path | string | ✅ | Dot path of the changed member. |
| from | any | ✅ | The older side's value. |
| to | any | ✅ | The newer side's value. |
DisablePackageRequest
Disable package request
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Package ID to disable |
DisablePackageResponse
Disable package response
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| package | { manifest: object; status?: Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'>; enabled?: boolean; installedAt?: string; … } | ✅ | Disabled package details |
| message | string | optional | Disable status message |
Nested Shape: DisablePackageResponse.package
| Property | Type | Required | Description |
|---|---|---|---|
| manifest | { id: string; namespace?: string; defaultDatasource?: string; version: string; … } | ✅ | Package manifest at the AUTHORING stage; a row installed by a defineStack() host carries the assembled body instead — see AssembledInstalledPackageSchema / InstalledPackageAtEitherStageSchema |
| status | Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'> | optional (default: "installed") | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
| enabled | boolean | optional (default: true) | Whether the package is currently enabled |
| installedAt | string | optional | Installation timestamp |
| updatedAt | string | optional | Last update timestamp |
| installedVersion | string | optional | Currently installed version for quick access |
| previousVersion | string | optional | Version before the last upgrade |
| statusChangedAt | string | optional | Status change timestamp |
| errorMessage | string | optional | Error message when status is error |
| settings | Record<string, any> | optional | User-provided configuration settings |
| upgradeHistory | { fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' | 'failed' | 'rolled_back'>; … }[] | optional | Version upgrade history |
| registeredNamespaces | string[] | optional | Namespace prefixes registered by this package |
EnablePackageRequest
Enable package request
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Package ID to enable |
EnablePackageResponse
Enable package response
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| package | { manifest: object; status?: Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'>; enabled?: boolean; installedAt?: string; … } | ✅ | Enabled package details |
| message | string | optional | Enable status message |
Nested Shape: EnablePackageResponse.package
| Property | Type | Required | Description |
|---|---|---|---|
| manifest | { id: string; namespace?: string; defaultDatasource?: string; version: string; … } | ✅ | Package manifest at the AUTHORING stage; a row installed by a defineStack() host carries the assembled body instead — see AssembledInstalledPackageSchema / InstalledPackageAtEitherStageSchema |
| status | Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'> | optional (default: "installed") | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
| enabled | boolean | optional (default: true) | Whether the package is currently enabled |
| installedAt | string | optional | Installation timestamp |
| updatedAt | string | optional | Last update timestamp |
| installedVersion | string | optional | Currently installed version for quick access |
| previousVersion | string | optional | Version before the last upgrade |
| statusChangedAt | string | optional | Status change timestamp |
| errorMessage | string | optional | Error message when status is error |
| settings | Record<string, any> | optional | User-provided configuration settings |
| upgradeHistory | { fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' | 'failed' | 'rolled_back'>; … }[] | optional | Version upgrade history |
| registeredNamespaces | string[] | optional | Namespace prefixes registered by this package |
FindDataRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | The unique machine name of the object to query (e.g. "account"). |
| query | { object: string; fields?: string[]; where?: [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any; search?: string | object; … } | optional | Structured query definition (filter, sort, select, pagination) — the canonical QueryAST or its transport spelling. |
Nested Shape: FindDataRequest.query
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name (e.g. account) |
| fields | string[] | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (owner.name) is not a projection: no driver resolves one, and the ingress refuses it with 400 INVALID_FIELD. Related data is read with expand, whose nested QueryAST both filters (where) and selects (fields) the related record's columns. The projection must RETAIN the foreign-key column: fields: ['title'] with expand: 'project_id' resolves nothing, because the relation is carried by that key — add 'project_id' and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
| where | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | optional | Filtering criteria (WHERE) — a filter condition, or the input-only FilterArray sugar (['status', '=', 'open']), which is lowered through parseFilterAST before the query is produced. |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
| searchFields | string[] | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sorting instructions (ORDER BY) |
| limit | number | optional | Max records to return (LIMIT) |
| offset | number | optional | Records to skip (OFFSET) |
| top | number | optional | Alias for limit (OData compatibility) |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| joins | never | optional | [REMOVED] query.joins was removed in @objectstack/spec 17 (ADR-0049) — no engine or driver ever read it: a query carrying joins behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through expand — expand: { owner_id: { object: 'user', fields: ['name'] } } — which the engine resolves via batch $in queries, and whose nested query selects the related record's own columns. Keep the foreign key in your own projection (fields: ['title', 'owner_id']): the relation is carried by that column, so projecting it away leaves expansion nothing to resolve. A dotted fields path is NOT a replacement — no driver ever resolved one and the ingress refuses it (400 INVALID_FIELD). |
| aggregations | { function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; field?: string; alias: string; filter?: any }[] | optional | Aggregation functions |
| groupBy | (string | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string })[] | optional | GROUP BY targets (strings or {field, dateGranularity?} objects for date bucketing) |
| having | any | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
| windowFunctions | never | optional | [REMOVED] query.windowFunctions was removed in @objectstack/spec 17 (ADR-0049) — find() never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind SqlDriver.findWithWindowFunctions(object, query) (embedder-level; not on the IDataDriver contract or the REST surface); request-level analytics are aggregations + groupBy. |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (fields) and filter (where, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent limit/offset/orderBy are NOT applied on this path. |
| $filter | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | null | optional | Transport spelling of where (OData $filter) |
| filters | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | null | optional | Transport spelling of where (plural of filter) |
| $top | number | string | null | optional | Transport spelling of limit (OData $top) — a number, or the digits a querystring carries it as |
| $skip | number | string | null | optional | Transport spelling of offset (OData $skip) — a number, or the digits a querystring carries it as |
| $orderby | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | null | optional | Transport spelling of orderBy (OData $orderby) |
| $select | string | string[] | null | optional | Transport spelling of fields (OData $select) |
| $expand | string | string[] | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | null | optional | Transport spelling of expand (OData $expand) |
| $search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | null | optional | Transport spelling of search (OData $search) |
| $searchFields | string | string[] | null | optional | Transport spelling of searchFields (OData $searchFields) |
| $count | boolean | Enum<'true' | 'false'> | null | optional | Transport spelling of the response total-count flag (OData $count) |
| count | boolean | Enum<'true' | 'false'> | null | optional | Response total-count flag — only an explicit false skips the COUNT query |
| filter | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | null | optional | Transport spelling of where |
| select | string | string[] | null | optional | Transport spelling of fields |
| sort | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | null | optional | Transport spelling of orderBy |
| skip | number | string | null | optional | Transport spelling of offset |
| populate | string | string[] | null | optional | Transport spelling of expand |
FindDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | The object name for the returned records. |
| records | Record<string, any>[] | ✅ | The list of matching records. |
| total | number | optional | Total number of records matching the filter (if requested). |
| nextCursor | string | optional | Cursor for the next page of results (cursor-based pagination). |
| hasMore | boolean | optional | True if there are more records available (pagination). |
FindReferencesToMetaResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| references | { type: string; name: string; label?: string; path: string; … }[] | ✅ | Every found reference to the addressed item. |
Nested Shape: FindReferencesToMetaResponse.references[number]
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type of the REFERRING item. |
| name | string | ✅ | Name of the referring item. |
| label | string | optional | Display label of the referring item, when it has one. |
| path | string | ✅ | Where inside the referring item the reference sits (dot path). |
| kind | string | ✅ | What kind of reference this is (e.g. which key carries it). |
FlowRegistration
Union Options
This schema accepts one of the following structures:
Option 1
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'registered' | ✅ | The engine holds a registration under the flow's name. |
Option 2
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'refused' | ✅ | The engine refused to register the stored flow and holds nothing under its name: it never runs. |
| reason | { code?: string; message: string } | ✅ | What the engine threw, verbatim. |
Nested Shape: FlowRegistration[state='refused'].reason
| Property | Type | Required | Description |
|---|---|---|---|
| code | string | optional | The machine code the engine's refusal carried, verbatim. Absent when the thrown error carried none. |
| message | string | ✅ | The engine's refusal message, verbatim. Render it as sent; do not parse it. |
Option 3
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'unregistered' | ✅ | The engine holds nothing under the flow's name and recorded no refusal for it. |
FlowRegistrationRefusal
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| code | string | optional | The machine code the engine's refusal carried, verbatim. Absent when the thrown error carried none. |
| message | string | ✅ | The engine's refusal message, verbatim. Render it as sent; do not parse it. |
GetDataRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | The object name. |
| id | string | ✅ | The unique record identifier (primary key). |
| select | string[] | optional | Fields to include in the response (allowlisted query param). |
| expand | string[] | optional | Lookup/master_detail field names to expand. The engine resolves these via batch $in queries, replacing foreign key IDs with full objects. |
GetDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | The object name. |
| id | string | ✅ | The record ID. |
| record | Record<string, any> | ✅ | The complete record data. |
GetDiscoveryRequest
Properties
| Property | Type | Required | Description |
|---|
GetDiscoveryResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | |
| version | string | ✅ | |
| environment | Enum<'production' | 'sandbox' | 'development'> | optional | Deployment posture a discovery response advertises. Deliberately three coarse buckets — a client reads this to answer "am I talking to production?", not to identify a specific environment (that is sys_environment / EnvironmentTypeSchema, a richer 7-member taxonomy). |
| routes | { data: string; metadata: string; discovery?: string; ui?: string; … } | optional | |
| locale | { default: string; supported: string[]; timezone: string } | optional | |
| services | Record<string, { enabled: boolean; status: Enum<'available' | 'registered' | 'unavailable' | 'degraded' | 'stub'>; handlerReady?: boolean; route?: string; … }> | optional | Per-service availability map keyed by CoreServiceName |
| capabilities | { comments: object; automation: object; cron: object; search: object; … } | optional | Hierarchical capability descriptors — the full WellKnownCapabilities vocabulary, every key present |
| schemaDiscovery | { openapi?: string; jsonSchema?: string } | optional | Schema discovery endpoints for API toolchain integration |
| scoping | { enabled: boolean; resolution: Enum<'required' | 'optional' | 'auto'>; scoped: boolean; environmentId?: string } | optional | Environment-scoping posture, added by the REST discovery endpoint |
| authFamilies | { admin: boolean } | optional | Which optional better-auth route families are mounted under routes.auth — the same source as GET {routes.auth}/config features; absent when no auth service answers |
| metadata | Record<string, any> | optional | Custom metadata key-value pairs for extensibility |
| apiName | string | optional | API name (deprecated — use name; removed in protocol 18) |
Nested Shape: GetDiscoveryResponse.routes
| Property | Type | Required | Description |
|---|---|---|---|
| data | string | ✅ | e.g. /api/v1/data |
| metadata | string | ✅ | e.g. /api/v1/meta |
| discovery | string | optional | e.g. /api/v1/discovery |
| ui | string | optional | e.g. /api/v1/ui |
| auth | string | optional | e.g. /api/v1/auth |
| automation | string | optional | e.g. /api/v1/automation |
| storage | string | optional | e.g. /api/v1/storage |
| analytics | string | optional | e.g. /api/v1/analytics |
| packages | string | optional | e.g. /api/v1/packages |
| datasources | string | optional | e.g. /api/v1/datasources — base for the datasources/:name/external/* federation-admin family; absent when no host mounts it |
string | optional | e.g. /api/v1/email — base for the email/send endpoint; absent when no host mounts it | |
| approvals | string | optional | e.g. /api/v1/approvals |
| realtime | string | optional | e.g. /api/v1/realtime — present only when a realtime transport is actually mounted |
| notifications | string | optional | e.g. /api/v1/notifications |
| ai | string | optional | e.g. /api/v1/ai |
| i18n | string | optional | e.g. /api/v1/i18n |
| mcp | string | optional | e.g. /api/v1/mcp — always the unscoped base; absent when MCP is disabled or unserveable |
Nested Shape: GetDiscoveryResponse.services[string]
| Property | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | ✅ | Whether the slot is filled by something this host delivers. NOT "a channel exists": subscribing requires handlerReady:true AND a connectable route (isSubscribableChannel). |
| status | Enum<'available' | 'registered' | 'unavailable' | 'degraded' | 'stub'> | ✅ | available = fully operational, registered = route declared but handler unverified, unavailable = not installed, degraded = partial, stub = placeholder that returns 501 |
| handlerReady | boolean | optional | Whether the HTTP handler is confirmed to be mounted. Omitted = readiness unknown/unverified; true = handler mounted; false = handler missing or stub (likely 501). |
| route | string | optional | e.g. /api/v1/analytics |
| provider | string | optional | e.g. "objectql", "plugin-redis", "driver-memory" |
| version | string | optional | Semantic version of the service implementation (e.g. "3.0.6") |
| message | string | optional | e.g. "Install plugin-workflow to enable" |
| rateLimit | { requestsPerMinute?: integer; requestsPerHour?: integer; burstLimit?: integer; retryAfterMs?: integer } | optional | Rate limit and quota info for this service |
Nested Shape: GetDiscoveryResponse.capabilities
| Property | Type | Required | Description |
|---|---|---|---|
| comments | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend supports record comments / chatter (the sys_comment object served via the data API) |
| automation | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend supports Automation CRUD (flows, triggers) |
| cron | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend supports cron scheduling |
| search | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend supports full-text search |
| export | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend supports async export |
| chunkedUpload | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend supports chunked (multipart) uploads |
| transactionalBatch | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend exposes the atomic cross-object batch endpoint (POST {basePath}/batch, /ADR-0034): all ops commit or roll back together in one transaction. Lets clients skip non-atomic client-side simulation instead of runtime-probing 404/405/501. True ⟺ the /batch route is mounted AND the runtime can honour a transaction. |
| websockets | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend mounts a realtime push surface (WebSocket/SSE) clients can subscribe to. Derived from isSubscribableChannel(services.realtime): handlerReady true AND a connectable route. |
| files | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether a file-storage surface (upload/download/attachments) is served |
| analytics | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend serves the analytics / BI query surface |
| ai | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend serves the AI surface (NLQ, chat, agents, suggest) |
| notifications | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend serves the notification surface (inbox, delivery) |
| i18n | { enabled: boolean; features?: Record<string, boolean>; description?: string } | ✅ | Whether the backend serves the i18n surface (translations, locale negotiation) |
Nested Shape: GetDiscoveryResponse.schemaDiscovery
| Property | Type | Required | Description |
|---|---|---|---|
| openapi | string | optional | URL to OpenAPI (Swagger) specification (e.g., "/api/v1/openapi.json") |
| jsonSchema | string | optional | URL to JSON Schema definitions |
Nested Shape: GetDiscoveryResponse.scoping
| Property | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | ✅ | Whether environment-scoped routes are mounted at all |
| resolution | Enum<'required' | 'optional' | 'auto'> | ✅ | How the environment id is resolved when scoping is enabled (mirrors RestApiConfig.projectResolution) |
| scoped | boolean | ✅ | Whether THIS response was served from the environment-scoped mount |
| environmentId | string | optional | The resolved environment id — present only on a scoped mount |
Nested Shape: GetDiscoveryResponse.authFamilies
| Property | Type | Required | Description |
|---|---|---|---|
| admin | boolean | ✅ | Whether the better-auth admin family ({routes.auth}/admin/*: list-users, set-role, update-user, ban-user, …) is mounted. Same value as features.admin on GET {routes.auth}/config, read from the same source; false means those routes answer a plain 404 on this deployment. |
GetEffectivePermissionsRequest
Properties
| Property | Type | Required | Description |
|---|
GetEffectivePermissionsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| objects | Record<string, { allowCreate?: boolean; allowRead?: boolean; allowEdit?: boolean; allowDelete?: boolean; … }> | ✅ | Effective object permissions keyed by object name |
| systemPermissions | string[] | ✅ | Effective system-level permissions |
Nested Shape: GetEffectivePermissionsResponse.objects[string]
| Property | Type | Required | Description |
|---|---|---|---|
| allowCreate | boolean | optional (default: false) | Create permission |
| allowRead | boolean | optional (default: false) | Read permission |
| allowEdit | boolean | optional (default: false) | Edit permission |
| allowDelete | boolean | optional (default: false) | Delete permission |
| allowExport | boolean | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
| allowTransfer | boolean | optional (default: false) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
| allowRestore | never | optional | [REMOVED] objects.<object>.allowRestore was removed in @objectstack/spec 17 (ADR-0049) — the restore ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched restore stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| allowPurge | never | optional | [REMOVED] objects.<object>.allowPurge was removed in @objectstack/spec 17 (ADR-0049) — the purge ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched purge stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| viewAllRecords | boolean | optional (default: false) | View All Data (Bypass Sharing) |
| modifyAllRecords | boolean | optional (default: false) | Modify All Data (Bypass Sharing) — bypasses sharing rules and ownership on the objects record sharing enforces on; on an object with NO owner field sharing abstains, so the platform created_by write floor still applies. |
| readScope | Enum<'own' | 'own_and_reports' | 'unit' | 'unit_and_below' | 'org'> | optional | [ADR-0057 D1] Read depth: own|unit|unit_and_below|org |
| writeScope | Enum<'own' | 'own_and_reports' | 'unit' | 'unit_and_below' | 'org'> | optional | [ADR-0057 D1] Write depth: own|unit|unit_and_below|org |
| apiOperations | Enum<'get' | 'list' | 'create' | 'update' | 'delete' | 'upsert' | 'bulk' | …>[] | optional | Server-resolved effective API operations for this object. Present only when the object tightens exposure via apiMethods; absent = default-allow. The frontend renders this effective set, never the raw whitelist. Vocabulary is the EFFECTIVE ApiOperation set (six primitives + eight derived verbs), not the authored six-value ApiMethod enum. |
GetFieldLabelsRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name |
| locale | string | ✅ | BCP-47 locale code |
GetFieldLabelsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name |
| locale | string | ✅ | Locale code |
| labels | Record<string, { label: string; help?: string; options?: Record<string, string> }> | ✅ | Field labels keyed by field name |
Nested Shape: GetFieldLabelsResponse.labels[string]
| Property | Type | Required | Description |
|---|---|---|---|
| label | string | ✅ | Translated field label |
| help | string | optional | Translated help text |
| options | Record<string, string> | optional | Translated option labels |
GetLocalesRequest
Properties
| Property | Type | Required | Description |
|---|
GetLocalesResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| locales | { code: string; label: string; isDefault?: boolean }[] | ✅ | Available locales |
Nested Shape: GetLocalesResponse.locales[number]
| Property | Type | Required | Description |
|---|---|---|---|
| code | string | ✅ | BCP-47 locale code (e.g., en-US, zh-CN) |
| label | string | ✅ | Locale label. Equals code on every serving surface today — the client names locales for its UI |
| isDefault | boolean | optional (default: false) | Whether this is the default locale |
GetMetaDiagnosticsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| entries | { type: string; name: string; diagnostics: object }[] | ✅ | One entry per item that failed validation (after filters). |
| total | number | ✅ | Number of entries in this answer. |
| scannedTypes | number | ✅ | How many metadata types the sweep visited. |
| scannedItems | number | ✅ | How many items the sweep visited. |
| stats | Record<string, { count: number; locked: number; packages: string[] }> | ✅ | Per-type aggregate stats, keyed by metadata type — computed in the same sweep so a directory page renders tile counts and a package filter in one round-trip. |
Nested Shape: GetMetaDiagnosticsResponse.entries[number]
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type of the failing item. |
| name | string | ✅ | Name of the failing item. |
| diagnostics | { valid: boolean; errors?: object[]; warnings?: object[] } | ✅ | The spec-validation verdict for the item — the same MetadataValidationResult the write path answers. |
Nested Shape: GetMetaDiagnosticsResponse.stats[string]
| Property | Type | Required | Description |
|---|---|---|---|
| count | number | ✅ | Items of this type present. |
| locked | number | ✅ | Items of this type currently lock-protected. |
| packages | string[] | ✅ | Packages contributing items of this type. |
GetMetaItemCachedRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name |
| name | string | ✅ | Item name |
| cacheRequest | { ifNoneMatch?: string; ifModifiedSince?: string; cacheControl?: object } | optional | Cache validation parameters |
| locale | string | optional | Resolved response locale. Folded into the ETag so a language switch never returns a stale-locale 304 — metadata is translated after the cache validator check (issue). |
Nested Shape: GetMetaItemCachedRequest.cacheRequest
| Property | Type | Required | Description |
|---|---|---|---|
| ifNoneMatch | string | optional | ETag value for conditional request (If-None-Match header) |
| ifModifiedSince | string | optional | Timestamp for conditional request (If-Modified-Since header) |
| cacheControl | { directives: Enum<'public' | 'private' | 'no-cache' | 'no-store' | 'must-revalidate' | 'max-age'>[]; maxAge?: number; staleWhileRevalidate?: number; staleIfError?: number } | optional | Client cache control preferences |
GetMetaItemCachedResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| data | any | optional | Metadata payload (omitted for 304 Not Modified) |
| etag | { value: string; weak?: boolean } | optional | ETag for this resource version |
| lastModified | string | optional | Last modification timestamp |
| cacheControl | { directives: Enum<'public' | 'private' | 'no-cache' | 'no-store' | 'must-revalidate' | 'max-age'>[]; maxAge?: number; staleWhileRevalidate?: number; staleIfError?: number } | optional | Cache control directives |
| notModified | boolean | optional (default: false) | True if resource has not been modified (304 response) |
| version | string | optional | Metadata version identifier |
| registration | { state: 'registered' } | { state: 'refused'; reason: object } | { state: 'unregistered' } | optional | The automation engine's verdict on the flow in data, read live on every call and never part of etag (see FlowRegistrationSchema). Present only for a flow read answered by a process that runs an automation service; a read that carries it is never answered notModified. Absent otherwise, and absence means no engine judged it, never registered. |
Nested Shape: GetMetaItemCachedResponse.etag
| Property | Type | Required | Description |
|---|---|---|---|
| value | string | ✅ | ETag value (hash or version identifier) |
| weak | boolean | optional (default: false) | Whether this is a weak ETag |
Nested Shape: GetMetaItemCachedResponse.cacheControl
| Property | Type | Required | Description |
|---|---|---|---|
| directives | Enum<'public' | 'private' | 'no-cache' | 'no-store' | 'must-revalidate' | 'max-age'>[] | ✅ | Cache control directives |
| maxAge | number | optional | Maximum cache age in seconds (unit per HTTP Cache-Control max-age (RFC 9111 §5.2.2.1)) |
| staleWhileRevalidate | number | optional | Allow serving stale content while revalidating (seconds) (unit per HTTP Cache-Control stale-while-revalidate (RFC 5861 §3)) |
| staleIfError | number | optional | Allow serving stale content on error (seconds) (unit per HTTP Cache-Control stale-if-error (RFC 5861 §4)) |
Nested Shape: GetMetaItemCachedResponse.registration[state='registered']
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'registered' | ✅ | The engine holds a registration under the flow's name. |
Nested Shape: GetMetaItemCachedResponse.registration[state='refused']
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'refused' | ✅ | The engine refused to register the stored flow and holds nothing under its name: it never runs. |
| reason | { code?: string; message: string } | ✅ | What the engine threw, verbatim. |
Nested Shape: GetMetaItemCachedResponse.registration[state='unregistered']
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'unregistered' | ✅ | The engine holds nothing under the flow's name and recorded no refusal for it. |
GetMetaItemLayeredRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name |
| name | string | ✅ | Item name |
| packageId | string | optional | Optional package ID — scopes the code layer so a same-name collision resolves to the requested package's artifact (ADR-0048). |
GetMetaItemLayeredResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name (canonical singular) |
| name | string | ✅ | Item name |
| code | any | ✅ | LAYER 1 — the packaged artifact baseline exactly as shipped, before any tenant customization. null when no artifact ships this item (it exists only as an overlay). |
| overlay | any | ✅ | LAYER 2 — the stored customization row ALONE, not merged with code. null when the environment has not customized the item. |
| overlayScope | Enum<'env'> | null | ✅ | Which scope the overlay row was read from: env, the environment layer — the only layer a read serves (ADR-0131 D6). null exactly when overlay is null. |
| effective | any | ✅ | LAYER 3 — the merged result, i.e. the value an ordinary GET /meta/:type/:name would return under item. null when the item resolves to nothing at all. |
| _diagnostics | { valid: boolean; errors?: object[]; warnings?: object[] } | optional | Load-time spec-validation verdict for effective, so the Studio edit page can raise invalid-metadata banners and inline field errors without a second round trip. ABSENT for metadata types that register no Zod schema (function / service / router) — absence means "no opinion", never "valid". |
| lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | ✅ | Resolved lock verdict (ADR-0010 §3.3), artifact winning over overlay. Always present on this path. |
| lockReason | string | optional | Human-readable explanation shown next to a refused write. Present only when the resolved item declares _lockReason. |
| lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Which layer asserted the lock. Present only when the resolved item declares _lockSource. |
| lockDocsUrl | string | optional | Documentation link surfaced beside lockReason. Present only when the resolved item declares _lockDocsUrl. |
| provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Where the item came from (package | org | env-forced). Present only when the resolved item declares _provenance. |
| packageId | string | optional | Owning package machine id. Present only when the resolved item declares _packageId. |
| packageVersion | string | optional | Owning package version. Present only when the resolved item declares _packageVersion. |
| editable | boolean | ✅ | Whether an overlay write is permitted. Always present on this path. |
| deletable | boolean | ✅ | Whether deleting the overlay is permitted. Always present on this path. |
| resettable | boolean | ✅ | Whether the item can be reset to its packaged default. Always present on this path. |
Nested Shape: GetMetaItemLayeredResponse._diagnostics
| Property | Type | Required | Description |
|---|---|---|---|
| valid | boolean | ✅ | Whether the metadata is valid |
| errors | { path: string; message: string; code?: string }[] | optional | Validation errors |
| warnings | { path: string; message: string }[] | optional | Validation warnings |
GetMetaItemRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name |
| name | string | ✅ | Item name: the item's own name. For type: 'app' only, it may instead be the id of the package that ships the app (manifest.id, reverse-domain, e.g. com.acme.crm): with one app per package the app IS the package container, and the package id is its route key (ADR-0048 §3.1). The two grammars are disjoint (an app name is snake_case with no dot; a package id matches MANIFEST_ID_PATTERN, with at least one dot), so an app key in the package-id grammar always names the package (isAppPackageIdKey). That read answers the package's one app as the by-name read answers it, under the app's own name, and the per-caller read gate judges it as it judges that app read by name (an app withheld from the caller answers 403 PERMISSION_DENIED). A package with no app, or with more than one, answers as an absent item. A packageId naming a different package is refused with 400 INVALID_REQUEST. Every other type, and every other key, is read by name. |
| packageId | string | optional | Optional package ID to filter items by |
| member | string | optional | Bundle member selector, for a metadata type whose identity is a pair (today only email_template, identified by (name, locale)): the member this request addresses, spelled as its discriminator value (zh-CN). Absent, the canonical member (en-US for email_template), which is what a bare-name read answers; naming the canonical value is the same request. It is not the response translation locale, which ?locale= and Accept-Language choose. Refused with 400 VALIDATION_ERROR on a type that has no members, and for a blank value or one longer than a stored member can be (100 characters). A well-formed member with nothing stored or shipped answers as that door answers an absent item. |
| state | Enum<'active' | 'draft'> | optional | Draft-visibility switch — which lifecycle row to read (strict mode): 'draft' opens the pending draft buffer (Studio's editor read) and fails when no draft exists; absent or 'active' reads the live published row. Distinct from previewDrafts, which FALLS BACK to the active row when no draft exists. Declaration ≠ authorization: this member only selects which stored row is read — ADR-0106 masking is unaffected, and draft access is gated upstream, not by this schema. |
| previewDrafts | boolean | optional | Draft-visibility switch (ADR-0033 draft-overlay preview, non-strict): when true and state is not 'draft', a pending draft row is preferred if one exists, else the read falls back to the active row — the render path degrades to the published value instead of erroring. A served draft is tagged _draft: true so UIs can badge it. Declaration ≠ authorization: this member only switches which row is read, and ADR-0106 masking is unaffected — draft preview is admin-gated upstream, not by this schema. |
GetMetaItemResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name |
| name | string | ✅ | Item name |
| item | any | ✅ | Metadata item definition |
| sortability | { fields: Record<string, object> } | optional | Per-column sortability projection — present exactly when type is object, on every serving branch. Computed at serve time from the served document via the spec's own storage predicates; consumers render sort affordances from this signal and never re-derive it from field type. See ObjectSortabilitySchema for the closed category set. |
| lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Resolved lock verdict for this item (ADR-0010 §3.3). none means unlocked; no-overlay / no-delete / full mean the write doors refuse the corresponding write with 403. Joins two refusals: the document's own _lock (ITEM_LOCKED; the packaged artifact wins over any overlay), and the locked packaged base — an item a code package ships, on a type with no per-org overlay channel (NOT_OVERRIDABLE, or ITEM_LOCKED when the write names the read-only package). |
| lockReason | string | optional | Human-readable explanation shown next to a refused write. Present only when the resolved item declares _lockReason. |
| lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Which layer asserted the lock. Present only when the resolved item declares _lockSource. |
| lockDocsUrl | string | optional | Documentation link surfaced beside lockReason. Present only when the resolved item declares _lockDocsUrl. |
| provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Where the item came from (package | org | env-forced). Present only when the resolved item declares _provenance. |
| packageId | string | optional | Owning package machine id. Present only when the resolved item declares _packageId. |
| packageVersion | string | optional | Owning package version. Present only when the resolved item declares _packageVersion. |
| editable | boolean | optional | Whether an overlay write is permitted — false iff lock is no-overlay or full. A derived verdict: do not recompute it from lock client-side. |
| deletable | boolean | optional | Whether deleting the overlay is permitted — false iff lock is no-delete or full. |
| resettable | boolean | optional | Whether the item can be reset to its packaged default — true iff it is artifact-backed, i.e. there is a baseline to reset TO. |
| version | string | null | optional | ADR-0008 version token of the STORED row a save to this item compares against, at this read's scope (the environment's partition and ?package=) and lifecycle (?state=draft reads the draft row, the plain read the active row). The same keyed token a save receipt's version serves for that row: send it back as the If-Match request header on PUT /meta/:type/:name (with ?mode=draft for the draft) and a concurrent edit is refused 409 METADATA_CONFLICT instead of overwritten. null when no stored row is there — an item served from code or a package artifact, or from a row in another scope (a package-less row a package read falls back to) — so the next save creates one: pin it with If-None-Match: *. ABSENT on the cached published-value branch and on a ?preview=draft read, which publish no version: absence means "not published here", never "no row". Opaque — echo it verbatim, never parse it. |
| registration | { state: 'registered' } | { state: 'refused'; reason: object } | { state: 'unregistered' } | optional | The automation engine's verdict on this flow (see FlowRegistrationSchema): registered, or refused with the engine's reason verbatim, or unregistered. Present only on a flow read answered by a process that runs an automation service; absent otherwise, and absence means no engine judged it, never registered. Unlike the protection keys it is published by the cached branch too, read live and never part of the ETag, so a cached read that carries it is never answered 304; it is absent on a draft read, whose pending body no engine holds. The verdict is the answering process's engine. Beside item, never inside it: the item's status stays what its author declared. |
Nested Shape: GetMetaItemResponse.sortability
| Property | Type | Required | Description |
|---|---|---|---|
| fields | Record<string, { sortable: boolean; reason?: 'virtual-type'; caveat?: 'unprovisioned-anchor' }> | ✅ | Verdict per sortable-addressable column, keyed by field name. The domain is the served field map plus the always-provisioned id, less any field the caller's own field permission withholds: a caller exempt from the object-schema mask (ADR-0106 D4) is served such fields in item and gets no entry for them; for every other caller the served map already excludes them. A name absent from this map (an unknown field, a dotted path, an unprovisioned audit column, a field the caller may not read) has no platform sort behind it and must get no sort affordance. |
Nested Shape: GetMetaItemResponse.registration[state='registered']
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'registered' | ✅ | The engine holds a registration under the flow's name. |
Nested Shape: GetMetaItemResponse.registration[state='refused']
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'refused' | ✅ | The engine refused to register the stored flow and holds nothing under its name: it never runs. |
| reason | { code?: string; message: string } | ✅ | What the engine threw, verbatim. |
Nested Shape: GetMetaItemResponse.registration[state='unregistered']
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'unregistered' | ✅ | The engine holds nothing under the flow's name and recorded no refusal for it. |
GetMetaItemsRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name (e.g., "object", "plugin") |
| packageId | string | optional | Optional package ID to filter items by |
| previewDrafts | boolean | optional | Draft-visibility switch (ADR-0033 draft-overlay preview): when true, pending state='draft' rows are overlaid on the active list — draft wins on name collision, draft-only items appear, and each overlaid item is tagged _draft: true so UIs can badge the preview. Absent/false = published world only. Declaration ≠ authorization: this member only switches which rows are read, and ADR-0106 masking is unaffected — callers without draft-preview authorization are refused upstream (admin-gated), not by this schema. |
GetMetaItemsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name |
| items | any[] | ✅ | Array of metadata items |
| registrations | Record<string, { state: 'registered' } | { state: 'refused'; reason: object } | { state: 'unregistered' }> | optional | The automation engine's verdict on each listed flow, keyed by the item's name: one entry per listed item that is not a pending draft. Present only on a flow list answered by a process that runs an automation service; absent otherwise, and absence means no engine judged them, never registered. The verdict is the answering process's engine. Beside the items, never inside one: an item's status stays what its author declared. |
Nested Shape: GetMetaItemsResponse.registrations[string][state='registered']
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'registered' | ✅ | The engine holds a registration under the flow's name. |
Nested Shape: GetMetaItemsResponse.registrations[string][state='refused']
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'refused' | ✅ | The engine refused to register the stored flow and holds nothing under its name: it never runs. |
| reason | { code?: string; message: string } | ✅ | What the engine threw, verbatim. |
Nested Shape: GetMetaItemsResponse.registrations[string][state='unregistered']
| Property | Type | Required | Description |
|---|---|---|---|
| state | 'unregistered' | ✅ | The engine holds nothing under the flow's name and recorded no refusal for it. |
GetMetaTypesRequest
Properties
| Property | Type | Required | Description |
|---|
GetMetaTypesResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| types | string[] | ✅ | Available metadata type names (e.g., "object", "plugin", "view") |
| entries | { type: string; label: string; description?: string; filePatterns: string[]; … }[] | optional | Enriched per-type registry entries (Phase 3a) |
Nested Shape: GetMetaTypesResponse.entries[number]
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Singular type identifier |
| label | string | ✅ | Human-readable label |
| description | string | optional | Brief description |
| filePatterns | string[] | ✅ | Glob patterns used to discover artifacts of this type |
| supportsOverlay | boolean | ✅ | Whether a stored overlay row of an item a managed package ships may be removed through the ordinary delete door without the type's environment-overlay permission: the removal removes no managed definition, and changes what is served for permission alone; it opens no save and it decides no read |
| allowEnvironmentOverlay | boolean | ✅ | True when the registry grants environment overlays of this type's packaged items, or when OS_METADATA_WRITABLE names the type (overrideSource env), which opens the type's writes of items no managed package ships and never a managed item (ADR-0131 D6) |
| allowRuntimeCreate | boolean | ✅ | New artifacts of this type can be created via runtime API |
| supportsVersioning | boolean | ✅ | History is tracked for this type |
| executionPinned | boolean | ✅ | Runtime transactions pin a specific historical version_hash (ADR-0009) |
| loadOrder | integer | ✅ | Loading priority (lower = earlier) |
| domain | Enum<'data' | 'ui' | 'automation' | 'system' | 'security' | 'ai'> | ✅ | Protocol domain |
| overrideSource | Enum<'registry' | 'env'> | ✅ | Whether allowEnvironmentOverlay comes from the static registry or is elevated by the OS_METADATA_WRITABLE environment variable |
| createSeed | any | optional | Authoritative minimal valid create seed for this type — Studio/CLI/API derive create defaults from it (single source of truth in @objectstack/spec). Absent for canvas-create types whose shape is built interactively. |
GetNotificationPreferencesRequest
Properties
| Property | Type | Required | Description |
|---|
GetNotificationPreferencesResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| preferences | { email?: boolean; push?: boolean; inApp?: boolean; digest?: Enum<'none' | 'daily' | 'weekly'>; … } | ✅ | Current notification preferences |
Nested Shape: GetNotificationPreferencesResponse.preferences
| Property | Type | Required | Description |
|---|---|---|---|
boolean | optional (default: true) | Receive email notifications | |
| push | boolean | optional (default: true) | Receive push notifications |
| inApp | boolean | optional (default: true) | Receive in-app notifications |
| digest | Enum<'none' | 'daily' | 'weekly'> | optional (default: "none") | Email digest frequency |
| channels | Record<string, { enabled?: boolean; email?: boolean; push?: boolean }> | optional | Per-channel notification preferences |
GetObjectPermissionsRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name to get permissions for |
GetObjectPermissionsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name |
| permissions | { allowCreate?: boolean; allowRead?: boolean; allowEdit?: boolean; allowDelete?: boolean; … } | ✅ | Object-level permissions |
| fieldPermissions | Record<string, { readable?: boolean; editable?: boolean }> | optional | Field-level permissions keyed by field name |
Nested Shape: GetObjectPermissionsResponse.permissions
| Property | Type | Required | Description |
|---|---|---|---|
| allowCreate | boolean | optional (default: false) | Create permission |
| allowRead | boolean | optional (default: false) | Read permission |
| allowEdit | boolean | optional (default: false) | Edit permission |
| allowDelete | boolean | optional (default: false) | Delete permission |
| allowExport | boolean | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
| allowTransfer | boolean | optional (default: false) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
| allowRestore | never | optional | [REMOVED] objects.<object>.allowRestore was removed in @objectstack/spec 17 (ADR-0049) — the restore ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched restore stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| allowPurge | never | optional | [REMOVED] objects.<object>.allowPurge was removed in @objectstack/spec 17 (ADR-0049) — the purge ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched purge stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| viewAllRecords | boolean | optional (default: false) | View All Data (Bypass Sharing) |
| modifyAllRecords | boolean | optional (default: false) | Modify All Data (Bypass Sharing) — bypasses sharing rules and ownership on the objects record sharing enforces on; on an object with NO owner field sharing abstains, so the platform created_by write floor still applies. |
| readScope | Enum<'own' | 'own_and_reports' | 'unit' | 'unit_and_below' | 'org'> | optional | [ADR-0057 D1] Read depth: own|unit|unit_and_below|org |
| writeScope | Enum<'own' | 'own_and_reports' | 'unit' | 'unit_and_below' | 'org'> | optional | [ADR-0057 D1] Write depth: own|unit|unit_and_below|org |
Nested Shape: GetObjectPermissionsResponse.fieldPermissions[string]
| Property | Type | Required | Description |
|---|---|---|---|
| readable | boolean | optional (default: true) | Field read access |
| editable | boolean | optional (default: false) | Field edit access |
GetPackageRequest
Get package request
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Package identifier |
GetPackageResponse
Get package response
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| package | { manifest: object; status?: Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'>; enabled?: boolean; installedAt?: string; … } | ✅ | Package details |
Nested Shape: GetPackageResponse.package
| Property | Type | Required | Description |
|---|---|---|---|
| manifest | { id: string; namespace?: string; defaultDatasource?: string; version: string; … } | ✅ | Package manifest at the AUTHORING stage; a row installed by a defineStack() host carries the assembled body instead — see AssembledInstalledPackageSchema / InstalledPackageAtEitherStageSchema |
| status | Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'> | optional (default: "installed") | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
| enabled | boolean | optional (default: true) | Whether the package is currently enabled |
| installedAt | string | optional | Installation timestamp |
| updatedAt | string | optional | Last update timestamp |
| installedVersion | string | optional | Currently installed version for quick access |
| previousVersion | string | optional | Version before the last upgrade |
| statusChangedAt | string | optional | Status change timestamp |
| errorMessage | string | optional | Error message when status is error |
| settings | Record<string, any> | optional | User-provided configuration settings |
| upgradeHistory | { fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' | 'failed' | 'rolled_back'>; … }[] | optional | Version upgrade history |
| registeredNamespaces | string[] | optional | Namespace prefixes registered by this package |
GetPresenceRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| channel | string | ✅ | Channel to get presence for |
GetPresenceResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| channel | string | ✅ | Channel name |
| members | { userId: string; status: Enum<'online' | 'away' | 'busy' | 'offline'>; lastSeen: string; metadata?: Record<string, any> }[] | ✅ | Active members and their presence state |
Nested Shape: GetPresenceResponse.members[number]
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | ✅ | User identifier |
| status | Enum<'online' | 'away' | 'busy' | 'offline'> | ✅ | Current presence status |
| lastSeen | string | ✅ | ISO 8601 datetime of last activity |
| metadata | Record<string, any> | optional | Custom presence data (e.g., current page, custom status) |
GetPublishedMetaItemResponse
The published metadata item body, opaque by ruling (1C). Shape is the item's own metadata-type schema, resolved at read time — never frozen into this contract.
GetTranslationsRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| locale | string | ✅ | BCP-47 locale code |
GetTranslationsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| locale | string | ✅ | Locale code |
| translations | { objects?: Record<string, object>; picklists?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; … } | ✅ | Translation data |
Nested Shape: GetTranslationsResponse.translations
| Property | Type | Required | Description |
|---|---|---|---|
| objects | Record<string, { label?: string; pluralLabel?: string; description?: string; fields?: Record<string, object>; … }> | optional | Object translations keyed by object name |
| picklists | Record<string, { label?: string; options: Record<string, string> }> | optional | Picklist translations keyed by picklist name |
| apps | Record<string, { label: string; description?: string; navigation?: Record<string, object> }> | optional | App translations keyed by app name |
| messages | Record<string, string> | optional | UI message translations keyed by message ID |
| globalActions | Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }> | optional | Global action translations keyed by action name |
| dashboards | Record<string, { label?: string; description?: string; actions?: Record<string, object>; widgets?: Record<string, object>; … }> | optional | Dashboard translations keyed by dashboard name |
| datasets | Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }> | optional | Analytics dataset translations keyed by dataset name |
| pages | Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }> | optional | Page translations keyed by page name |
| flows | Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }> | optional | Screen-flow translations keyed by flow name |
| metadataForms | Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }> | optional | Translations for metadata-type configuration forms keyed by metadata type |
| settingsCommon | { sourceLabels?: object } | optional | Cross-namespace Settings UI strings |
| settings | Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }> | optional | Settings manifest translations keyed by namespace |
GetUiViewRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name (snake_case) |
| type | Enum<'list' | 'form'> | ✅ | View type |
GetUiViewResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. |
| label | string | Record<string, string> | optional | Human-readable label shown in metadata lists. |
| object | string | optional | Object this container binds to — how a stack-level views: [...] entry says which object its views belong to; read by getViewsByObject() / GET /meta/view?object=. |
| list | { name?: string; label?: string | Record<string, string>; type?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>; data?: object | object | object; … } | optional | |
| form | { type?: Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>; layout?: Enum<'vertical' | 'horizontal'>; columns?: integer; title?: string; … } | optional | |
| listViews | Record<string, { name?: string; label?: string | Record<string, string>; type?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>; data?: object | object | object; … }> | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) |
| formViews | Record<string, { type?: Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>; layout?: Enum<'vertical' | 'horizontal'>; columns?: integer; title?: string; … }> | optional | Additional named form views |
| protection | { lock: Enum<'none' | 'no-overlay' | 'no-delete' | 'full'>; reason: string; docsUrl?: string } | optional | Package author protection block — lock policy for this view. |
| _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. |
Nested Shape: GetUiViewResponse.list
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Internal view name (lowercase snake_case) |
| label | string | Record<string, string> | optional | Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time |
| type | Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'> | optional (default: "grid") | |
| data | { provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> } | optional | Data source configuration (defaults to "object" provider) |
| columns | string[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[] | ✅ | Fields to display as columns — the PROJECTION of the per-view field composition columns x hiddenFields x fieldOrder: this list is the candidate set AND the baseline order; hiddenFields subtracts from it and fieldOrder orders what survives. hiddenFields and fieldOrder cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's ListView grid derives the object's default columns). |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Filter criteria (JSON Rules) |
| sort | { field: string; order: Enum<'asc' | 'desc'> }[] | optional | |
| searchableFields | string[] | optional | Fields enabled for search |
| filterableFields | string[] | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters |
| resizable | boolean | optional | Enable column resizing |
| compactToolbar | boolean | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover |
| selection | { type?: Enum<'none' | 'single' | 'multiple'> } | optional | Row selection configuration |
| navigation | { mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … } | optional | Configuration for item click navigation (page, drawer, modal, etc.) |
| pagination | { pageSize?: integer; pageSizeOptions?: integer[] } | optional | Pagination configuration |
| kanban | { groupByField: string; summarizeField?: string; titleField?: string; columns: string[] } | optional | Kanban-board configuration — applies when the view renders as a kanban layout |
| calendar | { startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … } | optional | Calendar configuration — applies when the view renders as a calendar layout |
| gantt | { startDateField: string; endDateField: string; titleField: string; progressField?: string; … } | optional | Gantt-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; … } | optional | Gallery/card view configuration |
| timeline | { startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … } | optional | Timeline view configuration |
| chart | { chartType?: Enum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>; dataset: string; dimensions?: string[]; values: string[] } | optional | Chart binding — applies when the view renders as a chart. A type: 'chart' view must bind one: it names the ADR-0021 dataset and the measures (values) the chart plots, and there is no default binding |
| map | { latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … } | optional | Map configuration — applies when the view renders as a map layout |
| tree | { parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout |
| pageName | never | optional | [REMOVED] view.pageName was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a type: 'page' view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — { type: 'page', pageName: '<page_name>' } under the app's navigation — which is a different key on a different surface and is the page mount that has always rendered. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| description | string | Record<string, string> | optional | View description for documentation/tooltips |
| sharing | { type?: Enum<'personal' | 'collaborative'>; lockedBy?: string } | optional | View sharing and access configuration |
| requiredPermissions | string[] | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through systemPermissions, the same list as requiredPermissions on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. |
| rowHeight | Enum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'> | optional | Row height / density setting |
| grouping | { fields: object[] } | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) |
| rowColor | { field: string; colors?: Record<string, string> } | optional | Color rows based on field value |
| hiddenFields | string[] | optional | Fields to hide in this specific view — the SUBTRACTION of the per-view field composition columns x hiddenFields x fieldOrder: each name listed here is removed from the columns projection before fieldOrder orders the remainder. A name columns never projected subtracts nothing. |
| fieldOrder | string[] | optional | Explicit field display order for this view — the ORDERING of the per-view field composition columns x hiddenFields x fieldOrder: it sorts what survives columns minus hiddenFields and never adds a field. A surviving column absent from fieldOrder sorts LAST, after every listed one, keeping its columns-relative order; a name listed here that did not survive orders nothing. |
| rowActions | string[] | optional | Actions available for individual row items |
| bulkActions | string[] | optional | Actions available when multiple rows are selected |
| bulkActionDefs | { name: string; label?: string; icon?: string; variant?: Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'>; … }[] | optional | Rich 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> }[] | optional | Conditional formatting rules for list rows |
| inlineEdit | boolean | optional | Allow inline editing of records directly in the list view |
| exportOptions | Enum<'csv' | 'xlsx' | 'json'>[] | { formats?: Enum<'csv' | 'xlsx' | 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … } | optional | Export 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; … } | optional | User action toggles for the view toolbar |
| appearance | { showDescription?: boolean; allowedVisualizations?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[] } | optional | Appearance and visualization configuration |
| tabs | never | optional | [REMOVED] view.list.tabs was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's listViews instead: the tab's name becomes the entry's key, its label the entry's label, and its filter rules join the view's own filter on that entry (copy the view's columns too); a tab whose view already named a list view needs nothing more. Every listViews entry renders as a tab in the switcher. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| addRecord | { enabled?: boolean; position?: Enum<'top' | 'bottom' | 'both'>; mode?: Enum<'inline' | 'form' | 'modal'>; formView?: string } | optional | Add record entry point configuration |
| showRecordCount | boolean | optional | Show record count at the bottom of the list |
| allowPrinting | boolean | optional | Allow users to print the view |
| emptyState | { title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object } | optional | Empty state configuration when no records found |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes for the list view |
| responsive | never | optional | [REMOVED] view.responsive was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| performance | never | optional | [REMOVED] view.performance was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| striped | never | optional | [REMOVED] view.striped was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| bordered | never | optional | [REMOVED] view.bordered was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| virtualScroll | never | optional | [REMOVED] view.virtualScroll was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via pagination. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| userFilters | { element?: Enum<'dropdown' | 'toggle'>; fields?: object[] } | optional |
Nested Shape: GetUiViewResponse.form
| Property | Type | Required | Description |
|---|---|---|---|
| type | Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'> | optional (default: "simple") | |
| layout | Enum<'vertical' | 'horizontal'> | optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set columns |
| columns | integer | optional | Number of columns for the form body |
| title | string | optional | Form title |
| description | string | optional | Form description |
| defaultTab | string | optional | Initially active tab (tabbed forms) |
| tabPosition | Enum<'top' | 'bottom' | 'left' | 'right'> | optional | Tab strip position (tabbed forms) |
| allowSkip | boolean | optional | Wizard step-gate opt-out: allow entering a later step without submitting the one before it. Navigation freedom, NOT a validation exemption — the final submit still re-validates every step and returns to the first failing one. Default (absent/false): steps unlock in array order as each prior step submits validly. UI admission only, never authorization. |
| showStepIndicator | boolean | optional | Show the wizard step indicator (renderer default: shown). Step progress (completed/current/upcoming/invalid) is derived from the step gate — this boolean is the only authorable progress surface. |
| splitDirection | Enum<'horizontal' | 'vertical'> | optional | Split orientation (split forms) |
| splitSize | number | optional | Primary split panel size, % (split forms) |
| splitResizable | boolean | optional | Whether the split is resizable (split forms) |
| drawerSide | Enum<'top' | 'bottom' | 'left' | 'right'> | optional | Drawer side (drawer forms) |
| drawerWidth | string | optional | [DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. |
| modalSize | Enum<'sm' | 'default' | 'lg' | 'xl' | 'full'> | optional | Modal size (modal forms) |
| data | { provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> } | optional | Data source configuration (defaults to "object" provider) |
| sections | { name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[] | optional | |
| groups | { name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[] | optional | [LEGACY ALIAS → sections] Accepted for back-compat and folded onto sections at parse; sections wins when both are present. Prefer sections. |
| subforms | { childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[] | optional | Inline master-detail child collections |
| defaultSort | never | optional | [REMOVED] form.defaultSort was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's sort. Delete the key and set the sort on the related list view instead. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| sharing | { enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … } | optional | Public sharing configuration for this form |
| submitBehavior | { kind: 'thank-you'; title?: string; message?: string } | { kind: 'redirect'; url: string; delayMs?: integer } | { kind: 'continue' } | { kind: 'next-record' } | optional | Post-submit behavior. On the redirect arm, url is relative-only and interpolates only declared record fields as {{record.field_name}}, URL-escaped. |
| buttons | { submit?: object; cancel?: object; reset?: object } | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| defaults | Record<string, any> | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). |
| aria | never | optional | [REMOVED] form.aria was removed in @objectstack/spec 17.0.0 (audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
Nested Shape: GetUiViewResponse.listViews[string]
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Internal view name (lowercase snake_case) |
| label | string | Record<string, string> | optional | Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time |
| type | Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'> | optional (default: "grid") | |
| data | { provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> } | optional | Data source configuration (defaults to "object" provider) |
| columns | string[] | { field: string; label?: string | Record<string, string>; width?: number; align?: Enum<'left' | 'center' | 'right'>; … }[] | ✅ | Fields to display as columns — the PROJECTION of the per-view field composition columns x hiddenFields x fieldOrder: this list is the candidate set AND the baseline order; hiddenFields subtracts from it and fieldOrder orders what survives. hiddenFields and fieldOrder cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's ListView grid derives the object's default columns). |
| filter | { field: string; operator: Enum<'equals' | 'not_equals' | 'contains' | 'not_contains' | 'icontains' | …>; value?: string | number | boolean | null | (string | number)[] }[] | optional | Filter criteria (JSON Rules) |
| sort | { field: string; order: Enum<'asc' | 'desc'> }[] | optional | |
| searchableFields | string[] | optional | Fields enabled for search |
| filterableFields | string[] | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters |
| resizable | boolean | optional | Enable column resizing |
| compactToolbar | boolean | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover |
| selection | { type?: Enum<'none' | 'single' | 'multiple'> } | optional | Row selection configuration |
| navigation | { mode?: Enum<'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full'>; … } | optional | Configuration for item click navigation (page, drawer, modal, etc.) |
| pagination | { pageSize?: integer; pageSizeOptions?: integer[] } | optional | Pagination configuration |
| kanban | { groupByField: string; summarizeField?: string; titleField?: string; columns: string[] } | optional | Kanban-board configuration — applies when the view renders as a kanban layout |
| calendar | { startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … } | optional | Calendar configuration — applies when the view renders as a calendar layout |
| gantt | { startDateField: string; endDateField: string; titleField: string; progressField?: string; … } | optional | Gantt-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; … } | optional | Gallery/card view configuration |
| timeline | { startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … } | optional | Timeline view configuration |
| chart | { chartType?: Enum<'bar' | 'line' | 'pie' | 'area' | 'scatter'>; dataset: string; dimensions?: string[]; values: string[] } | optional | Chart binding — applies when the view renders as a chart. A type: 'chart' view must bind one: it names the ADR-0021 dataset and the measures (values) the chart plots, and there is no default binding |
| map | { latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … } | optional | Map configuration — applies when the view renders as a map layout |
| tree | { parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout |
| pageName | never | optional | [REMOVED] view.pageName was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a type: 'page' view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — { type: 'page', pageName: '<page_name>' } under the app's navigation — which is a different key on a different surface and is the page mount that has always rendered. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| description | string | Record<string, string> | optional | View description for documentation/tooltips |
| sharing | { type?: Enum<'personal' | 'collaborative'>; lockedBy?: string } | optional | View sharing and access configuration |
| requiredPermissions | string[] | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through systemPermissions, the same list as requiredPermissions on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. |
| rowHeight | Enum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'> | optional | Row height / density setting |
| grouping | { fields: object[] } | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) |
| rowColor | { field: string; colors?: Record<string, string> } | optional | Color rows based on field value |
| hiddenFields | string[] | optional | Fields to hide in this specific view — the SUBTRACTION of the per-view field composition columns x hiddenFields x fieldOrder: each name listed here is removed from the columns projection before fieldOrder orders the remainder. A name columns never projected subtracts nothing. |
| fieldOrder | string[] | optional | Explicit field display order for this view — the ORDERING of the per-view field composition columns x hiddenFields x fieldOrder: it sorts what survives columns minus hiddenFields and never adds a field. A surviving column absent from fieldOrder sorts LAST, after every listed one, keeping its columns-relative order; a name listed here that did not survive orders nothing. |
| rowActions | string[] | optional | Actions available for individual row items |
| bulkActions | string[] | optional | Actions available when multiple rows are selected |
| bulkActionDefs | { name: string; label?: string; icon?: string; variant?: Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'>; … }[] | optional | Rich 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> }[] | optional | Conditional formatting rules for list rows |
| inlineEdit | boolean | optional | Allow inline editing of records directly in the list view |
| exportOptions | Enum<'csv' | 'xlsx' | 'json'>[] | { formats?: Enum<'csv' | 'xlsx' | 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … } | optional | Export 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; … } | optional | User action toggles for the view toolbar |
| appearance | { showDescription?: boolean; allowedVisualizations?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'>[] } | optional | Appearance and visualization configuration |
| tabs | never | optional | [REMOVED] view.list.tabs was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's listViews instead: the tab's name becomes the entry's key, its label the entry's label, and its filter rules join the view's own filter on that entry (copy the view's columns too); a tab whose view already named a list view needs nothing more. Every listViews entry renders as a tab in the switcher. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| addRecord | { enabled?: boolean; position?: Enum<'top' | 'bottom' | 'both'>; mode?: Enum<'inline' | 'form' | 'modal'>; formView?: string } | optional | Add record entry point configuration |
| showRecordCount | boolean | optional | Show record count at the bottom of the list |
| allowPrinting | boolean | optional | Allow users to print the view |
| emptyState | { title?: string | Record<string, string>; message?: string | Record<string, string>; icon?: string; link?: object } | optional | Empty state configuration when no records found |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes for the list view |
| responsive | never | optional | [REMOVED] view.responsive was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| performance | never | optional | [REMOVED] view.performance was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| striped | never | optional | [REMOVED] view.striped was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| bordered | never | optional | [REMOVED] view.bordered was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| virtualScroll | never | optional | [REMOVED] view.virtualScroll was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via pagination. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| userFilters | { element?: Enum<'dropdown' | 'toggle'>; fields?: object[] } | optional |
Nested Shape: GetUiViewResponse.formViews[string]
| Property | Type | Required | Description |
|---|---|---|---|
| type | Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'> | optional (default: "simple") | |
| layout | Enum<'vertical' | 'horizontal'> | optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set columns |
| columns | integer | optional | Number of columns for the form body |
| title | string | optional | Form title |
| description | string | optional | Form description |
| defaultTab | string | optional | Initially active tab (tabbed forms) |
| tabPosition | Enum<'top' | 'bottom' | 'left' | 'right'> | optional | Tab strip position (tabbed forms) |
| allowSkip | boolean | optional | Wizard step-gate opt-out: allow entering a later step without submitting the one before it. Navigation freedom, NOT a validation exemption — the final submit still re-validates every step and returns to the first failing one. Default (absent/false): steps unlock in array order as each prior step submits validly. UI admission only, never authorization. |
| showStepIndicator | boolean | optional | Show the wizard step indicator (renderer default: shown). Step progress (completed/current/upcoming/invalid) is derived from the step gate — this boolean is the only authorable progress surface. |
| splitDirection | Enum<'horizontal' | 'vertical'> | optional | Split orientation (split forms) |
| splitSize | number | optional | Primary split panel size, % (split forms) |
| splitResizable | boolean | optional | Whether the split is resizable (split forms) |
| drawerSide | Enum<'top' | 'bottom' | 'left' | 'right'> | optional | Drawer side (drawer forms) |
| drawerWidth | string | optional | [DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. |
| modalSize | Enum<'sm' | 'default' | 'lg' | 'xl' | 'full'> | optional | Modal size (modal forms) |
| data | { provider: 'object'; object: string } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> } | optional | Data source configuration (defaults to "object" provider) |
| sections | { name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[] | optional | |
| groups | { name?: string; label?: string | Record<string, string>; description?: string; collapsible?: boolean; … }[] | optional | [LEGACY ALIAS → sections] Accepted for back-compat and folded onto sections at parse; sections wins when both are present. Prefer sections. |
| subforms | { childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[] | optional | Inline master-detail child collections |
| defaultSort | never | optional | [REMOVED] form.defaultSort was removed in @objectstack/spec 17.0.0 (audit close-out) — nothing read it: a related list inside a form sorts by its own list view's sort. Delete the key and set the sort on the related list view instead. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
| sharing | { enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … } | optional | Public sharing configuration for this form |
| submitBehavior | { kind: 'thank-you'; title?: string; message?: string } | { kind: 'redirect'; url: string; delayMs?: integer } | { kind: 'continue' } | { kind: 'next-record' } | optional | Post-submit behavior. On the redirect arm, url is relative-only and interpolates only declared record fields as {{record.field_name}}, URL-escaped. |
| buttons | { submit?: object; cancel?: object; reset?: object } | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| defaults | Record<string, any> | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). |
| aria | never | optional | [REMOVED] form.aria was removed in @objectstack/spec 17.0.0 (audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand. |
Nested Shape: GetUiViewResponse.protection
| Property | Type | Required | Description |
|---|---|---|---|
| lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | ✅ | Lock policy — none | no-overlay | no-delete | full. |
| reason | string | ✅ | User-visible reason shown when the lock blocks an action. |
| docsUrl | string | optional | Optional URL the Studio banner links to for more context. |
HistoryMetaItemRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name |
| name | string | ✅ | Item name |
| sinceSeq | number | optional | Exclusive lower bound on seq for pagination: only events with seq > sinceSeq are returned. Absent means "from the beginning". |
| limit | number | optional | Maximum events to return, oldest first. Forwarded to the repository unclamped and with NO default — absent means the full remaining change log. (Deliberately no declared bounds: unlike the audit twin's [1, 500] clamp, nothing on this path clamps or refuses, so declaring .min()/.max() here would refuse values the shipped verb accepts.) |
| member | string | optional | Bundle member selector, for a metadata type whose identity is a pair (today only email_template, identified by (name, locale)): the member this request addresses, spelled as its discriminator value (zh-CN). Absent, the canonical member (en-US for email_template), which is what a bare-name read answers; naming the canonical value is the same request. It is not the response translation locale, which ?locale= and Accept-Language choose. Refused with 400 VALIDATION_ERROR on a type that has no members, and for a blank value or one longer than a stored member can be (100 characters). A well-formed member with nothing stored or shipped answers as that door answers an absent item. |
HistoryMetaItemResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| events | { seq: integer; op: Enum<'create' | 'update' | 'delete' | 'rename' | 'publish' | 'revert'>; ref: object; hash: string | null; … }[] | ✅ | The durable change-log for the item, oldest first. See the schema-level note for what an empty array means — and what it never means. |
Nested Shape: HistoryMetaItemResponse.events[number]
| Property | Type | Required | Description |
|---|---|---|---|
| seq | integer | ✅ | Sequence number this write produced in the org log (sys_metadata_history.event_seq) — the token the request's sinceSeq filters on, and the same key the write-verb receipts (SaveMetaItemResponseSchema.seq and siblings) carry. |
| op | Enum<'create' | 'update' | 'delete' | 'rename' | 'publish' | 'revert'> | ✅ | Which change-log operation the event records (ADR-0008 §2.4). Closed vocabulary, mirrored from the producer's own enum. |
| ref | { org: string; type: string; name: string; version?: string } | ✅ | Which item the event is about. |
| hash | string | null | ✅ | Content hash of the body this event wrote; null when the event wrote none (op="delete"). |
| parentHash | string | null | ✅ | Hash the written version was derived from; null for a first version. |
| version | integer | optional | Per-(org,type,name) monotonic lineage counter at this event — the token rollbackMetaItem({ toVersion }) pins. Absent when the row recorded none. |
| previousName | string | optional | Set on op="rename": the old machine name. |
| actor | string | null | ✅ | Who wrote this. null = system-initiated (boot sync, migration, scheduled job) — never a sentinel string: consumers that resolve this against sys_user must be able to tell "nobody" from "a user id". |
| message | string | optional | Optional commit message recorded with the write. |
| ts | string | ✅ | When the write happened (ISO-8601 string). |
| source | string | ✅ | Origin label of the write: "fs", "studio", "rest", "ai", "git-import", … |
HttpFindQueryParams
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| filter | string | optional | JSON-encoded filter expression (canonical, singular). |
| filters | string | optional | JSON-encoded filter expression (deprecated plural alias). |
| select | string | optional | Comma-separated list of fields to retrieve. |
| sort | string | optional | Sort expression (e.g. "name asc,created_at desc" or "-created_at"). |
| orderBy | string | optional | Alias for sort (OData compatibility). |
| top | number | optional | Max records to return (limit). |
| skip | number | optional | Records to skip (offset). |
| expand | string | optional | Comma-separated list of lookup/master_detail field names to expand. Resolved to populate array and passed to the engine for batch $in expansion. |
| search | string | optional | Full-text search query. |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| count | boolean | optional | Include total count in response. |
InstallPackageRequest
Install package request
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| manifest | { id: string; namespace?: string; defaultDatasource?: string; version: string; … } | ✅ | Package manifest to install |
| settings | Record<string, any> | optional | User-provided settings at install time |
| enableOnInstall | boolean | optional | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive honours it on the registry row: true enables, false disables, and ABSENT keeps the row's current lifecycle state (a fresh install lands enabled) |
| platformVersion | string | optional | Current platform version for compatibility verification |
Nested Shape: InstallPackageRequest.manifest
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
| namespace | string | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
| defaultDatasource | string | optional (default: "default") | Default datasource for all objects in this package |
| version | string | ✅ | Package version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1, 1.0.0+20230101) |
| type | Enum<'plugin' | 'ui' | 'driver' | 'server' | 'app' | 'theme' | 'agent' | 'objectql' | …> | ✅ | Type of package |
| scope | Enum<'cloud' | 'system' | 'project'> | optional (default: "project") | Deployment scope: cloud | system | project |
| installModes | Enum<'managed' | 'unmanaged'>[] | optional | Install modes this package permits (ADR-0131 D6): managed | unmanaged. Absent means ['managed']. |
| name | string | ✅ | Human-readable package name |
| description | string | optional | Package description |
| permissions | { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] } | optional | Required permissions at the AUTHORING stage: the structured plugin block { services, hooks, network, fs } (ADR-0025 §3.2) — the legacy flat string list is retired — and at the assembled stage the same key is the ADR-0090 PermissionSet[] collection instead (AssembledPackageBodySchema) |
| objects | string[] | optional | Glob patterns for ObjectQL schemas files |
| datasources | string[] | optional | Glob patterns for Datasource definitions |
| dependencies | Record<string, string> | optional | Package dependencies |
| configuration | never | optional | [REMOVED] manifest.configuration was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, properties.*.secret promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in defineStack({ plugins: [new MyPlugin({ … })] }), which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
| contributes | { kinds?: object[] } | optional | Platform contributions |
| data | { object: string; externalId?: string | string[]; mode?: Enum<'insert' | 'update' | 'upsert' | 'replace' | 'ignore'>; env?: Enum<'prod' | 'dev' | 'test'>[]; … }[] | optional | Initial seed data (prefer top-level data field) |
| capabilities | never | optional | [REMOVED] manifest.capabilities was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read implements, provides, requires, extensionPoints or extensions, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level manifest.dependencies, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
| extensions | never | optional | [REMOVED] manifest.extensions was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: contributes.kinds registers metadata kinds, navigationContributions injects navigation into other packages' apps, and code-level extension happens in the plugin itself (init/start). |
| navigationContributions | { app: string; group?: string; priority?: integer; items: (object | … +9 more)[] }[] | optional | Navigation items this package contributes into apps owned by other packages |
| loading | never | optional | [REMOVED] manifest.loading was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (strategy, preload, codeSplitting, dynamicImport, initialization, dependencyResolution, hotReload, caching, sandboxing, monitoring) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — defineStack registers them and the kernel runs init then start in an order topologically resolved from each composed plugin's own dependencies / optionalDependencies (resolvePluginOrder); the set is fixed until the process restarts. ⚠️ loading.sandboxing in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and allowedServices gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (manifest.runtime) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the node tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
| engine | { objectstack: string } | optional | Platform compatibility requirements (legacy; superseded by engines) |
| engines | { platform?: string; protocol?: string } | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes engine) |
| runtime | Enum<'node' | 'sandbox' | 'worker'> | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting node → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |
| packaging | Enum<'bundled' | 'manifest-deps'> | optional | Dependency packaging strategy (ADR-0025 §3.3) |
| main | string | optional | Entry module of a code-bearing plugin, relative to the plugin root; os plugin build bundles it and writes dist/index.mjs here in the compiled manifest (ADR-0025 §3.4) |
| integrity | Record<string, string> | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
InstallPackageResponse
Install package response
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| package | { manifest: object; status?: Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'>; enabled?: boolean; installedAt?: string; … } | ✅ | Installed package details |
| message | string | optional | Installation status message |
| dependencyResolution | { dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … } | optional | Dependency resolution result from install analysis |
Nested Shape: InstallPackageResponse.package
| Property | Type | Required | Description |
|---|---|---|---|
| manifest | { id: string; namespace?: string; defaultDatasource?: string; version: string; … } | ✅ | Package manifest at the AUTHORING stage; a row installed by a defineStack() host carries the assembled body instead — see AssembledInstalledPackageSchema / InstalledPackageAtEitherStageSchema |
| status | Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'> | optional (default: "installed") | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
| enabled | boolean | optional (default: true) | Whether the package is currently enabled |
| installedAt | string | optional | Installation timestamp |
| updatedAt | string | optional | Last update timestamp |
| installedVersion | string | optional | Currently installed version for quick access |
| previousVersion | string | optional | Version before the last upgrade |
| statusChangedAt | string | optional | Status change timestamp |
| errorMessage | string | optional | Error message when status is error |
| settings | Record<string, any> | optional | User-provided configuration settings |
| upgradeHistory | { fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' | 'failed' | 'rolled_back'>; … }[] | optional | Version upgrade history |
| registeredNamespaces | string[] | optional | Namespace prefixes registered by this package |
Nested Shape: InstallPackageResponse.dependencyResolution
| Property | Type | Required | Description |
|---|---|---|---|
| dependencies | { packageId: string; requiredRange: string; resolvedVersion?: string; installedVersion?: string; … }[] | ✅ | Resolution result for each dependency |
| canProceed | boolean | ✅ | Whether installation can proceed |
| requiredActions | { type: Enum<'install' | 'upgrade' | 'confirm_conflict'>; packageId: string; description: string }[] | ✅ | Actions required before proceeding |
| installOrder | string[] | ✅ | Topologically sorted package IDs for installation |
| circularDependencies | string[][] | optional | Circular dependency chains detected (e.g. [["A", "B", "A"]]) |
ListAiConversationsRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| agentId | string | optional | Filter by agent |
| limit | integer | optional | Maximum conversations to return |
| cursor | string | optional | The id of the last conversation on the previous page. The next page starts with the conversation created immediately before it, continuing newest first. Omit it to read the first page. An id that names no conversation of the caller is refused rather than read as the start of the list. |
ListAiConversationsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| conversations | { id: string; title?: string; agentId?: string; userId?: string; … }[] | ✅ | The caller's conversations, newest first — ordered by creation time, then id, both descending |
| hasMore | boolean | ✅ | Whether at least one more conversation follows this page. When true, send the id of the last conversation in conversations as cursor to read the next page. |
Nested Shape: ListAiConversationsResponse.conversations[number]
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Conversation id |
| title | string | optional | Title / summary |
| agentId | string | optional | Agent this conversation is bound to |
| userId | string | optional | Owning user |
| messages | ({ role: Enum<'system' | 'user' | 'assistant' | 'tool'>; content?: any; parts?: any[] } & Record<string, any>)[] | ✅ | Message history |
| createdAt | string | ✅ | Creation timestamp (ISO 8601) |
| updatedAt | string | ✅ | Last update timestamp (ISO 8601) |
| metadata | Record<string, any> | optional | Conversation metadata |
ListAiPendingActionsRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| status | Enum<'pending' | 'approved' | 'executed' | 'failed' | 'rejected'> | optional | Filter by status |
| conversationId | string | optional | Filter by proposing conversation |
| limit | integer | optional | Max rows (server default 100) |
ListAiPendingActionsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| items | { id: string; object_name: string; action_name: string; tool_name: string; … }[] | ✅ | Queued actions, newest first |
| total | number | ✅ | Number of rows returned (not a total across pages) |
Nested Shape: ListAiPendingActionsResponse.items[number]
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Pending action id |
| object_name | string | ✅ | Object the action targets |
| action_name | string | ✅ | Action name |
| tool_name | string | ✅ | Tool that would execute it |
| tool_input | string | ✅ | Serialized tool input |
| status | Enum<'pending' | 'approved' | 'executed' | 'failed' | 'rejected'> | ✅ | Lifecycle status |
| result | string | optional | Serialized result, once executed |
| error | string | optional | Failure message, when status is failed |
| rejection_reason | string | optional | Reason given at rejection |
| conversation_id | string | optional | Conversation that proposed it |
| message_id | string | optional | Message that proposed it |
| proposed_by | string | optional | Actor that proposed it |
| decided_by | string | optional | Actor that approved or rejected it |
| proposed_at | string | ✅ | Proposal timestamp (ISO 8601) |
| decided_at | string | optional | Decision timestamp (ISO 8601) |
ListDraftsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| drafts | { type: string; name: string; member?: string; label: string | Record<string, string> | null; … }[] | ✅ | Every pending draft visible to the caller, one row per item. |
Nested Shape: ListDraftsResponse.drafts[number]
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name (canonical singular). |
| name | string | ✅ | Item name. |
| member | string | optional | The bundle member this draft is, for a metadata type whose identity is a pair (today only email_template, identified by (name, locale)): its discriminator value (zh-CN), and the canonical member spelled out (en-US). Present on every draft of such a type, so two members of one name are two entries; absent for a type whose identity is its name alone. Pass it back as the by-name doors' member selector (?member=) to read, publish or discard this draft. |
| label | string | Record<string, string> | null | ✅ | The draft body's own top-level label, as authored: a plain string or an inline locale map (I18nLabel), resolved by the reader. null when the body declares none, or none this shape admits — never the item name standing in for it. |
| packageId | string | null | ✅ | Package the draft is bound to, null for a package-less draft. |
| updatedAt | string | null | ✅ | Last-touch timestamp of the draft row (ISO-8601 string), null when the row recorded none. |
| updatedBy | string | null | ✅ | Who last touched the draft, null when the row recorded none. |
ListNotificationsRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| read | boolean | optional | Filter by read status |
| type | string | optional | Filter by notification type |
| limit | number | optional | Maximum number of notifications to return — the newest N. Omitted leaves the window to the server, which is not a fixed part of this contract: the platform inbox answers 50 and clamps any requested value into 1..200 rather than refusing it. This endpoint is not paginated — there is no continuation token, so a larger window is the only way to see more. |
| cursor | never | optional | [REMOVED] cursor was removed from GET /api/v1/notifications in @objectstack/spec 17 (ADR-0049) — it was declared on the request AND the response and honoured on neither: the server reads only read/type/limit, and no emit site ever wrote the response key, so a caller paginating by it re-read the first window forever with no error and no 400. Delete the key; the cursor argument of client.notifications.list() was removed with it. This route is NOT paginated — it answers the newest limit notifications and stops, so ask for a bigger window (limit, clamped by the server into 1..200) instead of a next page. A first-class inbox cursor, if ever built, will be a response-minted opaque token, not this key. |
ListNotificationsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| notifications | { id: string; type: string; title: string; body: string; … }[] | ✅ | List of notifications — the newest window, not a page |
| unreadCount | number | ✅ | Total number of unread notifications |
| cursor | never | optional | [REMOVED] cursor was removed from GET /api/v1/notifications in @objectstack/spec 17 (ADR-0049) — it was declared on the request AND the response and honoured on neither: the server reads only read/type/limit, and no emit site ever wrote the response key, so a caller paginating by it re-read the first window forever with no error and no 400. Delete the key; the cursor argument of client.notifications.list() was removed with it. This route is NOT paginated — it answers the newest limit notifications and stops, so ask for a bigger window (limit, clamped by the server into 1..200) instead of a next page. A first-class inbox cursor, if ever built, will be a response-minted opaque token, not this key. |
Nested Shape: ListNotificationsResponse.notifications[number]
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Notification ID |
| type | string | ✅ | Notification type |
| title | string | ✅ | Notification title |
| body | string | ✅ | Notification body text |
| read | boolean | optional (default: false) | Whether notification has been read |
| data | Record<string, any> | optional | Additional notification data |
| actionUrl | string | optional | URL to navigate to when clicked |
| createdAt | string | ✅ | When notification was created |
ListPackagesRequest
List packages request
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| status | Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'> | optional | Filter by package status |
| type | Enum<'plugin' | 'ui' | 'driver' | 'server' | 'app' | 'theme' | 'agent' | 'objectql' | 'module' | 'gateway' | 'adapter'> | optional | Filter by package type |
| enabled | boolean | optional | Filter by enabled state |
ListPackagesResponse
List packages response
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| packages | { manifest: object; status?: Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'>; enabled?: boolean; installedAt?: string; … }[] | ✅ | List of installed packages |
| total | number | ✅ | Total package count |
Nested Shape: ListPackagesResponse.packages[number]
Installed package with runtime lifecycle state
| Property | Type | Required | Description |
|---|---|---|---|
| manifest | { id: string; namespace?: string; defaultDatasource?: string; version: string; … } | ✅ | Package manifest at the AUTHORING stage; a row installed by a defineStack() host carries the assembled body instead — see AssembledInstalledPackageSchema / InstalledPackageAtEitherStageSchema |
| status | Enum<'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error'> | optional (default: "installed") | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
| enabled | boolean | optional (default: true) | Whether the package is currently enabled |
| installedAt | string | optional | Installation timestamp |
| updatedAt | string | optional | Last update timestamp |
| installedVersion | string | optional | Currently installed version for quick access |
| previousVersion | string | optional | Version before the last upgrade |
| statusChangedAt | string | optional | Status change timestamp |
| errorMessage | string | optional | Error message when status is error |
| settings | Record<string, any> | optional | User-provided configuration settings |
| upgradeHistory | { fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' | 'failed' | 'rolled_back'>; … }[] | optional | Version upgrade history |
| registeredNamespaces | string[] | optional | Namespace prefixes registered by this package |
MarkAllNotificationsReadRequest
Properties
| Property | Type | Required | Description |
|---|
MarkAllNotificationsReadResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Whether the operation succeeded |
| readCount | number | ✅ | Number of notifications marked as read |
MarkNotificationsReadRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| ids | string[] | ✅ | Notification IDs to mark as read |
MarkNotificationsReadResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Whether the operation succeeded |
| readCount | number | ✅ | Number of notifications marked as read |
MetadataConflictError
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| error | string | ✅ | Human-readable refusal sentence, unchanged by currentVersion: it names the item and the version the door judged current. For display and logs; branch on code and read currentVersion, never parse this. |
| code | 'METADATA_CONFLICT' | ✅ | ADR-0112 registered code of the optimistic-lock refusal. |
| currentVersion | string | null | ✅ | The version token of the head the door judged current — the value the sentence names — or null when no row of the target lifecycle exists. Send it as If-Match to overwrite deliberately (or If-None-Match: * when it is null), or re-read the item to merge first. Opaque — echo it verbatim, never parse it. |
Notification
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Notification ID |
| type | string | ✅ | Notification type |
| title | string | ✅ | Notification title |
| body | string | ✅ | Notification body text |
| read | boolean | optional (default: false) | Whether notification has been read |
| data | Record<string, any> | optional | Additional notification data |
| actionUrl | string | optional | URL to navigate to when clicked |
| createdAt | string | ✅ | When notification was created |
NotificationPreferences
Properties
| Property | Type | Required | Description |
|---|---|---|---|
boolean | optional (default: true) | Receive email notifications | |
| push | boolean | optional (default: true) | Receive push notifications |
| inApp | boolean | optional (default: true) | Receive in-app notifications |
| digest | Enum<'none' | 'daily' | 'weekly'> | optional (default: "none") | Email digest frequency |
| channels | Record<string, { enabled?: boolean; email?: boolean; push?: boolean }> | optional | Per-channel notification preferences |
Nested Shape: NotificationPreferences.channels[string]
| Property | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | optional (default: true) | Whether this channel is enabled |
boolean | optional | Override email setting | |
| push | boolean | optional | Override push setting |
PublishMetaItemRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name |
| name | string | ✅ | Item name — lowercase snake_case segments, optionally dot-qualified (crm_lead, crm_lead.pipeline). The promotion door enforces the same grammar as saveMetaItem. |
| actor | string | optional | Identity recorded on the op='publish' history event. On the REST door this is the request's authenticated identity (one producer) — never a caller-supplied header. |
| message | string | optional | Optional human-readable note recorded with the publish history event. |
| packageId | string | null | optional | ADR-0048 — the software package the draft being promoted was listed under, when the caller has one to state (?package=<id> on the REST door). Named, the publish promotes that package's draft and no other. ABSENT names no package: the publish promotes the draft a draft save that names no package writes — the draft of the one package the item's active rows are bound to, else its package-unbound draft, promoted into that package — and answers 404 NO_DRAFT when neither exists, as the strict draft read does. For an item never published into a package it is the package-unbound draft, else the one package's draft. It is refused 409 RESOURCE_CONFLICT when the active rows (or, with none, the drafts) are bound to two or more packages. ⚠️ null is NOT the same as absent: the implementation branches on the KEY BEING PRESENT, and null pins the lookup to the package-UNBOUND row. Spread it in conditionally; a present-and-undefined key coerces to null downstream and makes a package-bound draft unfindable — a silent no_draft. |
| member | string | optional | Bundle member selector, for a metadata type whose identity is a pair (today only email_template, identified by (name, locale)): the member this request addresses, spelled as its discriminator value (zh-CN). Absent, the canonical member (en-US for email_template), which is what a bare-name read answers; naming the canonical value is the same request. It is not the response translation locale, which ?locale= and Accept-Language choose. Refused with 400 VALIDATION_ERROR on a type that has no members, and for a blank value or one longer than a stored member can be (100 characters). A well-formed member with nothing stored or shipped answers as that door answers an absent item. |
PublishMetaItemResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Always true on a 2xx — the draft was promoted. It does NOT cover the best-effort side effects below, each of which reports its own success. |
| version | string | ✅ | Version token of the row this promotion left, and the token the ADR-0008 optimistic-concurrency chain runs on: send it back as the If-Match request header on the next write to that item and a concurrent edit is refused 409 METADATA_CONFLICT instead of silently overwritten. A keyed digest of the stored content hash, never the hash itself — the same form SaveMetaItemResponseSchema.version and the item read's version serve. Opaque to callers — echo it verbatim, never parse it. Currently emitted as hmac-sha256: plus 64 hex characters, but the format is not part of this contract. |
| seq | integer | ✅ | Monotonic sequence number of the op='publish' metadata event this promotion appended to the item history (sys_metadata_history.event_seq). Orders writes; unlike version it is not an OCC token. |
| seedApplied | { success: boolean; inserted: integer; updated: integer; error?: string; … } | optional | Outcome of materializing a published seed body into data rows. Present ONLY when the published type is seed — publishing a seed is what makes its rows live, so the load rides along with the metadata promotion. Best-effort: a seed-load problem is surfaced here, never thrown, so a caller must check seedApplied.success instead of assuming the 200 covered the data. Absent on the batch path, which suppresses the per-item apply and loads every seed body in one later pass. |
| materializeApplied | { success: boolean; inserted: integer; updated: integer; error?: string } | optional | Outcome of the ADR-0086 P2 publish-time materializer — the step that applies the published body to its data-plane state (e.g. flow, whose draft credentials become the live ones). Present ONLY when a materializer is registered for this metadata type, which is why it is optional: its absence means "no materializer ran", never "it failed". Best-effort, same contract as seedApplied. |
| projectionApplied | { success: boolean; error?: string } | optional | Outcome of the awaited ADR-0094 mutation projector — the post-persist step that materializes this metadata into its derived data-plane read model. The same receipt {@link SaveMetaItemResponseSchema} carries, because the projector runs on BOTH write doors: a direct active save and this draft→active promotion. Present ONLY when a projector is registered for this metadata type. Best-effort — a projector failure is reported here and logged, never thrown. |
| advisories | { rule: string; path: string; where: string; message: string; … }[] | optional | Non-gating findings from the runtime authoring gate — the same shared author-time rules os validate / os build / os lint run, applied to the DRAFT body this promotion carried to active (the same key SaveMetaItemResponseSchema carries, because the gate runs on both write doors, D1). The promotion SUCCEEDED; these are what the gate has to say about it anyway. Present ONLY when at least one advisory was raised — an empty array is never emitted, so a clean publish's response bytes are unchanged and absence means "nothing to report", never "the gate did not run". Advisory by construction: every entry has severity warning or info, because an error finding refuses the promotion and arrives as the 422 invalid_metadata envelope instead of here. A caller that ignores this key behaves exactly as before. This door is the one Studio's designer takes on every edit (draft save, then publish), and a Studio / MCP / AI author has no CLI at all — which is the gap this key exists to close. |
| message | string | optional | Human-readable receipt, e.g. Published draft — type=view, name=cases [seq=3]. The producer sets it on every publish today; it stays optional to match the producer's own signature and its SaveMetaItemResponse twin, and because an absent human-readable string strips no data — the failure mode this key exists to prevent. |
Nested Shape: PublishMetaItemResponse.seedApplied
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | False when the seed rows did not fully land. The publish itself still succeeded — check this rather than assuming data went live. |
| inserted | integer | ✅ | Rows created by the externalId-keyed upsert. |
| updated | integer | ✅ | Rows updated by the externalId-keyed upsert. |
| error | string | optional | Single failure message, present when the seed apply threw before the loader ran (including "no readable seed bodies"). |
| errors | any[] | optional | Per-record failures reported by the seed loader. Present only when the loader ran and returned a non-empty error list. |
Nested Shape: PublishMetaItemResponse.materializeApplied
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | False when the materializer threw or reported failure; the publish still succeeded. |
| inserted | integer | ✅ | Data-plane rows created by the materializer. |
| updated | integer | ✅ | Data-plane rows updated by the materializer. |
| error | string | optional | Materializer failure message, present only when success is false. |
Nested Shape: PublishMetaItemResponse.projectionApplied
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | False when the projector threw; the metadata promotion itself still succeeded. |
| error | string | optional | Projector failure message, present only when success is false. |
Nested Shape: PublishMetaItemResponse.advisories[number]
| Property | Type | Required | Description |
|---|---|---|---|
| rule | string | ✅ | Stable diagnostic rule id (flow-multi-write-unfiltered, approval-expression-invalid, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. |
| path | string | ✅ | Config path inside the SUBMITTED body (flows[0].nodes[1].config.multi), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. The TOP-LEVEL collection entry follows one rule. The gate evaluates against a private per-write snapshot whose indexes no caller can resolve; a collection-resident write type is one whose collection that snapshot fills with the tenant's other stored items beside the written one, and there the entry is keyed by NAME, never by an array index — for example objects.acme_invoice.sharingModel (an object write) or datasets.acme_invoice_metrics.dimensions[0].field (a dataset write). Every other write type is the sole member of its own collection in that snapshot, so its [0] IS the written item and stays positional (for example flows[0]...). Which write types are collection-resident is derived, never listed: @objectstack/lint's runtime gate computes the set (NAME_KEYED_STACK_KEYS) as the collections it carries as resolution context that a gated write type also lands in, so it changes with the gate, not with this text. An entry whose name cannot be spliced into a dotted path keeps its index. Nested positions inside one named item stay positional (objects.acme_invoice.indexes[1]): they index the author's own document. |
| where | string | ✅ | Human-readable location — flow "leave_approval" · node "approve". Prose for a person; use path for anything mechanical. |
| message | string | ✅ | What is wrong, in the rule author's own words. |
| hint | string | ✅ | How to fix it. |
| severity | Enum<'error' | 'warning' | 'info'> | ✅ | How the gate treated this finding. error means the write was REFUSED (these appear on the 422, never on a 2xx); warning / info are advisory — the write succeeded and the finding is FYI. |
PublishPackageDraftsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | True only when every pending draft promoted (failed empty) AND at least one item published. A pre-flight refusal or an ADR-0067 D2 rollback answers false on a 200 — but so does a publish with nothing to promote, so false alone is NOT a refusal: read outcome, not this boolean or the HTTP status. Always equal to outcome === 'published' (pinned). It does NOT cover the best-effort receipts below, each of which reports its own success. |
| outcome | Enum<'published' | 'refused' | 'nothing_to_publish'> | ✅ | First-class discriminant for WHICH exit answered — the fact success compresses into one boolean. published: at least one draft promoted and none refused. refused: the batch was refused — a pre-flight violation or the ADR-0067 D2 all-or-nothing rollback; the per-item story is in failed[], which is non-empty exactly on this outcome (the invariant consumers previously had to reverse-engineer, now stated by the producer). nothing_to_publish: the package had no pending drafts — nothing landed AND nothing was refused; success stays false (a no-op is not a successful publish), which before this field made that answer indistinguishable from a refusal. Producer invariants, pinned in the conformance suites: success === (outcome === 'published'); refused if and only if failed.length > 0; nothing_to_publish if and only if published.length === 0 && failed.length === 0. Values are lowercase snake, matching the sys_metadata_audit outcome vocabulary. |
| publishedCount | integer | ✅ | Number of drafts promoted to active — published.length. 0 on every refusal path (the batch is all-or-nothing, ADR-0067 D2). |
| failedCount | integer | ✅ | Number of items that did not publish — failed.length. On a rollback this counts the WHOLE batch: the causal item plus every sibling marked BATCH_ABORTED. |
| published | { type: string; name: string; version: string; advisories?: object[] }[] | ✅ | Every draft promoted to active, in publish order. Empty on every refusal path. |
| failed | { type: string; name: string; error: string; code?: string; … }[] | ✅ | Items that did not publish. Because the batch is all-or-nothing (ADR-0067 D2), a non-empty list means NOTHING landed: published: [], publishedCount: 0. |
| seedApplied | { success: boolean; inserted?: integer; updated?: integer; error?: string; … } | optional | Aggregate outcome of materializing EVERY published seed body in one multi-pass loader run (cross-seed references need the whole set). Present ONLY when the batch published at least one seed. Two producers, one key: the batch itself self-applies (applySeedBodies), and the REST door back-fills the same key for custom protocols that do not — never both (an externalId-less seed would double-insert). Best-effort: a seed problem is surfaced here, never thrown. |
| materializeApplied | { success: boolean; inserted: integer; updated: integer; failures: object[] } | optional | ADR-0086 P2 — aggregate result of publish-time materializers across the batch (e.g. flow, whose draft credentials become the live ones), including side-effect failures surfaced by the per-item effects loop. Present ONLY when at least one published item had a registered materializer or a side-effect failure. Best-effort, same contract as seedApplied. |
| probes | any | optional | ADR-0038 L3 post-publish runtime probe report — one real read per published artifact (seeded objects have rows, views are readable, widget dataset selections execute). DELIBERATELY OPAQUE in this contract: the key is declared and carried through verbatim, but its inner shape is intentionally not modeled until a consumer needs a field of it. Present only when something was publishable; probes never fail the publish. |
| commitId | string | optional | ADR-0067 — id of the commit this publish recorded. Absent when nothing published. |
| unhiddenApps | string[] | optional | ADR-0045 §3 — names of the apps whose _unpublished gate this publish cleared (publish = live AND visible; a materialized additive build has no drafts, only this flip). Attached by the REST door, not the protocol helper. Present ONLY when at least one app flipped — on a mid-loop failure it names the apps that DID persist, beside unhideError ('s split report). Spelling is a permanent wire contract (see the door's note). |
| unhideError | string | optional | Present when the ADR-0045 visibility flip failed (wholly or partway): the drafts ARE published, but apps still stored _unpublished: true stay externally unobservable. Client-facing text only — undeclared driver text is withheld per ADR-0112; the full cause is in the server log. The route is idempotent: re-run it once the cause is resolved. |
| rebindError | string | optional | Present when the post-publish metadata:reloaded announce failed: everything is published and stored, but boot-cached consumers keep the pre-publish view until re-run or restart (a newly published record-triggered flow does not bind its trigger). Client-facing text only, same ADR-0112 withhold as unhideError. |
Nested Shape: PublishPackageDraftsResponse.published[number]
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type of the promoted draft (canonical singular). |
| name | string | ✅ | Item name of the promoted draft. |
| version | string | ✅ | Version token of the promoted row — the same keyed ADR-0008 optimistic-concurrency token the single-item doors return: echo it back as If-Match on the next write to this item. A keyed digest of the stored content hash, never the hash itself. Opaque to callers; currently hmac-sha256: plus 64 hex characters, but the format is not part of this contract. |
| advisories | { rule: string; path: string; where: string; message: string; … }[] | optional | Non-gating findings the runtime authoring gate raised against THIS draft's promotion (the same element shape and the same omitted-when-empty discipline as PublishMetaItemResponseSchema.advisories, riding each element rather than a parallel top-level map). Present ONLY when at least one finding was raised — an empty array is never emitted, so an advisory-free batch's response bytes are unchanged. Advisory by construction: every entry is warning/info, because an error finding refuses the promotion and — the batch being all-or-nothing — aborts the whole batch as failed[] instead; failed[] elements never carry this key. |
Nested Shape: PublishPackageDraftsResponse.failed[number]
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type of the item that did not publish. |
| name | string | ✅ | Item name. |
| error | string | ✅ | What refused it. On a rollback, the causal item carries its real error and every sibling carries the all-or-nothing explanation. A refusal that produced structured findings states a one-sentence HEADLINE here (what failed, where, which rules, how many); the per-path detail rides issues[] instead of being restated in this string (consumers rendering both channels were showing every finding twice). |
| code | string | optional | Machine code for the refusal class (SCREAMING_SNAKE, ADR-0112 vocabulary) — e.g. a pre-flight violation code, or BATCH_ABORTED on the non-causal items of a rolled-back batch. |
| issues | { rule: string; path: string; where: string; message: string; … }[] | optional | The structured findings behind the refusal, when the refusing error carried them — today the author-time gate's INVALID_METADATA refusal on the causal item. The producer has emitted this key; declaring it is what lets a typed consumer read it back, and what lets error stay a headline without losing the per-path detail. Same element shape as published[].advisories and the single-item 422's issues[] (— one dialect, declared once). Present ONLY on the causal item and only when the refusal produced structured findings; BATCH_ABORTED siblings never carry it. Absent means "this refusal carried no structured findings", never "no problems". |
Nested Shape: PublishPackageDraftsResponse.seedApplied
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | False when the seed rows did not fully land. The publish itself still succeeded — check this rather than assuming data went live. |
| inserted | integer | optional | Rows created by the externalId-keyed upsert. Optional ONLY because the route-level fallback producer (custom protocols that do not self-apply) reports early failures without counters; the in-batch producer always emits both counters. |
| updated | integer | optional | Rows updated by the externalId-keyed upsert. Same optionality rationale as inserted. |
| error | string | optional | Single failure message, present when the apply failed before the loader ran (including "no readable seed bodies"). When the failure is the seed bodies' own schema refusal, this is a one-sentence headline and the per-path detail rides issues[]. |
| errors | any[] | optional | Per-record failures reported by the seed loader, plus any seed-body read failures. May be present and empty on a clean load. |
| issues | { path: string; message: string; code?: string }[] | optional | Structured spec-validation findings behind error, present when the apply was refused by the seed bodies' own schema — the declared 422 seedRequestValidationError mints. The per-path detail lives HERE, once; error stays a one-sentence headline. Absent on non-validation failures (driver faults, unreadable bodies), whose whole story is error / errors[]. |
Nested Shape: PublishPackageDraftsResponse.materializeApplied
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | False when any item's materializer failed; the publish still succeeded. |
| inserted | integer | ✅ | Data-plane rows created across the batch. |
| updated | integer | ✅ | Data-plane rows updated across the batch. |
| failures | { type: string; name: string; error: string }[] | ✅ | Each item whose data-plane projection did NOT land — named per-item, unlike the single-item door's scalar error, because one aggregate boolean over N items would hide WHICH ones never went live. |
RealtimeConnectRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| transport | Enum<'websocket' | 'sse' | 'polling'> | optional | Preferred transport protocol |
| channels | string[] | optional | Channels to subscribe to on connect |
| token | string | optional | Authentication token |
RealtimeConnectResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| connectionId | string | ✅ | Unique connection identifier |
| transport | Enum<'websocket' | 'sse' | 'polling'> | ✅ | Negotiated transport protocol |
| url | string | optional | WebSocket/SSE endpoint URL |
RealtimeDisconnectRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| connectionId | string | optional | Connection ID to disconnect |
RealtimeDisconnectResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Whether disconnection succeeded |
RealtimeSubscribeRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| channel | string | ✅ | Channel name to subscribe to |
| events | string[] | optional | Specific event types to listen for |
| filter | Record<string, any> | optional | Event filter criteria |
RealtimeSubscribeResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| subscriptionId | string | ✅ | Unique subscription identifier |
| channel | string | ✅ | Subscribed channel name |
RealtimeUnsubscribeRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| subscriptionId | string | ✅ | Subscription ID to cancel |
RealtimeUnsubscribeResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Whether unsubscription succeeded |
RegisterDeviceRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| token | string | ✅ | Device push notification token |
| platform | Enum<'ios' | 'android' | 'web'> | ✅ | Device platform |
| deviceId | string | optional | Unique device identifier |
| name | string | optional | Device friendly name |
RegisterDeviceResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| deviceId | string | ✅ | Registered device ID |
| success | boolean | ✅ | Whether registration succeeded |
RejectAiPendingActionResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| status | 'rejected' | ✅ | Always "rejected" |
| id | string | ✅ | The rejected action id |
RollbackMetaItemResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Whether the rollback landed. |
| version | string | ✅ | The new live row's ADR-0008 optimistic-concurrency token — the same carrier saveItem returns; pass it back as options.ifMatch. |
| seq | number | ✅ | The new live row's history sequence number. |
| restoredFromVersion | number | ✅ | Which history version was restored. |
| message | string | optional | Rollback note, when one was recorded. |
RuntimeAuthoringIssue
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| rule | string | ✅ | Stable diagnostic rule id (flow-multi-write-unfiltered, approval-expression-invalid, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. |
| path | string | ✅ | Config path inside the SUBMITTED body (flows[0].nodes[1].config.multi), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. The TOP-LEVEL collection entry follows one rule. The gate evaluates against a private per-write snapshot whose indexes no caller can resolve; a collection-resident write type is one whose collection that snapshot fills with the tenant's other stored items beside the written one, and there the entry is keyed by NAME, never by an array index — for example objects.acme_invoice.sharingModel (an object write) or datasets.acme_invoice_metrics.dimensions[0].field (a dataset write). Every other write type is the sole member of its own collection in that snapshot, so its [0] IS the written item and stays positional (for example flows[0]...). Which write types are collection-resident is derived, never listed: @objectstack/lint's runtime gate computes the set (NAME_KEYED_STACK_KEYS) as the collections it carries as resolution context that a gated write type also lands in, so it changes with the gate, not with this text. An entry whose name cannot be spliced into a dotted path keeps its index. Nested positions inside one named item stay positional (objects.acme_invoice.indexes[1]): they index the author's own document. |
| where | string | ✅ | Human-readable location — flow "leave_approval" · node "approve". Prose for a person; use path for anything mechanical. |
| message | string | ✅ | What is wrong, in the rule author's own words. |
| hint | string | ✅ | How to fix it. |
| severity | Enum<'error' | 'warning' | 'info'> | ✅ | How the gate treated this finding. error means the write was REFUSED (these appear on the 422, never on a 2xx); warning / info are advisory — the write succeeded and the finding is FYI. |
SaveMetaItemRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | ✅ | Metadata type name |
| name | string | ✅ | Item name — lowercase snake_case segments, optionally dot-qualified (crm_lead, crm_lead.pipeline). Slash-compound names are refused at the publish door. |
| item | any | ✅ | Metadata item definition |
| parentVersion | string | null | optional | ADR-0008 optimistic-concurrency pin: the version token the caller believes is current (on the REST door, the If-Match request header; the item read's version and a save receipt's version serve it). Present as a string, a concurrent edit is reported as a 409 conflict instead of silently overwritten. ⚠️ null is NOT the same as absent: a present null asserts "no current row of this lifecycle" — the first-write pin, refused 409 when a row already exists (on the REST door, If-None-Match: *; that header takes * alone and never beside If-Match, and any other spelling is refused 400) — while an ABSENT key is unpinned: the implementation adopts the current row's hash as the parent (last-write-wins). Nullable because that is the implementation's parameter type, and unlike the reset twin (which folds a present null back to the current hash) this verb passes null through to the repository's conflict check unchanged. |
| actor | string | optional | Identity recorded on the write's history event (recorded_by, a lookup into sys_user) and audit row. On the REST door this is the request's authenticated identity (one producer) — never a caller-supplied header. Absent, the event is recorded actor-less (null), deliberately not attributed to "system". |
| force | boolean | optional | Destructive-change acknowledgement (?force=true on the REST door): skips the safety diff that refuses an object save whose body drops fields or narrows types the stored item still carries (409 with the findings otherwise). Only object saves reach that diff, so the flag is inert for every other type. Absent = the guard runs. |
| mode | Enum<'draft' | 'publish'> | optional | Per-item lifecycle (ADR-0005 drafts): draft stages the body as a pending draft overlay (?mode=draft on the REST door; the publish door promotes it later); publish or ABSENT writes straight to the live active row — the legacy default, kept so callers that predate the draft/publish split keep working. Any value other than draft is read as publish. |
| packageId | string | null | optional | ADR-0048 — the software package to bind the saved row to (sys_metadata.package_id; ?package=<id> on the REST door, where all and an empty value name no package). Set when authoring inside a Studio package workspace; a named read-only base package is refused. On create the row is stamped with this id; on update an existing binding is preserved, never silently re-bound. Absent or null names no package. An active save then writes the row of the one package the item's active rows are bound to, as a save naming that package does, is refused 409 RESOURCE_CONFLICT when they are bound to two or more, and with none writes the env-local overlay (no package stamp); a draft save writes the draft of the package the item's active row is bound to when it is package-bound (a pending change stays in the package it edits), else the env-local draft. The row written is also the one the parent-version resolution reads: an unpinned save takes its version as the parent (last-write-wins), a pinned one is judged against it, and the item read at the same scope serves it as version. |
| writeFace | Enum<'package-duplicate' | 'meta-envelope' | 'meta-dispatch' | 'external-import'> | optional | Which write door a refusal is being rendered FOR — stated by the SERVER, never by a remote caller: every door builds this request field by field and never spreads a request body into it, so there is no path for a client to smuggle a face in, and a face arriving in a wire body is simply never read. Two refusals branch on it, for different questions: the 409 destructive-change remedy names the acknowledgement mechanism that actually exists on the refusing door (?force=true on the REST doors; the dispatcher, the duplicate-package door and the external-table import have none), and the 422 invalid-metadata message adapts to whether a structured issues[] channel reaches the consumer beside it. Absent = the conservative default wording. |
SaveMetaItemResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | |
| version | string | ✅ | Version token of the row this write left, and the token the ADR-0008 optimistic-concurrency chain runs on: send it back as the If-Match request header on the next write to that item and a concurrent edit is refused 409 METADATA_CONFLICT instead of silently overwritten. The item read serves the same token for the same row (GetMetaItemResponseSchema.version). A keyed digest of the stored content hash, never the hash itself (under the registered crypto provider's key, or a process-scoped key while none is registered). Opaque to callers — echo it verbatim, never parse it. Currently emitted as hmac-sha256: plus 64 hex characters, but the format is not part of this contract. |
| seq | integer | ✅ | Monotonic sequence number of the metadata event this write appended to the item history (sys_metadata_history.event_seq). Orders writes; unlike version it is not an OCC token. |
| state | Enum<'draft' | 'active'> | ✅ | Lifecycle the body was written into: "draft" when the request asked for draft mode (?mode=draft), otherwise "active" (published and live). A draft is staged only — it is not served to the runtime until published. |
| projectionApplied | { success: boolean; error?: string } | optional | Outcome of the awaited ADR-0094 mutation projector — the post-persist step that materializes this metadata into its derived data-plane read model (e.g. datasource → its live connection pool). Present ONLY when a projector is registered for this metadata type, which is why it is optional: its absence means "no projector ran", never "the projection failed". Best-effort by design — a projector failure is reported here and logged, never thrown, so a caller that needs the read model to be live must check projectionApplied.success rather than rely on the 200. |
| advisories | { rule: string; path: string; where: string; message: string; … }[] | optional | Non-gating findings from the runtime authoring gate — the same shared author-time rules os validate / os build / os lint run, applied to this body on its way to active. The write SUCCEEDED; these are what the gate has to say about it anyway (closing D3). Present ONLY when at least one advisory was raised — an empty array is never emitted, so a clean save's response bytes are unchanged and absence means "nothing to report", never "the gate did not run". Advisory by construction: every entry has severity warning or info, because an error finding refuses the write and arrives as the 422 invalid_metadata envelope instead of here. A caller that ignores this key behaves exactly as before. Runtime-only: the CLI surfaces the same findings on its own stdout, and a Studio / MCP / AI author has no CLI at all, which is the gap this key exists to close. The gate runs on both write doors (D1), and both report: POST /meta/:type/:name/publish carries the same key on PublishMetaItemResponseSchema. |
| message | string | optional |
Nested Shape: SaveMetaItemResponse.projectionApplied
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | False when the projector threw; the metadata write itself still succeeded. |
| error | string | optional | Projector failure message, present only when success is false. |
Nested Shape: SaveMetaItemResponse.advisories[number]
| Property | Type | Required | Description |
|---|---|---|---|
| rule | string | ✅ | Stable diagnostic rule id (flow-multi-write-unfiltered, approval-expression-invalid, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. |
| path | string | ✅ | Config path inside the SUBMITTED body (flows[0].nodes[1].config.multi), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. The TOP-LEVEL collection entry follows one rule. The gate evaluates against a private per-write snapshot whose indexes no caller can resolve; a collection-resident write type is one whose collection that snapshot fills with the tenant's other stored items beside the written one, and there the entry is keyed by NAME, never by an array index — for example objects.acme_invoice.sharingModel (an object write) or datasets.acme_invoice_metrics.dimensions[0].field (a dataset write). Every other write type is the sole member of its own collection in that snapshot, so its [0] IS the written item and stays positional (for example flows[0]...). Which write types are collection-resident is derived, never listed: @objectstack/lint's runtime gate computes the set (NAME_KEYED_STACK_KEYS) as the collections it carries as resolution context that a gated write type also lands in, so it changes with the gate, not with this text. An entry whose name cannot be spliced into a dotted path keeps its index. Nested positions inside one named item stay positional (objects.acme_invoice.indexes[1]): they index the author's own document. |
| where | string | ✅ | Human-readable location — flow "leave_approval" · node "approve". Prose for a person; use path for anything mechanical. |
| message | string | ✅ | What is wrong, in the rule author's own words. |
| hint | string | ✅ | How to fix it. |
| severity | Enum<'error' | 'warning' | 'info'> | ✅ | How the gate treated this finding. error means the write was REFUSED (these appear on the 422, never on a 2xx); warning / info are advisory — the write succeeded and the finding is FYI. |
SearchAllHit
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Name of the object the hit belongs to. |
| id | string | ✅ | ID of the matched record. |
| title | string | ✅ | Display title for the hit, resolved in order: the object's titleFormat template → the declared primary-title pointer (nameField, ADR-0079; deprecated alias displayNameField still honored) → conventional name fields → first/last name → the record ID as a string. |
| snippet | string | optional | Excerpt cut around the first matched term in a searchable text column, ellipsized at both ends when truncated. ABSENT when no source column literally contains a term (e.g. a pinyin companion match) — absence is a correct answer, not a miss. |
| record | Record<string, any> | ✅ | The matched record as the engine's find path returns it (row-level security applied, internal fields already stripped). Object-specific — no cross-object field shape is promised beyond "a record of the named object". |
SearchAllPageHit
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| kind | 'page' | ✅ | Discriminant, always the literal page — kept on the hit (despite the dedicated array) so a client merging record and page hits into one palette list still holds a self-describing element. |
| name | string | ✅ | Machine name of the page (page.name) — the address a client routes to; the same name the metadata read door serves the page under. |
| title | string | ✅ | Display title: the page label resolved through the shared i18n label resolution (exact tag → base language → regional sibling → default → en → any), falling back to the machine name when no label resolves. |
| snippet | string | optional | Excerpt cut around the first matched term in the page description, ellipsized at both ends when truncated — the same geometry as a record hit's snippet. ABSENT when the match is on the name or label only (the title already shows it) — absence is a correct answer, not a miss. |
| pageType | string | optional | The page's own declared type (record | app | home | …) when the served document carries one — a routing hint, deliberately typed as a plain string so a future page type cannot invalidate an already-produced search response. |
SearchAllResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| query | string | ✅ | The TRIMMED query text the sweep ran with — empty string when the request carried none (the no-scan short-circuit). |
| hits | { object: string; id: string; title: string; snippet?: string; … }[] | ✅ | Matched records across objects, in scan order, capped at the overall limit (default 20, max 100) with at most perObject (default 5, max 25) per object. |
| pages | { kind: 'page'; name: string; title: string; snippet?: string; … }[] | ✅ | Published pages whose name, label, or description matches every term (each term may match in a different field; matching folds case), in the served listing's order, capped at perObject — the page store is swept as one more container, and more matching pages may exist beyond the cap. Produced ONLY on an unscoped sweep: a request that names objects asks for records of those objects and answers pages: []. The swept set is exactly what the caller's metadata read door (GET /api/v1/meta/page) serves — published state only, never drafts. |
| totalObjects | number | ✅ | Number of objects the sweep actually SCANNED (searchable, API-enabled, with a resolvable search-field set) — not the number of objects with hits. An object whose table was never provisioned is skipped and not counted. |
| totalHits | number | ✅ | Number of hits returned — equals hits.length. NOT a deployment-wide total-match count: matches beyond limit / perObject are not counted. |
| truncated | boolean | ✅ | True when the sweep stopped at the overall limit — more matches may exist beyond the returned set. |
Nested Shape: SearchAllResponse.hits[number]
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Name of the object the hit belongs to. |
| id | string | ✅ | ID of the matched record. |
| title | string | ✅ | Display title for the hit, resolved in order: the object's titleFormat template → the declared primary-title pointer (nameField, ADR-0079; deprecated alias displayNameField still honored) → conventional name fields → first/last name → the record ID as a string. |
| snippet | string | optional | Excerpt cut around the first matched term in a searchable text column, ellipsized at both ends when truncated. ABSENT when no source column literally contains a term (e.g. a pinyin companion match) — absence is a correct answer, not a miss. |
| record | Record<string, any> | ✅ | The matched record as the engine's find path returns it (row-level security applied, internal fields already stripped). Object-specific — no cross-object field shape is promised beyond "a record of the named object". |
Nested Shape: SearchAllResponse.pages[number]
| Property | Type | Required | Description |
|---|---|---|---|
| kind | 'page' | ✅ | Discriminant, always the literal page — kept on the hit (despite the dedicated array) so a client merging record and page hits into one palette list still holds a self-describing element. |
| name | string | ✅ | Machine name of the page (page.name) — the address a client routes to; the same name the metadata read door serves the page under. |
| title | string | ✅ | Display title: the page label resolved through the shared i18n label resolution (exact tag → base language → regional sibling → default → en → any), falling back to the machine name when no label resolves. |
| snippet | string | optional | Excerpt cut around the first matched term in the page description, ellipsized at both ends when truncated — the same geometry as a record hit's snippet. ABSENT when the match is on the name or label only (the title already shows it) — absence is a correct answer, not a miss. |
| pageType | string | optional | The page's own declared type (record | app | home | …) when the served document carries one — a routing hint, deliberately typed as a plain string so a future page type cannot invalidate an already-produced search response. |
SetPresenceRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| channel | string | ✅ | Channel to set presence in |
| state | { userId: string; status: Enum<'online' | 'away' | 'busy' | 'offline'>; lastSeen: string; metadata?: Record<string, any> } | ✅ | Presence state to set |
Nested Shape: SetPresenceRequest.state
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | ✅ | User identifier |
| status | Enum<'online' | 'away' | 'busy' | 'offline'> | ✅ | Current presence status |
| lastSeen | string | ✅ | ISO 8601 datetime of last activity |
| metadata | Record<string, any> | optional | Custom presence data (e.g., current page, custom status) |
SetPresenceResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Whether presence was set |
UninstallPackageRequest
Uninstall package request
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Package ID to uninstall |
UninstallPackageResponse
Uninstall package response
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Uninstalled package ID |
| success | boolean | ✅ | Whether uninstall succeeded |
| message | string | optional | Uninstall status message |
UnregisterDeviceRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| deviceId | string | ✅ | Device ID to unregister |
UnregisterDeviceResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Whether unregistration succeeded |
UpdateAiConversationRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| title | string | optional | New title |
| metadata | Record<string, any> | optional | New metadata |
UpdateDataRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | The object name. |
| id | string | ✅ | The ID of the record to update. |
| data | Record<string, any> | ✅ | The fields to update (partial update). |
| expectedVersion | string | optional | Optimistic concurrency token (typically the updated_at value the client read). When provided, the server compares it against the current record version and returns 409 CONCURRENT_UPDATE if they differ. Optional — omit to skip the check. The quoted-empty entity-tag ("") is refused 400 VALIDATION_FAILED, not treated as omitted. |
UpdateDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name |
| id | string | ✅ | Updated record ID |
| record | Record<string, any> | ✅ | Updated record |
| droppedFields | { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> }[] | optional | Write-observability: caller-supplied fields the engine LEGALLY stripped from the write before persisting — static readonly or a TRUE readonlyWhen predicate. Present ONLY when ≥1 field was dropped; the update still succeeded without them (status/success semantics unchanged — stripping is legitimate, not an error). REST additionally surfaces this as the X-ObjectStack-Dropped-Fields response header. Optional — omit-when-empty keeps the shape backward-compatible for existing clients that only read record. |
| warnings | { field: string; code: string; message: string; rule?: string; … }[] | optional | The advisory validation-rule hits of THIS write: each severity: 'warning' or 'info' rule whose verdict on the written record was "violated", with its rule name, severity, field, code and author-written message — the same element the validate-only preview reports in a row's warnings. Advisory rules never block, so the write succeeded; this is the advisory to show the person who made it. Present ONLY when at least one advisory rule hit; a write with no hit omits the key. Hits of a nested write a hook or a flow made inside this one are not included, and a rule that could not be evaluated is not a hit. |
Nested Shape: UpdateDataResponse.droppedFields[number]
A write-path strip event: caller-supplied fields legally dropped from the payload
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object the write targeted (resolved object name) |
| fields | string[] | ✅ | Caller-supplied field names the engine removed from the write payload |
| reason | Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> | ✅ | Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, the primary-key strip of a payload id the engine ruled is not an identifier, or a computed (formula) field no driver has a column for |
Nested Shape: UpdateDataResponse.warnings[number]
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | The field the finding is about (_record for an object-level rule). |
| code | string | ✅ | Machine-readable finding code, e.g. required, invalid_type, rule_violation. |
| message | string | ✅ | Human-readable message — a validation rule's author-written text where one exists. |
| rule | string | optional | The name of the ADVISORY validation rule this finding reports. Present only on an advisory rule hit. |
| severity | Enum<'warning' | 'info'> | optional | The advisory rule's declared severity, warning or info. Present only on an advisory rule hit. |
UpdateManyDataRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| records | { id: string; data: Record<string, any> }[] | ✅ | Array of records to update (server caps the count — see batch.maxBatchSize) |
| options | { atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean } | optional | Update options |
| object | string | ✅ | Object name |
Nested Shape: UpdateManyDataRequest.records[number]
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Record ID |
| data | Record<string, any> | ✅ | Fields to update |
Nested Shape: UpdateManyDataRequest.options
| Property | Type | Required | Description |
|---|---|---|---|
| atomic | boolean | optional (default: false) | Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort. |
| returnRecords | boolean | optional (default: false) | If true, return full record data in response |
| continueOnError | boolean | optional (default: false) | If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total. |
| validateOnly | never | optional | [REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. |
UpdateManyDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| success | boolean | ✅ | Operation success status |
| error | { code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … } | optional | Error details if success is false |
| meta | { timestamp: string; duration?: integer; requestId?: string; traceId?: string } | optional | Response metadata |
| operation | Enum<'create' | 'update' | 'upsert' | 'delete'> | optional | Operation type that was performed |
| total | number | ✅ | Total number of records in the batch |
| succeeded | number | ✅ | Number of records that succeeded |
| failed | number | ✅ | Number of records that failed |
| results | { id?: string; success: boolean; errors?: object[]; data?: Record<string, any>; … }[] | ✅ | Detailed results for each record |
Nested Shape: UpdateManyDataResponse.error
| Property | Type | Required | Description |
|---|---|---|---|
| code | Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …> | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
| declaredCode | string | optional | The producer-declared code, verbatim, when it is not a member of the closed code vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
| message | string | ✅ | Readable error message |
| userMessage | string | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces message. |
| refusal | true | optional | Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value. |
| category | string | optional | Error category (e.g. validation, authorization) |
| httpStatus | integer | optional | HTTP status of the response carrying this error |
| details | any | optional | Additional error context (e.g. field validation errors) |
| requestId | string | optional | Request ID for tracking |
Nested Shape: UpdateManyDataResponse.meta
| Property | Type | Required | Description |
|---|---|---|---|
| timestamp | string | ✅ | |
| duration | integer | optional | Server-side processing duration in milliseconds |
| requestId | string | optional | |
| traceId | string | optional |
Nested Shape: UpdateManyDataResponse.results[number]
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | optional | Record ID if operation succeeded |
| success | boolean | ✅ | Whether this record was processed successfully |
| errors | { code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }[] | optional | Array of errors if operation failed. Branch on errors[0].code — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error. A NON-atomic batch that stopped (the continueOnError: false default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back. |
| data | Record<string, any> | optional | Full record data (if returnRecords=true) |
| index | number | optional | Index of the record in the request array |
| droppedFields | { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> }[] | optional | Write-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the in-engine static readonly strip on create. Per-row because a batch can drop different fields on different rows (readonlyWhen is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit X-ObjectStack-Dropped-Fields for batches. Optional — omit-when-empty keeps the shape backward-compatible. |
UpdateNotificationPreferencesRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| preferences | { email?: boolean; push?: boolean; inApp?: boolean; digest?: Enum<'none' | 'daily' | 'weekly'>; … } | ✅ | Preferences to update |
Nested Shape: UpdateNotificationPreferencesRequest.preferences
| Property | Type | Required | Description |
|---|---|---|---|
boolean | optional (default: true) | Receive email notifications | |
| push | boolean | optional (default: true) | Receive push notifications |
| inApp | boolean | optional (default: true) | Receive in-app notifications |
| digest | Enum<'none' | 'daily' | 'weekly'> | optional (default: "none") | Email digest frequency |
| channels | Record<string, { enabled?: boolean; email?: boolean; push?: boolean }> | optional | Per-channel notification preferences |
UpdateNotificationPreferencesResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| preferences | { email?: boolean; push?: boolean; inApp?: boolean; digest?: Enum<'none' | 'daily' | 'weekly'>; … } | ✅ | Updated notification preferences |
Nested Shape: UpdateNotificationPreferencesResponse.preferences
| Property | Type | Required | Description |
|---|---|---|---|
boolean | optional (default: true) | Receive email notifications | |
| push | boolean | optional (default: true) | Receive push notifications |
| inApp | boolean | optional (default: true) | Receive in-app notifications |
| digest | Enum<'none' | 'daily' | 'weekly'> | optional (default: "none") | Email digest frequency |
| channels | Record<string, { enabled?: boolean; email?: boolean; push?: boolean }> | optional | Per-channel notification preferences |
ValidateDataIssue
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | The field the finding is about (_record for an object-level rule). |
| code | string | ✅ | Machine-readable finding code, e.g. required, invalid_type, rule_violation. |
| message | string | ✅ | Human-readable message — a validation rule's author-written text where one exists. |
| rule | string | optional | The name of the ADVISORY validation rule this finding reports. Present only on an advisory rule hit. |
| severity | Enum<'warning' | 'info'> | optional | The advisory rule's declared severity, warning or info. Present only on an advisory rule hit. |
ValidateDataRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | The object name. |
| data | Record<string, any> | Record<string, any>[] | ✅ | A candidate record, or an array of them. Nothing is persisted. |
| mode | Enum<'insert' | 'update'> | optional | Which write the verdict should predict. insert (default) walks every declared field, so a missing required field is a finding; update checks only the supplied keys, matching a PATCH. An update row that carries its id is judged as the by-id update judges it: its rules read the stored row that id names, merged with the supplied keys. The stored row is read with the caller's own read access, so a row the caller cannot read is judged on the supplied keys alone, as a row with no id is. |
ValidateDataResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | The object name. |
| mode | Enum<'insert' | 'update'> | ✅ | The write mode the verdict was reached for. |
| valid | boolean | ✅ | True when EVERY row is valid — the whole-set answer. |
| results | { valid: boolean; errors: object[]; warnings: object[]; droppedFields?: object[] }[] | ✅ | Per-row verdicts, in submission order. |
| posture | { valueShapeStrict: boolean; mediaValueShapeStrict: boolean } | ✅ | The ADR-0104 posture the verdict was reached under — reported because it is the difference between "this row is fine" and "this row is fine HERE". The same row can be an error on a self-certified deployment and an admitted warning on an un-migrated one, and a caller explaining a verdict needs to know which it got. An unconditionally-strict preview was considered and rejected (option B): it would fail rows on every un-migrated deployment that the write would have accepted. |
Nested Shape: ValidateDataResponse.results[number]
| Property | Type | Required | Description |
|---|---|---|---|
| valid | boolean | ✅ | True when this row would be accepted by the write path. |
| errors | { field: string; code: string; message: string; rule?: string; … }[] | ✅ | Findings that would REJECT this row. Empty when valid. |
| warnings | { field: string; code: string; message: string; rule?: string; … }[] | ✅ | Findings the target deployment ADMITS rather than rejects; the row is valid, and the write would store it. Two populations, in this order: ADR-0104 value shapes admitted under a warn-first posture (the write logs the same complaint), then the hits of ADVISORY validation rules (severity: 'warning' or 'info'), each naming its rule and severity — the same elements the write answers as its own warnings. An advisory hit is reported only on a row the verdict accepts, as on the write, which answers a refused row with its error. |
| droppedFields | { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> }[] | optional | Write-observability for the preview: caller-supplied fields the write would LEGALLY strip from THIS row, one event per reason, in the shape and reason vocabulary the write itself reports. A strip is not a finding: valid is unaffected, and the fields appear in neither errors nor warnings. Present only when at least one field would be dropped. A server that does not produce this report omits the key too, so an absent key alone does not prove nothing would be dropped. |
Nested Shape: ValidateDataResponse.posture
| Property | Type | Required | Description |
|---|---|---|---|
| valueShapeStrict | boolean | ✅ | True when this deployment rejects non-conforming value shapes (ADR-0104 self-certified). |
| mediaValueShapeStrict | boolean | ✅ | The same, for media field value shapes. |