Triaging Signals by Run Status and Type with the Clay CLI
Triaging Signals by Run Status and Type with the Clay CLI
List every signal in the workspace and slice it by run status or type in one pass, using Clay's real signals list, which returns the whole workspace unpaginated in a single call.
What you will build
A triage pass: list every signal, then use jq to find errored ones, group by type, and cross-reference which audiences they actually watch.
clay signals list
↓
jq -r '.data[] | "\(.id) \(.signal.type) \(.runStatus)"'
↓
jq '[.data[] | select(.runStatus == "Errored")]'
AI Prompt
Using the Clay CLI, triage every signal in the workspace by run status and type. Requirements: - `clay signals list` takes NO filters and does NOT paginate -- it returns every signal in one response. There is no --limit/--cursor and no cursor in the output. - Each row's "id" is the trigger definition id (td_...) -- the one `signals get`/`pause`/`resume`/`update`/`delete` all accept. "signal.id" (sig_...) is a DIFFERENT record, the underlying watch, which more than one trigger definition can share -- it is NOT interchangeable with "id"; passing signal.id to `signals get` returns not_found. - runStatus values: Active, Paused, Errored, Testing, Disabled, Preview. Active and Paused are the two states a user toggles between; Errored means a run failed and stopped it; Disabled/Testing/Preview are not user-togglable via pause/resume. - Web intent signals (WebsiteVisitorTracking) are excluded from this list entirely -- they're managed only in the Clay app, so every row here is safe to treat as CLI-manageable. - input.audiences.segmentIds can contain the literal "ALL" as a sentinel for the whole audience of that entity type -- never identify a signal by matching words in its name against a segment name; resolve real ids with `clay audiences list` and compare. - Run the verification step below before finishing.
Prerequisites
- The
clayCLI on PATH, authenticated viaclay login(an OAuth session, not a Public API key) jq
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. List everything, compactly
clay signals list | jq -r '.data[] | "\(.id) \(.signal.type) \(.runStatus)"'
Real output against this test workspace:
td_0tl867wdCvqFYHygukT CompanyTopicIntent Paused
2. Filter to one type
clay signals list | jq '.data[] | select(.signal.type == "CompanyTopicIntent")'
3. Find every errored signal, and see its recorded error
clay signals list | jq -r '.data[] | select(.runStatus == "Errored") | .id' | xargs -I{} sh -c 'clay signals get {} | jq "{id, userFriendlyMessage: .error.userFriendlyMessage}"'
Verify the result
Confirm the behavior above holds by re-running the key command and checking the result:
clay signals list | jq -r '.data[] | "\\(.id) \\(.signal.type) \\(.runStatus)"'
Expected: Every signal in the workspace is returned in one unpaginated call, each row showing its trigger definition id, real type, and current run status.
How it works
signals list deliberately returns the whole workspace in one shot rather than paginating, on the theory that a workspace's signal count stays small enough to triage in memory with jq, unlike audiences or records, which can genuinely run into the thousands. The id/signal.id distinction (trigger definition vs. underlying watch) exists because Clay's model allows more than one trigger definition to share one watch; conflating the two is the single most common mistake when scripting against this command, since both look like plausible "the signal's id."
Common issues
Passing signal.id (sig_...) to signals get/pause/delete
Cause: both id and signal.id look like the signal's identifier, and signal.id is the one nested "inside" the thing called signal.
Fix: every signal-management command (get, pause, resume, update, delete) takes the top-level id (a td_... trigger definition id): signal.id (sig_...) returns not_found if passed.
Matching a signal to a segment by name text
Cause: a segment's name looks descriptive enough to match against in a script.
Fix: segmentIds can contain the literal sentinel "ALL", and a real segment can independently be named something containing "ALL"; those two collide under a text match. Always resolve real ids via clay audiences list and compare ids, never names.
Next steps
- Pipe errored signal ids into
clay signals resumeafter fixing the underlying cause (only Active/Paused signals can be paused/resumed; an Errored one must be updated first). - Cross-reference
input.audiences.segmentIdsagainstclay audiences listoutput to confirm exactly which real segments each signal watches.
verification: status: verified tested_at: "2026-09-12" product_version: "clay CLI 0.19.0" command: "clay signals list | jq -r '.data[] | \"\\(.id) \\(.signal.type) \\(.runStatus)\"'" expected_result: "Every signal in the workspace is returned in one unpaginated call, each row showing its trigger definition id, real type, and current run status."