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

Terminal window
# Single operation
wh 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 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, --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.

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_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" } }
]
}
}
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 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_commit_submit.

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

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": "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:

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 warmhub_commit_submitThe 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".

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.

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.