Skip to content

Operations

Every write request contains one or more operations. There are five operation types — add, revise, retract, rename, and reaffirm — across shape, thing, assertion, and collection records.

A write request can include multiple operations. CLI (wh commit submit), SDK (client.commit.apply), and MCP (warmhub_commit_submit) writes apply operations one at a time — if a later operation fails, earlier ones are still in place, and you can retry just the failed ones. See Writes.

Create a new shape with field definitions:

{
"operation": "add",
"kind": "shape",
"name": "Location",
"data": { "fields": { "x": "number", "y": "number", "label": "string" } }
}

Create a new thing under an existing shape:

{
"operation": "add",
"kind": "thing",
"name": "Location/cave",
"data": { "x": 3, "y": 7, "label": "Dark Cave" }
}

To create a thing with data: null:

{
"operation": "add",
"kind": "thing",
"name": "Location/cave",
"data": null
}

Create an assertion about a shape or shaped thing. The about field is required:

{
"operation": "add",
"kind": "assertion",
"name": "Observation/cave-safe",
"about": "Location/cave",
"data": { "safe": true, "confidence": 0.8 }
}

To create an assertion with data: null:

{
"operation": "add",
"kind": "assertion",
"name": "Observation/cave-safe",
"about": "Location/cave",
"data": null
}

An ADD assertion payload may also include affirmedTargets — an array of pinned thing wrefs (in Shape/name@vN form) that records the specific versioned things whose state the assertion is vouching for. In practice, these are the things whose current state informed the assertion’s conclusion — for example, the exact versions of locations an agent inspected before concluding a route was clear. affirmedTargets differs from about: about identifies what the assertion is about (its subject), while affirmedTargets records the specific versioned things the assertion is vouching for. This is optional; omit it when you have no targets to affirm:

{
"operation": "add",
"kind": "assertion",
"name": "Observation/route-clear",
"about": "Location/cave",
"data": { "safe": true, "confidence": 0.8 },
"affirmedTargets": ["Location/cave@v2", "Location/dungeon@v5"]
}

Each entry in affirmedTargets must be a pinned version reference (Shape/name@vN). Unpinned wrefs (e.g. Location/cave) are rejected with a validation error.

about is an untyped wref, so it accepts local or canonical shape and shaped-thing targets. To assert about a collection, create a named collection operation first and point about at that collection’s wref.

Revise only changes data. It does not accept an active field — to mark an entity inactive, use a retract operation instead. TypeScript SDK users see this as a compile error because ReviseOperation declares active?: never.

Update a shape’s field definitions:

{
"operation": "revise",
"kind": "shape",
"name": "Location",
"data": { "fields": { "x": "number", "y": "number", "z": "number" } }
}

Update a thing’s data:

{
"operation": "revise",
"kind": "thing",
"name": "Location/cave",
"data": { "x": 5, "y": 3, "label": "Bright Cave" }
}

To clear a thing’s data payload, set data to null:

{
"operation": "revise",
"kind": "thing",
"name": "Location/cave",
"data": null
}

Update an assertion’s data. The about target cannot be changed:

{
"operation": "revise",
"kind": "assertion",
"name": "Observation/cave-safe",
"data": { "safe": false, "confidence": 0.2 }
}

To clear an assertion’s data payload, set data to null:

{
"operation": "revise",
"kind": "assertion",
"name": "Observation/cave-safe",
"data": null
}

A REVISE assertion payload may also include affirmedTargets — an array of pinned thing wrefs (in Shape/name@vN form) that records the specific versioned things the assertion is vouching for. When affirmedTargets is present, it replaces any previously stored set. When affirmedTargets is omitted, the prior affirmation snapshot is cleared — the new version will have no affirmed targets. To preserve existing targets across a data-only revise, either include them explicitly in affirmedTargets or use a reaffirm operation instead. Each entry must be a pinned version reference; unpinned wrefs are rejected with a validation error:

{
"operation": "revise",
"kind": "assertion",
"name": "Observation/cave-safe",
"data": { "safe": false, "confidence": 0.2 },
"affirmedTargets": ["Location/dungeon@v3"]
}

Update a collection’s members. The type field is a required discriminator that must match the existing collection’s shape — it cannot be changed:

{
"operation": "revise",
"kind": "collection",
"type": "arc",
"name": "Arc/A-B",
"members": ["Location/A", "Location/C"]
}

The name field must be the collection’s wref (e.g. Arc/A-B, Set/audit), not the bare caller-chosen name used at creation time. See Collections for the full collection model, including wh collection revise and the SDK shorthand.

