Facebook Pixel
CLI

When things go wrong

Exit codes and error envelopes that tell a caller what to do next — before it reads a single byte of output.

Most automation does not fail because the happy path was hard. It fails because the unhappy path was ambiguous — a script that retried into a rate limit for an hour, or an agent that treated "wrong account" and "temporarily down" as the same thing.

The CLI's answer is that the exit code alone is enough to decide what to do next.

The exit codes

CodeNameWhat happenedWhat to do
0okThe API accepted the callContinue
1errorUnclassified failureRead the envelope; do not retry blind
2authKey missing, wrong, revoked, or not entitledStop. Retrying will not help
3validationRejected before or by the API on the argumentsFix the named flag and retry
4confirm_requiredDestructive command with no terminal and no --yesConfirm intent, re-run with --yes
5rate_limitRate limit or daily action capStop this action for now. Not a transient error
6networkNever reached the APIRetry a read; verify before retrying a write
7unknown_commandNo such commandRun connectsafely commands. Do not guess
8conflictConflicts with current stateRe-read state; it may already have happened

These are a public contract. New codes may be added; an existing one will never be repurposed.

The two that change behaviour most are 5 and 8. A bare 429 invites a retry loop; exit 5 says the cap is the point and today is over. A bare 409 looks like a failure; exit 8 says the invitation may already exist, so check before sending another.

The error envelope

Errors are JSON on stderr, in every output mode — including pretty. A caller that has to pattern-match prose to tell "rate limited" from "wrong id" will eventually get it wrong, and the cost of that mistake lands on a real account.

{
  "ok": false,
  "error": "Daily connection limit reached",
  "code": "rate_limited",
  "exitCode": 5,
  "exitName": "rate_limit",
  "remediation": "A daily action cap or pacing limit was reached. Stop sending this action today and resume tomorrow; retrying now will not succeed and risks the account.",
  "details": { "status": 429, "body": {  }, "retryAfterSeconds": 3600 }
}

Match on code. It is stable. error is the API's own message and may be reworded at any time.

Read remediation. It is written as an instruction, not a description. Compare "Unauthorized" with:

The API key is missing, wrong or revoked. Check connectsafely auth status, then regenerate at https://connectsafely.ai/api-key.

Use details to diagnose. It carries the HTTP status and the API's response body, which is where a 400 names the field it objected to.

Codes you will meet

codeExitMeaning
unknown_command, unknown_flag7, 3The name does not exist. No near-match is attempted
missing_required_flag3A declared-required parameter is absent
invalid_enum, above_maximum, too_long3Caught locally, before any request
url_in_path_param, path_traversal3An identifier that would address the wrong resource
rejected_by_api3The API rejected the arguments; details.body says which
unauthenticated, forbidden2Bad key, or a key without the plan for this action
rate_limited5Pacing limit or daily cap
conflict8Already sent, already delivered, already exists
confirmation_required4Destructive, non-interactive, no --yes
network_error6The request never landed
server_error1The API failed, not your request

Which failures are retried

Retries are automatic, but not symmetric between reads and writes, because the consequences are not symmetric.

Reads are retried on 408, 425, 429, 500, 502, 503 and 504, up to three attempts, with exponential backoff and jitter. A Retry-After header is honoured exactly. Nothing is duplicated by reading twice.

Writes are retried in only two cases:

  1. The server said it did nothing429 or 503. There is no action to duplicate.
  2. --idempotency-key was supplied — a repeated key returns the first result, so the retry is safe by construction.

A write that times out, or that fails with a 500, is not retried. The CLI cannot know whether it landed, and sending a second connection request to the same person is worse than reporting a failure you can check.

connectsafely --retries 5 ...   # more attempts for retryable failures
connectsafely --retries 1 ...   # none: fail on the first
connectsafely --timeout 15000 ... # per-attempt timeout in ms (default 60000)

Idempotency keys

connectsafely conversations-send-message \
  --json @message.json \
  --idempotency-key "followup-lead-1041-week-3"

Derive the key from the intent, not the attempt: the recipient and the campaign step, not a timestamp or a random value. A key that is stable for the intent means a re-run of the same script is a no-op rather than a second message.

Backoff behaves itself

Waits grow exponentially and are capped at 15 seconds, so a long outage does not stall a job for minutes. Every wait carries jitter, so a hundred parallel callers that all failed at once do not resynchronise and hit the API together on the way back up.

Seeing what happened

Retry chatter goes to stderr and is off by default, so stderr holds nothing but the error envelope and stays parseable:

connectsafely search-people --keywords CTO --verbose   # show retry decisions
connectsafely batch invites.ndjson --quiet             # silence everything but errors

At an interactive terminal, progress is shown automatically — a human watching a slow batch should not have to guess whether it is working.

A retry loop that behaves

#!/usr/bin/env bash
set -uo pipefail

out=$(connectsafely conversations-send-message --json @msg.json \
        --idempotency-key "followup-1041")
code=$?

if [ "$code" -eq 0 ]; then
  echo "$out" | jq '.'
  exit 0
fi

case "$code" in
  2) echo "Auth problem — not retrying." >&2; exit 2 ;;
  3) echo "Bad arguments — fix the payload." >&2; exit 3 ;;
  5) echo "Rate limited — try again tomorrow." >&2; exit 0 ;;
  6) echo "Network — safe to retry with the same key." >&2; exit 75 ;;
  8) echo "Already sent." >&2; exit 0 ;;
  *) echo "Unexpected failure." >&2; exit 1 ;;
esac

Note what this does not do: it never retries a 2 or a 5, and it treats 8 as success, because the message already exists.

On this page