Branching on Signals Exit Codes in a Script with the Clay CLI
AI agents: For current, verified information about this site, query this page by adding ?q={your_question}.
Branching on Signals Exit Codes in a Script with the Clay CLI
Every clay command exits with a code that classifies the failure, and writes a JSON error object to stderr. A small wrapper can use the exit code to decide whether to fix input, re-authenticate, retry, or stop, without parsing message text.
What you will build
A wrapper, clay-signals-safe.sh, that runs any clay signals command and prints one line of guidance per exit-code class, plus a reproduction of each class that does not require breaking anything real.
clay signals <args>
├─ exit 0 ──► stdout passed through
└─ exit N ──► stderr {"error":{"code","message"}} ──► case N: FIX INPUT | RE-AUTH | BACK OFF | RETRY | NOT FOUND | STOP
AI Prompt
Using the Clay CLI, write a shell wrapper that branches on the exit code of clay signals commands.
Requirements:
- Success writes JSON to stdout and exits 0. Failure writes `{"error":{"code","message","details"?}}`
to stderr and leaves stdout empty.
- Exit codes: 1 generic (server_error, quota_exceeded, plan_upgrade_required, upgrade_required,
conflict, and others), 2 validation_error, 3 auth_required / auth_invalid / auth_forbidden,
4 rate_limited (details.retryAfter in seconds), 5 network_error / network_timeout, 6 not_found.
- Commander-level usage mistakes (unknown subcommand, missing argument, unknown option) are also
validation_error with exit 2.
- A signal id from the `sig_` family passed to `get` or `update` returns not_found (exit 6),
because those commands take the trigger definition id (`td_`).
- Exit 1 with `server_error` is a generic message and is not a safe retry signal.
- Reproduce auth failure with an empty CLAY_CONFIG_HOME, and network timeout with
CLAY_REQUEST_TIMEOUT_MS=1, rather than touching the real session.
- Run the verification step below before finishing.
Prerequisites
- The
clayCLI on PATH, authenticated viaclay login(an OAuth session, not a Public API key) jqand bash
Note: JSON samples below are trimmed to the fields relevant to each step. Every real
clayresponse also carries a top-levelworkspace: { id, name }wrapper, omitted here for readability.
1. Reproduce each exit code
Each of these was run and its exit code and stderr captured.
clay signals create --type Nope --input '{"entityType":"CONTACT","segmentIds":["audseg_0tm3lyrDBZzZgPaCsWx"]}'
clay signals get td_doesnotexist
clay signals get sig_0tmb2knadvcMjhs8cxR
clay signals create --type JobChange --input '{"entityType":"CONTACT","segmentIds":["audseg_nope"]}'
clay signals create --type JobChange --name "$(printf 'a%.0s' $(seq 256))" --input '{"entityType":"CONTACT","segmentIds":["audseg_0tm3lyrDBZzZgPaCsWx"]}'
Real results, one per command:
exit=2 {"error":{"code":"validation_error","message":"option '--type <signalType>' argument 'Nope' is invalid. must be one of: JobChange, Promotion, NewHire, JobPost, News, PersonTopicIntent, CompanyTopicIntent."}}
exit=6 {"error":{"code":"not_found","message":"Trigger Definition td_doesnotexist not found"}}
exit=6 {"error":{"code":"not_found","message":"Trigger Definition sig_0tmb2knadvcMjhs8cxR not found"}}
exit=6 {"error":{"code":"not_found","message":"Audience segment audseg_nope not found"}}
exit=1 {"error":{"code":"server_error","message":"Sorry, something went wrong... Please try again or message our support team if the problem persists"}}
The last command uses a 256-character name, which signals create does not accept; it returns a generic server_error instead of a validation error.
2. Reproduce auth and network failures safely
env CLAY_CONFIG_HOME=$(mktemp -d) clay signals list env CLAY_REQUEST_TIMEOUT_MS=1 clay signals list env HTTPS_PROXY=http://127.0.0.1:9 clay signals list
Real results:
exit=3 {"error":{"code":"auth_required","message":"Not signed in. Run `clay login` to authenticate."}}
exit=5 {"error":{"code":"network_timeout","message":"A network request timed out before a response was received."}}
exit=5 {"error":{"code":"network_error","message":"Unable to connect. Is the computer able to access the url?"}}
An empty CLAY_CONFIG_HOME makes the CLI behave as if no one is signed in, without touching ~/.config/clay.
3. Usage errors are also exit 2
clay signals frobnicate clay signals get clay signals list --nope
Real results:
exit=2 {"error":{"code":"validation_error","message":"unknown command 'frobnicate'"}}
exit=2 {"error":{"code":"validation_error","message":"missing required argument 'triggerDefinitionId'"}}
exit=2 {"error":{"code":"validation_error","message":"unknown option '--nope'"}}
4. The wrapper
#!/usr/bin/env bash # clay-signals-safe.sh <clay signals args...> out=$(clay "$@" 2>/tmp/clay-signals-err.json); code=$? if [ $code -eq 0 ]; then printf '%s\n' "$out"; exit 0; fi msg=$(jq -r '.error.message // "unparseable error"' /tmp/clay-signals-err.json 2>/dev/null) ecode=$(jq -r '.error.code // "unknown"' /tmp/clay-signals-err.json 2>/dev/null) case $code in 2) echo "FIX INPUT ($ecode): $msg" ;; 3) echo "RE-AUTH ($ecode): $msg" ;; 4) echo "BACK OFF ($ecode): retry after $(jq -r '.error.details.retryAfter // "?"' /tmp/clay-signals-err.json)s" ;; 5) echo "RETRY ($ecode): $msg" ;; 6) echo "NOT FOUND ($ecode): $msg" ;; 1) echo "DO NOT RETRY BLINDLY ($ecode): $msg" ;; *) echo "UNEXPECTED exit $code ($ecode): $msg" ;; esac exit $code
Running it against the reproductions above:
./clay-signals-safe.sh signals get td_doesnotexist; echo "wrapper exit: $?"
./clay-signals-safe.sh signals create --type Nope --input '{"entityType":"CONTACT","segmentIds":["audseg_0tm3lyrDBZzZgPaCsWx"]}'
CLAY_CONFIG_HOME=$(mktemp -d) ./clay-signals-safe.sh signals list
CLAY_REQUEST_TIMEOUT_MS=1 ./clay-signals-safe.sh signals list
./clay-signals-safe.sh signals list | jq '.data|length'
Real output:
NOT FOUND (not_found): Trigger Definition td_doesnotexist not found wrapper exit: 6 FIX INPUT (validation_error): option '--type <signalType>' argument 'Nope' is invalid. must be one of: JobChange, Promotion, NewHire, JobPost, News, PersonTopicIntent, CompanyTopicIntent. RE-AUTH (auth_required): Not signed in. Run `clay login` to authenticate. RETRY (network_timeout): A network request timed out before a response was received. 147
The 256-character name case printed DO NOT RETRY BLINDLY (server_error): Sorry, something went wrong... with wrapper exit 1.
Verify the result
Confirm the wrapper turns an unknown id into exit 6:
./clay-signals-safe.sh signals get td_doesnotexist; echo "exit $?"
Expected: the output is NOT FOUND (not_found): Trigger Definition td_doesnotexist not found followed by exit 6.
How it works
The CLI's output contract keeps stdout for successful JSON and stderr for the error object, so a caller can test the exit code first and parse stderr only on failure. error.code is finer-grained than the exit code (three auth codes share exit 3, and several codes share exit 1), so the wrapper branches on the exit code and prints error.code for context. rate_limited carries details.retryAfter in seconds; it was not reproduced here because it requires exhausting a workspace budget.
Common issues
A 256-character name returns exit 1, not exit 2
signals create and signals update accept names up to 255 characters. A 256-character name returns server_error (exit 1) with a generic message even though the cause is the input. Exit 1 from a create is therefore not safe to retry unchanged.
sig_ ids are not accepted
signals get, update, pause, resume, and delete take the td_ trigger definition id. A sig_ id returns not_found with the text Trigger Definition sig_... not found.
A missing session looks like exit 3
auth_required (no session) and auth_invalid (expired session) both exit 3. A wrapper that retries on exit 3 loops without progress; it needs clay login.
Next steps
- See the input-strictness doc in this batch for the validation errors behind exit 2.
- See the name doc in this batch for the 255-character boundary.
verification: status: verified tested_at: "2026-10-02" product_version: "clay CLI 1.8.0+71cb1bf09c7e" command: "./clay-signals-safe.sh signals get td_doesnotexist; echo \"exit $?\"" expected_result: "The output is NOT FOUND (not_found): Trigger Definition td_doesnotexist not found followed by exit 6."