Collections create grouped things (directed arcs, symmetric bonds, sets, and lists). A collection is an ordinary named thing with a built-in shape.

Collection naming works in two stages:

  • At creation (ADD): supply a bare caller-chosen name in the name field (e.g. "A-B"). WarmHub stores the collection under the full wref formed by combining the collection type with that name (e.g. Arc/A-B).
  • After creation (REVISE, RETRACT, etc.): supply the full collection wref in the name field (e.g. "Arc/A-B"), just as you would for any other shaped thing.

Create a collection thing explicitly:

{
"operation": "add",
"kind": "collection",
"type": "arc",
"name": "A-B",
"members": ["Location/A", "Location/B"]
}

The name field is required. Names are chosen by the caller and are not derived from the members. members are untyped wrefs and may identify shapes or shaped things.

See Collections for the collection model and first-class CLI/SDK operations.

To mark a collection inactive, use a RETRACT operation (see RETRACT Operations):

{
"operation": "retract",
"name": "Arc/A-B"
}

Mark an entity inactive. The entity’s data and version history are preserved; it is hidden from default HEAD queries.

{
"operation": "retract",
"name": "Location/cave",
"reason": "replaced by Location/dungeon",
"expectedVersion": 3
}

The reason field is optional (max 500 chars). The kind field is an optional safety hint — the operation will error if the resolved entity’s kind doesn’t match. expectedVersion is also optional; when supplied, the retract succeeds only if the target is still at that version.

Retract is the only path to setting an entity inactive. Once retracted, you can add a new entity at the same name to create a fresh identity — with one exception for built-in collection shapes (Arc, Bond, Set, and List). See Collections for the full collection model. The exception works as follows:

  • Same name, same members: re-adding a retracted built-in collection with the same membership resurrects the original — its version history continues (the write is recorded as a revise) rather than minting a new identity. Membership is compared exactly, including each member’s pinned version, so re-adding after any member has advanced to a newer version counts as different members and mints a fresh identity instead.
  • Same name, different members: a new collection identity is created as normal.

Change a thing’s, assertion’s, or shape’s name in place. Rename is a version-less metadata operation: it rewrites the name but creates no new version and adds no history entry. The current data and version number are untouched.

{
"operation": "rename",
"name": "Location/cave",
"newName": "cavern"
}
  • name is the current target — Shape/oldName for a thing or assertion, or the plain shape name for a shape rename.
  • newName is the bare new name: a new name segment within the same shape for a thing or assertion, or the new shape name for a shape rename. The shape of a thing or assertion does not change.
  • kind is an optional safety hint (thing, assertion, shape, or collection) — the operation errors if the resolved entity’s kind doesn’t match.

Because stored references are pinned by identity, an assertion, data field, or collection pointing at a renamed shape or shaped thing follows the rename automatically. Every input wref that uses the old name breaks, including old @vN spellings; the new name addresses current and historical versions. A rename mints no version, so it does not trigger subscription matching. See Retract, Rename & Schema Changes for the full ripple-effect rules and the wh thing rename / client.thing.rename shorthands.

An assertion’s affirmedTargets records which pinned thing versions the assertion is vouching for — the specific things whose state informed its conclusion. A reaffirm operation updates that list (adding or removing individual pinned targets) without touching the assertion’s data or about. Use it when the underlying things have advanced to new versions and you want to bring the assertion’s provenance up to date without issuing a full data revise.

A reaffirm edits the stored affirmedTargets list via add and/or remove sublists. When the add and remove lists together make no effective change to the stored set — every target in add is already present and every target in remove is already absent — the operation returns status: "noop" and no new version is created. Otherwise, a new version is created with updated provenance metadata.

At least one of add or remove must be present; a reaffirm with neither is rejected with a VALIDATION_ERROR (Reaffirm requires at least one target in "add" or "remove").

{
"operation": "reaffirm",
"name": "Observation/cave-safe",
"add": ["Location/cave@v3"],
"remove": ["Location/cave@v2"]
}
  • name is the wref of the assertion to reaffirm.
  • add is an optional array of pinned target wrefs (Shape/name@vN) to add to the affirmation snapshot.
  • remove is an optional array of pinned target wrefs to remove from the affirmation snapshot.
  • kind is an optional safety hint (assertion) — the operation errors if the resolved entity’s kind doesn’t match.
  • expectedVersion is optional; when supplied, the reaffirm succeeds only if the assertion is still at that version.

All entries in add and remove must be pinned version references (Shape/name@vN). Unpinned wrefs are rejected with a validation error.

