JSON output
Add --format json to any command and it prints one JSON object instead of a table, on the same stream it would have used anyway: success on stdout, errors on stderr. Nothing about the command's behavior changes; only the rendering does.
schema_version is 1 today, and it only moves for a rename, a removal, or a retype in some command's data shape. A new field never bumps it.
The envelope
Two verbs add a fourth field: shep flock adds host, and shep reload adds refused, only sometimes, see a reload that was refused part of its fold. Here's shep flock --format json against a two-sheep flock:
{
"schema_version": 1,
"command": "flock",
"data": [
{
"id": 0,
"name": "web",
"status": "online",
"pid": 14955,
"restarts": 0,
"uptime_ms": 3047,
"fold": "backend",
"depends_on": [],
"out_file": "/tmp/shepdemo/logs/web-0-out.log",
"err_file": "/tmp/shepdemo/logs/web-0-err.log",
"cpu_percent": null,
"memory_bytes": 3129344,
"cpu_ms": 4,
"dog": null,
"lambs": null,
"last_exit": null,
"smit": null,
"instance": 0,
"handshook": null,
"dog_stale": null,
"max_memory": null
},
{
"id": 1,
"name": "worker",
"status": "online",
"pid": 14979,
"restarts": 0,
"uptime_ms": 3020,
"fold": "backend",
"depends_on": [],
"out_file": "/tmp/shepdemo/logs/worker-0-out.log",
"err_file": "/tmp/shepdemo/logs/worker-0-err.log",
"cpu_percent": null,
"memory_bytes": 3129344,
"cpu_ms": 3,
"dog": null,
"lambs": null,
"last_exit": null,
"smit": null,
"instance": 0,
"handshook": null,
"dog_stale": null,
"max_memory": null
}
],
"host": {
"cpu_percent": 18.083654,
"memory_used_bytes": 38749880320,
"memory_total_bytes": 51539607552,
"disk_bytes_per_second": [16384, 851968],
"network_bytes_per_second": [17480, 4174]
}
}host sits beside data rather than inside it, so a script reading data[0].name sees nothing new. Its pairs are read then written, and received then transmitted. Three shapes, three answers: an object, as here; null where sysinfo cannot read the platform; and no key at all from a shepherd too old to be asked. Each rate is null on its own until the shepherd has a window to divide by. Same figures as the host line.
command is the verb you typed, spelled out in full: run it as shep ls --format json and command still reads "flock", because that's what the daemon answered to, not the alias that got you there. data is always the same shape for a given command: a single object for something like shep ping, an array for anything that lists multiple sheep.
Errors get their own envelope
A failed command prints a different, smaller shape to stderr, no command key, because the command never produced a result:
{
"schema_version": 1,
"error": {
"code": "not_found",
"message": "the daemon reported NotFound: selector matched no registered sheep"
}
}code is the same word that appears in error[<code>]: … under the default table format, and it's the same taxonomy as the process exit code: 3 for not_found, 4 for invalid_config, and so on. A script can switch on error.code without parsing English, and the exit status confirms it independently.
A reload that was refused part of its fold
shep reload is the one verb that can half-answer. A fold reloads in stages, and an app already reloading is skipped instead of failing the rest, so the run has two things to report. It still prints one object: refused sits beside data, and the exit status is 1.
{
"schema_version": 1,
"command": "reload",
"data": [
{
"id": 1,
"name": "api",
"status": "online",
"pid": 63850,
"restarts": 0,
"uptime_ms": 7059,
"fold": null,
"depends_on": ["db"],
"out_file": "/tmp/shepdemo/logs/api-0-out.log",
"err_file": "/tmp/shepdemo/logs/api-0-err.log",
"cpu_percent": null,
"memory_bytes": null,
"cpu_ms": null,
"dog": null,
"lambs": null,
"last_exit": null,
"smit": null,
"instance": 0,
"handshook": null,
"dog_stale": null
}
],
"refused": [
{ "name": "db", "reason": "db is already being reloaded" }
]
}The key appears only on a run that refused something, so an ordinary reload carries the same three fields as everything else. Nothing goes to stderr either: the names are already in the envelope, and a second object would break the one-per-invocation rule the rest of this page rests on.
What schema_version promises
A consumer that reads the keys it cares about and ignores the rest survives an additive release untouched. The contract that's actually worth writing code against:
- Check
schema_versiononce, up front, rather than per field: it's the one thing that tells you whether the shape you coded against still applies. - Don't assume every key is present on every row.
dogisnullfor a plain sheep.lambsisnullfromshep flock(the plain listing never walks the process tree) but a populated array (empty if the sheep has spawned no children of its own) from bothshep describeandshep fold, since fold renders the same per-sheep detail the table format's Lambs section shows. Check the key itself, not which command you ran, if you're writing something generic. A lamb is apidand aname, plus alabelonly when the sheep named it. last_exitisnulluntil a sheep's process has stopped at least once under this daemon, and afterwards holds one of two shapes:{"code": 1, "signal": null}for a normal exit, or{"code": null, "signal": 15}for one ended by a signal. Both are never set at once, and both beingnullis not a state the daemon produces. It is the field that distinguishes a process that started and crashed from one that never started at all, which thestatusalone cannot: both readerrored.cpu_percentreadsnull, not0, when a sample wasn't available yet. A fresh sheep a few hundred milliseconds old, most often. Table format prints the same case as-, for the same reason: a confident zero would be a claim shep can't back up.cpu_msis cumulative CPU-milliseconds the sheep's process tree has spent since it started, the raw countercpu_percentis one view of.nullwhen the shepherd isn't sampling the sheep (not running, or the peer daemon predates the field), and on any verb that answers without taking a live reading: onlyshep listandshep describepay for one, so theshep reloadreply above shows anullcounter on an online sheep; it's the one casecpu_percentcan't cover the same way, since a sheep spawned since the last periodic tick has a counter already but no baseline yet to turn it into a percent.shep lookoutreads this field directly and differences two polls of its own rather than trustingcpu_percent's window, which is why the two can show different numbers for the same sheep at the same instant.handshookis about dogs and isnullfor every sheep.falseis the one to act on: the dog's process is up and the shepherd has never heard from it, so it is not doing its job.statusstill readsonlinethere, truthfully — it describes the process. Table format shows this row assilent.dog_staleis the other half of that and isnullfor every sheep.truemeans the shepherd has given up: it restarted the dog once for never answering, that did not help, and it will not restart it again. You cannot work this out fromhandshook— a dog spawned a second ago and a dog the shepherd will never touch again are bothfalse. Why it gave up is in that dog's own log rather than on the row, because the shepherd could only see the evidence at the moment it happened:shep bleats <dog>.smitisnulluntil a dog attaches one over the client protocol'sSetSmitrequest. shep stores and reports the string verbatim, and never parses it — see terminology.max_memoryis the sheep's memory ceiling in bytes, ornullwhen it has none configured. It's the same number--format tableand lookout'sMEM/CEILgauge divide against; anullhere is why that gauge draws an all-tail bar instead of a fraction.reload_deadline_msis the one key that appears onshep reload's rows and nowhere else. It is how long the shepherd gives one swap of that instance before it gives up: the instance's ownlisten_timeoutplus itsgraceful_timeoutplus five seconds of slack. Per instance, not per app, so a client waiting a reload out reads the number the shepherd is actually keeping to rather than one worked out from a Flockfile copy. The key is absent, notnull, on a row no swap is queued for, and on every other command.
Streaming output carries no envelope
shep bleats --format json is the one exception. A log follow has no end, so there's nothing to close an envelope around: it prints one JSON object per line, newline-delimited, with no command or data wrapper.{"schema_version":1,"id":0,"name":"web","stream":"out","instance":null,"line":"…"}instance is the sheep's slot within its app, when that app has more than one instance registered; null otherwise, and also null for a backlog line read from a log file several instances share, since nothing in that file says which one wrote it.
Table format is not a lesser version of this
The default --format table and --format json render from the same underlying data (there's no separate code path that could drift between them) but table adds human formatting JSON doesn't: uptime_ms: 1019 becomes 1s, memory_bytes: 1212416 becomes 1.2M. If you're piping shep's output anywhere, reach for --format json and do the formatting yourself rather than parsing the table's columns.