Standards Docs
Concepts

Schema

How to define objects and register them in the standards schema.

An object is a typed entity definition — think "Contact", "Company", or "Invoice". The schema is the registry of all objects your application exposes.

// @noverify
import { object, text, status, relation, registry } from "@stndrds/schema";

const CONTACT = object({ name: "contacts", label: "Contact" })
  .icon("User")
  .pluralLabel("Contacts")
  .description("A person your team interacts with.")
  .order(1)
  .labelExpression("{{ lastName | UPPER }} {{ firstName | capitalize }}")
  .attribute(text({ name: "firstName", label: "First name" }).required())
  .attribute(text({ name: "lastName", label: "Last name" }).required())
  .attribute(
    status({ name: "stage", label: "Stage" })
      .options([
        { id: "lead", label: "Lead", value: "lead", color: "gray" },
        { id: "qualified", label: "Qualified", value: "qualified", color: "blue" },
      ])
  );

registry.register(CONTACT);

Breaking in vNEXT: .system() has been removed. Builders are now system: true by default — anything you declare in code is immutable at runtime. Use .runtime() to opt out, but only in tests, seeds, and fixtures.

object() modifiers

ModifierPurposeExample
.order(n)Display order in sidebar lists; lower values appear first.order(1)
.description(s)Admin-facing description shown in the object browser.description("A person your team interacts with.")
.icon(name)Lucide icon name rendered next to the object label.icon("User")
.runtime()Opt out of the default system: true — only for tests, seeds, fixtures.runtime()
.labelExpression(template)Template string that renders a record's display label.labelExpression("{{ name }}")
.pluralLabel(s)Plural form used in UI list headers.pluralLabel("Contacts")
.attribute(builder)Adds a typed attribute to the object.attribute(text({ name: "name", label: "Name" }))

Label expressions

Every object requires a .labelExpression(). The template is evaluated at read time to produce the human-readable title shown in relation pickers, record headers, and search results.

The syntax uses {{ attributeName }} interpolation with optional pipe formatters:

// @noverify
.labelExpression("{{ lastName | UPPER }} {{ firstName | capitalize }}")

Available pipes: UPPER, LOWER, capitalize, trim.

labelExpression is required — registry.register() (via .build()) throws if it is missing.

Registry

Every object must be registered before the runtime can use it. Call registry.register() once per object, typically in a dedicated schema/index.ts entry point loaded at startup.

// @noverify
import { registry } from "@stndrds/schema";
import { CONTACT } from "./contact";
import { COMPANY } from "./company";

// Register dependencies first — CONTACT references COMPANY via a relation
registry.register(COMPANY);
registry.register(CONTACT);

Order matters when objects reference each other via relation(). Register the target object before the object that references it so the runtime can resolve the edge at startup.

Migrations

When you rename an attribute, change its type, or otherwise make a destructive change the boot-time schema sync cannot resolve unambiguously, declare a .migration(version, callback) directly on the object builder:

// @noverify
import { object, enum_ } from "@stndrds/schema";

object({ name: "deal", label: "Deal" })
  .attribute(enum_({ name: "urgency", label: "Urgency" }).options(["HIGH", "MEDIUM", "LOW"]))
  .migration(2, (m) =>
    m.renameAttribute("priority", "urgency").changeType("urgency", "text", "enum", {
      transform: "toString",
    })
  );

Migrations apply in batch at boot, are idempotent (re-runnable if interrupted), and are tracked per object in schema_sync_log. Configure batch behavior via STANDARDS_MIGRATION_BATCH_SIZE (default 1000) and STANDARDS_MIGRATION_MAX_DURATION_MS (default 300000). Without a declared migration, ambiguous diffs throw SchemaAmbiguityError; destructive but non-ambiguous diffs (remove_attribute, remove_object) demote to runtime by default, or delete from the DB if STANDARDS_ALLOW_DESTRUCTIVE_SYNC=1 is set.

changeAttributeType via the runtime admin API is disabled — it throws ChangeTypeNotSupportedError. Type changes must be declared in code with .migration(N, m => m.changeType(...)).

See the Attributes page for the full list of attribute primitives and their chainable modifiers.

On this page