Skip to content

Latest commit

 

History

History
401 lines (325 loc) · 36.5 KB

metadata-plugin.mdx

File metadata and controls

401 lines (325 loc) · 36.5 KB
title Metadata Plugin — Kernel Protocol reference
navTitle Metadata Plugin
description Defines the specification for the Metadata Plugin — the central authority responsible for managing ALL metadata across the ObjectStack platform.

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Metadata Plugin Protocol

Defines the specification for the Metadata Plugin — the central authority responsible for managing ALL metadata across the ObjectStack platform.

Architecture

The Metadata Plugin consolidates all scattered metadata operations into a single, cohesive plugin that "takes over" the entire platform's metadata management:

┌──────────────────────────────────────────────────────────────────┐
│                     Metadata Plugin                             │
│                                                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐  │
│  │ Type Registry │  │  Loader      │  │ Customization Layer  │  │
│  │ (all types)   │  │  (file/db/s3)│  │ (overlay / merge)    │  │
│  └──────────────┘  └──────────────┘  └──────────────────────┘  │
│                                                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐  │
│  │ Persistence  │  │  Query       │  │ Lifecycle            │  │
│  │ (db records) │  │  (search)    │  │ (validate/deploy)    │  │
│  └──────────────┘  └──────────────┘  └──────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘

Alignment

  • Salesforce: Metadata API (deploy, retrieve, describe)
  • ServiceNow: System Dictionary + Metadata API
  • Kubernetes: API Server + CRD Registry

References

**Source:** `packages/spec/src/kernel/metadata-plugin.zod.ts`

TypeScript Usage

import { MetadataBulkResultSchema, MetadataDependencySchema, MetadataPluginConfigSchema, MetadataPluginManifestSchema, MetadataQuerySchema, MetadataQueryResultSchema, MetadataTypeSchema, MetadataTypeRegistryEntrySchema, MetadataValidationResultSchema } from '@objectstack/spec/kernel';
import type { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';

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

MetadataBulkResult

Properties

Property Type Required Description
total integer ✅ Total items processed
succeeded integer ✅ Successfully processed
failed integer ✅ Failed items
errors { type: string; name: string; error: string }[] optional Per-item errors

Nested Shape: MetadataBulkResult.errors[number]

Property Type Required Description
type string ✅ Metadata type
name string ✅ Item name
error string ✅ Error message

MetadataDependency

Properties

Property Type Required Description
sourceType string ✅ Dependent metadata type
sourceName string ✅ Dependent metadata name
targetType string ✅ Referenced metadata type
targetName string ✅ Referenced metadata name
kind Enum<'reference' | 'extends' | 'includes' | 'triggers'> ✅ How the dependency is formed

MetadataPluginConfig

Properties

Property Type Required Description
storage { datasource?: string; tableName?: string; fallback?: Enum<'filesystem' | 'memory' | 'none'>; rootDir?: string; … } ✅ Storage backend configuration
customizationPolicies never optional [REMOVED] config.customizationPolicies was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: no code ever read a customization policy, and the overlay protocol it configured was itself unreachable from any served surface (ADR-0126 supersedes it on the record). Delete the key. What a customization may touch is governed by the real mechanisms: ADR-0005's environment overlay (opt-in via allowEnvironmentOverlay on DEFAULT_METADATA_TYPE_REGISTRY, enforced at the metadata protocol's write doors and its served read) and ADR-0126's packaged-metadata model (clone + ledger disable).
mergeStrategy never optional [REMOVED] config.mergeStrategy was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: no 3-way merge engine ever existed to read it, and package upgrades do not merge customizations (ADR-0126: upgrades rewrite the packaged base; customer choices live in the ledger and are never merged). Delete the key. There is no replacement — upgrade-vs-customization separation is the model, not a configurable strategy.
additionalTypes never optional [REMOVED] config.additionalTypes was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: the only production writer of the metadata type registry is setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY), which replaces the array outright, so nothing ever merged these entries and the live type set was exactly the built-in registry whatever you declared here. Delete the key. There is no declared-kind channel: a kind enters the live metadata-type set as a side effect of registering an ITEM of that kind (SchemaRegistry.registerItem during app/manifest registration, or MetadataManager.register at runtime); bind its schema with registerMetadataTypeSchema(type, schema) from your plugin's init(ctx) so GET /api/v1/meta serves a real JSON Schema for it.
enableEvents boolean optional (default: true) Emit metadata change events
validateOnWrite boolean optional (default: true) Validate metadata on write
enableVersioning boolean optional (default: false) Track metadata version history
cacheMaxItems integer optional (default: 10000) Max items in memory cache
bootstrap Enum<'eager' | 'lazy' | 'artifact-only'> optional (default: "eager") How metadata is primed at plugin start (eager / lazy / artifact-only)

