Skip to content
pdcli
Get started

Exit codes

pdcli returns deterministic sysexits exit codes so scripts and agents can branch on the failure without parsing text. Every command shares the same ladder.

| Code | Meaning | Pipedrive trigger | What a script should do | | ---- | ------- | ----------------- | ----------------------- | | 0 | Success | 2xx | Continue. | | 1 | Generic error | — | Inspect the message; safe to surface and stop. audit --strict returns 1 on must-severity findings; a partial bulk/import failure returns 1 when some rows failed for non-data reasons. | | 3 | No match (lookup only) | — | pdcli lookup exits 3 when it finds zero rows, so a script can branch create-vs-update: pdcli lookup … && pdcli … update \|\| pdcli … create. A distinct code (not 1) so a real error isn’t mistaken for “not found”. Not part of the general ladder — specific to lookup. | | 64 | Usage / bad input | — | A bad invocation: an unknown flag or a missing/invalid argument (caught by the parser), or a value pdcli validated and rejected (invalid --period, unknown --checks name, several pipelines with no --pipeline, a CSV missing its name column, --ids over 100). Fix the invocation — retrying won’t help. | | 65 | Data / validation | 400, 422 | The request body or input data was rejected: a bad field value, a non-numeric id cell in a CSV, an ambiguous upsert match, malformed --body JSON, or a conversion the API rejected. Fix the payload; don’t retry unchanged. | | 69 | Service unavailable | 5xx, network | Pipedrive is down/erroring, or the host is unreachable (DNS failure, connection refused, or a --timeout). pdcli already retried with backoff; safe to retry later. | | 70 | Internal CLI bug | — | An unexpected error inside pdcli — and nothing else (network, usage, and API failures all map elsewhere). File an issue; retrying won’t help. | | 75 | Rate limited | 429 | Token budget exhausted. pdcli retries 429s with backoff; if they never clear it surfaces 75 (not 69), and --no-retry surfaces the first one. Back off and retry after the reset window. | | 77 | Auth / permission | 401, 403 | Token is invalid, expired, or lacks scope — including a failed OAuth refresh (invalid_grant). Re-authenticate (pdcli auth login). Don’t loop. A 403 after repeated 429s is a rate-limit hard stop — wait for the reset, don’t retry. | | 78 | Config / account | 402, missing domain/token, host-lock violation, no keychain | The CLI or account is misconfigured: no company domain, no keychain to write to, a pdcli api URL outside your host, a redirect off your host, or a 402 (plan lacks the feature). Fix config or the account — don’t retry. | | 8 | Findings present (watch only) | — | pdcli watch exits 8 when it surfaces new anomalies. Test for it explicitly (pdcli watch; [ $? -eq 8 ] && notify) — a bare pdcli watch \|\| notify also fires on any other failure, not just findings. Not part of the general ladder — specific to watch. |

The HTTP-status mapping lives in src/lib/errors.js (exitCodeForStatus): 400/422 → 65, 401/403 → 77, 402 → 78, 429 → 75, 5xx → 69. On top of that: an oclif parse error (unknown flag, missing/invalid argument) exits 64; an unreachable host or timeout exits 69; and only a genuinely unexpected throw exits 70.

The error format mirrors the success output format (the same --output / default_output / TTY rule). Whenever output is not an interactive table — an explicit --output json|yaml|csv, a non-table default_output in the profile, or stdout piped — the error is written to stderr as a single JSON object, so a machine consumer that gets JSON on success always gets a parseable failure. stdout stays clean so a successful-looking pipe can’t swallow a failure:

{
"error": "ApiError",
"message": "Pipedrive API 422: Deal title must not be empty",
"exitCode": 65,
"statusCode": 422,
"path": "/api/v2/deals",
"errorInfo": "Please provide a valid title"
}

error is the error class name. statusCode, path, and errorInfo appear only for API errors (they’re omitted for usage/config errors). Under --verbose the full Pipedrive body is added too. Only an interactive table context prints the message to stderr in human form; --verbose there adds the request path, status code, and error_info.

Terminal window
pdcli lookup deal --field "PO Number" --value PO-1234 --output json > deal.json
case $? in
0) echo "ok" ;;
3) echo "no match — create it" ;;
64) echo "bad invocation — fix flags/args" ; exit 1 ;;
65) echo "bad request — not retrying" ; exit 1 ;;
75|69) echo "transient — retry later" ; exit 1 ;;
77) echo "re-auth needed" ; pdcli auth status ; exit 1 ;;
78) echo "config/account problem" ; pdcli doctor ; exit 1 ;;
*) echo "failed with $?" ; exit 1 ;;
esac

pdcli also exits 127 (via the command-not-found hook) when you invoke a command name that doesn’t exist.

See also: Troubleshooting for what to do per failure, and How pdcli talks to Pipedrive for the retry/backoff rules.

pdcli v0.22.0 · MIT · not affiliated with Pipedrive