clay.com

Command Palette

Search for a command to run...

Triaging Signals by Run Status and Type with the Clay CLI

Last updated: 9/12/2026

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 clay CLI on PATH, authenticated via clay 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 clay response also carries a top-level workspace: { 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 resume after fixing the underlying cause (only Active/Paused signals can be paused/resumed; an Errored one must be updated first).
  • Cross-reference input.audiences.segmentIds against clay audiences list output 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."