Skip to content

Schema

A Schema declares one collection: the JSON Schema for each entry's data, its indexes, its Admin presentation and its lifecycle mode. Schemas are never exposed directly; Views read them and Procedures write them. This page is the field-level contract; the concepts are in The four atoms and Lifecycle and locales. Envelope rules are in Manifest envelope and conventions.

Fields

FieldTypeRequiredDefaultRules
titleLocalizedTextyesAdmin label. Non-empty string or locale map.
descriptionLocalizedTextnoSame shape as title.
schemaJSON Schema 2020-12yesMust be an object. Walked by the subset validator.
uiSchemaobjectnoAccepts fields, list, and nav. Violations are SCHEMA_UI_INVALID.
uniqueIndexesstring[][]no[]Ordered tuples of top-level scalar fields. See Indexes.
indexesstring[][]no[]Ordered non-unique tuples. Must not repeat a uniqueIndexes tuple.
searchableFieldsstring[]no[]Top-level string fields for Admin and Staff MCP substring search.
localizedbooleannofalseWhen false, a locale property is rejected. Must be a boolean.
translates{ parent, on }noMarks a translation child. Requires localized: true.
lifecyclepublishing | operationalnopublishingSelects the state machine.

Reserved entry columns

Every entry carries id, status, version, createdAt, updatedAt and authorId as native columns outside data. They cannot be indexed (SCHEMA_INDEX_INVALID) but are valid in View fields, filter, orderBy and uiSchema.list. locale is a reserved data field: only a localized Schema may declare it, and the runtime requires it on writes to a localized Schema. Do not name data properties after the native columns; SQL Views project the native column. Do not declare expectedVersion under spec.schema.properties — that name is the reserved Procedure OCC token; validate fails closed with INVALID_MANIFEST_ENVELOPE (ADR-0022). New reserved Procedure input names need an ADR.

Example

yaml
apiVersion: cms.mantle.aotter.net/v1
kind: Schema
metadata:
  name: support-requests
spec:
  title: { en: Support requests, "zh-TW": 客服請求 }
  description: Requests submitted from the public contact form.
  lifecycle: operational
  schema:
    type: object
    additionalProperties: false
    required: [ticketNumber, subject, body, requestStatus]
    properties:
      ticketNumber: { type: string, pattern: "^SR-[0-9]{6}$" }
      subject: { type: string, minLength: 1, maxLength: 200 }
      body: { type: string, x-mcp-hint: markdown }
      requestStatus: { type: string, enum: [open, waiting, closed], default: open }
      customerId: { type: string, x-mantle-ref: customers }
      attachmentId: { type: string, x-mantle-ref: media_assets, x-mcp-hint: media-file }
      submittedBy: { type: string, x-mantle-bind: ctx.user }
      submittedAt: { type: integer, x-mcp-hint: timestamp-ms, x-mantle-bind: now }
  uiSchema:
    fields:
      body: { widget: textarea }
    list:
      filterField: requestStatus
      primaryField: ticketNumber
      columns: [subject, requestStatus, submittedAt]
  uniqueIndexes: [[ticketNumber]]
  indexes: [[requestStatus, submittedAt], [customerId]]
  searchableFields: [ticketNumber, subject]

translates

A translation child holds the locale-specific fields of a non-localized parent, joined on a shared field.

yaml
apiVersion: cms.mantle.aotter.net/v1
kind: Schema
metadata:
  name: product-translations
spec:
  title: Product translations
  localized: true
  translates: { parent: products, on: sku }
  schema:
    type: object
    required: [sku, name]
    properties:
      sku: { type: string }
      locale: { type: string }
      name: { type: string, minLength: 1 }
      description: { type: string, x-mcp-hint: markdown }
  uniqueIndexes: [[sku, locale]]

locale is declared but not listed in required; the write-time gate enforces presence.

PhaseRuleDiagnostic
parseparent and on are non-empty strings; no other keys.INVALID_MANIFEST_ENVELOPE
parselocalized: true is set.TRANSLATES_REQUIRES_LOCALIZED
parseAt least one property besides locale and the join field.TRANSLATES_REQUIRES_CONTENT_FIELD
parseon is declared in this Schema's properties.TRANSLATES_FIELD_NOT_IN_CHILD
validate and bootparent names a declared Schema.TRANSLATES_PARENT_UNKNOWN
validate and bootThe parent is not localized: true.TRANSLATES_PARENT_IS_LOCALIZED
validate and booton is declared in the parent's properties.TRANSLATES_FIELD_NOT_IN_PARENT
bootA localized Schema needs at least one site locale.SCHEMA_LOCALIZED_REQUIRES_SITE_LOCALES