Nested Shape: MetadataPluginConfig.storage

Property Type Required Description
datasource string optional Datasource name reference for database persistence
tableName string optional (default: "sys_metadata") Database table name for metadata storage
fallback Enum<'filesystem' | 'memory' | 'none'> optional (default: "none") Fallback strategy when datasource is unavailable
rootDir string optional Root directory path
formats Enum<'yaml' | 'json' | 'typescript' | 'javascript'>[] optional (default: ["typescript","json","yaml"]) Enabled formats
cache { databaseLoader?: object } optional Cache settings — only databaseLoader is read at runtime; the outer keys are retired
watch boolean optional (default: false) Enable file watching
watchOptions { ignored?: string[]; persistent?: boolean; ignoreInitial?: boolean } optional File watcher options
validation { strict?: boolean; throwOnError?: boolean } optional Validation settings
loaderOptions Record<string, any> optional Loader-specific configuration
persistence { writable?: boolean } optional Persistence write gates

MetadataPluginManifest

Properties

Property Type Required Description
id 'com.objectstack.metadata' ✅ Metadata plugin ID
name 'ObjectStack Metadata Service' ✅ Plugin name
version string ✅ Plugin version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1)
type 'standard' ✅ Plugin type
description string optional (default: "Core metadata management service for ObjectStack platform") Plugin description
capabilities { crud?: boolean; query?: boolean; overlay?: boolean; watch?: boolean; … } ✅ Plugin capabilities
config { storage: object; enableEvents?: boolean; validateOnWrite?: boolean; enableVersioning?: boolean; … } optional Plugin configuration

Nested Shape: MetadataPluginManifest.capabilities

Property Type Required Description
crud boolean optional (default: true) Supports metadata CRUD
query boolean optional (default: true) Supports metadata query
overlay boolean optional (default: true) Supports customization overlays
watch boolean optional (default: false) Supports file watching
importExport boolean optional (default: true) Supports import/export
validation boolean optional (default: true) Supports schema validation
versioning boolean optional (default: false) Supports version history
events boolean optional (default: true) Emits metadata events

Nested Shape: MetadataPluginManifest.config

Property Type Required Description
storage { datasource?: string; tableName?: string; fallback?: Enum<'filesystem' | 'memory' | 'none'>; rootDir?: string; … } ✅ Storage backend configuration
customizationPolicies never optional [REMOVED] config.customizationPolicies was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: no code ever read a customization policy, and the overlay protocol it configured was itself unreachable from any served surface (ADR-0126 supersedes it on the record). Delete the key. What a customization may touch is governed by the real mechanisms: ADR-0005's environment overlay (opt-in via allowEnvironmentOverlay on DEFAULT_METADATA_TYPE_REGISTRY, enforced at the metadata protocol's write doors and its served read) and ADR-0126's packaged-metadata model (clone + ledger disable).
mergeStrategy never optional [REMOVED] config.mergeStrategy was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: no 3-way merge engine ever existed to read it, and package upgrades do not merge customizations (ADR-0126: upgrades rewrite the packaged base; customer choices live in the ledger and are never merged). Delete the key. There is no replacement — upgrade-vs-customization separation is the model, not a configurable strategy.
additionalTypes never optional [REMOVED] config.additionalTypes was removed from MetadataPluginConfig in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect: the only production writer of the metadata type registry is setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY), which replaces the array outright, so nothing ever merged these entries and the live type set was exactly the built-in registry whatever you declared here. Delete the key. There is no declared-kind channel: a kind enters the live metadata-type set as a side effect of registering an ITEM of that kind (SchemaRegistry.registerItem during app/manifest registration, or MetadataManager.register at runtime); bind its schema with registerMetadataTypeSchema(type, schema) from your plugin's init(ctx) so GET /api/v1/meta serves a real JSON Schema for it.
enableEvents boolean optional (default: true) Emit metadata change events
validateOnWrite boolean optional (default: true) Validate metadata on write
enableVersioning boolean optional (default: false) Track metadata version history
cacheMaxItems integer optional (default: 10000) Max items in memory cache
bootstrap Enum<'eager' | 'lazy' | 'artifact-only'> optional (default: "eager") How metadata is primed at plugin start (eager / lazy / artifact-only)

