Skip to content

Write Submit Deep-Dive

wh commit submit is the primary write command (bare wh commit is equivalent). There are several ways to specify operations: inline JSON, file-based, streaming JSONL for large datasets, and shorthand flags, which accept one operation by default and up to 20 paired operations per call.

Operations stream one-at-a-time and apply with per-operation results: the response tells you what succeeded, what no-op’d, and what failed, and later failures do not roll back earlier successes. See Writes Overview.

Pass a JSON array of operations directly:

Terminal window
wh commit submit --ops '[
{"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"}}
]' -m "Add sensor with reading" --committer Agent/bot-1

This is the most flexible form — supports any number of operations with any combination of adds, revises, and retracts.

Load operations from a JSON file:

Terminal window
wh commit submit -f operations.json -m "Batch update"

Where operations.json contains a JSON array:

[
{ "operation": "add", "kind": "thing", "name": "Sensor/temp-1", "data": { "location": "Building A", "type": "temperature" } },
{ "operation": "add", "kind": "thing", "name": "Sensor/humidity-1", "data": { "location": "Building A", "type": "humidity" } }
]

This is useful for moderate-sized pre-generated operation sets. For larger datasets (thousands of operations), use the streaming JSONL format below.

3. Streaming JSONL with —file or —stream

Section titled “3. Streaming JSONL with —file or —stream”

For large datasets (hundreds to millions of operations), the streaming protocol sends operations in chunks rather than packing them into one payload. This avoids payload size limits and provides interactive progress feedback. Each chunk hits stream.append and applies its operations one at a time on the server.

Terminal window
wh commit submit --file dataset.jsonl -m "Bulk ingest" --progress

Where dataset.jsonl is a newline-delimited JSON file (one operation per line):

{"operation":"add","kind":"thing","name":"Sensor/temp-1","data":{"location":"Building A","type":"temperature"}}
{"operation":"add","kind":"thing","name":"Sensor/temp-2","data":{"location":"Building B","type":"temperature"}}
{"operation":"add","kind":"assertion","name":"Reading/temp-1-v1","about":"Sensor/temp-1","data":{"value":72.5}}

Pipe operations from any producer:

Terminal window
cat dataset.jsonl | wh commit submit --stream -m "Pipe ingest"
# or from a generator:
my-etl-tool --format jsonl | wh commit submit --stream -m "ETL ingest"

Operations are sent to the backend in chunks. The CLI chooses safe defaults for large streams, and you can override the per-request chunk size when you need smaller request bodies:

Terminal window
wh commit submit --file dataset.jsonl --chunk-size 100 -m "Smaller chunks"

All chunks share one streamId so $N / #N token allocations carry across chunk boundaries. The CLI returns once the last chunk has been applied — there is no separate finalize step.

If an append fails after earlier chunks were acknowledged, the CLI reports the acknowledged operation count. The failed append may also have landed, so inspect repository state before submitting any remaining JSONL operations.

Add --progress to see a live progress bar on stderr (TTY only):

Terminal window
wh commit submit --file dataset.jsonl --progress -m "10k sensors"
# Appending [####------] 4000/10000 (40%) 8 chunks 2.3s

For --stream input (where total count is unknown), progress shows running totals without a percentage.

Progress output is stderr-only and suppressed when stderr is not a TTY, so --json stdout remains machine-readable.

$N allocates an opaque identifier (replaced by a unique server-generated suffix), and #N references the value allocated by the matching $N. These tokens work across chunk boundaries — a $1 allocated in chunk 1 can be referenced with #1 in chunk 2+:

{"operation":"add","kind":"thing","name":"Player/player-$1","data":{"score":0}}
{"operation":"add","kind":"assertion","name":"Score/score-$2","about":"Player/player-#1","data":{"value":0}}

Pass --debug to see a detailed timing breakdown (per-chunk append, server resolve-repo, token resolve, apply) and throughput metrics on stderr.

For shorthand commits, use named flags instead of writing JSON. Add and retract shorthands are repeatable; mixed operation batches should use --ops or --file.

Terminal window
wh commit submit --add temp-1 --shape Sensor --data '{"location":"Building A","type":"temperature"}' -m "Add sensor"

This produces: { operation: "add", kind: "thing", name: "Sensor/temp-1", data: {...} }

The --shape flag is prefixed to the --add name to form the wref, WarmHub’s typed reference format (Shape/name).

When --about is provided, the kind auto-infers to assertion:

Terminal window
wh commit submit --add temp-1-v1 --shape Reading --about Sensor/temp-1 --data '{"value":72.5}'