JSON Schema subset

Authors write JSON Schema; the runtime validates with zod via z.fromJSONSchema. The parser accepts a bounded subset so every manifest converts.

Recognized keywords
$defs, $ref, oneOf, const, type, properties, required, items, enum, format, pattern, minLength, maxLength, minimum, maximum, minItems, maxItems, nullable, readOnly, default, additionalProperties, title, description, and any x- keyword
Rejected keywords (JSON_SCHEMA_UNSUPPORTED)
anyOf, allOf, not, if, then, else, $anchor, $dynamicAnchor, $dynamicRef, definitions, patternProperties, prefixItems, contains, dependentSchemas, propertyNames, unevaluatedProperties

Other standard keywords are not rejected by the parser, but only the recognized set is part of the documented contract.

RuleDiagnostic
Nesting deeper than 100 levels, or more than 10,000 schema nodes.JSON_SCHEMA_LIMIT_EXCEEDED
$ref must begin #/$defs/ and resolve to an object in the same document.JSON_SCHEMA_REF_INVALID
pattern must compile as a JavaScript regular expression.INVALID_PATTERN
Every required entry of spec.schema must be declared under properties.REQUIRED_FIELD_UNKNOWN
properties and $defs must be objects; oneOf a non-empty array; additionalProperties a boolean or a schema.INVALID_MANIFEST_ENVELOPE

Two compatibility normalizations run at the boundary: nullable: true becomes a type array that includes "null", and format: url becomes format: uri. additionalProperties keeps standard semantics: omitted or true preserves extra keys, false rejects them, a schema validates them.

x-mantle-bind

Marks a property as server-stamped. The value is a closed enum; anything else is BIND_VALUE_NOT_IN_ENUM.

ValueStamped asTypical field
ctx.userctx.user?.id ?? nullauthorId, submittedBy
ctx.staffctx.staff?.id ?? nullapprovedBy, grantedBy
nowwrite-time Unix epoch millisecondscreatedAt, submittedAt

Stamping semantics: on create the computed value replaces whatever the caller sent. On update the existing stamp is preserved and only computed when the stored row lacks it, so a now bind does not become an updated-at field. A null stamp is dropped before JSON Schema validation, so type: string passes for anonymous callers. Bound properties are removed from Staff MCP create_* and update_* tool schemas. Stamped values appear in View output as ordinary fields.

x-mantle-ref

An informational foreign-key marker on a string property that holds another collection's entry id (x-mantle-ref: customers). Nothing is enforced: no constraint, cascade or orphan check. Admin uses it for pickers and related rows; x-mantle-ref: media_assets marks a media asset id. Declare a single-field index on the ref field when reverse lookups must stay bounded. On a Procedure input property the marker exposes a row action; see Procedure.

x-mcp-hint

A free-form string that tells agents and Admin widgets how to render or produce a value. The grammar accepts any string; these values are conventional.

ValueMeaning
markdown, richtext, codeText editor and authoring format.
media, media-image, media-video, media-fileMedia-shaped URL or asset reference (isMediaMcpHint).
money-minorInteger amount in minor currency units.
timestamp-msUnix epoch milliseconds.
idempotency-keyOn a Procedure input: Admin generates and hides one UUID per form; other callers generate one and reuse it on retry.

Do not use a hint for optimistic concurrency. The reserved Procedure input name expectedVersion is the OCC token (observed entry.version at read time). First-party Admin binds and hides it by that name. Schema spec.schema.properties must not declare it (INVALID_MANIFEST_ENVELOPE).

Root readOnly: true

schema.readOnly: true at the root marks a Procedure-managed collection. Staff MCP emits no create_* or update_* tool for it, and Admin's generic create, update, status change and delete return CONFLICT with the message that the Schema is read-only on generic authoring surfaces. Declared Views and Procedures (builtin or ref) keep working.

uiSchema

Closed Admin-only roots: fields, list, nav. Nested keys are closed too. Unknown roots or nested keys are SCHEMA_UI_INVALID.