The TypeScript SDK and MCP (warmhub_commit_submit) both accept the same reaffirm operation payload described above.

Reaffirm is only valid on assertions. To update an assertion’s data, use a revise operation instead.

  • data is required on revise for shape, thing, and assertion — the field must be present, though it may be null for thing and assertion revises (see below)
  • Revise is a full replacement — you must include all fields in data, not just the ones that changed
  • Omitted fields will be absent in the new version, which will fail shape validation if they are required
  • about is required on ADD assertion, immutable on REVISE assertion
  • data: null is valid on ADD and REVISE for things and assertions — it creates or clears the data payload. data: null is not valid on shape revises; shape operations always require an object payload.

A null body means data: null — a thing or assertion stored with no data payload, as in the examples above.

Writes like that are valid, but you cannot make one with --data. The flag accepts only a JSON object, so --data null fails with --data must be a JSON object, got null. This affects wh thing create, wh thing revise, wh assertion create, wh assertion revise, and wh commit submit --add / --revise. The single-object --file shortcut on wh thing create has the same limitation, so a file containing null fails the same way.

To write a null body from the CLI, pass a full operation payload instead of the shorthand:

Terminal window
# Inline JSON array of operations
wh commit submit --ops '[{"operation":"add","kind":"thing","name":"Location/cave","data":null}]' \
-m "Add cave with no body" --repo acme/world
# Or the same operations from a .json file
wh commit submit -f ops.json -m "Add cave with no body" --repo acme/world

The --ops, -f/--file, and newline-delimited .jsonl stream paths all forward data to the backend as written, so null survives. The SDK and MCP surfaces accept data: null directly. See the write submit deep dive for the full operation-payload reference.

Fields typed as wrefs are resolved when the operation is applied. Untyped fields accept shapes and shaped things; typed constraints use a shaped target’s resolved type, and a shape target never satisfies one. A required wref whose target is missing fails that operation with NOT_FOUND and stores no version for it. An optional wref (wref?) coalesces to null only for thing_absent — a named shaped thing missing under an existing active shape. Missing shapes and every other resolution failure remain hard errors for the affected operation. See Optional Wref Fields for the complete rules.

When an optional wref coalesces to null, the operation result carries a coalescedWrefs warning so the substitution is not silent:

{
"opIndex": 0,
"name": "Task/cleanup",
"operation": "add",
"status": "applied",
"warnings": {
"coalescedWrefs": [
{ "fieldPath": "owner", "wref": "User/nobody", "reason": "thing_absent" }
]
}
}

Each entry names the fieldPath that coalesced, the wref string that was dropped, and the reason (currently always thing_absent). The warnings struct is additive — the same object can also carry undeclared-field warnings. If more than 500 fields coalesce in one operation, the list is capped, coalescedWrefsTruncated: true is set, and totalCoalescedWrefs reports the full count.

The wh CLI prints each coalesced wref as a warning line under the operation:

+ Task/cleanup@v1
⚠ coalesced wref owner: User/nobody (thing_absent)

The same warnings.coalescedWrefs array is returned by every write surface — the SDK (client.commit.apply), the CLI, and MCP (warmhub_commit_submit) — which share one commit pipeline. WarmHub has no REST write endpoint; see HTTP Writes.

When you retry a write or have more than one writer touching the same data, you often want an operation to apply only under a condition: skip it if the target already exists, reject it if someone changed the target first, or do nothing when the data is unchanged. WarmHub supports three conditional patterns. The first two — skipExisting and expectedVersion — are opt-in fields you set on an operation. The third happens automatically.

By default, adding a name that already exists fails with a CONFLICT error (Thing "Location/cave" already exists). Set skipExisting on an add to return operation: "noop" instead — the existing entity is left untouched and the write does not fail.

{
"operation": "add",
"kind": "thing",
"name": "Location/cave",
"data": { "x": 3, "y": 7, "label": "Dark Cave" },
"skipExisting": true
}

This makes an add idempotent, which is what you want when re-running a seed script or retrying a write safely. skipExisting is available on add for shapes, things, assertions, and collections. On the CLI it is the --skip-existing flag on wh commit submit; in the SDK it is a field on each add operation or a top-level option on client.commit.apply.

By default, a revise or retract acts on the current version no matter who wrote it last. To guard against a lost update — you read v3, another writer commits v4, and your write would silently revise or withdraw the newer state — pass expectedVersion. The operation applies only if the target is still at that version; otherwise it is rejected with a CONFLICT error whose details.reason is "expected_version_mismatch", carrying the expected and current version numbers so you can refetch and retry.

