Writes
All mutations in WarmHub flow through one operation pipeline. Operations apply
one at a time, and each change is recorded as a new version on its thing’s own
history (wh thing history) — not in a separate commit or batch record.
Operation Kinds
Section titled “Operation Kinds”| Operation | Effect |
|---|---|
add | Create a thing, shape, assertion, or collection at version 1 |
revise | Append a new version with changed data |
retract | Withdraw a thing/shape/assertion/collection from default reads (name remains reserved) |
Use retract for lifecycle changes. revise changes data only and does not
accept active.
Atomicity
Section titled “Atomicity”CLI, SDK, and MCP writes (wh commit submit, client.commit.apply, and
warmhub_thing_create with its sibling write tools) apply
operations one at a time. Each MCP write tool carries one operation kind;
mixing kinds in one commit is a CLI and SDK capability. If a later operation fails, earlier ones are still in
place — the per-operation results array tells you what succeeded, what no-op’d,
and what failed, so you can retry just the failed ones. Request-level failures
such as authentication, malformed operation JSON, rate limits, or infrastructure
errors still reject the whole request.
Use explicit names to chain operations within a request: create a thing or collection with a deterministic name, then reference that wref from later operations. A dependent operation fails when its prerequisite didn’t land.
Submitting Operations
Section titled “Submitting Operations”Via CLI
Section titled “Via CLI”# Single operationwh commit submit --add temp-1 --shape Sensor \ --data '{"location": "Building A", "type": "temperature"}' \ -m "Add sensor" --committer bot-1
# Multiple operations — repeat --add paired with --data (≤20 ops)wh commit submit \ --add temp-1 --data '{"location":"A"}' \ --add temp-2 --data '{"location":"B"}' \ --shape Sensor -m "Two sensors"
# Batch from JSON filewh commit submit --file ops.json -m "Bulk import"
# Stream large datasets from a JSONL file (--stream-id + --skip-existing required)wh commit submit --file dataset.jsonl --stream-id 100k-import --skip-existing --progress -m "100k import"
# Stream from stdinproducer | wh commit submit --stream --stream-id pipe-ops --skip-existing -m "Pipe ops"
# Rerun an add-only partial seed — --skip-existing makes adds idempotentwh commit submit --file dataset.jsonl --stream-id 100k-import --skip-existing -m "Rerun seed"The --committer flag is optional. The shorthand flags (--add, --data,
--shape, --about, --retract, --reason) are repeatable and
pair by index. See Write Submit Deep-Dive.
committer is an untyped wref, not a free-form label.
It accepts a Thing by its name (bot-1) or a Shape (hub/shapes/Agent) that
already exists, including canonical cross-repo forms such as
wh:other-org/other-repo/hub/shapes/Agent and
wh:other-org/other-repo/bot-1. A label such as eval-runner is parsed
as a Thing name, so it succeeds only when a Thing with that exact name exists;
there is no implicit actor creation.
Previewing the same operation batch
Section titled “Previewing the same operation batch”wh commit submit --dry-run evaluates the batch with the real server commit
evaluator and returns one ordered result per input without persisting repository
state or consuming a receipt. It accepts the same operation sources and semantic
flags as submit, including JSONL files and stdin, but sends one bounded request:
at most 10,000 operations and 4 MiB encoded. See Validate without
committing
for JSONL framing, exit codes, disclosure, and caveats.
Via MCP
Section titled “Via MCP”{ "name": "warmhub_thing_create", "arguments": { "orgName": "my-org", "repoName": "my-repo", "submissionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "committer": "claude", "message": "Add sensor with reading", "items": [ { "name": "temp-1", "shapes": ["Sensor"], "data": { "location": "Building A", "type": "temperature" } }, { "name": "temp-1-v1", "shapes": ["Reading"], "data": { "about": "temp-1", "value": 72.5, "unit": "fahrenheit" } } ] }}Via SDK
Section titled “Via SDK”await client.commit.apply(orgName, repoName, "Add sensor", [ { operation: "add", name: "temp-1", shapes: ["Sensor"], data: { location: "Building A" } },])
const preview = await client.commit.validate(orgName, repoName, [ { operation: "add", name: "temp-1", shapes: ["Sensor"], data: { location: "Building A" } },])if (!preview.canCommit) console.error(preview.operations)
// Direct streaming surface for large or chunked workloadsawait client.stream.append({ orgName, repoName, submissionId: crypto.randomUUID(), chunkOrdinal: 0, streamId: "my-import-stream", allocatedTokenRanges: [], operations: [...],})Alongside the repo scope and operations, every stream chunk carries four
stream-control fields:
submissionId— the UUID you own for this logical submission. Reuse it to retry.chunkOrdinal— this chunk’s zero-based position in that submission.streamId— correlates the chunks of one import for diagnostics.allocatedTokenRanges— reserved; pass[], since a non-empty value is rejected.
submissionId and chunkOrdinal are the pair that derives the chunk’s
eventRequestId, which is the identity you look a receipt up by. Retrying is
not only about that pair, though: the rest of the request is bound into a
digest, so an otherwise-identical retry that changes streamId, the
operations, or the message conflicts instead of replaying. To retry a chunk,
resend it unchanged. submissionId is the
identity you reuse when retrying an ambiguous outcome, and chunkOrdinal
orders the chunks within it; see
Streaming Write Failures for that contract. The
remaining parameters, and the equivalent MCP arguments, are covered on
client.stream and
warmhub_thing_create.
HTTP-Oriented Clients
Section titled “HTTP-Oriented Clients”WarmHub does not mount a REST endpoint for writes. HTTP clients should use the SDK, CLI, or MCP surfaces; see the HTTP API note.
Where to Next
Section titled “Where to Next”| Need | Page |
|---|---|
| Every operation kind in full — fields, conditional writes, and no-op semantics | Operations |
| Read the version trail for a thing, with attribution per version | Write History |
| Type-safe SDK write calls and the shape of the result they return | Write Methods |
| Recover from a timeout, a retry, or a partial submission | Transient Retry |
| Validate a batch before committing it, or drive writes from the CLI | wh commit submit |
| Submit operations from an agent over MCP | warmhub_thing_create and the other write tools |
| Write from an HTTP-oriented client, and validate without committing | HTTP writes |
| Fire a webhook when an operation lands | Subscriptions |
| Understand the wref an operation names | Wrefs |
The Pipeline
Section titled “The Pipeline”When a chunk of operations is submitted, each one:
- Validates against shape and structural constraints (preflight)
- Resolves wref references
- Pins version references (bare wrefs → current HEAD version)
- Computes data hashes (server-side only — clients never compute hashes)
- Records a new thing version
A failed operation is recorded in the per-op results array. Successful operations earlier in the same chunk remain.
Operation Results
Section titled “Operation Results”On the commit-oriented surfaces, a write request returns per-operation results under a top-level aggregate:
{ "operationCount": 3, "partial": true, "statusCounts": { "applied": 2, "noop": 0, "error": 1 }, "operations": [ { "opIndex": 0, "name": "temp-1@v1", "operation": "add", "version": 1, "dataHash": "abc123", "status": "applied" }, { "opIndex": 1, "name": "bad", "operation": "add", "status": "error", "errors": [ { "code": "NOT_FOUND", "message": "Shape \"MissingShape\" not found", "retryable": false } ] }, { "opIndex": 2, "name": "temp-2@v1", "operation": "add", "version": 1, "dataHash": "def456", "status": "applied" } ], "receipts": [ { "eventRequestId": "…", "outcome": "event", "…": "…" } ]}Every operation row here carries status (applied, noop, or error).
Clean-success responses may omit the top-level partial and statusCounts
fields for backward compatibility.
The exact chunk receipts sit under receipts — one per chunk, in submission
order. A receipt carries eventRequestId, requestDigest, schemaVersion,
an event header (or null), an optional submissionId, and its own
operations array. Its top-level outcome is "event" when the chunk
produced write events, or "no_event" when it did not (for example, all
operations were no-ops). New writes return schemaVersion: "operation-event-receipt/v2", whose rows use the same applied / noop /
error status vocabulary as the aggregate above and always carry opIndex and
status. MCP callers get these receipt fields directly, without the aggregate
wrapper.
A retry that replays a receipt recorded before v2 returns it unchanged, with
schemaVersion: "operation-event-receipt/v1". Its rows keep the older shape:
opIndex and status are optional, and stream rows report success /
noop / failed with a failure carried as a singular error instead of
errors[]. Branch on schemaVersion before reading per-operation results.
The write surfaces do not all return the same shape, so check which one you called before reading a result:
| Surface | Returns |
|---|---|
client.commit.apply(), OperationBuilder.commit(), CLI --json | The aggregate above: root operationCount, optional partial and statusCounts, operations rows using the public commit statuses applied / noop / error, plus one exact receipt per chunk under receipts. More in Write Methods. |
client.stream.append() | Per-operation rows under results, alongside that chunk’s receipt fields. More on client.stream. |
MCP write tools (warmhub_thing_create and its siblings) | The chunk receipt fields at the top level (also nested under receipt), plus a results array with the same per-operation entries as operations. On a v2 receipt those entries use applied / noop / error; a replayed v1 receipt keeps its older row shape, so branch on schemaVersion. More in the tool reference. |
Idempotent revises (same data hash) return operation: "noop". With
skipExisting: true on an add, an existing target also returns
operation: "noop".
Structured Error Details
Section titled “Structured Error Details”A failed operation row carries an errors array. Each element may carry an
optional details object alongside code and message. The details payload is present when the failure carries
enough structured context to support programmatic recovery — for example,
optimistic-concurrency conflicts on a revise, or validation failures that
include per-field issue data.
The following example shows an optimistic-concurrency conflict. The message
field is human-readable; branch your recovery logic on the structured fields
under details:
{ "opIndex": 1, "name": "temp-1", "operation": "revise", "status": "error", "errors": [ { "code": "CONFLICT", "message": "Expected version 3 but current version is 5 for temp-1", "details": { "reason": "expected_version_mismatch", "expectedVersion": 3, "currentVersion": 5 }, "retryable": false } ]}When reason is expected_version_mismatch, expectedVersion is the version
your operation pinned against and currentVersion is the version the backend
observed when it rejected the operation. Another writer may have advanced the
thing since, so treat currentVersion as a lower bound: fetch the thing’s
current head, reconcile your changes, and resubmit.
Validation failures may also carry structured details under reason: "validation_failed" with an issues[] payload describing the per-field
problems. details is absent on failures that do not carry structured
recovery data — for example, NOT_FOUND.
For the full set of failure reasons — including others that carry recovery
context such as lease_held and rate_limit_reset — see the failure-contract
reference in Write Methods.
There is no commitId — graph history lives in per-thing version trails,
visible through wh thing history.
Subscriptions
Section titled “Subscriptions”Successful operations emit per-operation write events. Subscriptions match against those events to fire webhook calls. See Subscriptions.
Key Rules
Section titled “Key Rules”- Per-operation atomicity. Each operation succeeds or fails on its own.
- Data hashes are server-computed. Clients store hashes from results and compare against DB values.
- Convenience wrappers exist (
wh thing revise,wh assertion create,wh thing retract) but they all call the same write pipeline. - Auditing is per-thing. Use
wh thing history <wref>for the version trail and attribution for each version.