KeyRule
fields.<field>.widgetOnly textarea. The field must be a top-level property with a string type (string or [string, null]).
list.filterFieldOperational Schemas only. A declared property with a non-empty string enum that is the first field of some indexes or uniqueIndexes tuple. Admin renders the enum as sidebar links and list tabs.
list.primaryFieldOperational Schemas only. A non-empty top-level scalar property; rendered as the linked leading column.
list.columnsOperational Schemas only. Top-level scalar properties, no repeats and not repeating primaryField.
nav.standaloneBoolean. true also emits a main Admin Nav list entry with a parent autocomplete filter. It does not unfold: required x-mantle-ref children still compose under the parent. Omit or false means fold-only (discover via the parent-entry workbench). Rejected on top-level Schemas, translates children, and Schemas with no eligible required-ref parent.
nav.parentFieldAllowed only with standalone: true. Names a required x-mantle-ref field used as the parent filter. One eligible required ref is inferred; more than one requires an explicit parentField. Do not rely on property-order heuristics when multiple refs exist.
yaml
uiSchema:
  list:
    primaryField: name
    columns: [status]
  nav:
    standalone: true
    parentField: organizationId

Every violation is SCHEMA_UI_INVALID. Without primaryField and columns, Admin lists an operational collection with platform metadata only. nav is operational Admin navigation only — it does not change runtime, MCP, or publishing validation. Parent autocomplete coexists with list.filterField enum tabs as a separate control.

Keep implementation-detail children fold-only. Use nav.standalone: true when staff also need a cross-parent list; see the inventory example.

Indexes

uniqueIndexes and indexes are arrays of ordered field tuples. Shape errors are INVALID_MANIFEST_ENVELOPE; semantic errors are SCHEMA_INDEX_INVALID unless noted. Bare strings and the retired indexedFields key are rejected; each index is an array of field names, even when it has one field.

RuleDiagnostic
When any index is declared, metadata.name matches /^[A-Za-z][A-Za-z0-9_.-]*$/.SCHEMA_INDEX_INVALID at /metadata/name
Each tuple is a non-empty array of strings.INVALID_MANIFEST_ENVELOPE (shape) or SCHEMA_INDEX_INVALID (empty)
No field repeats within a tuple; field names match the same safe pattern.SCHEMA_INDEX_INVALID
Fields are not reserved entry columns.SCHEMA_INDEX_INVALID
Fields are exact top-level keys of properties.UNIQUE_INDEX_FIELD_UNKNOWN or SCHEMA_INDEX_FIELD_UNKNOWN
Fields are indexable scalars: exactly one non-null type, optionally nullable. string maps to TEXT, integer and boolean to INTEGER, number to REAL.SCHEMA_INDEX_INVALID
No tuple repeats within a list; indexes does not repeat a uniqueIndexes tuple.SCHEMA_INDEX_INVALID

On SQLite storage each tuple becomes a partial index over generated columns of entries.data; queries benefit from a leftmost prefix. Unique indexes are also checked before every write; a conflicting row is CONFLICT.

searchableFields

An array of unique top-level string properties (string or [string, null]). Non-array or non-string entries are INVALID_MANIFEST_ENVELOPE; duplicates and non-string properties are SCHEMA_SEARCH_INVALID; unknown fields are SCHEMA_SEARCH_FIELD_UNKNOWN. The entry id is always searched. Substring search does not use indexes.

Lifecycle

publishing is the default state machine.

FromTo
draftpublished, archived
publishedarchived, draft
archiveddraft

operational has no transitions. Entries are created with status: published, edited in place, and any publish, unpublish or archive request returns CONFLICT. Admin hides lifecycle controls; Staff MCP emits create_record_<segment> and update_record_<segment> instead of the draft tools. A builtin archive Procedure cannot target an operational Schema.

Deleting a published entry of a publishing Schema is CONFLICT (unpublish first). Operational entries can be deleted in any status.

Write-time locale gate

Every authoring path (Admin, Staff MCP, builtin Procedures) runs the same guard after stamping, per request, reading the current site locales.

ConditionResult
Non-localized Schema and data.locale is present.INPUT_VALIDATION_FAILED
Localized Schema and data.locale missing or empty.INPUT_VALIDATION_FAILED (skipped for partial draft saves; publish re-checks).
Localized Schema and data.locale not in the site locales.INPUT_VALIDATION_FAILED with the enabled locales as candidates.
Site locales list is empty.Locale membership is not checked.

Site locales are configured in Site config.

Source