MetadataQuery

Properties

Property Type Required Description
types Enum<'object' | 'field' | 'hook' | 'seed' | 'mapping' | 'picklist' | 'view' | 'page' | 'dashboard' | 'app' | 'action' | 'report' | 'dataset' | 'flow' | 'job' | 'datasource' | 'external_catalog' | 'translation' | 'api' | 'email_template' | 'doc' | 'book' | 'permission' | 'position' | 'capability' | 'sharing_rule' | 'queue' | 'assignment_rule' | 'agent' | 'tool' | 'skill'>[] optional Filter by metadata types
namespaces string[] optional Filter by namespaces
packageId string optional Filter by owning package
search string optional Full-text search query
scope Enum<'system' | 'platform' | 'user'> optional Filter by scope
state Enum<'draft' | 'active' | 'archived' | 'deprecated'> optional Filter by lifecycle state
tags string[] optional Filter by tags
sortBy Enum<'name' | 'type' | 'updatedAt' | 'createdAt'> optional (default: "name") Sort field
sortOrder Enum<'asc' | 'desc'> optional (default: "asc") Sort direction
page integer optional (default: 1) Page number
pageSize integer optional (default: 50) Items per page

MetadataQueryResult

Properties

Property Type Required Description
items { type: string; name: string; namespace?: string; label?: string; … }[] ✅ Matched metadata items
total integer ✅ Total matching items
page integer ✅ Current page
pageSize integer ✅ Page size

Nested Shape: MetadataQueryResult.items[number]

Property Type Required Description
type string ✅ Metadata type
name string ✅ Item name
namespace string optional Namespace
label string optional Display label
scope Enum<'system' | 'platform' | 'user'> optional
state Enum<'draft' | 'active' | 'archived' | 'deprecated'> optional
packageId string optional
updatedAt string optional

MetadataType

Allowed Values

  • object
  • field
  • hook
  • seed
  • mapping
  • picklist
  • view
  • page
  • dashboard
  • app
  • action
  • report
  • dataset
  • flow
  • job
  • datasource
  • external_catalog
  • translation
  • api
  • email_template
  • doc
  • book
  • permission
  • position
  • capability
  • sharing_rule
  • queue
  • assignment_rule
  • agent
  • tool
  • skill

MetadataTypeRegistryEntry

Properties

Property Type Required Description
type Enum<'object' | 'field' | 'hook' | 'seed' | 'mapping' | 'picklist' | 'view' | 'page' | 'dashboard' | 'app' | 'action' | 'report' | 'dataset' | 'flow' | 'job' | … +16 more> ✅ Metadata type identifier
label string ✅ Display label for the metadata type
description string optional Description of the metadata type
filePatterns string[] ✅ Glob patterns to discover files of this type
supportsOverlay boolean optional (default: true) Whether a stored overlay row of an item a managed package ships may be removed through the ordinary delete door without the type's environment-overlay permission: the removal removes no managed definition, and changes what is served for permission alone; it opens no save and it decides no read
allowEnvironmentOverlay boolean optional (default: false) Whether an environment may overlay an item of this type that a managed package ships (ADR-0131 D6 regime O). Decides the save door, the managed-content seal on write and on read, and Studio's write gate; the OS_METADATA_WRITABLE hatch does not reach a managed item. Default false.
allowOrgOverride never optional [REMOVED] MetadataTypeRegistryEntry.allowOrgOverride was renamed to allowEnvironmentOverlay in @objectstack/spec 18 (ADR-0131 D6) — the per-organization overlay axis was retired, and the key now names the one scope it governs: whether an environment may overlay an item of this type that a managed package ships. Rename the key to allowEnvironmentOverlay; the value is unchanged.
allowRuntimeCreate boolean optional (default: true) Allow runtime creation via API
supportsVersioning boolean optional (default: false) Whether version history is tracked
executionPinned boolean optional (default: false) Transaction rows reference a specific version_hash; history GC is disabled and getByHash() MUST resolve old hashes (ADR-0009)
loadOrder integer optional (default: 100) Loading priority (lower = earlier)
domain Enum<'data' | 'ui' | 'automation' | 'system' | 'security' | 'ai'> ✅ Protocol domain
actions { name: string; label: string | Record<string, string>; description?: string | Record<string, string>; objectName?: string; … }[] optional Declarative type-level actions (e.g. datasource "Test connection"), reusing ActionSchema; merged with plugin-registered actions when emitted

