Schema sources
Understand who owns each object on a workspace, what a sync may change, and how a source lives and dies over the API.
A schema source is the slice of a workspace's schema that one codebase owns. You declare objects and views with the builders, give the set an id, and push it with schema.sync. The workspace applies it as system definitions. Users keep working on top: their attributes and views survive every sync.
import { createStandards, defineSchemaSource, object, text } from "@standards/client";
const service = object("services", "Service")
.labelExpression("{{ name }}")
.attribute(text("name", "Name").required());
export const statusSource = defineSchemaSource({
id: "status-page",
label: "Status page",
objects: [service],
});
const standards = createStandards({
baseUrl: "https://api.standards.new/v1",
apiKey: process.env.STANDARDS_API_KEY!,
schema: statusSource,
});
const result = await standards.schema.sync(statusSource);
// { applied: true, hash, objects: { created, updated, deleted }, views: { created, updated, adopted, released } }Three tiers
Every object on a workspace belongs to one of three tiers: native, system or runtime.
- Native objects ship with the platform:
skill,memory,drives,artifact,meeting. Their names are reserved. Declaring one in code fails at build time withReservedObjectNameError, and the API refuses them too. - System objects and attributes are declared in code and pushed by
schema.sync. They carrysystem: trueand are immutable in the app. - Runtime objects and attributes are created by users in the app, or over
POST /schema/objectsand its attribute routes underarchitect:create. They carrysystem: false.
| System | Runtime | |
|---|---|---|
| Who creates | Your code, through schema.sync. | Users in the app, or an API call under architect:create. |
| Who changes | Your code, on the next sync. Users cannot edit, retype or delete it. | Users in the app. |
| Who deletes | Your code. An attribute removed from the source is dropped with its values and the view contents sourced on it; an object removed from the source is demoted, not deleted. | Users in the app. |
| What sync does | Owns the definition and rewrites it to match the code. | Keeps it. Promotes it to system when the code declares the same name; refuses with a 400 when the types clash. |
A runtime attribute added on a system object is merged back attribute by attribute: it stays where the user put it, and it becomes a system attribute only if your code later declares a compatible attribute with that name.
Removing an attribute reaches its views
Whoever removes an attribute, your code on the next sync or a user in the app, the views that name it follow:
- A view content sourced on it is dropped from its view: a collection whose query reads
related(attribute)orrelatedFrom(attribute)through it, and acontent.richtext(attribute)editing it. A content a source declared is hidden rather than deleted. - Any other content loses its mentions of the attribute: columns, card fields, form fields, filter rules and sorts.
- The removal is refused with
409SCHEMA_ATTRIBUTE_IN_USE, naming the view and the content, when a kanban is grouped by the attribute or when a view would lose its last main content. Regroup the board or give the view another main content first. A sync judges the views your code declares in that same sync, not the ones the previous sync left: re-sourcing a content on the attribute that replaces the removed one, or declaring another main content, in the same push keeps the view valid. Aschema.syncthat removes such an attribute is refused as a whole with a400(SchemaSourceValidationError) carrying the same sentence, and nothing is applied.
documents.relations and the emailAttributes of emails and meetings are not rewritten: repair them yourself.
Renames and retypes carry a resolution
Renaming a system attribute in code reads as one attribute gone and another of the same type appearing, and changing its type would leave the stored values unconverted. The sync refuses both before writing anything and answers 400 SCHEMA_AMBIGUOUS_CHANGE, naming the object, the attributes and the resolution that resolves them. Switching between text and richtext is the exception: both store a string, so it is applied in place. To keep the data, send that resolution with the next push — schema.sync(source, { resolutions }) renames an attribute and keeps its values, or converts text → phone and number → currency; relations and any other retype still take a SQL migration. To drop the data, ship the removal and the addition in separate syncs. See Data model / Objects.
Sealed objects
An object marked .sealed() turns every undeclared runtime attribute into a standards diff failure unless .tolerate() names it; neither flag deletes user data. See Data model / Objects.
Demotion keeps records
Remove an object from the source, or remove the whole source, and the object is demoted to runtime: system: false, records untouched, attributes untouched. Users can then edit or delete it in the app. A sync demotes an object; it never deletes one.
Views are adopted and released
A view declared by a source is owned by it. When the source declares a view whose object and name already exist as a runtime view, the row is adopted: it becomes source-owned and workspace overlays on it are preserved. When a view leaves the source, the row is released back to runtime with its overlays. The sync never overwrites a user's view with a copy of its own. The one thing a sync carries into a tab the workspace customized is a column new to the source: it is appended, visible, after the workspace's columns. Columns the workspace removed stay removed, and its order stays its own.
An object your source declares without views gets the workspace's default views, a table and a record page, just like an object created in the app. Each is created once, when the object has no view of that kind, as an ordinary workspace view: users change them, and later syncs leave them alone. Declare a list view for the object and only the record page is created; declare both and neither is.
Lifecycle over the API
schema.sync is a thin client over four routes.
| Route | Permission | Answer |
|---|---|---|
GET /schema/sources | architect:read | { sources: [{ sourceId, hash, appliedAt, objectNames, label, icon }] } |
GET /schema/sources/:id | architect:read | One summary. 404 with code SCHEMA_SOURCE_NOT_FOUND when the source was never pushed. |
PUT /schema/sources/:id | architect:update | { applied, hash, objects?, views? } |
DELETE /schema/sources/:id | architect:delete | 204. Owned objects are demoted, owned views released. |
The push works like this:
- The client hashes
{ objects, views }(SHA-256 over canonical JSON) and readsGET /schema/sources/:id. - Same hash: nothing is applied and the client answers
{ applied: false }. A changedlabeloriconis still stored; they are not part of the hash. - Different hash or never pushed:
PUT /schema/sources/:idwith{ hash, objects, views, label?, icon?, resolutions? }. The server recomputes the hash and answers400when it differs. It then checks ownership, applies theresolutions(outside the hash, likelabelandicon), validates every object and view with the builders' own rules, and applies the source under a per-workspace lock before persisting it. Resolutions and source are one transaction: a refusal anywhere leaves both unapplied.
Scope. A push, like a DELETE, writes only the source's own objects and the inverse rows of the bilateral relations it declares; the objects of core, bundles and other sources are read, never written, and cannot refuse it.
The routes are exempt from rate limiting: a CI push carries the whole schema in one call.
Conflicts. An object or view already owned by the core, a bundle or another source is a 409 (DuplicateError), with a message like Object "services" is owned by source:legacy-site. The owner is always the other party; the client throws SchemaSourceConflictError with objectName and owner. A declared view that would be a second default for the same object and context is a 409 as well, before anything is written; objectName and owner are then empty.
Limits. A source declares at most 50 objects and 200 views, and only views for its own objects. A source id is a lowercase slug (^[a-z0-9][a-z0-9-]{0,62}$).
From a terminal
standards sources list # id, hash, last apply, object names
standards sources remove status-page --yes # demote its objects, keep the records
standards pull # read the deployed source against the shared runtime
standards diff # exit 0 when equal, 1 on differences, 2 on errorstandards pull and standards diff compare what is deployed with what users changed at runtime. They never modify files or the workspace. See CLI / Schema and sources.
Next steps
- Data model / Sync — the result shape, the errors and how to run it in CI.
- Data model / Objects — builders,
.sealed(),.tolerate(), renames and retypes. - Access / API keys — which key may push a source.
- CLI / Schema and sources —
sources,pullanddiffin detail. - Book a demo