For the complete documentation index, see llms.txt. Every page is also served as Markdown at its URL followed by .md.

standards
BundlesData model

Bundles

Declare objects, views, pages and agents as one bundle, sync it to a workspace from code, a script or the CLI, and read what it changed and why it was refused.


A bundle is a set of objects, views, pages and agents declared as one value. bundle(name, label) starts one; the workspace applies it as system definitions, and your users keep working on top of it.

import { bundle, object, text } from "@standards/client";
import { getStandards } from "./client";

const incident = object("incidents", "Incident")
  .attribute(text("title", "Title").required())
  .labelExpression("{{ title }}");

export const status = bundle("status-site", "Status page")
  .description("Incidents shown on the public status page")
  .add(incident);

const result = await getStandards().bundles.sync(status);
console.log(result.applied ? "applied" : "unchanged", result.hash);

Declare a bundle

bundle(name, label) takes the identity as two positional arguments, like object(name, label). The rest is chained:

MethodWhat it does
.description(text)A sentence shown wherever the bundle is listed.
.add(...pieces)Adds objects, views, pages and agents, in any mix. Call it as many times as you like.
.uses(...bundles)Declares the bundles this one depends on, as imported values.
.samples(records)Sample records offered when the bundle is added as a copy (see Synced and added).
.translations(locale, table)A translation table for one locale, used when the bundle is added as a copy.
  • name is a lowercase slug (^[a-z0-9][a-z0-9-]{0,62}$) and is the identity. core is reserved for the framework's own bundle. A bundle has no icon.
  • Pieces are added explicitly. A relation points at a name, so a relation target that is in neither the bundle nor a .uses() bundle is expected to exist already in the workspace; the server checks it at sync.
  • A piece belongs to one bundle. build() refuses an object present in the bundle and in one of its .uses() bundles with a DuplicateError naming both. Across unrelated bundles, the server refuses at sync (see Refusals).
  • Nothing registers globally and nothing runs at import. await status.build() does the work, once, and returns the plain artifact a sync sends: { name, label, description, hash, objects, views, pages, agents, uses, samples, translations }. Any change to the builder clears the memo.
  • The hash is the sha256 of the canonical JSON of the objects, views, pages and agents, each sorted by name, computed with Web Crypto. A label or description change alone does not change it.
  • Every object builder stays on the bundle so createStandards({ bundles: [status] }) can infer record types. The client key bundles is reserved: an object named bundles is refused.

The hash is the version a workspace holds. There is no version number to bump.

Three ways to sync

Each layer is optional above the previous one, and none locks you in.

1. The call

standards.bundles.sync(bundle, { resolutions? }) reads GET /bundles/:name (a 404 only means the workspace never received it), returns { applied: false, hash } when the hash, label and description all match, and otherwise sends PUT /bundles/:name with the built artifact. It runs from boot, a script, CI or a serverless function. Behind it is one public route, so any language can sync without the SDK.

resolutions resolve a rename or retype the server refused as ambiguous: see Objects. They travel outside the hash.

2. Discovery

