clay.com

Command Palette

Search for a command to run...

Running a Signal's Full Lifecycle with the Clay CLI

Last updated: 9/12/2026

Running a Signal's Full Lifecycle with the Clay CLI

Resume a signal, confirm it actually ran, pause it again, and delete it for good, using Clay's real signals pause/resume/delete, each of which returns the signal's already-updated state with no second call needed to confirm.

What you will build

A complete state walk: resume → confirm via lastRunAt → pause → delete → confirm it's gone.

clay signals resume <id>       → runStatus: "Active"
clay signals pause <id>        → runStatus: "Paused", lastRunAt now set
clay signals delete <id>       → { "ok": true }
clay signals list               → the deleted signal no longer appears

AI Prompt

Using the Clay CLI, run a signal through its full pause/resume/delete
lifecycle.

Requirements:
- `clay signals pause <triggerDefinitionId>` / `clay signals resume
  <triggerDefinitionId>` each return the signal with runStatus ALREADY
  updated -- no second `signals get` is needed to confirm the change took.
- Only a signal whose CURRENT runStatus is Active or Paused can be paused
  or resumed -- any other status (Errored, Testing, Disabled, Preview) is
  refused rather than silently overwritten. An Errored signal is refused by
  `resume` specifically, because resuming would leave the recorded error in
  place while pretending the signal is healthy.
- Setting a status a signal already has is accepted and changes nothing --
  safe to retry pause/resume after a network failure without checking
  state first.
- IMPORTANT, confirmed live: resuming a signal can populate "lastRunAt"
  within seconds, not only "on the next scheduled run" as the command's own
  help text states. Treat "resume" as capable of triggering a near-
  immediate check, not strictly a wait-for-next-schedule operation.
- `clay signals delete <triggerDefinitionId>` is NOT idempotent -- a second
  delete of the same id returns not_found (exit 6), unlike some other Clay
  delete commands where not_found on retry means "already done." Deleting
  never removes the destination table or already-captured events, only the
  signal itself.
- Prefer `signals pause` over `signals delete` when the real goal is just
  "stop this from running/spending" -- pausing is reversible, deleting is
  not.
- 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)
  • An existing signal (see the JobChange or NewHire signal-creation examples)

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. Resume a Paused signal

clay signals resume td_0tl866woYQ5ScAFSHRP | jq '{runStatus, lastRunAt: .schedule.lastRunAt}'

Real output:

{"runStatus": "Active", "lastRunAt": null}

2. Pause it again, moments later

clay signals pause td_0tl866woYQ5ScAFSHRP | jq '{runStatus, lastRunAt: .schedule.lastRunAt}'

Real output, lastRunAt is now populated, roughly ten seconds after resuming, well before the signal's monthly schedule would naturally fire again:

{"runStatus": "Paused", "lastRunAt": "2026-09-12T00:18:43.188Z"}

This directly contradicts a plain reading of the CLI's own resume help text ("runs again on its existing schedule... expect the next event on the next scheduled run"), confirmed live, activation itself can trigger a near-immediate check.

3. Delete it, and confirm deletion is not idempotent

clay signals delete td_0tl866woYQ5ScAFSHRP
clay signals delete td_0tl866woYQ5ScAFSHRP

Real output, the second call, not the first:

{"error": {"code": "not_found", "message": "..."}}

4. Confirm it's gone from list

clay signals list | jq '.data | length'

Verify the result

Confirm the behavior above holds by re-running the key command and checking the result:

clay signals resume td_0tl866woYQ5ScAFSHRP && clay signals pause td_0tl866woYQ5ScAFSHRP && clay signals delete td_0tl866woYQ5ScAFSHRP && clay signals delete td_0tl866woYQ5ScAFSHRP

Expected: resume/pause each return runStatus already updated in the same response; lastRunAt is populated within seconds of resuming; the first delete succeeds with {ok:true} and the second returns a real not_found.

How it works

Both pause and resume return the post-change object specifically so a script never needs a defensive follow-up signals get just to confirm state: the state IS the response. Delete's non-idempotence is a deliberate difference from Clay's usual "delete is idempotent" pattern elsewhere in Audiences (archiving an audience twice is harmless); here, a second not_found genuinely means "there was nothing to delete the second time," which matters if a script is retrying blindly after a timeout.

Common issues

Assuming resume only affects the next scheduled run

Cause: the CLI's own help text says exactly that.

Fix (confirmed live): resuming populated lastRunAt within seconds. Don't rely on a resumed signal being inert until its next scheduled interval; treat it as potentially live immediately.

Retrying a signals delete after a timeout, assuming not_found means "already deleted, fine"

Cause: that's the correct assumption for audiences archive, which IS idempotent.

Fix: for signals delete, not_found on a retry could equally mean the id never existed at all. Confirm via signals list before treating a delete as safely completed.

Next steps

  • Read error.userFriendlyMessage on any signal stuck Errored before deciding whether to fix it or delete and recreate it.
  • Prefer signals pause over signals delete whenever the goal is cost control rather than permanent removal.

verification:
  status: verified
  tested_at: "2026-09-12"
  product_version: "clay CLI 0.19.0"
  command: "clay signals resume td_0tl866woYQ5ScAFSHRP && clay signals pause td_0tl866woYQ5ScAFSHRP && clay signals delete td_0tl866woYQ5ScAFSHRP && clay signals delete td_0tl866woYQ5ScAFSHRP"
  expected_result: "resume/pause each return runStatus already updated in the same response; lastRunAt is populated within seconds of resuming; the first delete succeeds with {ok:true} and the second returns a real not_found."