Objects
Declare an object with the fluent builder, give it a label expression, and know every method the builder accepts.
An object is a typed table. object({ name, label }) starts one; each .attribute() call adds a column and refines the inferred record type; .labelExpression() says how a record is displayed. The builder never touches the network. defineSchemaSource builds it, and schema.sync pushes it.
import { object, relation, status, text } from "@stndrds/client";
export const contact = object({ name: "contacts", label: "Contact" })
.pluralLabel("Contacts")
.icon("user")
.description("A person you talk to.")
.attribute(text({ name: "firstName", label: "First name" }).required())
.attribute(text({ name: "lastName", label: "Last name" }).required())
.attribute(text({ name: "email", label: "Email" }).email())
.attribute(
relation({ name: "company", label: "Company" })
.to("companies")
.bilateral({ object: "companies", attribute: "contacts" })
)
.attribute(
status({ name: "status", label: "Status" }).options([
{ value: "lead", label: "Lead", color: "yellow", group: "idle" },
{ value: "active", label: "Active", color: "green", group: "in_progress" },
{ value: "churned", label: "Churned", color: "red", group: "finished" },
])
)
.labelExpression("{{ firstName }} {{ lastName }}");Three rules apply at declaration time:
nameis kebab-case, 63 characters at most. Anything else throws aSchemaErrorwith codeSCHEMA_INVALID_OBJECT_NAME.nameis not one the platform ships.skill,memory,drives,artifactandmeetingare native objects.object({ name: "meeting" })is a type error in the editor and throwsReservedObjectNameError(codeSCHEMA_RESERVED_OBJECT_NAME) at runtime. The class is exported by@stndrds/schema; from@stndrds/client, catch it as aSchemaErrorand readerror.code.labelExpressionis mandatory.build()throws aSchemaErrorwith codeSCHEMA_VALIDATION_FAILEDwithout it, anddefineSchemaSourcecallsbuild()for you.
Builder methods
Every method returns the builder, so the order is yours. Only .attribute() changes the inferred type.
| Method | Default | Effect |
|---|---|---|
.labelExpression(template) | none, required | The record's display label. {{ attribute }} placeholders with pipes: UPPER, LOWER, capitalize, trim, prefix:"…", suffix:"…", wrap:"…", default:"…". |
.description(text) | none | Shown in the schema explorer and to agents. |
.pluralLabel(text) | none | Used where a collection is named. |
.icon(name) | none | An icon name from the platform set. |
.order(n) | alphabetical | Sidebar position, lowest first. Written to metadata.order. |
.hidden() | visible | Removes the object from the sidebar and the command palette. The API, search, permissions and agent tools still see it. |
.metadata(object) | {} | Merges into the object's metadata. Keys written by .order() and .hidden() survive. |
.embeddingExpression(template) | none | Opts the object into vector search. Same syntax as the label expression. Without it, records carry no embedding. |
.sealed() | extensible | Declares that no custom attribute belongs on this object. standards diff reports any custom attribute not listed in .tolerate() as unexpected. |
.tolerate(names) | [] | Custom attribute names standards diff accepts on a sealed object. On an extensible object it is documentary. |
.documentLayout(layout) | none | Folder variants and presets for the object's drive. Preset trees are validated at declaration. |
.migration(version, cb) | none | A schema migration. Versions start at 2 and must be sequential. |
.attribute(builder) | — | Adds one attribute and refines the type. Throws DuplicateError on a repeated name. |
.attributes(list) | — | Adds several attributes. They do not enter the inferred type. Same DuplicateError rule, inside the list and against earlier calls. |
.runtime() | system: true | Marks the object system: false. Meaningless over a schema source: the server forces system: true on every object a source declares. Keep it for test fixtures. |
.build() | — | Returns the ObjectDefinition. defineSchemaSource calls it; you rarely do. |
.$infer | — | record, create and update types with no import. See Type safety. |
The builder has no openMode, no visibility, no permissions and no softDelete. Visibility is decided per record by the workspace; permissions belong to roles and API keys; deletion goes through the records API. An object's id is assigned by the server at sync time; pass id in the config only for deterministic fixtures.
Duplicate attribute names
Names are compared with ===, so email and Email are two attributes. The same name twice throws at the second call, not at build():
import { object, text } from "@stndrds/client";
object({ name: "contacts", label: "Contact" })
.labelExpression("{{ email }}")
.attribute(text({ name: "email", label: "Email" }))
.attribute(text({ name: "email", label: "Work email" }));
// DuplicateError: Object "contacts": duplicate attribute name "email"Sealed objects
By default an object is extensible: workspace users add their own attributes on top of yours, and schema.sync leaves those attributes alone. .sealed() states that the code is the whole definition. It does not delete anything; it turns every custom attribute into a standards diff failure unless .tolerate() names it:
import { number, object } from "@stndrds/client";
export const invoice = object({ name: "invoices", label: "Invoice" })
.sealed()
.tolerate(["legacyRef"])
.attribute(number({ name: "amount", label: "Amount" }).decimal(2).required())
.labelExpression("{{ amount }}");Use it on objects whose shape a downstream integration depends on, and run standards diff in CI.
Migrations
A plain diff cannot tell a rename from a drop-and-add, and schema.sync refuses to change a system attribute's type on its own. Declare the intent with .migration(version, cb):
import { number, object, text } from "@stndrds/client";
export const contact = object({ name: "contacts", label: "Contact" })
.migration(2, (m) => m.renameAttribute("fullName", "displayName"))
.migration(3, (m) => m.changeType("age", "text", "number"))
.attribute(text({ name: "displayName", label: "Display name" }).required())
.attribute(number({ name: "age", label: "Age" }))
.labelExpression("{{ displayName }}");The callback receives a builder with renameAttribute, changeType, removeAttribute, addAttribute and updateConfig. Versions start at 2. A version below 2 or one already declared throws from .migration(); a gap in the sequence is rejected by build(). A retype pushed without its changeType migration is refused by the server with a SchemaSourceValidationError. Migrations the server has already applied are skipped on the next sync.
Vector search
embeddingExpression is opt-in. Give it the attributes that carry meaning, and semantic search indexes the rendered text:
import { object, richtext, text } from "@stndrds/client";
export const note = object({ name: "notes", label: "Note" })
.attribute(text({ name: "title", label: "Title" }).required())
.attribute(richtext({ name: "body", label: "Body" }))
.labelExpression("{{ title }}")
.embeddingExpression("{{ title }}\n{{ body }}");An object without the expression renders an empty embedding text and is left out of vector results.
The inference ceiling
Each .attribute() call deepens the accumulated record type. Measured with a uniform chain of required text attributes, inference is clean at 98 attributes and fails at 99 with a single TS2589 on the object(...) call. Real objects mix attribute types and cost more per step, so treat 98 as an upper bound. Past it, the earliest-declared attributes degrade to any while every assertion keeps passing. Split objects that large.
System and runtime
Every object a schema source declares is a system object: users can add attributes and views on top of it, but cannot delete or retype what your code declared. Attributes and views created in the workspace are runtime, and they belong to the workspace, not to the source. Schema sources covers who creates, changes and deletes each kind.
Next steps
- Attributes: every attribute builder and its options.
- Views: list and detail views declared next to the object.
- Sync: push the source and read what changed.
- Type safety: what the declaration gives you at compile time.