ObjectStackObjectStack

Metadata Plugin

Metadata Plugin protocol schemas

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

PropertyTypeRequiredDescription
totalintegerTotal items processed
succeededintegerSuccessfully processed
failedintegerFailed items
errors{ type: string; name: string; error: string }[]optionalPer-item errors

Nested Shape: MetadataBulkResult.errors[number]

PropertyTypeRequiredDescription
typestringMetadata type
namestringItem name
errorstringError message

MetadataDependency

Properties

PropertyTypeRequiredDescription
sourceTypestringDependent metadata type
sourceNamestringDependent metadata name
targetTypestringReferenced metadata type
targetNamestringReferenced metadata name
kindEnum<'reference' | 'extends' | 'includes' | 'triggers'>How the dependency is formed

MetadataPluginConfig

Properties

PropertyTypeRequiredDescription
storage{ datasource?: string; tableName: string; fallback: Enum<'filesystem' | 'memory' | 'none'>; rootDir?: string; … }Storage backend configuration
customizationPoliciesneveroptional[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 org-scoped overlay (opt-in via allowOrgOverride on DEFAULT_METADATA_TYPE_REGISTRY, enforced at the REST meta write doors) and ADR-0126's packaged-metadata model (clone + ledger disable).
mergeStrategyneveroptional[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.
additionalTypesneveroptional[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.
enableEventsbooleanoptional (default: true)Emit metadata change events
validateOnWritebooleanoptional (default: true)Validate metadata on write
enableVersioningbooleanoptional (default: false)Track metadata version history
cacheMaxItemsintegeroptional (default: 10000)Max items in memory cache
bootstrapEnum<'eager' | 'lazy' | 'artifact-only'>optional (default: "eager")How metadata is primed at plugin start (eager / lazy / artifact-only)

Nested Shape: MetadataPluginConfig.storage

PropertyTypeRequiredDescription
datasourcestringoptionalDatasource name reference for database persistence
tableNamestringoptional (default: "sys_metadata")Database table name for metadata storage
fallbackEnum<'filesystem' | 'memory' | 'none'>optional (default: "none")Fallback strategy when datasource is unavailable
rootDirstringoptionalRoot directory path
formatsEnum<'yaml' | 'json' | 'typescript' | 'javascript'>[]optional (default: ["typescript","json","yaml"])Enabled formats
cache{ databaseLoader?: object }optionalCache settings — only databaseLoader is read at runtime; the outer keys are retired
watchbooleanoptional (default: false)Enable file watching
watchOptions{ ignored?: string[]; persistent: boolean; ignoreInitial: boolean }optionalFile watcher options
validation{ strict: boolean; throwOnError: boolean }optionalValidation settings
loaderOptionsRecord<string, any>optionalLoader-specific configuration
persistence{ writable: boolean }optionalPersistence write gates

MetadataPluginManifest

Properties

PropertyTypeRequiredDescription
id'com.objectstack.metadata'Metadata plugin ID
name'ObjectStack Metadata Service'Plugin name
versionstringPlugin version
type'standard'Plugin type
descriptionstringoptional (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; … }optionalPlugin configuration

Nested Shape: MetadataPluginManifest.capabilities

PropertyTypeRequiredDescription
crudbooleanoptional (default: true)Supports metadata CRUD
querybooleanoptional (default: true)Supports metadata query
overlaybooleanoptional (default: true)Supports customization overlays
watchbooleanoptional (default: false)Supports file watching
importExportbooleanoptional (default: true)Supports import/export
validationbooleanoptional (default: true)Supports schema validation
versioningbooleanoptional (default: false)Supports version history
eventsbooleanoptional (default: true)Emits metadata events

Nested Shape: MetadataPluginManifest.config

PropertyTypeRequiredDescription
storage{ datasource?: string; tableName: string; fallback: Enum<'filesystem' | 'memory' | 'none'>; rootDir?: string; … }Storage backend configuration
customizationPoliciesneveroptional[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 org-scoped overlay (opt-in via allowOrgOverride on DEFAULT_METADATA_TYPE_REGISTRY, enforced at the REST meta write doors) and ADR-0126's packaged-metadata model (clone + ledger disable).
mergeStrategyneveroptional[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.
additionalTypesneveroptional[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.
enableEventsbooleanoptional (default: true)Emit metadata change events
validateOnWritebooleanoptional (default: true)Validate metadata on write
enableVersioningbooleanoptional (default: false)Track metadata version history
cacheMaxItemsintegeroptional (default: 10000)Max items in memory cache
bootstrapEnum<'eager' | 'lazy' | 'artifact-only'>optional (default: "eager")How metadata is primed at plugin start (eager / lazy / artifact-only)

MetadataQuery

Properties

PropertyTypeRequiredDescription
typesEnum<'object' | 'field' | 'hook' | 'seed' | 'mapping' | 'view' | 'page' | 'dashboard' | 'app' | 'action' | 'report' | 'dataset' | 'flow' | 'job' | 'datasource' | 'external_catalog' | 'translation' | 'api' | 'email_template' | 'doc' | 'book' | 'permission' | 'position' | 'capability' | 'agent' | 'tool' | 'skill'>[]optionalFilter by metadata types
namespacesstring[]optionalFilter by namespaces
packageIdstringoptionalFilter by owning package
searchstringoptionalFull-text search query
scopeEnum<'system' | 'platform' | 'user'>optionalFilter by scope
stateEnum<'draft' | 'active' | 'archived' | 'deprecated'>optionalFilter by lifecycle state
tagsstring[]optionalFilter by tags
sortByEnum<'name' | 'type' | 'updatedAt' | 'createdAt'>optional (default: "name")Sort field
sortOrderEnum<'asc' | 'desc'>optional (default: "asc")Sort direction
pageintegeroptional (default: 1)Page number
pageSizeintegeroptional (default: 50)Items per page

MetadataQueryResult

Properties

PropertyTypeRequiredDescription
items{ type: string; name: string; namespace?: string; label?: string; … }[]Matched metadata items
totalintegerTotal matching items
pageintegerCurrent page
pageSizeintegerPage size

Nested Shape: MetadataQueryResult.items[number]

PropertyTypeRequiredDescription
typestringMetadata type
namestringItem name
namespacestringoptionalNamespace
labelstringoptionalDisplay label
scopeEnum<'system' | 'platform' | 'user'>optional
stateEnum<'draft' | 'active' | 'archived' | 'deprecated'>optional
packageIdstringoptional
updatedAtstringoptional

MetadataType

Allowed Values

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

MetadataTypeRegistryEntry

Properties

PropertyTypeRequiredDescription
typeEnum<'object' | 'field' | 'hook' | 'seed' | 'mapping' | 'view' | 'page' | 'dashboard' | 'app' | 'action' | 'report' | 'dataset' | 'flow' | 'job' | 'datasource' | … +12 more>Metadata type identifier
labelstringDisplay label for the metadata type
descriptionstringoptionalDescription of the metadata type
filePatternsstring[]Glob patterns to discover files of this type
supportsOverlaybooleanoptional (default: true)Whether overlay customization is supported
allowOrgOverridebooleanoptional (default: false)Allow per-org overlay writes via runtime metadata API
allowRuntimeCreatebooleanoptional (default: true)Allow runtime creation via API
supportsVersioningbooleanoptional (default: false)Whether version history is tracked
executionPinnedbooleanoptional (default: false)Transaction rows reference a specific version_hash; history GC is disabled and getByHash() MUST resolve old hashes (ADR-0009)
loadOrderintegeroptional (default: 100)Loading priority (lower = earlier)
domainEnum<'data' | 'ui' | 'automation' | 'system' | 'security' | 'ai'>Protocol domain
actions{ name: string; label: string | Record<string, string>; description?: string | Record<string, string>; objectName?: string; … }[]optionalDeclarative 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
  • view
  • page
  • dashboard
  • app
  • action
  • report
  • dataset
  • flow
  • job
  • datasource
  • external_catalog
  • translation
  • api
  • email_template
  • doc
  • book
  • permission
  • position
  • capability
  • agent
  • tool
  • skill

Nested Shape: MetadataTypeRegistryEntry.actions[number]

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

MetadataValidationResult

Properties

PropertyTypeRequiredDescription
validbooleanWhether the metadata is valid
errors{ path: string; message: string; code?: string }[]optionalValidation errors
warnings{ path: string; message: string }[]optionalValidation warnings

Nested Shape: MetadataValidationResult.errors[number]

PropertyTypeRequiredDescription
pathstringJSON path to the invalid field
messagestringError description
codestringoptionalError code

Nested Shape: MetadataValidationResult.warnings[number]

PropertyTypeRequiredDescription
pathstringJSON path to the field
messagestringWarning description

On this page