{
"operation": "revise",
"kind": "thing",
"name": "Location/cave",
"data": { "x": 5, "y": 3, "label": "Bright Cave" },
"expectedVersion": 3
}

The same fence applies to a retract:

{
"operation": "retract",
"name": "Location/cave",
"expectedVersion": 3
}

expectedVersion is available on revise for things, shapes, assertions, and collections, and on all retract operations. CLI support for --expected-version varies by target kind:

Target kindCLI shorthand with --expected-version
thingwh thing revise Player/alice --data '{"score":2}' --expected-version 3
assertionwh commit submit --revise Reading/temp-1-v1 --kind assertion --data '{"value":72.5}' --expected-version 3
shapewh commit submit --revise Location --kind shape --data '{"fields":{"x":"number"}}' --expected-version 3
collectionRetract: wh thing retract Arc/edge-1 --kind collection --expected-version 3 ✓. Revise: not available on a shorthand — use wh commit submit with the full operation payload

Revise shorthands always need their target and --data alongside --expected-version; the fence is an addition to a complete revise, not a substitute for it.

In the SDK and via MCP, expectedVersion is a field on any revise or retract operation regardless of target kind. When you need exclusive access across a read-modify-write rather than an optimistic check, use a read lease instead.

If a revise produces the same data as the current version, it returns operation: "noop" — no new version is created. (WarmHub compares server-computed data hashes; clients never generate them.) Re-submitting an unchanged revise is therefore safe and leaves version history untouched.

For the full SDK signatures see Write Methods; for the CLI flags see Commands; for the MCP tool schema see MCP Tools Reference.

Within a single chunk, the following sequences on the same target are rejected by preflight:

SequenceAllowed?
ADD + ADDNo — duplicate add
REVISE + ADDNo — can’t add something that already exists
ADD + REVISEYes — create then immediately update
REVISE + REVISEYes — multiple updates in sequence

TypeScript callers binding an inline operation literal to a variable should either annotate it : Operation[] or use satisfies Operation[] to keep the operation discriminant narrowed — see SDK Write Methods — Typing Operation arrays.

The --file flag on wh commit submit accepts a JSON file containing an array of operations. For large datasets, prefer JSONL streaming (one operation per line):

Terminal window
# JSON array (small/medium batches)
wh commit submit --file operations.json --message "batch update" --repo org/repo
# JSONL stream (large datasets — chunked automatically; --stream-id + --skip-existing required)
wh commit submit --file dataset.jsonl --stream-id bulk-import --skip-existing --progress -m "bulk import" --repo org/repo

The JSON file must contain a JSON array of operation objects:

[
{
"operation": "add",
"kind": "thing",
"name": "Location/cave",
"data": { "x": 3, "y": 7, "label": "Dark Cave" }
},
{
"operation": "add",
"kind": "assertion",
"name": "Observation/cave-safe",
"about": "Location/cave",
"data": { "safe": true, "confidence": 0.8 }
}
]

Use wh shape template to scaffold operations from shape definitions (template generation lives in the shape domain):

Terminal window
# Single shape -> thing scaffold
wh shape template Hypothesis --repo org/repo
# Multiple shapes at once
wh shape template Hypothesis Evidence Decision --repo org/repo
# Assertion scaffold for a shaped claim about a target
wh shape template Hypothesis --kind assertion \
--about ResearchTopic/example --repo org/repo
# Write to file, then edit and commit
wh shape template Hypothesis Evidence -o experiment.json --repo org/repo
$EDITOR experiment.json
wh commit submit --file experiment.json -m "add experiment" --repo org/repo

The template fills fields with placeholder values ("", 0, false) and FILL_IN: hints for fields with descriptions. Replace placeholders with real data before writing.

Shapes define the payload schema, not whether an operation is a thing or an assertion. To scaffold an assertion, pass --kind assertion; add --about <TargetShape/name> when you want a concrete target instead of the default placeholder.

For large datasets, use JSONL format (one operation per line) with streaming. JSONL paths require --stream-id and --skip-existing:

Terminal window
wh commit submit --file dataset.jsonl --stream-id bulk-ingest --skip-existing --progress -m "bulk ingest" --repo org/repo

WarmHub no longer supports $N/#N commit-token syntax. Create things and collections with deterministic explicit names, then reference those names directly in later operations.

For example, add Location/cave first, then use about: "Location/cave" or members: ["Location/cave"] in later operations.

Order still matters: if an operation depends on an earlier operation that fails, the dependent operation can fail too.