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.
Each append runs one transaction over its ordered operations and returns per-operation results: the response tells you what succeeded, what no-op’d, and what failed. Ordinary operation failures do not erase successful sibling results. 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.--affirmis only valid with a single--addor with--reviseon assertion writes. It is rejected on--ops,--file, and--streampaths, and also rejected when more than one--addis present.
If none of --add, --revise, --retract, --type, --ops, --file, or --stream are provided, the command prints a usage error.
--data accepts only a JSON object, so it cannot write a null body: --data null fails with --data must be a JSON object, got null. Storing a thing or assertion with data: null is valid, but you have to write it as a full operation payload via --ops, -f/--file, or a .jsonl stream. See writing a null body from the CLI.
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 atomic chunks rather than packing them into one payload. This avoids payload size limits and provides interactive progress feedback. Each accepted chunk returns one exact operation-event receipt.
Both JSONL paths — --file <name>.jsonl and --stream — require --stream-id and --skip-existing. The CLI mints one submission UUID, prints it before sending, and prints each derived event-request UUID before its chunk. Use --submission-id <uuid> when an external workflow must choose and record the identity itself. streamId groups chunks for observability; it is not the recovery or idempotency identity.
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 submissionId; each zero-based chunk ordinal derives a stable eventRequestId. streamId remains observation-only.
If an append response is ambiguous, the CLI prints the exact recovery command:
wh commit receipt <event-request-id> --repo <org/repo>A returned receipt is the outcome. Opaque not-found means no visible receipt exists; only then retry the identical request identity. Earlier acknowledged chunks remain available as their ordered exact receipts. 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). Machine readers use
error.recovery.attemptedAppendOutcome, not message parsing, for the recovery
branch.
Interactive progress
Section titled “Interactive progress”Add --progress to follow a long append as it runs. On a TTY, progress renders as a live bar on stderr:
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 in both modes, so --json stdout stays machine-readable.
When stderr is not a TTY, --progress emits newline-delimited JSON progress
events instead of the bar: one stream.progress object per appended chunk,
then a single terminal stream.summary. Both carry sourceKind and elapsed
timing; stream.summary reports status as success or error. This is what
a wrapper, CI job, or agent process sees.
{"type":"stream.progress","chunksAppended":4,"elapsedMs":2300,"opsAppended":4000,"sourceKind":"file","totalOps":10000}{"type":"stream.summary","chunkCount":10,"elapsedMs":5400,"opCount":10000,"sourceKind":"file","status":"success","appendThroughput":2100,"appendMs":4700,"totalOps":10000}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 --type arc --name edge-1 --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.
Add --expected-version <n> to retract only if the target is still at that version. Because one fence cannot describe several targets, the shorthand accepts --expected-version with exactly one --retract; use inline expectedVersion fields in --ops/--file/--stream batches when each retract needs its own fence.
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. --affirmrequires exactly one--add. It cannot be used with multi---addshorthand.
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 | Report append progress on stderr: a live bar on a TTY, newline-delimited JSON events otherwise (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 client-observed append and request timing details to a local JSON sidecar for debugging or benchmarks. Server-phase timing is unavailable from exact receipts and is omitted rather than reported as zero. 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 observability and partial-submission diagnostics. Required for --stream or .jsonl --file. | |
--submission-id | Caller-supplied UUID that becomes the submission identity for the entire call. Use this when an external workflow must choose and record the submission UUID itself rather than letting the CLI mint one. Accepted on all real submit paths; accepted but unused (with a note on stderr) during --dry-run. | |
--skip-existing | For fixed-name add operations, return noop when the target already exists. This makes add-only full-input reruns safe, but is not evidence about an ambiguous append. Required for --stream or .jsonl --file. | |
--return-repo-seq | Request repository-sequence acknowledgement from the backend. Each exact event receipt carries the sequence as decimal text at event.repoSeq. The JSON result also carries a top-level repoSeq number, but only when the submission produced at least one event — a submission whose operations were all no-ops has no sequence to acknowledge and omits the field entirely. | |
--include-would-be-body | Include projected result bodies during --dry-run when disclosure rules permit. Submitted bodies may be echoed to an authorized writer; bodies derived from stored data require read authority. | |
--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 or single --retract only if its target is still at this version (optimistic concurrency). Requires --revise or exactly one --retract; for --ops/--file/--stream, carry expectedVersion inline on each operation. | |
--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. Use with --name and --members instead of --add. | |
--name | Explicit name for the collection created by --type. Only valid with --type. | |
--members | Comma-separated member wrefs for collection shorthand. Only valid with --type. | |
--affirm | Records which exact target version(s) an assertion is affirmed for. Takes a version-pinned wref in Shape/name@vN format — unpinned targets are rejected with targets must be pinned ("Shape/name@vN"). Only valid with a single --add or with --revise on assertion writes (i.e. shorthand paths where --about is set or the kind resolves to assertion). Rejected on --ops, --file, and --stream paths, and also rejected when more than one --add is present. Example: --add temp-1-v1 --shape Reading --about Sensor/temp-1 --affirm Sensor/temp-1@v1 --data '{"value":72.5}' | |
--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”Before the network request, the CLI prints the caller-known submission UUID and every derived event-request UUID to stderr. Pretty stdout then shows each exact receipt and its per-operation details:
event request 1a6955bd-154e-5870-becc-7b0b30b24d36submission 2f7047f7-642a-453f-a2bd-2a63450a365eoutcome eventrepo seq 42 digest sha256:... + Sensor/temp-1@v1 + Reading/temp-1-v1@v1When --committer is supplied, pretty output echoes that caller-known wref on a committer: line above the operation markers. The input is not added to the exact receipt or JSON output; version reads expose the backend-resolved committerWref.
Markers: + adds, ~ revises, - retracts or non-mutating results such as noop from --skip-existing, and ! per-operation failures:
+ 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.
Validate without committing
Section titled “Validate without committing”Add --dry-run to evaluate the complete submission with the same server-side
evaluator used by a real write. This is an executable preview, not the generic
CLI dispatch envelope and not a client-side approximation:
wh commit submit --file operations.json --dry-runwh commit submit --file operations.jsonl --dry-run --format jsonlproducer | wh commit submit --stream --dry-run --format jsonlValidation makes no durable repository change, creates no receipt or submission
identity, consumes no write-rate allowance, and does not dispatch asynchronous
actions. It still requires write authority. WarmHub evaluates one complete request
of at most 10,000 operations and 4 MiB encoded; it does not chunk the preview.
JSONL files and stdin therefore do not require --stream-id or
--skip-existing. When copied from a real submit, --chunk-size, --stream-id,
--submission-id, and --return-repo-seq are accepted but unused, with one
note on stderr.
Semantic submit flags keep their ordinary meaning. In particular,
--skip-existing affects adds, and --include-would-be-body opts into projected
result bodies where disclosure rules permit. WarmHub may echo a body supplied in
the request to an authorized writer; bodies derived from stored data require read
authority. The preview’s baseline is a repository sequence only for an
unrestricted repo reader; otherwise it is { "kind": "withheld" }.
Pretty output reports the baseline, caveats, every ordered input result, nested
lowered effects, and a summary. --json emits the raw preview result object.
JSONL emits one type: "operation" row per input in opIndex order,
then exactly one type: "summary" row carrying the same summary fields. A
completed preview always emits the whole report before exiting: 0 when every
operation would apply or no-op (canCommit: true in the JSON); otherwise embedded authorization errors select exit 5, caller-correctable
validation/conflict errors select 2, and other backend errors select 4. A
request-level failure emits no operation or summary rows. Cancellation remains
exit 130.
The result is a snapshot verdict, not a reservation. A concurrent write or a write-time admission check can still prevent a subsequent real write, and asynchronous action effects are not projected. Unbounded and chunked commit evaluation is tracked separately in issue #8874.
With --json, stdout is the raw preview result object. Its top-level
fields are: operationCount, canCommit, counts, baseline, caveats,
operations[], and optionally committer and message. There are no
receipts, submissionId, eventRequestId, or applied fields — those belong
to real write output only. With --format jsonl, stdout carries one
{ "type": "operation", ... } row per input in opIndex order, then exactly
one { "type": "summary", ... } row whose fields match the top-level summary
fields of the preview result object; neither row type carries receipt fields.
{ "operationCount": 1, "canCommit": true, "counts": { "wouldApply": 1, "noop": 0, "error": 0 }, "baseline": { "kind": "repo_seq", "repoSeq": 41 }, "caveats": [], "operations": [ { "opIndex": 0, "name": "Player/alice", "operation": "add", "status": "would_apply" } ]}A noop entry means the operation would produce no change (for example,
--skip-existing matched an existing thing). An error entry carries an
errors array, where each element describes why the operation would be
rejected. On a partial preview, canCommit is false and counts reflects
the per-status breakdown at the root.
Each operation result includes the operation type, opIndex, and status.
When details are disclosable, name is rewritten to the resolved target value.
When that resolved value differs from the caller’s submitted identifier,
both submittedName (the original input) and resolvedName (the resolved
target) are added to the entry alongside name. warnings is omitted entirely
when an operation produced none; when present it can carry undeclaredFields,
coalescedWrefs, or
both. A failed entry can carry warnings too, so treat the two as independent:
read errors for why it failed and warnings for anything else worth surfacing.
Exit codes. A completed preview whose operations all have would_apply or
noop exits 0. Auth-class failures exit 5, caller-correctable failures
(VALIDATION_ERROR, SHAPE_MISMATCH, CONFLICT, etc.) exit 2, and other
backend failures exit 4.
There is no commitId field in the result. Per-thing version trails are the audit source — use wh thing history <wref>.
Real write output (—json)
Section titled “Real write output (—json)”For reference, here is the machine-readable shape produced by a real wh commit submit (without --dry-run). This is distinct from the preview output described above.
There are three different shapes on this page. Find yours before reading the detail:
| If you are reading | Look at | Status values |
|---|---|---|
--dry-run preview output | Validate without committing | would_apply / noop / error |
| a real write’s root rows | operations[] below | applied / noop / error |
| a real write’s exact receipts | receipts[] below | depends on the receipt’s schemaVersion |
The rest of this section covers the root object, the CLI-only envelope, how the SDK differs, and the versioned receipt rows — in that order. Recovery shapes for partial and ambiguous writes follow after.
The root object. With --json, stdout is one root JSON object: the aggregate write result — operationCount, operations[], and (when any operation failed) partial plus statusCounts — with the exact receipt objects nested under receipts, one receipt per physical chunk, in submission order. With --format jsonl, stdout carries the same root object on one line.
CLI envelope. The CLI adds a top-level schema field set to "wh.commit.submit.result/v0.2" to the JSON and JSONL output. This field is always present in CLI output and serves as a stable contract discriminator for machine consumers: parsers and contract checks should match against the field’s value, "wh.commit.submit.result/v0.2", rather than relying on its position in the object.
SDK difference. The TypeScript SDK’s client.commit.apply returns the same aggregate payload fields but does not add the CLI’s schema envelope field. The two outputs are therefore not the same shape — machine consumers must not assume they are interchangeable.
Root operation rows. operations[] entries use status: "applied" | "noop" | "error", and a failed entry carries an errors array where each element has code and message.
Entries inside receipts[] are versioned, and do not all match the root shape — branch on the receipt’s schemaVersion before reading its operations[]:
operation-event-receipt/v2—operations[]matches the root projection:status: "applied" | "noop" | "error", with failures undererrors[].operation-event-receipt/v1— the historical vocabulary.statusis optional and may be absent; when present it is"success" | "noop" | "failed"on stream-appended writes and"applied" | "noop"on direct writes, with failure detail undererrorrather thanerrors[].
A parser that assumes the root vocabulary for every receipt will break on a valid v1 receipt.
{ "schema": "wh.commit.submit.result/v0.2", "message": "Add alice", "operationCount": 1, "operations": [ { "opIndex": 0, "name": "Player/alice", "operation": "add", "status": "applied", "version": 1, "dataHash": "sha256:..." } ], "receipts": [ { "schemaVersion": "operation-event-receipt/v2", "submissionId": "2f7047f7-642a-453f-a2bd-2a63450a365e", "eventRequestId": "1a6955bd-154e-5870-becc-7b0b30b24d36", "requestDigest": "sha256:...", "outcome": "event", "event": { "repo": { "orgName": "acme", "repoName": "world" }, "repoSeq": "42", "committedAt": "2026-08-02T12:00:00.000Z", "eventDigest": "sha256:...", "operationCount": 1 }, "operations": [ { "opIndex": 0, "name": "Player/alice", "operation": "add", "status": "applied", "version": 1, "dataHash": "sha256:..." } ] } ]}On a partial write, partial: true and statusCounts: { "applied": N, "noop": N, "error": N } appear at the root, and each failed root operation carries an errors array where each element has code and message. submittedName is optional in the output contract, so a parser must not require it. Do not read its presence as a signal that the name was rewritten either — CLI failure rows carry it alongside name even when the two are identical. If you need to know whether a name resolved to something different, compare name against resolvedName. On a fully successful write, partial and statusCounts are absent.
An event receipt has outcome: "event" and decimal-text event.repoSeq. A no-event receipt has outcome: "no_event" and event: null. opIndex is the zero-based position of the source operation within that chunk. warnings is omitted entirely when an operation produced none; when present it can carry undeclaredFields, coalescedWrefs, or both.
On a partial JSONL failure, human output reports the acknowledged operation count, the last acknowledged repository sequence when known, and whether the attempted append was definitely not applied or remains outcome-unknown. When at least one chunk was acknowledged, stdout first carries the completed aggregate — confirmed operations with their acknowledged receipts under receipts, plus submissionId, eventRequestId, and chunkOrdinal at the root. The error output exposes the same recovery facts and the pending receipt identity under error.recovery:
{ "error": { "code": "BACKEND", "message": "Stream append outcome is unknown after 2 acknowledged operation(s). Last acknowledged repo sequence: 42.", "recovery": { "acknowledgedOperationCount": 2, "lastAcknowledgedRepoSeq": 42, "attemptedAppendOutcome": "unknown", "eventRequestId": "019f...", "submissionId": "019f...", "chunkOrdinal": 1 } }}For "unknown", stop writes and use eventRequestId with wh commit receipt. The last acknowledged sequence is a lower bound for known work, not proof that the unknown append did not land. For "not_applied", retain the acknowledged prefix receipt and correct or replan the rejected and unsent operations.
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. It is submission-global: on a streamed multi-chunk submit each chunk’s indexes are rebased onto the whole submission, unlike the chunk-local opIndex inside each receipt. 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. A failed entry can carry warnings as well: warnings lives in the shared operation-result shape and is not stripped from error rows, so check both rather than assuming the error is the only signal.
Exit codes. Mixed-result writes exit 0 — at least one operation did not fail, and the root object communicates every result. A completed submission whose operations all failed still emits the aggregate root object (so the ! rows and machine-readable failures remain visible, with statusCounts.error equal to operationCount), then exits non-zero. The exit code is picked deterministically from the worst per-op failure: auth-class failures (FORBIDDEN, UNAUTHENTICATED) exit 5, caller-correctable failures (VALIDATION_ERROR, SHAPE_MISMATCH, CONFLICT, etc.) exit 2, and other backend failures exit 4. If a later chunk fails or remains ambiguous, the completed aggregate is emitted before the non-zero error — the confirmed operations and their acknowledged receipts under receipts, plus the identity fields recovery needs (submissionId, eventRequestId, chunkOrdinal). The error’s lookup hint is exactly wh commit receipt <event-request-id> --repo <org>/<repo>. A first-chunk ambiguity emits no stdout object because nothing has been acknowledged yet.
There is no commitId field in the result. Per-thing version trails are the audit source — use wh thing history <wref>.