For API-backed command executions, --json switches human-readable output to a stable, single-line JSON envelope. Built-in help (solo help and --help) and the root solo --version command remain human-readable even when --json is supplied. Combined with meaningful exit codes, JSON output makes the CLI easy to use from shell scripts, CI jobs, and other tools.
The JSON envelope
For a command that supports JSON output, a successful invocation with --json prints one JSON object to stdout:
{"ok": true, "command": "projects list", "data": { "...": "..." }}
okis alwaystrueon success.commandnames the command that ran, such as"projects list"or"todos create".dataholds the command-specific payload.
Failures print an error envelope to stderr instead, and exit non-zero:
{"ok": false, "error": {"code": "not_found", "message": "process 99 not found", "command": "processes get"}}
error.codeis a stable machine-readable code (see below).error.messageis a human-readable explanation.error.commandnames the failing command when it's known.
Because success goes to stdout and errors go to stderr, you can pipe stdout straight into jq without error output getting mixed in.
Error codes
Connection, discovery, and CLI errors:
| Code | Meaning |
|---|---|
app_not_running |
The discovery file is absent from an explicitly selected app data directory; the app may not be running or local CLI access may be off. |
stale_discovery |
The saved app process is no longer running, or the saved connection cannot be reached, times out, or otherwise fails during the request. |
invalid_discovery |
The discovery file is malformed. |
discovery_unavailable |
No usable discovery file was found in any default location. |
version_mismatch |
The CLI and app speak different API versions; this can be detected from discovery or from the live /api/version response. Update one of them. |
permission_denied |
The discovery file exists but could not be read due to filesystem permissions. |
input_error |
A local input problem, such as a missing --body-file path. |
protocol_error, internal_error |
Unexpected transport/response protocol or internal failure; a protocol error can occur after the app responds. |
Usage errors (bad arguments or flags) use granular codes describing the specific problem, such as unknown_command, duplicate_flag, or invalid_project_id, and always exit with code 64.
Errors returned by the app map to codes like bad_request, unauthorized, forbidden, not_found, conflict, precondition_failed (for example a scratchpad --expected-revision mismatch), service_unavailable, and internal.
Exit codes
The CLI uses sysexits-style exit codes so scripts can branch on the failure class without parsing anything:
| Exit code | Meaning |
|---|---|
0 |
Success. |
64 |
Usage error: bad arguments or flags. |
65 |
The app rejected the request (not found, conflict, revision mismatch, bad request). |
66 |
An input file (such as --body-file or --content-file) could not be read. |
69 |
The app is unavailable: not running, stale discovery, or starting up. |
70 |
Protocol or internal error. |
74 |
I/O error reading input from stdin. |
77 |
Permission denied: a bad or rejected token, or an unreadable discovery file. |
78 |
Configuration error, including CLI/app version mismatches. |
List responses and pagination
list commands return their items in an array (projects, processes, todos, or scratchpads) alongside pagination fields:
{ "ok": true, "command": "processes list", "data": { "scope": { "type": "project", "projectId": 2, "projectName": "my-app", "projectDisplayName": null }, "processes": [ { "id": 9, "name": "dev", "status": "running", "...": "..." } ], "totalCount": 12, "offset": 0, "limit": 50, "limitClamped": false, "hasMore": false, "nextOffset": null }}
Project, process, todo, and scratchpad list responses each include limitClamped. It is true when an oversized requested limit was reduced to the server maximum. When hasMore is true, pass --offset with the returned nextOffset to fetch the next page.
Scripting examples
Get a project's ID by name:
project_id=$(solo projects list --json | jq '.data.projects[] | select(.name == "my-app") | .id')
List the names of everything currently running:
solo processes list --status running --json | jq -r '.data.processes[].name'
Restart every failed process in a project:
solo processes list --project-id 2 --status failed --json \ | jq -r '.data.processes[].id' \ | xargs -n1 solo processes restart
Count open high-priority todos, and fail a CI step if any exist:
count=$(solo todos list --project-id 2 --status open --priority high --json \ | jq '.data.totalCount')if [ "$count" -gt 0 ]; then echo "There are $count open high-priority todos" >&2 exit 1fi
Read a scratchpad's current revision before writing to it:
rev=$(solo scratchpads read 5 --project-id 2 --json | jq '.data.scratchpad.revision')solo scratchpads append 5 --project-id 2 --expected-revision "$rev" \ --newline --content "deployed at $(date -u +%FT%TZ)"
Branch on failure class using exit codes:
solo status --json > /dev/null 2>&1case $? in 0) echo "Solo is up" ;; 69) echo "Solo is not running" ;; 77) echo "CLI access denied; re-check the HTTP API settings" ;; *) echo "Unexpected failure" ;;esac
Related pages
- CLI overview — installation, discovery, and diagnostics
- HTTP API — call the same API directly over HTTP