Standards Docs
Concepts

Attributes

The attribute primitives available in standards and their chainable modifiers.

Each attribute is a typed field on an object. You build it from a primitive function — text, relation, status, and so on — then chain modifiers before passing it to .attribute().

// @noverify
import { object, text, number, select, relation } from "@stndrds/schema";

const DEAL = object({ name: "deals", label: "Deal" })
  .labelExpression("{{ name }}")
  .attribute(text({ name: "name", label: "Name" }).required())
  .attribute(number({ name: "amount", label: "Amount" }))
  .attribute(
    select({ name: "currency", label: "Currency" })
      .options([
        { label: "EUR", value: "EUR", color: "blue" },
        { label: "USD", value: "USD", color: "green" },
      ])
  )
  .attribute(relation({ name: "company", label: "Company" }).to("companies").many());

Breaking in vNEXT: builders now default to system: true. The .system() modifier has been removed — anything declared in code is automatically system. Call .runtime() only inside tests, seeds, and fixtures to opt out.

Breaking in vNEXT: the attribute model has been consolidated around a canonical capability layer. Several legacy types and modifiers were removed without aliases — migrate as follows:

  • textarea()text({ ... }).multiline()
  • rating()number({ ... }).renderAs("rating").max(5)
  • user.allowedRoles(roles)user({ ... }).types(["user", "agent"]) (default is both)
  • relation.minItems(n) removed — only .maxItems(n) is supported
  • document.maxDocuments(n), .autoProcess(), and .attribute(...) removed — use .slots([...]) and .qualifyWith(...) for properties
  • location.enableAutocomplete(), .enableMap(), .displayFormat(...) removed — these are UI/view concerns now
  • .disabled() removed — set read-only on the view or form field, not on the schema attribute
  • .featureGate(...) removed entirely
  • Option objects no longer accept id or icon — use { value, label, color?, description?, group?, inverse? }

Primitives

PrimitiveUse caseExample
textSingle-line or multi-line stringstext({ name: "notes", label: "Notes" }).multiline()
richtextRich text editor (blocks)richtext({ name: "body", label: "Body" })
numberNumeric values, including rating UInumber({ name: "score", label: "Score" }).renderAs("rating").max(5)
checkboxBooleanscheckbox({ name: "active", label: "Active" })
dateDates and timestampsdate({ name: "closedAt", label: "Closed" })
phoneE.164 phone numbersphone({ name: "phone", label: "Phone" })
currencyMonetary amountscurrency({ name: "price", label: "Price" })
statusWorkflow status with groupsstatus({ name: "stage", label: "Stage" }).options([...])
selectOne value from a fixed setselect({ name: "kind", label: "Kind" }).options([...])
multiselectMany values from a fixed setmultiselect({ name: "tags", label: "Tags" }).options([...])
locationAddress / coordinateslocation({ name: "address", label: "Address" })
fileFile attachmentsfile({ name: "avatar", label: "Avatar" })
userReference to an actoruser({ name: "ownedBy", label: "Owner" })
relationForeign-key edge to another objectrelation({ name: "company", label: "Company" }).to("companies").many()
documentDocument referencedocument({ name: "contract", label: "Contract" })
formulaComputed read-only valueformula({ name: "fullName", label: "Full name" })
rollupAggregate across a relationrollup({ name: "totalAmount", label: "Total" })

formula and rollup are now backed by the v1 computed-fields engine. The runtime compiles each expression into a ComputedPlan, persists evaluation state in computed_attribute_states (status: pending | fresh | stale | failed), and recomputes affected records via a background job when dependencies change. The recomputed value reaches the client through the realtime channel as a live patch — the client no longer polls for pending state or renders a per-cell pending/stale indicator.

Formula expressions are now validated semantically when you create or update an attribute, not just at boot. A reference to an attribute that doesn't exist (yet) comes back as a non-blocking warnings entry on the save response — the save still proceeds. A return type that doesn't match the formula's inferred type is rejected outright.

Common chainable modifiers

ModifierEffect
.required()Marks the attribute as required; validates presence on write
.optional()Explicitly marks the attribute as nullable (default for most primitives)
.runtime()Opt out of the default system: true (advanced — tests, seeds, fixtures only)
.icon(name)Attaches a Lucide icon to the attribute in the UI
.description(s)Admin tooltip shown in the attribute editor
.autofill(config)Enable AI autofill for this field (see callout below)

.required() is a schema-level rule enforced by the runtime validator. Your database adapter must separately configure NOT NULL constraints to guarantee hard enforcement at the storage layer.

AI autofill

.autofill(config) lets the runtime populate a field from attached-document OCR content and other attributes on the same record. The from array is mandatory: an empty array makes the field document-only (filled exclusively from attached document content), while a non-empty array names sibling attributes whose changes also trigger a contextual fill. An optional instructions string is injected verbatim into the generation prompt.

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

const INVOICE = object({ name: "invoices", label: "Invoice" })
  .attribute(
    select({ name: "docType", label: "Type" })
      .options([{ value: "invoice", label: "Invoice" }])
      // document-only: filled from the attached file's OCR content
      .autofill({ from: [] })
  )
  .attribute(
    text({ name: "region", label: "Region" })
      // contextual: re-filled when city or country changes
      .autofill({ from: ["city", "country"], instructions: "deduce from the address" })
  );

Autofill is eligible only for text, number, checkbox, date, phone, currency, status, select, and multiselect attributes — eligibility is validated at schema sync, so an .autofill() on an unsupported type fails fast at boot. Fills run automatically through the generation pipeline (idempotent and usage-tracked) when a document is attached or a source attribute changes; the field is never overwritten if it already holds a user-entered value. To fill pre-existing records, the backend exposes POST /autofill/backfill (dry-run estimate by default, or enqueue a backfill job with confirm).

On this page