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_commit_submit) apply
operations one at a time. 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 Agent/bot-1
# Multiple operations — repeat --add paired with --data (≤20 ops)wh commit submit \ --add Sensor/temp-1 --data '{"location":"A"}' \ --add Sensor/temp-2 --data '{"location":"B"}' \ -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, --kind, --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 shape (Agent) or shaped thing (Agent/bot-1) that already
exists, including canonical cross-repo forms such as
wh:other-org/other-repo/Agent and
wh:other-org/other-repo/Agent/bot-1. A label such as eval-runner is parsed
as a shape wref, so it succeeds only when a shape 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_commit_submit", "arguments": { "submissionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "committer": "Agent/claude", "message": "Add sensor with reading", "operations": [ { "operation": "add", "kind": "thing", "name": "Sensor/temp-1", "data": { "location": "Building A", "type": "temperature" } }, { "operation": "add", "kind": "assertion", "name": "Reading/temp-1-v1", "about": "Sensor/temp-1", "data": { "value": 72.5, "unit": "fahrenheit" } } ] }}Via SDK
Section titled “Via SDK”await client.commit.apply(orgName, repoName, "Add sensor", [ { operation: "add", kind: "thing", name: "Sensor/temp-1", data: { location: "Building A" } },])
const preview = await client.commit.validate(orgName, repoName, [ { operation: "add", kind: "thing", name: "Sensor/temp-1", 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_commit_submit.
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.
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": "Sensor/temp-1@v1", "operation": "add", "version": 1, "dataHash": "abc123", "status": "applied" }, { "opIndex": 1, "name": "MissingShape/bad", "operation": "add", "status": "error", "error": { "code": "NOT_FOUND", "message": "Shape not found" } }, { "opIndex": 2, "name": "Sensor/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). Receipt rows use the backend statuses success,
noop, and failed rather than the normalized applied/error spelling
above, and carry status only on failures — so test status === "failed"
rather than expecting a value on every row. MCP callers get these receipt
fields directly, without the aggregate wrapper.
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 warmhub_commit_submit | The chunk receipt fields at the top level, with rows using the backend statuses success / noop / failed. More in the tool reference. |
The practical consequence: the status vocabulary depends on the surface. The
commit-oriented surfaces normalize backend success/failed into
applied/error, while a receipt keeps the backend spelling — so code written
against one surface’s status values will not read the other’s correctly.
MCP may omit opIndex on rows from an ordinary clean-success
response where result indexes match local array positions; resumed submissions
and partial responses always include opIndex so callers can correlate every
result with its submitted operation. 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”Failed operation rows may carry an optional error.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": "Sensor/temp-1", "operation": "revise", "status": "error", "error": { "code": "CONFLICT", "message": "Expected version 3 but current version is 5 for Sensor/temp-1", "details": { "reason": "expected_version_mismatch", "expectedVersion": 3, "currentVersion": 5 } }}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. error.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.