Allowed Values: MetadataTypeRegistryEntry.type

  • object
  • field
  • hook
  • seed
  • mapping
  • picklist
  • view
  • page
  • dashboard
  • app
  • action
  • report
  • dataset
  • flow
  • job
  • datasource
  • external_catalog
  • translation
  • api
  • email_template
  • doc
  • book
  • permission
  • position
  • capability
  • sharing_rule
  • queue
  • assignment_rule
  • agent
  • tool
  • skill

Nested Shape: MetadataTypeRegistryEntry.actions[number]

Property Type Required Description
name string ✅ Machine name (lowercase snake_case)
label string | Record<string, string> ✅ Display label
description string | Record<string, string> optional Explanatory line shown under the title in the action's param dialog. Carries the confirm question for an action that collects params (one dialog, not two). Not the LLM-facing ai.description.
objectName string optional Target object this action belongs to. When set, the action is auto-merged into the object's actions array by defineStack().
icon string optional Icon name
locations Enum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | …>[] optional Locations where this action is visible
component Enum<'action:button' | 'action:icon' | 'action:menu' | 'action:group'> optional Visual component override
type Enum<'script' | 'url' | 'modal' | 'flow' | 'api' | 'form'> optional (default: "script") Action functionality type — the dispatch route. The declarative single-record field write is not a type: it is operation: 'update' + patch on the default script route.
target string optional URL, Script Name, Flow ID, or API Endpoint. Supports ${param.X} and ${ctx.X} interpolation. For type:'url': an http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ (a console route; a record is /apps/APP/OBJECT/record/ID, APP being the app's route segment — see onSuccess.navigate) — every other scheme and a protocol-relative //host are refused.
openIn Enum<'self' | 'new-tab'> optional For type:'url' — where to open target. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of params (which is user-input-collection only).
body { language: 'expression'; source: string } | { language: 'js'; source: string; capabilities?: Enum<'api.read' | 'api.write' | 'api.transaction' | 'crypto.uuid' | 'log'>[]; timeoutMs?: integer; … } optional Action body — expression (L1) or sandboxed JS (L2). Only used when type is script.
operation Enum<'update'> optional The declarative single-record field write, mirroring a list view's bulkActionDefs: 'update' applies patch (merged under the collected params) to the current record on the data plane AS THE CALLER — never system-elevated — so the caller's permissions, the object's hooks and its validations fire as for a user edit. type stays at its default 'script' (the platform action route the write is performed on); target/body/method/bodyExtra are refused beside it. 'delete' and 'custom' have no row-level form.
patch Record<string, any> optional For operation: 'update' — static field values written to the current record, merged UNDER the user-supplied params so a fixed value can be declared without exposing it in the dialog. Written on the data plane as the caller: object permissions, hooks and validations fire as for a user edit. Refused on an action without operation: 'update' (it would be silently dropped).
execution Enum<'perRecord' | 'aggregate'> optional The bulk dispatch contract this action's BODY is written for, in bulkActionDefs' own vocabulary: 'perRecord' = one dispatch per selected row carrying that row's recordId (the view's bulkActions: ['<name>'] bare-string form); 'aggregate' = ONE dispatch for the whole selection carrying every id in params._selectedIds (a bulkActionDefs entry with execution: 'aggregate'). Optional with NO default — omit it only when the body genuinely serves both. A list view wiring a declared action under the other contract is refused by @objectstack/lint (action-dispatch-contract-mismatch).
params { name?: string; field?: string; objectOverride?: string; label?: string | Record<string, string>; … }[] optional Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in bodyExtra).
variant Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'link'> optional Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent)
order number optional Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without order keep their registration order.
confirmText string | { dialect: 'template'; source?: string; ast?: any; meta?: object } | string | Record<string, string> optional Confirmation message before execution — a template slot (ADR-0032): a bare string is shorthand for { dialect: 'template', source }, and text with no hole is shown as written. Holes are {{ path }} or {{ path | formatter[:arg] }} over record, the record the action runs against (the clicked row on a list, else the page's record), and — only when the action's params is an object of static values — params ({{ params.tier }}); an input list binds nothing, since its values are collected after the confirmation. Formatters come from the template engine's whitelist (TEMPLATE_FORMATTERS, @objectstack/formula); a hole holds no logic (no CEL, no arithmetic). Example: 'Delete {{ record.name }}?'. A hole over any other root, a ${…} or single-brace {x} token (each named with its {{ }} spelling), a hole holding logic or an unknown formatter, and a blank sentence are named and refused at objectstack validate, compile and the publish gate; the text still parses, so a stored one loads until its next save. An inline locale map ({ en, "zh-CN" }) is accepted too, each value such a sentence. On a registered action, pairing this with a non-empty params is refused — that opens a second dialog for one decision; put the question on description instead. Correct on a param-LESS action, where the confirm is the only dialog there is.
successMessage string | { dialect: 'template'; source?: string; ast?: any; meta?: object } | string | Record<string, string> optional Success message shown after the action succeeds — a template slot (ADR-0032): a bare string is shorthand for { dialect: 'template', source }, and text with no hole is shown as written. Holes are {{ path }} or {{ path | formatter[:arg] }} over result, the payload the action's handler answered ({{ result.email }}; for an api or script action the server response), and record, the record the action ran against. Formatters come from the template engine's whitelist (TEMPLATE_FORMATTERS, @objectstack/formula); a hole holds no logic (no CEL, no arithmetic). Example: 'Invitation sent to {{ result.email }}'. A hole over any other root, a ${…} or single-brace {x} token (each named with its {{ }} spelling), a hole holding logic or an unknown formatter, and a blank sentence are named and refused at objectstack validate, compile and the publish gate; the text still parses, so a stored one loads until its next save. An inline locale map ({ en, "zh-CN" }) is accepted too, each value such a sentence. On a registered action that declares outcomeMessages, the entry named by the response's outcome is shown instead, and this message is the fallback.
outcomeMessages Record<string, string | Record<string, string>> optional Success copy per handler outcome, for type:'api' and type:'script' actions: keys are the snake_case outcome values the handler returns in its success payload (e.g. archived, already_archived), values the message shown for that outcome. Each message may interpolate ${result.*}, the scope onSuccess.navigate declares. An outcome with no entry here falls back to successMessage, then to the default text. Not allowed beside resultDialog (which suppresses the success toast) or operation: 'update' (no handler, so no outcome).
errorMessage string | Record<string, string> optional Error message to show when the action fails (overrides the raw error).
refreshAfter boolean optional (default: false) Refresh view after execution
undoable boolean optional Offer an Undo affordance after this single-record update action succeeds. operation: 'update' is the one declared operation and the declared form of that action: what the undo captures is the prior value of EVERY field the action writes — the merged write bag, patch UNDER the collected params, not patch alone. An action with no operation declares no write set here, but type: 'api' is fulfilled by the console, which builds the undo envelope from undoable alone. Those two shapes — operation: 'update' and type: 'api' — are the whole fulfillable set; on a registered action undoable beside any other shape is refused, because nothing would anchor the capture.
resultDialog { title?: string | Record<string, string>; description?: string | Record<string, string>; acknowledge?: string | Record<string, string>; format?: Enum<'qrcode' | 'code-list' | 'secret' | 'text' | 'json'>; … } optional Render API response in a one-shot reveal dialog (suppresses successMessage when set).
visible boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } optional Visibility predicate — true/false literal, CEL string, or {dialect, source} envelope. The action is offered when it evaluates TRUE. Omit = always visible.
requiresFeature Enum<'twoFactor' | 'organization' | 'multiOrgEnabled' | 'degradedTenancy' | …> optional Public auth feature flag gating this action; lowered into visible at parse time.
requiresMembershipReach Enum<'invite_member' | 'cancel_invitation' | 'update_member_role' | …> optional Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into visible over current_user.positions at parse time.
disabled boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } optional Disabled predicate — true/false literal, CEL string, or {dialect, source} envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled.
disabledReason string | Record<string, string> optional Display-only reason a greyed action shows (its hover title and accessible description, or a menu item's second line) while disabled evaluates TRUE — the default-language string, or an inline locale map; translated at objects.OBJECT._actions.ACTION.disabledReason (globalActions.ACTION.disabledReason for an object-less action). Not a gate: the server refuses a call on its own rules (hooks, requiredPermissions), and nothing reads this as a condition. Omit = the default reason, "Not available for this record". A blank reason is refused.
pressed string | { dialect: Enum<'cel' | 'cron' | 'template'>; source: string; ast?: any; meta?: object } optional Pressed-state predicate (CEL string or {dialect, source} envelope) over the same scope as visible: the action surface draws the button as already pressed for this reader (a toggle face, aria-pressed) while it evaluates TRUE. Display only — it never gates, greys or routes the action. A per-reader fact (e.g. the reader's own vote) is read from a block the object declares under attachedOnRead, as has(record.viewer) && record.viewer.vote == 'helpful'. A boolean literal is refused: a constant is a style, not a reader's state. Omit = never drawn pressed.
requiredPermissions string[] optional [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on every call the platform action door (POST /actions/:object/:action and MCP run_action) dispatches, and mirrored as a UI hide. ADR-0066 D4, as amended 2026-10-29, brings a type: api action whose declared target is a same-origin platform path (exactly one leading /, followed by neither / nor a backslash, with a path after it) under that door; its target endpoint stays directly reachable, so it MUST still enforce this capability itself, the door's gate being the added check. A type: api action with an off-origin target is browser-only and not gated server-side.
ai { exposed?: boolean; description?: string; category?: Enum<'data' | 'action' | 'flow' | 'integration' | 'vector_search' | 'analytics' | 'utility'>; paramHints?: Record<string, object>; … } optional AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents.
recordIdParam string optional Body key to inject the row id into when running from a list_item context.
recordIdField string optional Row field whose value seeds recordIdParam. Defaults to "id".
bodyShape 'flat' | { wrap: string } optional Body wrapping: flat (default) or { wrap: key } to nest user-collected params under a key.
method Enum<'POST' | 'PATCH' | 'PUT' | 'DELETE'> optional HTTP method for type:"api" actions. Defaults to POST.
bodyExtra Record<string, any> optional Static request-body fields for a type:"api" action, merged last (overrides user params). {{page.<var>}} tokens are resolved by the runtime. This — not params — is where a payload goes.
mode Enum<'create' | 'edit' | 'delete' | 'custom'> optional Semantic mode of the action.
opensInNewTab boolean optional Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl.
newTabUrl string optional New-tab path template on this backend, starting with / ({recordId} placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab to the backend origin + this path immediately — no action POST. The endpoint must enforce auth itself. An absolute URL is refused; an external page is a type:'url' action with openIn: 'new-tab'.
onSuccess { navigate: string; openIn?: Enum<'self' | 'newTab'> } optional Post-success navigation for type:'api', type:'script' and type:'flow' actions. navigate is a route/URL template interpolating ${param.*}, ${ctx.*} and ${result.*} (the server response; for a flow, the completed run's answer); openIn defaults 'self'. A flow run that pauses at a screen or ends refused performs no navigation. The handler-return convention ({ redirectUrl } without openIn) keeps its 17.0.0 new-tab behavior.
aria never optional [REMOVED] action.aria was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's label and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required label — the visible button or menu-item text, and the aria-label of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author ariaLabel / ariaDescribedBy / role in the aria block of the placing node: page.components[].aria (the component that renders the actions) or the list view aria. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
_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.

MetadataValidationResult

Properties

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

Nested Shape: MetadataValidationResult.errors[number]

Property Type Required Description
path string ✅ JSON path to the invalid field
message string ✅ Error description
code string optional Error code

Nested Shape: MetadataValidationResult.warnings[number]

Property Type Required Description
path string ✅ JSON path to the field
message string ✅ Warning description