Skip to content

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.

OperationEffect
addCreate a thing, shape, assertion, or collection at version 1
reviseAppend a new version with changed data
retractWithdraw 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.

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.

Terminal window
# Single operation
wh 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 file
wh 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 stdin
producer | wh commit submit --stream --stream-id pipe-ops --skip-existing -m "Pipe ops"
# Rerun an add-only partial seed — --skip-existing makes adds idempotent
wh 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.

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.

{
"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" } }
]
}
}
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 workloads
await 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.

WarmHub does not mount a REST endpoint for writes. HTTP clients should use the SDK, CLI, or MCP surfaces; see the HTTP API note.

NeedPage
Every operation kind in full — fields, conditional writes, and no-op semanticsOperations
Read the version trail for a thing, with attribution per versionWrite History
Type-safe SDK write calls and the shape of the result they returnWrite Methods
Recover from a timeout, a retry, or a partial submissionTransient Retry
Validate a batch before committing it, or drive writes from the CLIwh commit submit
Submit operations from an agent over MCPwarmhub_thing_create and the other write tools
Write from an HTTP-oriented client, and validate without committingHTTP writes
Fire a webhook when an operation landsSubscriptions
Understand the wref an operation namesWrefs

When a chunk of operations is submitted, each one:

  1. Validates against shape and structural constraints (preflight)
  2. Resolves wref references
  3. Pins version references (bare wrefs → current HEAD version)
  4. Computes data hashes (server-side only — clients never compute hashes)
  5. Records a new thing version

A failed operation is recorded in the per-op results array. Successful operations earlier in the same chunk remain.

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:

SurfaceReturns
client.commit.apply(), OperationBuilder.commit(), CLI --jsonThe 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".

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.

Successful operations emit per-operation write events. Subscriptions match against those events to fire webhook calls. See Subscriptions.

  • 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.