This produces: { operation: "add", kind: "assertion", name: "Reading/temp-1-v1", about: "Sensor/temp-1", data: {...} }

Terminal window
wh commit submit --revise Sensor/temp-1 --data '{"location":"Building B","type":"temperature"}' -m "Relocate sensor"

This produces: { operation: "revise", kind: "thing", name: "Sensor/temp-1", data: {...} }

Terminal window
wh commit submit --retract Sensor/temp-1 --reason "duplicate" -m "Retract duplicate sensor"

This produces: { operation: "retract", kind: "thing", name: "Sensor/temp-1", reason: "duplicate" }

--kind is optional for thing retractions and can be used as a safety check or to retract non-thing identities such as assertions, shapes, and collections. --reason is repeatable and can be provided once for all retractions or once per --retract.

To write under a read lease acquired with wh thing lease, add --lease-id <id> to the --revise or --retract short-form:

  • The lease auto-releases on a successful or no-op write.
  • If the lease has already expired, the write proceeds as an ordinary write; but if an active lease is still in force and your --lease-id doesn’t match it, the write is rejected with LEASE_UNAVAILABLE.
  • --lease-id requires a --revise/--retract short-form. For --ops/--file/--stream payloads, carry leaseId inline on the operation object instead.
Terminal window
wh commit submit --add Location --kind shape --data '{"fields":{"x":"number","y":"number"}}'

Shape data supports an optional top-level description, typed field objects with descriptions, and field constraints:

Terminal window
# Typed field with description
wh commit submit --add Location --kind shape --data '{"description":"A point in 2D space","fields":{"x":{"type":"number","description":"Horizontal position"},"y":"number"}}'
# Field constraints (string enum, number range, wref constrained to a shape, array bounds)
wh commit submit --add GameState --kind shape \
--data '{
"fields": {
"status": { "type": "string", "enum": ["active", "paused", "ended"] },
"score": { "type": "number", "minimum": 0, "integer": true },
"owner": { "type": "wref", "shape": "Player" },
"tags": { "type": "array", "items": "string", "maxItems": 5 }
}
}'

Override the auto-inferred kind with --kind. For example, to explicitly mark an operation as a thing:

Terminal window
wh commit submit --add my-item --kind thing --shape Player --data '{"name":"Alice"}'

Repeat --add to pack up to 20 operations into a single write request. Each --add pairs with its own --data by position:

Terminal window
wh commit submit \
--add alice --data '{"score":1}' \
--add bob --data '{"score":2}' \
--shape Player \
-m "seed players"

Rules:

  • --data must appear exactly once per --add, in the same order. A mismatched count errors rather than silently dropping ops.
  • --shape, --about, and --kind accept 0, 1 (broadcast to every op), or N values (paired by position).
  • The short-form path is capped at 20 ops. Beyond that, use --ops '<json>' or -f operations.json to keep the bulk path explicit.
FlagShortDescription
--opsOperations JSON array (full control)
--file-fPath to operations file (.json array or .jsonl newline-delimited)
--streamRead newline-delimited operations from stdin
--progressShow interactive progress on TTY stderr (requires --stream or .jsonl --file)
--chunk-sizeOps per append chunk for --stream or .jsonl --file (default: 1000, max: 10000)
--timing-outWrite per-append timing details to a local JSON sidecar for debugging or benchmarks
--stream-idUse a caller-managed stream id for JSONL token continuity
--allocated-tokensContinue $N / #N token state as JSON, for example '[{"tokenNumber":1}]'
--operation-offsetOriginal zero-based operation index for the first JSONL operation in this input
--skip-existingFor add operations, return noop when the target shape, thing, assertion, or collection already exists instead of failing
--addName for add operation (shorthand). Repeatable; pair each with --data.
--reviseName for revise operation (shorthand)
--retractName for retract operation (shorthand). Repeatable.
--reasonOptional retraction reason for --retract operations. Repeatable; one per --retract or one value broadcast to all.
--expected-versionApply the --revise only if its target is still at this version (optimistic concurrency). Requires --revise.
--lease-idRead-lease token from wh thing lease, bound to a single --revise/--retract (auto-released on a successful or no-op write). Broadcast to every op of a multi-target --retract. Requires --revise or --retract; for --ops/--file/--stream writes, carry leaseId inline on the operation instead.
--kindKind override: thing, assertion, shape, collection. Repeatable; 1 (broadcast) or N (paired).
--shapeShape name (prefixed to --add name). Repeatable; 1 (broadcast) or N (paired).
--dataData payload as JSON string. Repeatable; must match --add count exactly.
--aboutTarget wref for assertions. Repeatable; 1 (broadcast) or N (paired).
--typeCollection type shorthand: pair, triple, set, list
--membersComma-separated member wrefs for collection shorthand
--message-mOptional message recorded with each thing-version produced by this call. When omitted for --ops, --file (JSON array), and shorthand flag paths, a message is synthesized automatically from the operations (see Default message synthesis). For --stream and .jsonl --file paths, the message is not synthesized.
--committerOptional wref identifying the actor on whose behalf the writes are made (e.g. Agent/bot-1). When omitted, attribution falls back to the authenticated user via createdByEmail.

