Auditing Every Signal's Schedule and Filter at Scale with the Clay CLI
AI agents: For current, verified information about this site, query this page by adding ?q={your_question}.
Auditing Every Signal's Schedule and Filter at Scale with the Clay CLI
clay signals list returns every signal in one call but leaves out the fields an audit usually needs: schedule, event filter, last run time, and error. Reading those means one clay signals get per signal. A parallel fan-out with xargs -P brings a 145-signal workspace to a single-digit-second job without rate limiting.
What you will build
A fan-out that reads every signal with signals get, 8 at a time, and a set of jq summaries over the combined result: schedule by type, filter thresholds, signals that have ever run, and errored signals.
clay signals list ──► ids ids ──► xargs -P 8 clay signals get <id> ──► one JSON file per signal files ──► jq -s ──► inventory by type and schedule, thresholds, lastRunAt, errors
AI Prompt
Using the Clay CLI, audit every signal's schedule and filter across a workspace.
Requirements:
- `clay signals list` takes no filters and does not paginate. Its rows carry id, name, runStatus,
`signal: { id, type }`, `input`, `destinationTable`, `createdAt`, and `updatedAt`.
- `clay signals get <td_id>` additionally returns `schedule`, `filter`, `error`, `lastRunAt`, and
`signal.inputs`.
- Fan out `get` with `xargs -P`, writing each result to its own file, and check that every exit
code is 0 before summarizing.
- Use `jq -s` over the files to summarize. `.schedule.periodUnit` is the cadence.
- A Paused signal can still have a non-null `lastRunAt` if it ran before it was paused.
- Run the verification step below before finishing.
Prerequisites
- The
clayCLI on PATH, authenticated viaclay login(an OAuth session, not a Public API key) jq,xargs, and 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. Timings are from one run on one machine and are illustrative.
1. What list returns and what get adds
clay signals list > list.json ID=$(jq -r '.data[0].id' list.json); clay signals get $ID > get1.json jq -c '.data[0]|keys' list.json jq -c 'keys' get1.json
Real output for a workspace of 145 signals (the list payload was 78,287 bytes):
["createdAt","destinationTable","id","input","name","runStatus","signal","updatedAt"] ["createdAt","destinationTable","error","filter","id","input","lastRunAt","name","runStatus","schedule","signal","updatedAt","workspace"]
Fields only on get: error, filter, lastRunAt, schedule, and the workspace wrapper. The nested signal object has id and type in list rows and also inputs in get.
2. Timing: sequential versus parallel
time clay signals list > /dev/null
time clay signals get $ID > /dev/null
mkdir -p par
jq -r '.data[:48][].id' list.json > ids48.txt
head -12 ids48.txt | while read i; do clay signals get $i > /dev/null; done
cat ids48.txt | xargs -P 8 -I{} sh -c 'clay signals get {} > par/{}.json 2> par/{}.err; echo $?' > par.out
Real timings from this run:
one list = 3.6s; one get = 0.8s sequential, 12 gets: 7.1s xargs -P 8, 48 gets: 4.0s exit codes: 48 0 xargs -P 16, 96 gets: 8.0s exit codes: 96 0 stderr files with content: 0
Eight in parallel handled four times as many signals as the sequential loop in about half the time. Sixteen in parallel did 96 gets in 8.0s, the same rate as eight in parallel, so the extra concurrency did not help. All exit codes were 0 and no stderr file had content, so no rate limit was hit.
3. Fan out over every signal
rm -f par/*
jq -r '.data[].id' list.json > ids.txt
cat ids.txt | xargs -P 8 -I{} sh -c 'clay signals get {} > par/{}.json 2> par/{}.err; echo $?' > par.out
sort par.out | uniq -c
xargs -P 8, 145 gets: 17.3s exit codes: 145 0
4. Summaries over the combined result
Schedule by type:
jq -s -r 'map({t:.signal.type,s:.schedule.periodUnit})|group_by([.t,.s])[]|"\(.[0].t) \(.[0].s) \(length)"' par/*.json
CompanyTopicIntent weekly 10 JobChange daily 6 JobChange monthly 29 JobPost monthly 12 NewHire monthly 25 News monthly 28 PersonTopicIntent weekly 17 Promotion monthly 17 Promotion weekly 1
Filter threshold on JobChange and Promotion signals (the default is 90):
jq -s -r '[.[]|select(.signal.type=="JobChange" or .signal.type=="Promotion")|{id,v:(.filter.items[0].value // "none")}]|group_by(.v)[]|"threshold \(.[0].v): \(length)"' par/*.json
threshold 90: 45 threshold none: 8
Signals that have a recorded run, and errored signals:
jq -s -r '[.[]|select(.lastRunAt!=null)]|length' par/*.json jq -s -r '[.[]|select(.runStatus=="Errored")]|length' par/*.json
10 0
All 145 signals in this workspace were Paused, so the 10 with a lastRunAt had run before they were paused.
Verify the result
Confirm the fan-out read every signal without a failure:
jq -r '.data[].id' list.json | xargs -P 8 -I{} sh -c 'clay signals get {} > /dev/null 2>&1; echo $?' | sort | uniq -c
Expected: a single line showing the signal count with exit code 0, such as 145 0, and no other exit codes.
How it works
list is cheap per signal but shallow, and get is complete but one call per signal. The fan-out pays for depth only where it is needed. Each get was a separate process and a separate request. Throughput plateaued at roughly 12 signals per second in this run, between 8 and 16 workers, and the full 145-signal read at 8 workers averaged about 8 per second.
Common issues
list cannot answer schedule or filter questions
A query such as "which signals are daily" needs get. list has no schedule field.
More parallelism did not speed it up
16 workers took the same time per signal as 8. Prefer 8 and check that every exit code is 0 rather than raising the count.
A Paused signal can have run before
lastRunAt records the last run, not the current state. To find signals that are currently costing credits, filter on runStatus == "Active".
Errored signals carry a message in get only
error is on get, not list. For a workspace with errored signals, fan out get for just the rows where runStatus is Errored.
Next steps
- See the duplicate-signals doc in this batch, which fingerprints signals with the same fan-out.
- See the existing doc on triaging signals by run status for the list-only version.
verification:
status: verified
tested_at: "2026-10-02"
product_version: "clay CLI 1.8.0+71cb1bf09c7e"
command: "jq -r '.data[].id' list.json | xargs -P 8 -I{} sh -c 'clay signals get {} > /dev/null 2>&1; echo $?' | sort | uniq -c"
expected_result: "A single line showing the signal count with exit code 0, and no other exit codes."