> For the complete documentation index, see [llms.txt](https://codemash.gitbook.io/codemash/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://codemash.gitbook.io/codemash/cloud/database/schema.md).

# Schema

How to write a collection schema by hand or from an AI agent — the one contract, the data schema vs the UI schema, the canonical example, and the refusals you will meet.

A collection is defined by **two JSON documents** that the builder writes for you and that you can also write yourself — from the API, an SDK, the CLI, or an AI agent through MCP (Model Context Protocol). This page is for the person or agent who writes them by hand. For the click-through, see [Create a schema](/codemash/cloud/database/collections/create-schema.md).

## One contract, no second dialect

Both documents are checked against a **meta-schema** that lives in the gateway and is mirrored byte for byte in Cloud:

| Document                       | `$schema`                                | What it holds                                                                        | Checked by                                                                |
| ------------------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| **Data schema**                | `https://norbix.ai/schemas/meta/v1.json` | what is stored and the rules the server enforces on every write                      | `CM-ERRORS-SCHEMA-010` at save; `CM-ERRORS-DATABASE-030` … `055` at write |
| **UI schema** (`visualSchema`) | `https://norbix.ai/schemas/ui/v1.json`   | how the record form looks: tabs → containers → cells, a widget per field, form hints | `CM-ERRORS-SCHEMA-012` at save                                            |

Every object in both documents is **closed**: a key the meta-schema does not list is refused when the schema is saved, with the key named. There is no older spelling that still works and nothing is migrated silently:

```
CM-ERRORS-SCHEMA-012 — Visual schema violates meta-schema: Property 'watermark' has not been defined
and the schema does not allow additional properties. Path 'tabs[0].containers[0].cells[0].element.meta.watermark' …
```

The same for `hint` (use `help`), `permissions`, `minDate` outside the `date` object, `sort` outside the `select` object, and for `"widget": "uri"` (the widget is `website`). On the data side, a `sort` or a `meta` on a field type that has none is `CM-ERRORS-SCHEMA-010` with the field and the type it was checked as.

The Cloud builder, the SDK schema types and the AI tools all speak this one contract. A schema the builder saves is byte-identical to what the gateway stores; a schema you post by hand is held to the same meta-schema.

## Data rules vs form hints

The split is simple: **if the server must refuse a record because of it, it is a data rule and goes into the data schema. If only the form needs it, it is a hint and goes into the UI schema.**

| Data schema (rules the server checks)                        | UI schema (hints the form uses)                                                                                     |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| field type, `required`                                       | `widget`, `label`, `placeholder`, `help`                                                                            |
| `minLength` / `maxLength` / `pattern` / `format`             | `readonly`, `hidden`                                                                                                |
| `minimum` / `maximum` / `multipleOf`, a date's fixed bounds  | `date.minDate` / `maxDate` — bounds **relative to today** (`Now`, `DaysAfter[30]`)                                  |
| an enum's `values`, a currency's code list and step          | `select.sort` — the order the options are shown in                                                                  |
| a file field's `meta.storages`, `allowedFileType`, `maxSize` | `file.path` — the folder new uploads go into                                                                        |
| a reference's target and `displayField`                      | `nestedForm.collapsible` / `collapsed` / `inline`, `array.allowAdd` / `allowRemove` / `allowReorder` / `showMinMax` |
| `minItems` / `maxItems` / `uniqueItems`                      | the grid: `colSpan` (1–12), tabs, containers, headers                                                               |

Two consequences:

* A hint cannot protect data. `date.minDate: "Now"` keeps the picker from choosing yesterday; an API write with yesterday's date passes unless the data schema has a `minimum`.
* A rule cannot be hidden from the form. Cloud reads the data schema and shows every rule under the field in the server's words (`must be at most 5 characters`) before the form is sent.

## The shape

### Data schema

```json
{
  "$schema": "https://norbix.ai/schemas/meta/v1.json",
  "title": "orders",
  "type": "object",
  "additionalProperties": true,
  "properties": {
    "number":   { "type": "string", "pattern": "^ORD-[0-9]{4}$", "unique": true },
    "customer": { "source": "collection", "collectionId": "people", "displayField": "firstName" },
    "status":   { "source": "enum", "values": ["new", "paid", "shipped"], "default": "new" },
    "total":    { "type": "object", "properties": {
                    "value": { "type": "number", "minimum": 0, "multipleOf": 0.01 },
                    "currency": { "type": "string", "enum": ["EUR", "USD"], "default": "EUR" } } },
    "dueDate":  { "type": "integer", "minimum": 16777216 },
    "lines":    { "type": "array", "minItems": 1, "items": { "type": "object",
                    "properties": { "sku": { "type": "string", "minLength": 2 }, "qty": { "type": "integer", "minimum": 1 } },
                    "required": ["sku", "qty"] } },
    "notes":    { "type": "object", "additionalProperties": true, "maxBytes": 4096 }
  },
  "required": ["number", "customer", "lines"]
}
```

* `title` is the collection name; `type: "object"` and `additionalProperties: true` are fixed at the root (a record may carry undeclared top-level keys; a nested form may not).
* A field name matches `^[a-zA-Z][a-zA-Z0-9_-]*$`. Names the platform keeps in `_meta` (`createdOn`, `modifiedOn`, `responsibleUserId`, …) are refused: `CM-ERRORS-SCHEMA-026`.
* A field is **exactly one** of the 17 field types. The type is told from the keys: `type` for the primitives, `source` for the selections (`enum`, `user`, `role`, `taxonomy`, `collection`), and the shape for the built-ins (`value` + `currency` = currency, `type` + `coordinates` = geolocation, a string array with `uniqueItems` = tags, a string array with `format: uuid` = file, an object with `additionalProperties: true` and no `properties` = JSON). The [Field type reference](/codemash/cloud/database/schema/field-types.md) has one page per type with every keyword.
* `required` names the fields a record must carry; a nested form has its own `required`.

### UI schema

```json
{
  "$schema": "https://norbix.ai/schemas/ui/v1.json",
  "metadata": { "title": "Order" },
  "tabs": [
    {
      "tabName": "Order",
      "containers": [
        {
          "header": { "title": "Basics" },
          "cells": [
            { "colSpan": 4, "element": { "fieldId": "number",   "meta": { "widget": "text",     "label": "Number", "required": true, "placeholder": "ORD-0001" } } },
            { "colSpan": 4, "element": { "fieldId": "customer", "meta": { "widget": "select",   "label": "Customer", "required": true } } },
            { "colSpan": 4, "element": { "fieldId": "status",   "meta": { "widget": "select",   "label": "Status", "select": { "sort": "none" } } } },
            { "colSpan": 6, "element": { "fieldId": "total",    "meta": { "widget": "currency", "label": "Total" } } },
            { "colSpan": 6, "element": { "fieldId": "dueDate",  "meta": { "widget": "date",     "label": "Due", "date": { "minDate": "Now", "maxDate": "MonthsAfter[6]" } } } }
          ]
        },
        {
          "cells": [
            { "colSpan": 12, "element": { "fieldId": "lines", "meta": { "widget": "array", "label": "Lines", "array": { "showMinMax": true } },
              "children": [
                { "colSpan": 8, "element": { "fieldId": "sku", "meta": { "widget": "text",   "label": "SKU", "required": true } } },
                { "colSpan": 4, "element": { "fieldId": "qty", "meta": { "widget": "number", "label": "Qty", "required": true } } }
              ] } },
            { "colSpan": 12, "element": { "fieldId": "notes", "meta": { "widget": "json", "label": "Notes" } } }
          ]
        }
      ]
    }
  ]
}
```

* `metadata` is the one open bag (the form title, builder state). Everything else is closed.
* A **cell** is `colSpan` (1–12 on a 12-column grid) plus an **element**: the `fieldId` it renders and its `meta` (`widget` required; `label`, `placeholder`, `help`, `required`, `readonly`, `hidden` optional; one typed options object per widget family: `date`, `select`, `file`, `nestedForm`, `array`).
* The widgets: `text`, `textarea`, `html`, `number`, `currency`, `date`, `datetime`, `select`, `boolean`, `file`, `email`, `website`, `rate`, `nested_form`, `array`, `geo_location`, `tags`, `json`, `computational`.
* A `nested_form` cell, or an `array` cell whose items are an object, lays out the child fields in `children` — cells whose `fieldId` is the child's own name, nested again for deeper levels.
* You may leave the UI schema out of a save: the server then generates a flat one-container layout from the data schema. A UI schema **with** cells that breaks the meta-schema is refused, never replaced.

## The canonical example

The gateway keeps a fixture that is **generated from the two meta-schemas**: one field per field type × keyword, one cell per widget and per layout option, plus the nesting shapes — 129 fields on 7 tabs, with a valid and an invalid record per keyword and the code the server answers. It is the schema the Playwright suite builds through the builder, publishes and fills in, and the screenshots in the [Field type reference](/codemash/cloud/database/schema/field-types.md) show it.

* data schema: gateway `docs/testing/testing-plan/23-schema-content/fixtures/all-types.v1.data.json`
* UI schema: `all-types.v1.ui.json` · cases: `all-types.v1.cases.json`
* the same files in the cloud repo under `tests/e2e/fixtures/schema-content/`

When you are unsure how a keyword is spelled or where an option goes, find the field named after it in that file (`decimal_multipleOf`, `date_default`, `file_meta_storages`, `collection_displayField`, …).

<figure><img src="https://712692422-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHy372bqsaex2cqS3ikhL%2Fuploads%2Fgit-blob-a1ab4046eab36b167aea5fee5809759979dfa22a%2Ffield-types-builder-1.png?alt=media" alt="New schema sc_e2e_all_types in the Visual Builder: the tabs Text, Numbers, Dates, flags &#x26; places, Lists &#x26; files, References, Nested and JSON; on the Text tab a container Text — part 1 with String plain (Required, 6/12), String type, String format email, String format uri, String pattern and String min length (12/12)"><figcaption><p>The generated all-types fixture open in the builder.</p></figcaption></figure>

## Save it

```ts
const { id } = await norbix.hub.database.saveDatabaseSchema({
  schemaName: 'orders',
  dataSchema: JSON.stringify(dataSchema),
  visualSchema: JSON.stringify(uiSchema),   // optional — a flat layout is generated when left out
});
await norbix.hub.database.publishDatabaseSchema({ id });
```

```sh
norbix db schema create --name orders --file orders.data.json --ui-file orders.ui.json
norbix db schema publish --id sch_5QdZnkIC7qMbVLbNXysByI
```

From an AI agent: `validate_schema` checks a schema without saving it, `save_database_schema` / `update_database_schema_draft` take the two JSON strings above, and `apply_database_schema_bundle` builds whole linked collections from the catalog — see [What agents can do](/codemash/ai/developers/what-agents-can-do.md#database-schemas-from-an-agent). An agent never needs to invent a dialect: the tool descriptions and the refusals name the keys.

API: [Save Database Schema](/codemash/api-reference/database/schemas/save-database-schema.md) · [Update Database Schema Draft](/codemash/api-reference/database/schemas/update-database-schema-draft.md) · [Publish Database Schema](/codemash/api-reference/database/schemas/publish-database-schema.md)

## Common refusals

| You sent                                                                                                      | The answer                                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| a key the meta-schema does not know (`watermark`, `hint`, `permissions`, `sort` on a text field)              | `CM-ERRORS-SCHEMA-010` / `012` — `Property 'x' has not been defined and the schema does not allow additional properties. Path '…'`                           |
| a field that is no type (`type: "money"`)                                                                     | `CM-ERRORS-SCHEMA-010` — `Field 'properties.total' matches none of the 17 field types (no 'source' and an unknown 'type').`                                  |
| a nested form with the members `value` + `currency` or `type` + `coordinates`; a JSON field with `properties` | `CM-ERRORS-SCHEMA-010` — `Field 'properties.x' does not match exactly one field type (checked as …).`                                                        |
| `"widget": "uri"`                                                                                             | `CM-ERRORS-SCHEMA-012` — `Value "uri" is not defined in enum.` (use `website`)                                                                               |
| a fixed date in `date.minDate`                                                                                | `CM-ERRORS-SCHEMA-012` — `String '2026-01-01' does not match regex pattern '^(Now\|DayBefore\|…)$'` — put it in the data schema's `minimum`                  |
| `minimum` above `maximum`, or a number the keyword's type cannot hold                                         | `CM-ERRORS-SCHEMA-022` — `Schema field 'x' has an invalid range: …`                                                                                          |
| a `pattern` that does not compile                                                                             | `CM-ERRORS-SCHEMA-023` — `Schema field 'x' has an invalid regex pattern '[': …`                                                                              |
| a `default` that breaks the field's own rules                                                                 | `CM-ERRORS-SCHEMA-037` — `Schema field 'x' has an invalid default: …`                                                                                        |
| a reference without its target, or at a target the project does not have                                      | `CM-ERRORS-SCHEMA-025` / `027` (taxonomy) / `028` (collection) — the message tells you to create the target first and to use its name or id, never its title |
| a `displayField` the target schema does not have                                                              | `CM-ERRORS-SCHEMA-039`                                                                                                                                       |
| a field nested deeper than five levels                                                                        | `CM-ERRORS-SCHEMA-036` — `Schema field 'a.b.c.d.e.f' is nested 6 levels deep; the limit is 5.`                                                               |
| a field name the platform owns (`createdOn`)                                                                  | `CM-ERRORS-SCHEMA-026`                                                                                                                                       |
| a publish that drops a field                                                                                  | `CM-ERRORS-SCHEMA-020` — send `confirmed: true` to publish the breaking change                                                                               |

Every answer carries the `ErrorCode` in its metadata, so a client or an agent can branch on the code instead of reading the sentence. Record-level codes (`CM-ERRORS-DATABASE-030` … `056`) are listed on the [Field type reference](/codemash/cloud/database/schema/field-types.md#error-codes).

## Related

[Field type reference](/codemash/cloud/database/schema/field-types.md) · [References](/codemash/cloud/database/schema/references.md) · [Nested forms and arrays](/codemash/cloud/database/schema/nesting.md) · [Create a schema](/codemash/cloud/database/collections/create-schema.md) · [Edit a schema](/codemash/cloud/database/collections/edit-schema.md) · [What agents can do](/codemash/ai/developers/what-agents-can-do.md)