The shorthand flags are resolved as follows:

  1. --add X --shape Y → name becomes Y/X, kind defaults to thing
  2. --add X --about Z → kind auto-infers to assertion
  3. --add X --shape Y --about Z → name becomes Y/X, kind is assertion
  4. --revise X → kind defaults to thing
  5. --retract X → kind defaults to thing and does not require --data
  6. --kind overrides the auto-inferred kind in all cases

If none of --add, --revise, --retract, --type, --ops, or --file are provided, the command prints a usage error.

When -m / --message is omitted and the input path is --ops, a JSON-array --file, or shorthand flags, the CLI synthesizes a commit message from the resolved operations:

OperationsSynthesized message
Single add (thing, assertion, shape)add <name>
Single add (collection)add <type> <members>
Single reviserevise <name>
Single retract (non-shape)retract <name>
Single retract --kind shaperetract shape <name>
Two or more operationsbatch: N operations

An explicit -m value always takes precedence over synthesis.

This synthesis does not apply to --stream or .jsonl --file paths — for those, omitting -m leaves the commit message unset.

Pretty output shows the message, operation count, and per-operation details:

Add sensor with reading (2 ops)
+ Sensor/temp-1@v1
+ Reading/temp-1-v1@v1

Markers: + adds, ~ revises, - retracts or non-mutating results such as noop from --skip-existing, and ! per-operation failures. The header includes , <n> failed when any operation in the batch failed:

Seed players (2 ops, 1 failed)
+ Player/alice@v1
! Player/bob caller is not a member

When an operation’s data carries top-level fields not declared in the target shape, the CLI prints a non-blocking warning line under the op (see Shapes — Undeclared Fields):

seed finding (1 ops)
+ DocFinding/issue-001@v1
⚠ 3 fields not declared in shape DocFinding: status, filePath, category

The warning is informational; it does not turn the operation into a failure. If the field count exceeds the server-side cap, the line carries a (+N more) tail with the count of additional undeclared fields.

With --json, returns the structured per-operation result list. Each operation includes an opIndex (its zero-based position in the submitted operations array) and a status field ('applied', 'noop', or 'error'); on status === 'error', an error object with code and message accompanies the row. Mixed-result responses include partial: true and statusCounts. When validation found undeclared fields, a warnings object lists them:

{
"committer": "Agent/bot-1",
"message": "Seed players",
"operationCount": 2,
"partial": true,
"statusCounts": { "applied": 1, "noop": 0, "error": 1 },
"operations": [
{
"opIndex": 0,
"name": "Player/alice",
"operation": "add",
"status": "applied",
"version": 1,
"dataHash": "abc123",
"warnings": { "undeclaredFields": ["b", "d", "u"] }
},
{
"opIndex": 1,
"name": "Player/bob",
"operation": "add",
"status": "error",
"error": {
"code": "FORBIDDEN",
"message": "caller is not a member"
}
}
]
}

Each operation result includes the resolved name, operation type, opIndex, status, and (on success) version number and server-computed data hash. opIndex is the zero-based position of the source operation in the submitted array and is the correlation key callers use to map results back for retry or recovery. committer appears only when the caller passed --committer. warnings is omitted entirely when no undeclared fields were found, so callers can branch on its presence. Failed entries never carry warnings — when an op fails, the error is the relevant signal.

Exit codes. Mixed-result commits exit 0 — at least some operations landed, and the per-op status/error fields communicate the partial result. The CLI exits non-zero only when every operation failed; in that case stdout stays empty (no JSON success payload) and stderr carries the error message. The exit code is picked deterministically from the worst per-op failure: any auth-class failure (FORBIDDEN, UNAUTHENTICATED) exits 5 (auth), otherwise any user-input failure (VALIDATION_ERROR, SHAPE_MISMATCH, CONFLICT, etc.) exits 2 (user input), otherwise 4 (backend). Order of operations in the request does not affect the exit code.

There is no commitId field in the result. Per-thing version trails are the audit source — use wh thing history <wref>.