| title | Metadata API — read and publish packages |
|---|---|
| navTitle | Metadata & Package API |
| description | REST endpoints for object schemas, metadata types, UI views, and package management. |
Manage object schemas, metadata types, UI views, and installed packages over REST. All paths are relative to the base URL (defaults to /api/v1) — see the API Overview for discovery and service availability.
Retrieve and manage object schemas, metadata types, and UI views. Always available — provided by the kernel.
A type segment these routes cannot recognise, but which reaches for a metadata type the
platform declares, is refused 400 INVALID_REQUEST rather than served — see
unrecognised type spelling
in the error catalog for the envelope and the correction it names.
List all registered metadata types.
Response: { types: ["object", "view", "plugin", ...] }
The answer carries a weak ETag and Cache-Control: private, no-cache; send the
ETag back as If-None-Match and an unchanged list answers 304 with no body.
List all items of a metadata type.
| Parameter | Location | Description |
|---|---|---|
type |
path | Metadata type name — the singular spelling (object, view, flow) |
Metadata type names are singular (Prime Directive #3) and this segment is the
type name itself, so the spelling to send is object — never objects. The
plurals elsewhere in this API name something else: a collection of records
(/data/:object) or a package-manifest key (objects: [...]). Neither carries
across to a /meta type segment.
Listing applies the ADR-0046 §6.7 audience gate to books and docs, RBAC filtering to privileged apps, and the usual response shaping.
Response: { type: "object", items: [{ name: "account", ... }, ...] }
The list carries a weak ETag and Cache-Control: private, no-cache, and
If-None-Match with the current ETag answers 304 with no body. The ETag
hashes the list this caller was served, after the audience and RBAC filtering
above, so two callers who see different items hold different tags and one
caller's tag never revalidates another caller's list. Keep the response in the
client only: private means no shared cache may store it.
Get a specific metadata item with optional ETag caching.
| Parameter | Location | Description |
|---|---|---|
type |
path | Metadata type name |
name |
path | Item name (snake_case) |
If-None-Match |
header | ETag for conditional request |
Response: { type: "object", name: "account", item: { ... } }
304 if ETag matches (not modified).
Create or update a metadata item.
Saving requires the manage_metadata capability. Under a group or isolated
tenancy posture it also requires the platform operator, for every type: the
environment's metadata is shared by every organization (ADR-0131 D12 items 7–8,
D11), so a manage_metadata holder inside one organization is refused
403 FORBIDDEN naming the posture. The same holds for DELETE (reset),
POST /meta/:type/:name/publish and POST /meta/:type/:name/rollback, and for
POST /meta/_migrate-stored, the install-wide rewrite of the stored metadata,
preview included.
Creates are gated per type: MetadataTypeRegistryEntry.allowRuntimeCreate decides
whether a brand-new name may be minted under that type at all, separately from
whether an item that already exists may be overlaid (allowEnvironmentOverlay). A type that
declines the create half still accepts updates to names that already exist, and its
refusal — 403 NOT_CREATABLE — names the route that does work. The flags are
documented on
MetadataTypeRegistryEntry;
which types currently decline which half is tracked in the
overlay whitelist
rather than enumerated on this page, so this caveat doesn't go stale the next time a
type is retired.
This verb also refuses a :type segment that is not a metadata type the platform
declares at all — not a recognised spelling, and not a misspelling of one either —
with 400 INVALID_REQUEST, before any namespace is minted for it. The refusal is
scoped to this verb: GET and DELETE on an item that already exists are
unaffected. See unrecognised type spelling
in the error catalog for the envelope — the same code and status cover both this
refusal and the misspelling one above; the message text tells them apart.
An item an installed package ships is customized in that package's pair
(ADR-0146
D2, D11). A PUT naming no package onto an item one installed read-only package
hotcrm ships is refused 403 NOT_OVERRIDABLE; for a type an override applies to,
the sentence names the route: save it with ?package=hotcrm.custom, the body
declaring overrides: { package: 'hotcrm', basedOn: '3.2.0' } (basedOn is the
manifest.version of hotcrm it was taken from, recorded and never judged). Only the pair declares an override: an item of any other
package that depends on hotcrm and bears a hotcrm item's name is refused at save
and publish, and its fix is a new name. A permission set, position or capability is
never overridden: a permission set or position is copied under a new name, and a
capability changes only with a new version of the package that declares it. An
overrides on a type that takes none is refused 403 NOT_OVERRIDABLE, and the
sentence names that type's own route. What the reads then serve is in
Metadata lifecycle.
POST /packages/hotcrm/ensure-customization creates the pair when absent (Studio calls
it on the first change), and GET /packages/hotcrm/customizations lists its overrides,
extensions and switched-off items in one read, beside contributions: the fields (kind field)
and flows (kind flow) other installed packages contribute to hotcrm's objects, and the
navigation items (kind nav) they contribute to hotcrm's apps, each entry naming its package.
Body: { type: "object", name: "account", item: { ... } }
Response: { success: true }
Get an auto-generated UI view for an object.
| Parameter | Location | Description |
|---|---|---|
object |
path | Object name (snake_case) |
type |
path | list or form |
Response: Full ViewSchema definition with columns/fields derived from the object schema.
Manage installed plugins/packages.
List installed packages.
Install a package from its manifest (SDK: client.packages.install). Re-installing an
already-installed id returns 409 Conflict unless overwrite: true.
Body: { manifest: { id: "com.objectstack.plugin-auth", name: "Plugin Auth", version: "1.0.0", ... }, settings?: { ... }, enableOnInstall?: true, overwrite?: false }
Response: { package: { id: "com.objectstack.plugin-auth", version: "1.0.0", ... }, message?: "..." }
Publish a package (manifest + metadata) to the package marketplace registry. This is publisher tooling, not part of the app SDK surface.
Body: { manifest: { id: "com.objectstack.plugin-auth", name: "Plugin Auth", version: "1.0.0", ... }, metadata: { objects: [...], views: [...], ... } }
Response: { success: true, message: "...", package: { id: "com.objectstack.plugin-auth", version: "1.0.0" } }
Get package details.
Uninstall a package.
Under a group or isolated tenancy posture every /packages write is the
platform operator's (ADR-0131 D12 items 7–8, D11): install (POST /packages),
the manifest PATCH, enable, disable, publish, publish-drafts,
discard-drafts, revert, rollback, commits/:commitId/revert, duplicate,
adopt-orphans and this delete. Any other caller, a manage_metadata holder
inside one organization included, is refused 403 PERMISSION_DENIED naming the
posture, before anything is read or written, whatever the package carries.
Reads are unchanged. Under single every manage_metadata holder writes as
before.
Enable a disabled package.
The mirror of the disable door's refusal below: the enable is refused
409 PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE while a validation rule it
would put into service (the package's own, or one on an object it owns) reads a
field that another, still disabled package adds through objectExtensions:
that field stays withheld, and every write to the object would fail the rule.
The message names each rule as package · object · rule (reads FIELD) and the
disabled package to enable first, and nothing changes. A rule is judged as the
object will serve it once the package is enabled: one rule per name, a tenant
overlay's switch honoured. A rule the enable does not bring into service is not
counted. POST /packages answers the same 409 for an overwrite of a disabled
package with enableOnInstall: true; the overwrite lands and the package stays
disabled. A boot whose stored state leaves such a rule served (the state records
only disables) logs one warning naming every rule and the remedies.
Disable a package without uninstalling.
A package that adds fields to another object through objectExtensions is
refused 409 PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE while a validation
rule of any enabled package reads one of those fields (declared dependent or
not, an app's .custom pair included, any severity): withheld, the field could
not be read, and every write to that object would fail the rule. The message
names each rule as package · object · rule (reads FIELD), and nothing changes.
A rule is judged as the object serves it: one rule per name, a tenant overlay's
switch honoured. Switch off or change each named rule first, or disable the
package that owns it. A formula over the field does not refuse the disable; it
answers null. POST /packages answers the same 409 for an overwrite of an
enabled package with enableOnInstall: false that would leave such a package
disabled; the overwrite lands and the package stays enabled. A fresh install
with enableOnInstall: false lands disabled, as before.
- API Overview — discovery, error handling, and protocol types
- Data API — CRUD, batch operations, and analytics