Part V. Commands
A command is a typed, validated change to a model. Commands are how a host changes a model on behalf of a person or an agent: each is checked before anything changes, applied all or nothing, and answered with what it touched. Hand edits of files are also allowed (Part III, section 14); they are checked when the folder is read.
1. The command
- A command is a JSON object whose
kindnames it; its other members are fixed by its kind. - A command processor MUST refuse a command whose
kindit does not implement, and a command that does not match its kind's schema, naming the member that is wrong. - The records a command carries (an element in
create, a type intype.upsert, a view inview.upsert) and an element'spatchMUST match their schemas. A member the schemas do not know MUST be kept in the record it is written to (Part I, section 10); the processor SHOULD warn, naming it, since it is often a misspelling. An extension member (Part I, 11.2) is no misspelling, and needs no warning. But a processor MUST refuse a command that would write an extension member whose value isnullinto a record that did not hold it, naming the record and the member (Part I, 11.2, rule 3): in a record a command writes whole (createand the upserts) and in the value a patch gives a member,nullis a value, and only a member of the patch itself takes a member away withnull(4.2). One that the record held, and that the command carries back as it was read, is kept (Part I, 10, rule 1). - A member a command's kind does not define at the top level of the command (for example a misspelt
expectedRevison) MUST NOT be dropped silently: the processor SHOULD refuse the command or warn. - A processor MUST NOT create an element or a type of a category it does not know, and MUST keep those a model holds, through every command that does not delete them (Part I, 10, rule 5). A processor of 0.2 knows the categories 0.2 adds, in every model of 0.2: Part XIII's
electrical,cableCarrierandcable, Part II, 6.2.1'smember, and Part II, 6.10'sprovision. It makes and changes records of them by the rules of the part that adds them: it holds each to that part's schema, refuses what those rules refuse where it refuses the like of a category of 0.1 (a host of another category, a level that is not there, a size the type sets: section 6, stage 2), and checks the whole model by them (stage 3). In a model of 0.1, read by 0.1's rules (Part I, 9.6, rule 3), they are a later version's categories, which it does not know, unless it is asked for the proposal of 0.1 that adds them.
Reference: commandSchema in src/core/model/model.ts (a union of 91 kinds keyed by kind).
2. The command set
The reference vocabulary has 91 kinds. They fall into core record operations, which every command processor MUST implement for the core records, and domain operations, which a processor MAY implement as a vocabulary profile.
2.1 Core record operations
| Kind | Does | Precondition |
|---|---|---|
create | adds an element; its id MUST be new | expectedHostRevision (optional) |
update | patches an element (section 4) | expectedRevision |
delete | removes an element and what depends on it (section 5) | expectedRevision |
type.upsert | writes a type whole | expectedRevision (optional) |
type.delete | removes a type no element uses | expectedRevision |
type.duplicate | copies a type under a new id and name, optionally assigning it to one element | expectedSourceRevision |
level.upsert | adds or changes a level; a new level gets its own plan unless plan: false | expectedLevel (optional) |
level.delete | removes a level with its plans; refused while anything stands on it unless moveTo names a level | expectedLevel |
material.upsert | writes a material | expectedRevision (optional) |
grid.upsert | writes a grid line | expectedRevision (required when the grid exists) |
project.rename, project.elevationOffset, project.site, project.envelope, rooms.settings, project.information | change the project record | expectedName, expectedOffset, expectedSite, expectedEnvelope, expectedSettings, expectedInformation |
project.excavation, project.precast, project.standards | set or clear a project rule | none |
original.make, original.add, original.remove, original.rename, original.dissolve, original.frame, original.edit, copy.place, copy.move, copy.remove, copy.unique | groups placed many times | expectedRevision on most |
2.2 Documentation profile
| Kind | Does | Precondition |
|---|---|---|
view.upsert | writes a view whole | expectedRevision (optional) |
view.update | changes a view's settings by name; display merges into the view's own | expectedRevision |
view.delete | removes a view; refused while a sheet, a dependent view, a callout or a report uses it | expectedRevision |
view.camera | sets a 3D view's camera for a sheet | expectedRevision |
detail.place, detail.update, detail.remove, detail.draw | place, change, remove or draw one 2D item of a view | expectedRevision (the view's) |
scopeBox.upsert, dimensionStyle.upsert, detailFamily.upsert | write those records | expectedRevision (required when the record exists) |
2.3 Domain operations (the reference vocabulary)
| Group | Kinds |
|---|---|
| Transforms | move, rotate, mirror, offset, array, radialArray, align, split, trim, sketch.edit |
| Walls, columns and outlines | wall.join, wall.attach, wall.detach, wall.flip, column.attach, column.detach, edge.attach |
| Timber framing and trusses | framing.rule.upsert, floor.framing.upsert, framing.materialise, framing.update, framing.detach, trusses.layout.upsert, trusses.plan, trusses.materialise, trusses.update, trusses.detach |
| Systems | mep.fall, mep.downpipe |
| Reinforcement | rebar.quick, mesh.lay |
| Fabrication | piece.upsert, piece.delete, joint.upsert, joint.delete |
| Application records | sheet.upsert, sheet.delete, titleBlock.upsert, titleBlock.delete, image.upsert, image.delete, render.upsert, render.delete, report.upsert, report.delete, deviation.upsert, deviation.delete, areaAdjustment.upsert, areaAdjustment.delete, reference.upsert, reference.delete |
A domain operation is defined by the records it produces: it MUST leave the model exactly as some sequence of core record operations would. Several are written that way in the reference: framing.rule.upsert and floor.framing.upsert become a type.upsert, trusses.layout.upsert and mep.fall an update, trusses.plan a view.upsert, mesh.lay a create, rebar.quick a batch of creates, view.update and detail.draw a view.upsert.
Reference: run in src/core/model/store.ts.
3. Preconditions
A precondition names the state a command was based on; a mismatch is refused, so two writers cannot overwrite each other unseen.
- When a command gives
expectedRevision, the target's current revision MUST equal it, or the command MUST be refused (the reference message: "Revision conflict: expected 3, current 4. Read and retry."). - Upserts. For
grid.upsert,scopeBox.upsert,reference.upsert,detailFamily.upsertanddimensionStyle.upsertthe precondition is strict: when the record exists,expectedRevisionMUST be given and MUST equal its revision; when it does not,expectedRevisionMUST be absent. For the other upserts a givenexpectedRevisionMUST equal the record's revision;material.upsert,report.upsert,deviation.upsertandareaAdjustment.upsertrefuse one for a new record, andtype.upsert,view.upsert,sheet.upsertandrender.upsertcount a new record as revision 0. - Other preconditions compare the named state:
expectedLevelthe whole level (every member of it, those the processor does not know and extension members included: Part I, 10 and 11.2, rule 5),expectedNamethe project's name,expectedSitethe site (ornullfor none),expectedHostRevisionthe host's revision. A state is compared as a JSON value: the order of its members and the way a number is written do not count (Part III, 6), and a member that one side has and the other lacks is a difference. - Where a precondition is optional and left out, the command is applied to whatever is there. Writers SHOULD always give one.
Reference: the revision checks in run.
4. Patches
An update carries patch: the element members to write.
- Each member of the patch replaces the element's member of that name, whole: nested objects are not merged.
nullremoves a member the element may lack (top,joins,taper,hostId…). The members every element has (name,typeId,levelId,offset,height,width,profile) can only be replaced, andid,categoryandrevisioncannot be patched.- A patch MUST NOT say
undefined(or leave out a member it means to remove): JSON drops an undefined member, so a logged command would not remove it. A processor MUST refuse a patch that holds an undefined value for a member the record has, and tell the caller to sendnull. - A patch that changes
typeIdMUST name a type of the element's category. - A patch that sets a
widthorheightthe element's type sets (Part II, 5.3) MUST be refused. - A patch MUST NOT write the legacy members
topRoofId,topCeilingIdandroofCutId. - After the patch, the element's revision is the old one plus one.
view.update patches a view the same way, except that display is merged into the view's own. detail.update merges its patch into one detail item, null removing a member, and the item is checked whole again.
Reference: elementPatchSchema and patchedElement in src/core/model/model.ts; refuseLostClears and the update branch of run in src/core/model/store.ts; refuseTypeSetSizes in src/core/model/typeSets.ts.
5. Deletes and what depends on them
- Deleting an element MUST also delete the elements that depend on it: those hosted by it (
hostId) and the framing members or trusses materialised from it (frameHost). The answer names them (removedDependents). - A shaft hosted by a deleted stair survives as a plain shaft: its
hostIdgoes and its revision goes up by one. - 2D items of views that name the deleted element are removed with it, and views stop hiding it.
type.deleteMUST be refused while a record names the type, an element or a view's assembly (a folder holds every type its records name, Part III, 12);view.deletewhile a sheet, a dependent view, a callout, a view reference or a report uses the view;level.deletewhile anything stands on the level or reaches up to it, unlessmoveTonames the level they move to.
Reference: dependsOn and the delete branch of run; viewDeleteRefusal in src/core/views/viewUsers.ts; deleteLevel in src/core/model/levels.ts.
6. Validation and refusal
A command is checked in three stages, and refused at the first that fails:
- Shape: the command matches its kind's schema (section 1).
- The command's own rules: its target exists, its preconditions hold, its references name records of the right kind, and the rules of its kind (a wall attaches only to a host that covers it; an opening fits its wall).
- The whole model: the model after the command MUST pass every rule of Parts I and II. A command that would leave the model invalid MUST be refused, even when the command itself is well formed.
A refusal MUST say, in the terms of the domain, what is wrong and SHOULD say how to fix it. In a batch, it MUST name the command by its index, kind and id.
From the reference processor, on examples/one-storey-house, after the south wall was attached (revision 1):
{
"ok": false,
"dryRun": true,
"failed": 3,
"results": [
{"index": 0, "ok": false, "kind": "update", "commandId": "wall-south",
"error": "Command 0 (update wall-south): Revision conflict: expected 0, current 1. Read and retry."},
{"index": 1, "ok": false, "kind": "update", "commandId": "wall-north",
"error": "Command 1 (update wall-north): A wall or beam requires two distinct endpoints."},
{"index": 2, "ok": false, "kind": "update", "commandId": "window-south",
"error": "Command 2 (update window-south): Opening must fit within its host wall: it reaches 10500 mm along a wall 10000 mm long."},
{"index": 3, "ok": true, "kind": "update"}
]
}
Reference: validateWhole and validateManifest; batchError and dryRunReport in src/core/model/store.ts.
7. Batches, atomicity and dry runs
- A batch is an ordered list of commands applied as one: each command sees the model as the commands before it left it, and the whole model is checked once after the last. If any command or the final check fails, the batch MUST change nothing.
- One command is a batch of one.
- A dry run (
dryRun: true) MUST run every check a real run does and MUST change nothing. A dry run of a batch that fails SHOULD answer for every command: each is tried in order on a copy, a failing one is reported and left out, and the rest go on, so that the caller learns everything it has to fix at once. - A batch that changes the model is one undo step.
- A generic element is moved to a category that fits by a batch of a
deleteand acreatewith the same id (Part II, 6.6, rule 7); no command kind does it alone.
Reference: dispatchBatch (a preview store, all or nothing) and dryRunReport in src/core/model/store.ts; execute_batch in src/automation.ts.
8. Determinism and replay
- Applying a command MUST be a function of the model and the command alone.
- A command MUST be applied as its JSON text would be stored: what JSON cannot hold (undefined members,
NaN, infinities, dates) is resolved before the command runs, so the processor that sends a command and one that replays it later get the same model. - A command MUST NOT mint a random id or read the clock. An id a command makes is named by the caller or derived from its input: the reference derives
<host>:bars, then<host>:bars-2,-3and so on. - A processor that replays commands it has already accepted (opening a project from a log) MUST NOT refuse them by a rule made after they were accepted. Admission rules apply to new commands; application is deterministic.
Reference: asLogged and madeId in src/core/model/store.ts; replay (the replaying flag lifts the newer refusals, such as type-set sizes and edits of placed copies, for accepted commands).
9. The answer
A command processor answers an accepted command or batch with at least:
| Member | Meaning |
|---|---|
ok | true |
dryRun | whether anything was committed |
affected | the ids of every record the command or batch changed, including those changed as a consequence (joined walls, dependents) |
verify | how to check the result: a filter that selects exactly the affected elements (Part VII), and the views and snapshots to look at |
sequence | the host's change sequence after the command |
warnings | what the caller should know but that did not refuse the command (optional) |
and, per command, its result: the record written, or for a delete {deleted, removedDependents?}.
From the reference processor, renaming the west wall:
{
"ok": true,
"dryRun": false,
"sequence": 1,
"affected": ["wall-west"],
"verify": {
"filter": {"elementIds": ["wall-west"]},
"snapshot3d": {"filter": {"elementIds": ["wall-west"]}, "corners": ["SW", "NE"], "hideOthers": false},
"snapshot2d": [{"viewId": "view-plan-0", "filter": {"elementIds": ["wall-west"]}}]
}
}
Reference: the execute and execute_batch actions and verifyHints in src/automation.ts.
10. The log (informative)
The reference implementation keeps a cloud project as a snapshot plus an append-only log of accepted batches, each stored as JSON and sequenced with a compare-and-swap; opening a project replays the log (section 8). 0.1 does not specify the log's format. A common command envelope is 0.2's (decided 11 October 2026). The plan proposed it around each batch: an operation id for idempotency, attribution per person and per agent, a target branch and base, a mode (apply, dryRun, propose) and an intent text. Part XVII, of 0.2, gives it in full, with what a processor that keeps a log records of each operation.
11. A supplier's parts
This section says what a supplier's parts (Part II, 12) ask of commands.
- No new command kind. A supplier writes a type with declared geometry, or with the railing members of Part II, 12.2, by
type.upsert; places or changes elements bycreateandupdate, where a patch ofdeclaredGeometryor ofrailingreplaces it whole (section 4); and materialises a railing's post positions by anupdatethat writesrailing.posts.at. - Transforms carry declared geometry.
move,rotate,mirror,array,radialArrayand the other transforms MUST move, turn, mirror or copy an element's declared geometry with the element (Part II, 12.1, rule 6). - A proposal's quote is not a command. It travels with the proposal on the host, and no command writes it into the model, nor into a proposal file (Part VI, 8.7).
Reference: mappedDeclaration in src/core/model/declaredGeometry.ts, called by the transforms in src/core/geometry/transforms.ts and src/core/geometry/modifications.ts.