discoverBundles({ cwd?, patterns? }) and defineConfig come from @standards/cli. Discovery imports every file matching the patterns, with a TypeScript loader, and returns each exported bundle() value, default or named. The default pattern is standards/bundles/*.ts. A standards.config.ts overrides it:

standards.config.ts
import { defineConfig } from "@standards/cli";

export default defineConfig({ bundles: ["src/**/*.bundle.ts", "packages/*/schema/index.ts"] });

Several bundles per file are allowed and file names carry no meaning: the identity is name. Two bundles with the same name are an error naming both files. A compiled app does not discover; it imports its bundles and calls bundles.sync.

3. The CLI

standards bundles sync              # discover, then sync each bundle, dependencies first
standards bundles list              # name, mode, origin, hash, installedAt
standards bundles remove status-site --yes

sync orders bundles by .uses() and prints <name> applied <hash> or <name> unchanged <hash> for each; the first refusal stops it with exit code 1. remove demotes the bundle's objects to custom objects and refuses to run without --yes. See CLI / Schema and bundles.

What one sync does

The server recomputes the hash, checks ownership of every name, applies the resolutions, and writes the bundle's own slice under a per-workspace lock. A sync writes and checks only that slice: objects owned by core or by other bundles are read, never written, and cannot refuse it.

interface SyncBundleResult {
  applied: boolean;
  hash: string;
  objects?: { created: number; updated: number; deleted: number };
  views?: { created: number; updated: number; adopted: number; released: number };
}

objects and views are present after a PUT. deleted counts attributes and objects that left the bundle; adopted counts workspace views the bundle claimed for the first time; released counts bundle views handed back to the workspace. Print it in CI so a deploy log says what the schema did.

Synced and added

A workspace holds a bundle in one of two modes.

ModeWhat happensWho uses it
SyncedThe bundle keeps owning its pieces. Each new hash converges them, and their structure is not editable in the app.Core, the bundles a host declares in code, and the bundles an app pushes over HTTP.
AddedA copy lands in the workspace, which owns it and edits it freely.Bundles a host offers for adding. See the licensed docs.

Each installation records its mode, hash, origin (code for core and host bundles, http for a pushed one, none for an added copy), when and by whom. The same bundle can be synced in one workspace and added in another.

Owners

Every piece belongs to an owner: core for the framework's own objects, views and system agents, or bundle:<name>. Only views store the owner; an object's owner follows from the bundle that declares it. Core is tracked by hash like any bundle, so there is no core version to bump.

Refusals

A workspace actor never writes, over HTTP, a bundle declared in code, and never turns an added bundle into a synced one. PUT /bundles/:name refuses, with the bundle named in the message:

  • the name core;
  • a name the host declares in code;
  • a name installed as an added copy;
  • a bundle whose uses names a bundle the workspace does not hold;
  • agents or pages, which arrive over HTTP later: an HTTP bundle carries objects and views.

DELETE /bundles/:name refuses core, a name declared in code, an added copy, and a bundle another installed bundle uses, naming the dependents.

An HTTP bundle declares at most 50 objects and 200 views, only views for its own objects. The routes are exempt from rate limiting: a CI push carries the whole schema in one call.

Errors

Two errors are mapped on the PUT only; the GET lets everything but a 404 through as a StandardsRequestError.

ErrorStatusCarries
BundleConflictError409objectName and owner (core or bundle:<name>). A declared view that clashes with the workspace's views, such as a second default view for the same object and context, is a 409 too; objectName and owner are then empty and the message says what clashed.
BundleValidationError400The server message: bad name, oversize payload, hash mismatch, invalid definition, a refusal above, an ambiguous rename or retype (SCHEMA_AMBIGUOUS_CHANGE), or a declared view the workspace cannot seed: Failed to sync declared views: <object>:<view> (<reason>).

Both extend StandardsRequestError; a 401 or 403 arrives as StandardsAuthError. Check the specific classes first.

import { BundleConflictError, BundleValidationError } from "@standards/client";

try {
  await standards.bundles.sync(status);
} catch (error) {
  if (error instanceof BundleConflictError) {
    console.error(`"${error.objectName}" is owned by ${error.owner}`);
  } else if (error instanceof BundleValidationError) {
    console.error(error.message);
  } else {
    throw error;
  }
}

A sync script for CI

Give the sync its own script and let the process exit code carry the outcome. 0 means applied or unchanged; 1 means the workspace refused the bundle and a human has to look.

import { BundleConflictError, BundleValidationError, StandardsAuthError } from "@standards/client";
import { getStandards } from "./client";
import { status } from "./schema";

try {
  const result = await getStandards().bundles.sync(status);
  console.log(result.applied ? "schema.applied" : "schema.unchanged", result.hash);
  process.exit(0);
} catch (error) {
  if (error instanceof BundleConflictError) {
    console.error("schema.conflict", error.objectName, error.owner);
    process.exit(1);
  }
  if (error instanceof BundleValidationError || error instanceof StandardsAuthError) {
    console.error("schema.rejected", error.message);
    process.exit(1);
  }
  throw error;
}
Exit codeMeaning
0Applied, or unchanged
1Conflict (409), rejected definition (400), or refused key (401 / 403)

Anything else (a timeout, a 5xx) is rethrown and fails the step with a stack trace, which is what you want from an outage.

Next steps