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.
Operation sources are mutually exclusive
Section titled “Operation sources are mutually exclusive”wh commit submit requires exactly one operation source per call. The available sources are:
--ops— inline JSON array--file— JSON array or JSONL file--stream— newline-delimited operations from stdin--add— shorthand add--revise— shorthand revise--retract— shorthand retract--type— collection shorthand
You cannot combine sources in a single call (for example, --ops alongside --add, or --file alongside --stream). The command rejects any invocation that mixes sources.
Certain companion flags are also bound to a specific source and are rejected on any other path:
--datais valid with--addand--revise.--shapeand--aboutare only valid with--add.--reasonis only valid with--retract.--nameand--membersare only valid with--type.
If none of --add, --revise, --retract, --type, --ops, --file, or --stream are provided, the command prints a usage error.
1. Inline JSON with —ops
Section titled “1. Inline JSON with —ops”Pass a JSON array of operations directly:
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-1This is the most flexible form — supports any number of operations with any combination of adds, revises, and retracts.
2. From File with —file / -f
Section titled “2. From File with —file / -f”Load operations from a JSON file:
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.
Both JSONL paths — --file <name>.jsonl and --stream — require --stream-id and --skip-existing. The stream id is a stable, caller-chosen name that lets an interrupted run resume by re-submitting from the start; --skip-existing makes those replayed add operations idempotent (an already-applied add returns noop instead of failing).
From a JSONL file
Section titled “From a JSONL file”wh commit submit --file dataset.jsonl --stream-id bulk-ingest --skip-existing -m "Bulk ingest" --progressWhere 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}}From stdin
Section titled “From stdin”Pipe operations from any producer:
cat dataset.jsonl | wh commit submit --stream --stream-id pipe-ingest --skip-existing -m "Pipe ingest"# or from a generator:my-etl-tool --format jsonl | wh commit submit --stream --stream-id etl-ingest --skip-existing -m "ETL ingest"Chunking
Section titled “Chunking”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:
wh commit submit --file dataset.jsonl --stream-id smaller-chunks --skip-existing --chunk-size 100 -m "Smaller chunks"All chunks share one streamId for observability and partial-submission recovery. 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. When the
backend supplies a machine-readable discriminator for the failure, it is
available as errorCode on the error — in pretty output it appears as
(backend: <code>) appended to the error message, and in --json output the
error object includes an errorCode field (some responses also include
backendCode as a compatibility alias for errorCode). Use errorCode to
distinguish ambiguous append failures programmatically without parsing the
message string.
Interactive progress
Section titled “Interactive progress”Add --progress to see a live progress bar on stderr (TTY only):
wh commit submit --file dataset.jsonl --stream-id 10k-sensors --skip-existing --progress -m "10k sensors"# Appending [####------] 4000/10000 (40%) 8 chunks 2.3sFor --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.
Explicit Names Across Chunks
Section titled “Explicit Names Across Chunks”$N/#N batch tokens are retired. When later chunks need to reference earlier writes, choose explicit deterministic names before submitting:
{"operation":"add","kind":"thing","name":"Player/player-001","data":{"score":0}}{"operation":"add","kind":"assertion","name":"Score/player-001","about":"Player/player-001","data":{"value":0}}Debug timing
Section titled “Debug timing”Pass --debug to see a detailed timing breakdown (per-chunk append, server resolve-repo, apply) and throughput metrics on stderr.
4. Shorthand Flags
Section titled “4. Shorthand Flags”For shorthand commits, use named flags instead of writing JSON. Add and retract shorthands are repeatable; mixed operation batches should use --ops or --file.
Add a thing
Section titled “Add a thing”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).
Add an assertion
Section titled “Add an assertion”When --about is provided, the kind auto-infers to assertion. --about accepts a wref pointing to the target thing, collection, or shape:
# Thing targetwh commit submit --add temp-1-v1 --shape Reading --about Sensor/temp-1 --data '{"value":72.5}'
# Shape targetwh commit submit --add location-note --shape Note --about Location --data '{"text":"2D coordinate space"}'To assert about a collection, first create the collection, then pass its collection wref to --about:
# Create the collection firstwh commit submit --add edge-1 --kind collection --type arc --members Node/a,Node/b -m "Create edge collection"
# Then assert about it by wrefwh commit submit --add link-1 --shape Link --about Arc/edge-1 --data '{"weight":1.0}'A plain wref produces: { operation: "add", kind: "assertion", name: "Reading/temp-1-v1", about: "Sensor/temp-1", data: {...} }
Revise a thing
Section titled “Revise a thing”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: {...} }
Retract a thing
Section titled “Retract a thing”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-iddoesn’t match it, the write is rejected withLEASE_UNAVAILABLE. --lease-idrequires a--reviseor a single--retracttarget. Multi-target--retractbatches are rejected when--lease-idis present. For--ops/--file/--streampayloads, carryleaseIdinline on the operation object instead.
Add a shape
Section titled “Add a shape”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:
# Typed field with descriptionwh 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 } } }'Specify kind explicitly
Section titled “Specify kind explicitly”Override the auto-inferred kind with --kind. For example, to explicitly mark an operation as a thing:
wh commit submit --add my-item --kind thing --shape Player --data '{"name":"Alice"}'Add multiple things in one write request
Section titled “Add multiple things in one write request”Repeat --add to pack up to 20 operations into a single write request. Each --add pairs with its own --data by position:
wh commit submit \ --add alice --data '{"score":1}' \ --add bob --data '{"score":2}' \ --shape Player \ -m "seed players"Rules:
--datamust appear exactly once per--add, in the same order. A mismatched count errors rather than silently dropping ops.--shape,--about, and--kindaccept 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.jsonto keep the bulk path explicit.
Flag Reference
Section titled “Flag Reference”| Flag | Short | Description |
|---|---|---|
--ops | Operations JSON array (full control) | |
--file | -f | Path to operations file (.json array or .jsonl newline-delimited) |
--stream | Read newline-delimited operations from stdin | |
--progress | Show interactive progress on TTY stderr (requires --stream or .jsonl --file) | |
--chunk-size | Ops per append chunk for --stream or .jsonl --file (default: 1000, max: 10000) | |
--allow-nul-bytes | Skip the client-side pre-flight check that rejects literal U+0000 NUL bytes in operation data. By default the CLI rejects NUL bytes before submitting; pass this flag to allow them through to the backend. | |
--timing-out | Write per-append timing details to a local JSON sidecar for debugging or benchmarks. Requires a .jsonl --file source (combined with --stream-id and --skip-existing). Rejected on --ops, JSON-array --file, and --stream paths. | |
--stream-id | Caller-chosen stream id for JSONL token continuity and resumable reruns. Required for --stream or .jsonl --file. | |
--skip-existing | For add operations, return noop when the target shape, thing, assertion, or collection already exists instead of failing. Required for --stream or .jsonl --file. | |
--return-repo-seq | When present, the CLI requests a repository-sequence acknowledgement from the backend. On success, pretty output includes a repo seq <n> line and --json output includes a top-level repoSeq field (emitted only when the write actually recorded a new write result, as opposed to a noop or an all-failed request). | |
--add | Name for add operation (shorthand). Repeatable; pair each with --data. | |
--revise | Name for revise operation (shorthand) | |
--retract | Name for retract operation (shorthand). Repeatable. | |
--reason | Optional retraction reason for --retract operations. Repeatable; one per --retract or one value broadcast to all. | |
--expected-version | Apply the --revise only if its target is still at this version (optimistic concurrency). Requires --revise. | |
--lease-id | Read-lease token from wh thing lease, bound to --revise or one --retract target (auto-released on a successful or no-op write). Multi-target --retract batches are rejected when this flag is present. Requires --revise or --retract; for --ops/--file/--stream writes, carry leaseId inline on the operation instead. | |
--kind | Kind override: thing, assertion, shape, collection. Repeatable; 1 (broadcast) or N (paired). | |
--shape | Shape name (prefixed to --add name). Repeatable; 1 (broadcast) or N (paired). | |
--data | Data payload as JSON string. Repeatable; must match --add count exactly when used with --add. Also valid with --revise. | |
--about | Target wref for assertions. Accepts a wref pointing to a thing, collection, or shape. To assert about a collection, create the collection first and pass its collection wref here. Repeatable; 1 (broadcast) or N (paired). | |
--type | Promoted collection type shorthand for new models: arc, bond, set, list | |
--members | Comma-separated member wrefs for collection shorthand | |
--message | -m | Optional 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. |
--committer | Optional untyped wref identifying the actor on whose behalf the writes are made. Shapes (Agent) and shaped things (Agent/bot-1) are accepted. See Committer attribution for resolution order and restrictions. |
--skip-existing and --expected-version are the CLI form of WarmHub’s conditional writes. See Conditional Operations for the full model across the CLI, SDK, and result statuses.
NUL-byte pre-flight check
Section titled “NUL-byte pre-flight check”Before submitting any write, the CLI scans operation data for literal U+0000 NUL bytes. If any are found the submit is rejected locally with an error — the request never reaches the backend. This catches payloads that would be silently truncated or rejected server-side.
If your data intentionally contains NUL bytes, pass --allow-nul-bytes to skip the pre-flight check:
wh commit submit --ops '[...]' --allow-nul-bytes -m "payload with nul bytes"Note that the backend may still reject such payloads depending on the field type and shape constraints.
Shorthand Resolution Rules
Section titled “Shorthand Resolution Rules”The shorthand flags are resolved as follows:
--add X --shape Y→ name becomesY/X, kind defaults tothing--add X --about Z→ kind auto-infers toassertion;Zmust be a wref pointing to the target thing, collection, or shape--add X --shape Y --about Z→ name becomesY/X, kind isassertion--revise X→ kind defaults tothing--retract X→ kind defaults tothingand does not require--data--kindoverrides the auto-inferred kind in all cases
If none of --add, --revise, --retract, --type, --ops, --file, or --stream are provided, the command prints a usage error.
Default Message Synthesis
Section titled “Default Message Synthesis”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:
| Operations | Synthesized message |
|---|---|
Single add (thing, assertion, shape) | add <name> |
Single add (collection) | add <type> <members> |
Single revise | revise <name> |
Single retract (non-shape) | retract <name> |
Single retract --kind shape | retract shape <name> |
| Two or more operations | batch: 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.
Committer attribution
Section titled “Committer attribution”The backend resolves committer attribution in this order:
- Explicit
--committer— the wref you pass via--committer. This takes precedence over everything else. - Token-bound committer identity — if you omit
--committerand the token was created withwh token create --committer-identity <wref>, that wref is stamped as the committer on every write made with that token. No per-call flag is needed. This is the typical path for service accounts and automated pipelines: bake the acting identity into the token once, and every subsequent call is attributed automatically. - Signed-in account identity — if neither of the above applies, your own signed-in account’s
warmhub/usersIdentity is stamped.
In other words: an explicit --committer always wins; a token-bound identity applies only when --committer is omitted; and your own account identity is the final fallback.
Restriction on explicit committers. When you do pass --committer, the wref must not resolve to another user’s warmhub/users Identity. Attempting to claim a different user’s public Identity as the committer fails with IDENTITY_USE_DENIED. Your own public Identity and agent things (e.g. Agent/bot-1) continue to work normally.
Output
Section titled “Output”Pretty output shows the message, operation count, and per-operation details. When --committer is passed and the write succeeds, an indented committer: line appears before the op markers:
Add sensor with reading (2 ops) committer: Agent/bot-1 + Sensor/temp-1@v1 + Reading/temp-1-v1@v1When --return-repo-seq is passed and the write recorded a new write result, a repo seq <n> line appears in the output after the header and before the committer echo and per-operation markers:
Add sensor with reading (2 ops) repo seq 42 committer: Agent/bot-1 + Sensor/temp-1@v1 + Reading/temp-1-v1@v1Markers: + 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 memberWhen 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, categoryThe 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.
The same op-marker view prints a ⚠ coalesced wref … line when an optional wref coalesced to null because resolution returned thing_absent — also non-blocking. Missing shapes and other failures do not coalesce.
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 an operation produced warnings — undeclared fields, coalesced optional wrefs, or both — a warnings object rides on its result. When --return-repo-seq is passed and the write recorded a new write result, a top-level repoSeq field is included:
{ "committer": "Agent/bot-1", "message": "Seed players", "operationCount": 2, "repoSeq": 42, "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" } } ]}repoSeq is omitted when --return-repo-seq was not passed or when the write did not record a new write result.
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 an operation produced no warnings, so callers can branch on its presence; when present it can carry undeclaredFields, coalescedWrefs, or both. 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. For completed commit responses, 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. Auth denials exit 5 whether the denial occurs before any operation runs or after per-operation processing starts, so script authors can treat exit 5 uniformly as an auth failure. Note that signal cancellation (e.g. SIGINT or SIGTERM) can also produce a non-zero exit independent of operation results: SIGINT exits 130 and SIGTERM exits 143.
There is no commitId field in the result. Per-thing version trails are the audit source — use wh thing history <wref>.