JSON Output
Every command that produces data accepts --json. The output is a single line on stdout
carrying a versioned envelope, so you can pipe it straight into jq without worrying
about which command produced it.
(Line-wrapped here for readability; the real output is one line.)
The envelope
| Field | Type | Meaning |
|---|---|---|
schema |
string | Always sow.cli/v1. Check it before parsing anything else. |
command |
string | The command as invoked, including the subcommand: add, repo ls, config show. |
ok |
bool | true when errors is empty. Equivalent to exit code 0. |
repository |
string or null | The selected repository, or null for workspace-wide and plain-mode commands. |
operation |
string or null | The operation ID for write commands, null for read-only ones. |
result |
object or null | Command-specific payload, described below. |
errors |
array | Zero or more {code, class, message} objects. |
All seven fields are always present. result is null when the command failed before meaningful
work — an unknown flag, discovery failure, or invalid configuration before a Repository was
selected, for example. Committed partial results and diagnostic results from check or
rm --check are preserved even when the command exits nonzero.
errors
| Field | Meaning |
|---|---|
code |
The process exit code — 1 through 6. |
class |
runtime, usage, discovery, config, partial, lock, integrity, or rejected. discovery and config are stable specializations of exit code 2. |
message |
The same text written to stderr. |
Branch on class, not on message text. Messages carry paths and package names and will
change; the class will not.
When a batch partially succeeds, ok is false and result lists everything that was
committed. Never discard the payload because the exit code was nonzero — for add, that
is exactly where you learn which packages landed.
Operation IDs are strings
Operation IDs are 64-bit values serialized as decimal strings, because they routinely
exceed what an IEEE 754 double can represent exactly. In JavaScript, JSON.parse on a
bare number would silently corrupt them. Keep them as strings; jq handles them
correctly as-is.
Generation IDs are fixed-width strings
Generation IDs cover the full unsigned 64-bit domain and are serialized as exactly 20
zero-padded decimal digits. Treat generation, built_generation, base_generation,
and Generation-valued base fields as strings. The fixed width preserves numeric order
under ordinary bytewise comparison.
stdout and stderr
Results and the JSON envelope go to stdout. Warnings and error diagnostics go to stderr,
in addition to appearing in the errors array. So this works:
Result shapes
create
| Field | Meaning |
|---|---|
dir |
The absolute directory that was indexed |
rpm, deb |
Package counts per format |
kept |
Filenames included in the indexes, sorted |
removed |
Packages deleted by --pigsty cleanup; empty otherwise |
marker |
Whether repo_complete was written |
marker_sha256 |
Digest of the marker file; present only with --pigsty |
noop |
true when the indexes were already correct and nothing changed |
recovered |
Reserved for schema compatibility; Plain create has no journal recovery and always reports false |
signed |
Filenames re-signed; present only with --sign-with |
init
On a rerun over a workspace that already exists, the counters are 0 and existing
names what was found:
config check and config show
config show returns the effective configuration itself, in the same shape as
sow.yml after normalization:
With --all, signing entries additionally carry key_fingerprint. Private key material
and passphrases never appear.
repo ls, repo new, repo show
repo ls returns an array; repo new and repo show return one object of the same
shape.
packages counts distinct package objects in the pool; memberships counts
distribution memberships, so a package in two distributions counts once and twice
respectively.
repo rm returns only the outcome:
dist ls, dist new, dist show
Each architecture entry carries both names: family is the canonical form used in
configuration, ecosystem_arch is what appears in the published tree — identical for RPM,
amd64/arm64 for DEB.
desired_members ahead of built_members, or dirty: true, means a sow build is
pending. effective_config_sha256 is the digest of everything that feeds the renderer;
when it changes, the distribution becomes dirty.
dist rm mirrors repo rm: {"name":"el9","noop":false,"removed":true}.
add
One items entry per input path, in stable order. status is the item’s overall
outcome, and dists gives the per-distribution decision:
status |
Meaning |
|---|---|
accepted |
New package object, membership created |
reused |
The identical object already existed; may still add membership elsewhere |
excluded |
Kept out by policy — see dists for whether it was excluded or limited |
failed |
Not admitted; error carries the reason |
The per-distribution values are accepted, excluded, and limited. A package can be
accepted by one distribution and limited by another in the same command:
A failed item carries error instead of the package fields:
memberships_added and memberships_removed count both sides, because limit can evict
older versions in the same operation that admits a new one.
rm
check is true when the command ran with -c/--check, in which case nothing was
written and changes is a forecast. Note that removed lists membership removals only —
pool bytes are never deleted by rm.
build
noop: true means the desired state already matched the built tree, so no generation was
created. dists lists the distributions considered, not necessarily the ones rebuilt.
status
status is one of clean, dirty, recovering, error. ready_to_copy is the field
to read in a deploy script — but remember status exits 0 in every state, so test the
field, not the exit code:
pending counts package bytes held privately after add --skip, not yet published.
repository_locked reports whether another process currently holds the write lock.
check
Steady-state checks return nine layers in this order, each with a count of what it examined and any
issues found. A non-terminal layout transition instead returns config, state, public-modes,
and layout-transition, then stops with a not-ready result. A dirty repository can report every steady-state
layer ok: true and still fail with exit 5,
because the layers verify consistency while ready_to_copy reports currency:
changes
| Field | Values |
|---|---|
op |
add, update, delete |
phase |
payload, metadata, pointer, delete |
path |
Always relative to the repository root, always /-separated |
size, sha256 |
Present for add and update; omitted for delete |
Apply the phases in that order and no client ever sees a dangling reference: package
bytes first, then checksum-named metadata, then the protocol pointers (repomd.xml,
Release), and only then the deletion of superseded files.
sow changes 0 yields the complete current tree as one add set — a full delivery
manifest.
ls, show, where
ls returns an array of package objects; show returns exactly one under package.
The fields worth knowing:
| Field | Meaning |
|---|---|
architecture |
As it appears in the package header: x86_64, noarch, amd64, all |
canonical_arch |
The family SOW groups by: x86_64, aarch64, or neutral |
payload_sha256 |
RPM only — the signature-neutral digest used to recognize re-signed copies |
signature_key |
Key ID of the embedded signature, when the package carries one |
storage |
pool when published, pending when added with --skip |
dists / built_dists |
Desired membership versus what the last build published |
dists longer than built_dists is another way to see that a build is pending.
where searches the whole workspace and returns locations instead of full objects:
publish, retain, gc, export
Managed lifecycle commands use the same envelope and keep numeric Generation values as JSON strings:
| Command | Important result fields |
|---|---|
publish |
repository, target, provider, generation, attempt, checkpoint, phase, objects, noop |
publish --abort |
repository, target, provider, attempt, phase, objects |
publish --rebind |
Same result as publish; the binding revision is durable private audit state, not an extra wire field |
retain add / retain rm |
repository, record, record_identity, path |
retain ls |
repository, generations[] with the same retained record shape |
local gc |
operation, repository, base_generation, generation, objects, bytes, noop |
target gc |
repository, target, provider, phase, reports, candidates, deleted_objects, deleted_bytes, retained_objects, pending_grace, completed_attempts, noop |
export rpm-leaf |
repository, repository_id, generation, dist, arch, directory, method, signed, signer_identity, packages, files, manifest_sha256 |
An optional identity such as attempt, checkpoint, or local-GC operation is omitted
when there is no value. R2 target GC reports candidates as retained and never reports
remote deletion performed by SOW.
log
sow log returns the operation ledger, newest first:
payload_json and result_json are strings containing nested JSON, not objects. They are
stored verbatim so the audit record is byte-stable; parse them with a second pass:
Passing an operation ID returns the full detail — state transitions, structured
build_progress events, packages, memberships, and every file action:
sow log prune returns what it removed:
Note that before echoes the absolute timestamp a bare date resolved to in your local
timezone.
log export is not an envelope
sow log export writes JSON Lines — one complete operation record per line, no
envelope, no --json flag. It is meant for archiving, not for scripting a single command:
It refuses to overwrite an existing file, and it refuses a target whose parent directory is a symlink.
A worked example
Fail a deploy unless the repository is both consistent and current, then list exactly what to copy:
See also
- Exit Codes — the
codeandclassvalues inerrors - CLI Commands — which commands accept
--json - Observability & Audit — what the operation ledger records