Protocol
Protocol protocol schemas
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, 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, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, 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, 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, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, 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 |
| organizationId | string | null | optional | Organization (tenant) scope for the read. With an organization, the trail includes that org's rows AND the env-wide (organization_id IS NULL) rows — the env-wide limb is load-bearing, because env-level writes are stamped org-less. null and absent are equivalent and both mean the env-wide rows only — the fail-closed direction: an unresolved organization reads env-wide rows, never every tenant's. |
| 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?: number; 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. |
| 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.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'> }[] | optional | Write-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the create-ingress strip. 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. |
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'> }[] | 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 (ingress strip), 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. |
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'> | ✅ | Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, or the primary-key strip of a payload id the engine ruled is not an identifier |
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'> }[] | optional | Write-observability: caller-supplied readonly fields the create-ingress strip 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 the insert-time strip is static-readonly only — schema-uniform, so every row drops the same set. 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 per-row insertMany/batch paths carry per-row droppedFields on each result instead — 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'> | ✅ | Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, or the primary-key strip of a payload id the engine ruled is not an identifier |
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. |
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?: number; 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. |
| 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.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'> }[] | optional | Write-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the create-ingress strip. 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 |
| organizationId | string | optional | Organization (tenant) scope for the reset. Load-bearing, not advisory: it selects the ADR-0005 overlay partition, so it decides WHICH row the reset destroys — an org-scoped delete removes that tenant's own overlay, while an org-less delete reaches the environment-wide row and would blank the item for every tenant. Absent = environment-wide. |
| 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. |
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; … } | ✅ | Full package manifest |
| 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; … } | ✅ | Full package manifest |
| 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?: any; search?: string | object; … } | optional | Structured query definition (filter, sort, select, pagination). |
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 | any | optional | Filtering criteria (WHERE) |
| 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. |
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). |
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 |
| 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 |
| 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 | ✅ | |
| 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. False while realtime is an in-process bus with no mounted HTTP/WS surface (ADR-0076 D12). |
| 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 |
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; apply them 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; apply them 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). |
| organizationId | string | optional | Organization (tenant) scope for the read. Selects the org partition in the ADR-0005 overlay read order — org overlay wins over env-wide overlay wins over packaged artifact — exactly as on the uncached read. Also folded into the ETag, so a scope switch never returns a stale 304 from another scope's cached representation. Absent = environment-wide read. |
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 |
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 |
| staleWhileRevalidate | number | optional | Allow serving stale content while revalidating (seconds) |
| staleIfError | number | optional | Allow serving stale content on error (seconds) |
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). |
| organizationId | string | optional | Organization (tenant) scope for the read. Selects the org partition in the ADR-0005 overlay read order, so it decides which tenant's customization row is reported as the overlay layer (and merged into effective). Absent = environment-wide read: overlay reports the env-level row only. |
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 this tenant has not customized the item. |
| overlayScope | Enum<'org' | 'env'> | null | ✅ | Which scope the overlay row was read from — org for a tenant overlay, env for an environment-level one. 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 (snake_case identifier) |
| packageId | string | optional | Optional package ID to filter items by |
| organizationId | string | optional | Organization (tenant) scope for the read. Selects the org partition in the ADR-0005 overlay read order — org overlay wins over env-wide overlay wins over packaged artifact — so it decides which tenant's customization row is served as the item. Absent = environment-wide read: only env-level overlays apply and no org partition is consulted. |
| 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 refuse the corresponding write with 403 ITEM_LOCKED. Resolved from the document's _lock, with the packaged artifact winning over any org overlay. |
| 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. |
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; a name absent from this map (an unknown field, a dotted path, an unprovisioned audit column) has no platform sort behind it and must get no sort affordance. |
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 |
| organizationId | string | optional | Organization (tenant) scope for the read. Selects the org partition in the ADR-0005 overlay read order — org overlay wins over env-wide overlay wins over packaged artifact — so it decides which tenant's customization rows are merged into the list. Absent = environment-wide read: only env-level overlays apply and no org partition is consulted. |
| 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 |
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 | ✅ | Loader can merge per-org overlays on top of artifact |
| allowOrgOverride | boolean | ✅ | Per-org overlay writes accepted at runtime (may be env-elevated) |
| 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 allowOrgOverride is set in the static registry or via OS_METADATA_WRITABLE env var |
| 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; apply them 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; apply them 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; … } | ✅ | Full package manifest |
| 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>; apps?: Record<string, object>; messages?: Record<string, string>; globalActions?: Record<string, object>; … } | ✅ | 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 |
| 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 |
| pages | Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }> | optional | Page translations keyed by page name |
| flows | Record<string, { label?: string; screens?: Record<string, object> }> | optional | Screen-flow translations keyed by flow name |
| settings | Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }> | optional | Settings manifest translations keyed by namespace |
| 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 |
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' | …>; data?: object | … +3 more; … } | optional | |
| form | { type?: Enum<'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'>; layout?: Enum<'vertical' | 'horizontal' | 'inline' | 'grid'>; columns?: integer; title?: string; … } | optional | |
| listViews | Record<string, { name?: string; label?: string | Record<string, string>; type?: Enum<'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' | …>; data?: object | … +3 more; … }> | 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' | 'inline' | 'grid'>; 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' | …> | optional (default: "grid") | |
| data | { provider: 'object'; object: string } | { provider: 'api'; read?: object; write?: object } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> } | 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 |
| 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 | string | { 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'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … } | optional | Configuration for item click navigation (page, drawer, modal, etc.) |
| pagination | { pageSize?: integer; pageSizeOptions?: integer[] } | optional | Pagination configuration |
| kanban | { groupByField: string; summarizeField?: 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; … } & Record<string, any> | 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 | List chart view configuration |
| 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 } & Record<string, any> | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout |
| pageName | string | optional | Published page this view mounts — required when type: 'page', and refused on every other view type. Rendering is delegated to the existing page renderer; the page keeps its own assignedProfiles audience. |
| description | string | Record<string, string> | optional | View description for documentation/tooltips |
| sharing | { type?: Enum<'personal' | 'collaborative'>; lockedBy?: string } | optional | View sharing and access configuration |
| rowHeight | Enum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'> | optional | Row height / density setting |
| grouping | { fields: object[] } | optional | Group records by one or more fields |
| rowColor | { field: string; colors?: Record<string, string> } | optional | Color rows based on field value |
| hiddenFields | string[] | optional | Fields to hide in this specific view |
| fieldOrder | string[] | optional | Explicit field display order for this view |
| 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 | { name: string; label?: string | Record<string, string>; icon?: string; view?: string; … }[] | optional | Tab definitions for multi-tab view interface |
| 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 } | 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; apply them 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; apply them 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; apply them 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; apply them 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; apply them 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' | 'inline' | 'grid'> | optional | Field layout direction |
| 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 | Allow skipping steps (wizard forms) |
| showStepIndicator | boolean | optional | Show the step indicator (wizard forms) |
| 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: 'api'; read?: object; write?: object } | { 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?: any[]; 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; apply them 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 (ruled 2026-08-11). |
| 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; apply them 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' | …> | optional (default: "grid") | |
| data | { provider: 'object'; object: string } | { provider: 'api'; read?: object; write?: object } | { provider: 'value'; items: any[] } | { provider: 'schema'; schemaId: string; schema?: Record<string, any> } | 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 |
| 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 | string | { 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'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … } | optional | Configuration for item click navigation (page, drawer, modal, etc.) |
| pagination | { pageSize?: integer; pageSizeOptions?: integer[] } | optional | Pagination configuration |
| kanban | { groupByField: string; summarizeField?: 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; … } & Record<string, any> | 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 | List chart view configuration |
| 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 } & Record<string, any> | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout |
| pageName | string | optional | Published page this view mounts — required when type: 'page', and refused on every other view type. Rendering is delegated to the existing page renderer; the page keeps its own assignedProfiles audience. |
| description | string | Record<string, string> | optional | View description for documentation/tooltips |
| sharing | { type?: Enum<'personal' | 'collaborative'>; lockedBy?: string } | optional | View sharing and access configuration |
| rowHeight | Enum<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'> | optional | Row height / density setting |
| grouping | { fields: object[] } | optional | Group records by one or more fields |
| rowColor | { field: string; colors?: Record<string, string> } | optional | Color rows based on field value |
| hiddenFields | string[] | optional | Fields to hide in this specific view |
| fieldOrder | string[] | optional | Explicit field display order for this view |
| 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 | { name: string; label?: string | Record<string, string>; icon?: string; view?: string; … }[] | optional | Tab definitions for multi-tab view interface |
| 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 } | 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; apply them 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; apply them 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; apply them 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; apply them 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; apply them 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' | 'inline' | 'grid'> | optional | Field layout direction |
| 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 | Allow skipping steps (wizard forms) |
| showStepIndicator | boolean | optional | Show the step indicator (wizard forms) |
| 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: 'api'; read?: object; write?: object } | { 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?: any[]; 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; apply them 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 (ruled 2026-08-11). |
| 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; apply them 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 |
| organizationId | string | optional | Organization (tenant) partition the change log is read from. Absent means the env-wide partition (the implementation resolves the overlay repository as organizationId ?? null, keyed org: 'env'). Declared because the implementation declares and reads it; plain string, not nullable, because that is the implementation's parameter type — unlike the audit twin, whose door always sends ctx?.tenantId ?? null and whose implementation declares string | null. The REST history door currently sends no organization at all (whether it should is a tenant-scoping question measured separately for that door — declaring the member records the implementation contract, it does not answer that question). |
| 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.) |
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 (default: true) | Whether to enable immediately after install |
| platformVersion | string | optional | Current platform version for compatibility verification |
Nested Shape: InstallPackageRequest.manifest
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | Unique package identifier (reverse domain style) |
| 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 (semantic versioning) |
| 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 |
| name | string | ✅ | Human-readable package name |
| description | string | optional | Package description |
| permissions | string[] | { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] } | optional | Required permissions: legacy string[] or structured plugin block (ADR-0025 §3.2) |
| 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 | … +8 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. Use the permission declarations, which are enforced. |
| 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) |
| 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; … } | ✅ | Full package manifest |
| 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 | Pagination cursor |
ListAiConversationsResponse
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| conversations | { id: string; title?: string; agentId?: string; userId?: string; … }[] | ✅ | Matching conversations |
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; organizationId: string | null; packageId: 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. |
| organizationId | string | null | ✅ | Owning organization of the draft row, null for an environment-wide draft. |
| 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; … } | ✅ | Full package manifest |
| 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 |
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. |
| organizationId | string | optional | Organization (tenant) scope for the promotion. The implementation resolves the draft through the org partition (ADR-0005), so a draft authored org-scoped must be published under the same scope or the lookup answers 404 [no_draft]. Absent = environment-wide. |
| 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). ⚠️ null is NOT the same as absent, and the difference is load-bearing: the implementation branches on the KEY BEING PRESENT, so an ABSENT key keeps the historical "match any package" resolution while 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 on the untouched path. |
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 | ✅ | Content hash of the just-promoted body, 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 reported as 409 metadata_conflict instead of silently overwritten. Opaque to callers — echo it verbatim, never parse it. Currently emitted as sha256:<64 hex chars>, 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 projects the published body into its data-plane row (e.g. permission → sys_permission_set, under the owning package). 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. For the collection-resident write types (object / permission / book) the TOP-LEVEL collection entry is keyed by NAME (objects.acme_invoice.sharingModel), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve. Every other write type is the sole member of its own collection, so its [0] is trivially stable and stays positional (flows[0]...), as do nested positions inside one named item (objects.acme_invoice.indexes[1]), which 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. permission → sys_permission_set), 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 | ✅ | Content hash of the just-promoted body — the same ADR-0008 optimistic-concurrency token the single-item doors return: echo it back as If-Match on the next write to this item. Opaque to callers; currently sha256:<64 hex chars>, 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. For the collection-resident write types (object / permission / book) the TOP-LEVEL collection entry is keyed by NAME (objects.acme_invoice.sharingModel), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve. Every other write type is the sole member of its own collection, so its [0] is trivially stable and stays positional (flows[0]...), as do nested positions inside one named item (objects.acme_invoice.indexes[1]), which 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 |
| organizationId | string | optional | Organization (tenant) scope for the write. Load-bearing, not advisory: it selects the overlay partition (ADR-0005) the row lands in — an org-scoped save writes that tenant's own overlay, while an org-less save writes the environment-wide row every tenant reads — and it is the scope stamped on the write's audit row. An org-scoped write of a type whose registry entry declares allowOrgOverride: false is refused (403). Absent = environment-wide. |
| 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). 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 — 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, sent only when it names a real 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 = env-local overlay (no package stamp); it also scopes which row the unpinned parent-version resolution reads. |
| writeFace | Enum<'package-duplicate' | 'meta-envelope' | 'meta-dispatch'> | 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 and the duplicate-package door 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 | ✅ | Content hash of the just-committed body, 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 reported as 409 metadata_conflict instead of silently overwritten. Opaque to callers — echo it verbatim, never parse it. Currently emitted as sha256:<64 hex chars>, 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. permission → sys_permission_set). 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. For the collection-resident write types (object / permission / book) the TOP-LEVEL collection entry is keyed by NAME (objects.acme_invoice.sharingModel), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve. Every other write type is the sole member of its own collection, so its [0] is trivially stable and stays positional (flows[0]...), as do nested positions inside one named item (objects.acme_invoice.indexes[1]), which 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". |
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. |
| 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". |
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. |
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'> }[] | 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. |
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'> | ✅ | Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, or the primary-key strip of a payload id the engine ruled is not an identifier |
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?: number; 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. |
| 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.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'> }[] | optional | Write-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the create-ingress strip. 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. |
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 judges only the supplied keys, matching a PATCH. |
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[] }[] | ✅ | 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 }[] | ✅ | Findings that would REJECT this row. Empty when valid. |
| warnings | { field: string; code: string; message: string }[] | ✅ | Findings the target deployment ADMITS rather than rejects — today, ADR-0104 value shapes under a warn-first posture. The row is valid; the write would store it and log the same complaint. |
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. |