Every command that returns data has a machine-readable mode (--json) and a conventional exit code. (login and logout have no --json — they print a fixed status line.) What it does not have is a machine-readable error format, retries, or paging — so a script has to check the exit status itself and pace its own calls. This page covers what a non-interactive caller can rely on.
Authenticate unattended
Set ZILFU_TOKEN in the environment and drop the --token flag entirely:
export ZILFU_TOKEN="$(cat /run/secrets/zilfu_token)"
zilfu spaces list --json
--token <token> works too, but it lands in shell history and is visible in ps output to anyone on the machine. Use the environment variable, or zilfu login --token <token> once on a persistent machine to write the stored config — see Get started for the precedence rules and where credentials are stored.
An empty
ZILFU_TOKENis used as-is rather than falling back to the stored config, because onlynull/undefined values fall through.ZILFU_TOKEN=""in a CI environment producesNo API token found.even when~/.config/zilfu/config.jsonholds a perfectly good token. Unset the variable instead of blanking it.
zilfu health is the only API call that needs no token (zilfu logout needs none either — it just deletes the local file).
JSON output
--json writes pretty-printed JSON (two-space indent, one trailing newline) to stdout and nothing else. It never emits NDJSON, and it never wraps the payload.
| Command | --json emits |
|---|---|
zilfu whoami --json |
Object — id, name, email |
zilfu health --json |
Object — {"status": "ok"} |
zilfu spaces list --json |
Bare array of space objects |
zilfu accounts list --space <id> --json |
Bare array of account objects |
zilfu posts list --space <id> --json |
Bare array of post resources |
The list commands emit a bare array. The API's response envelope is unwrapped before printing, so the pagination fields on posts list (links, meta) are discarded — --json gives you no page count, no total, and no next-page URL. spaces and accounts are not paginated server-side, so nothing is lost there.
--json has to come after the leaf subcommand (zilfu posts list --json, never zilfu --json posts list). See Commands.
--jsonand the table are different shapes forposts list. The table is a four-column projection with a human status label;--jsonis the full post resource, wherestatusis the numeric code the API stores. A script reading--jsonhas to map the codes itself — the table'sscheduledis"status": 0. The mapping is in Commands.
Exit codes
| Code | Meaning |
|---|---|
0 |
The command succeeded, including when it returned no results |
1 |
Every failure — API error, bad argument, missing token, unreachable host |
130 |
Ctrl-C at the interactive token prompt of zilfu login |
There is no exit-code granularity.
401,403,404,422,429,5xx, an invalid--space, a missing token and a host that never answered all exit1. To tell them apart, parse theError <status>:prefix on stderr. A network-level failure — DNS, connection refused, TLS — reports status0, soError 0:means no HTTP response happened at all.
Error output
Errors always go to stderr as plain text, one line, with a status prefix:
Error 401: Unauthenticated.
No read-only command can currently return a 422 — nothing the CLI sends is validated. If one ever does, validation failures add one indented line per field message:
Error 422: The per page field must be at least 1.
- per_page: The per page field must be at least 1.
Errors raised before any request is made carry no status prefix — Invalid space id: abc, or the three-line No API token found. block.
--json never applies to errors. There is no JSON error envelope, no error code field, and stdout stays empty on failure.
Usage text is printed to stdout, not stderr, when an argument is missing or wrong.
zilfu posts list --json > out.jsonwith--spaceomitted writes a usage block intoout.jsonand exits1(ANSI-coloured on a normal shell; plain whenCI,NO_COLOR, orTERM=dumbis set). Always check the exit code before parsing stdout:if ! zilfu posts list --space 42 --json > posts.json; then echo "zilfu failed" >&2 exit 1 fi
Empty results are not errors
An empty list prints No results. to stdout and exits 0; with --json it prints []. An empty record prints (empty). None of these are failures — a script that treats "no output" as an error will misfire on a space with nothing in it.
count=$(zilfu posts list --space 42 --json | jq 'length')
Filter and page with jq
The CLI has no --status, --account, --from, --to, --page or --per-page flags, even though the API supports status, account_id, from, to, per_page, and page. Pipe --json into jq instead:
# Scheduled posts only (status 0), as id + scheduled time
zilfu posts list --space 42 --json \
| jq -r '.[] | select(.status == 0) | "\(.id)\t\(.scheduled_at)"'
# Accounts that need reconnecting
zilfu accounts list --space 42 --json \
| jq -r '.[] | select(.is_active == false) | .handler'
# The space id for a space you know by name
zilfu spaces list --json | jq -r '.[] | select(.name == "Acme") | .id'
Filtering downstream does not work around paging. zilfu posts list returns only the API's first page — 15 posts — and there is no flag to ask for more, so jq is filtering 15 rows, not your whole history. For anything larger, call the API directly with per_page and the filter parameters.
Timeouts, retries, and rate limits
The CLI sets no request timeout and never retries. A hung server ties up the command until Node's own fetch timeout fires (about five minutes), then fails with Error 0: fetch failed — wrap invocations in your own timeout if a stuck job would block a pipeline:
timeout 30 zilfu spaces list --json
A 429 surfaces as Error 429: Too Many Attempts. and nothing more. The underlying error object carries no response headers, so Retry-After, X-RateLimit-Remaining and X-RateLimit-Reset are unreachable from the CLI. Pace your own calls: the ceiling is 120 requests per minute per token, detailed in Rate limits. If you need header-driven backoff, call the API directly.
Do not parse the table
Table rows have trailing whitespace stripped per line, so a row whose last cells are empty simply ends early — the column count varies from row to row:
id status scheduled_at published_at
-- --------- --------------------------- ---------------------------
10 scheduled 2026-08-01T09:00:00.000000Z
11 published 2026-07-01T09:00:00.000000Z
12 draft
Row 10 has three fields, row 11 has three fields carrying different data, and row 12 has two. awk '{print $3}' returns a scheduled time on one row and a published time on the next. Nulls render as empty strings, and the table is a lossy projection anyway. Use --json for anything a program reads.
One more pipeline hazard: closing the pipe early can kill the process. zilfu spaces list | head -1 may abort with a Node EPIPE stack trace on stderr and exit 1 even though the command worked. Buffer to a file first, or tolerate the non-zero status.