Cloning a Signal with the Clay CLI
AI agents: For current, verified information about this site, query this page by adding ?q={your_question}.
Cloning a Signal with the Clay CLI
clay signals get returns a signal's signal.inputs in the same shape clay signals create --input accepts, so a signal can be copied by piping one command into the other. The inputs carry only what the signal watches; the name, schedule, and event filter are stored elsewhere on the signal and have to be copied separately.
What you will build
A shell recipe that copies a signal in two stages, first with --input alone to show which settings do not carry over, then with the name, schedule, and filter copied as well.
clay signals get <td_id>
↓ .signal.inputs
clay signals create --type <same type> --input -
↓ plus .name, .schedule.periodUnit, .filter
a new Paused signal with the same targeting, schedule, and filter
AI Prompt
Using the Clay CLI, clone an existing signal into a new one. Requirements: - Read the source with `clay signals get <td_id>`. Its `signal.inputs` object can be passed straight back to `clay signals create --type <type> --input -` (stdin) or `--input <file>`. - `signal.inputs` includes a `type` key. If it disagrees with `--type`, create fails with validation_error (exit 2). - `--input` copies what the signal watches and its per-type settings (for example `lookBackTimeWindowInMonths`). It does not copy the name, the schedule, or the event filter. Without `--name`, the clone gets the type's default name; without `--schedule`, the type's default schedule; without `--filter`, the type's default filter. - To copy those three, pass `--name`, `--schedule` (from `.schedule.periodUnit`), and `--filter` (from `.filter`) explicitly. - A new signal is created Paused unless `--activate` is passed. - Run the verification step below before finishing.
Prerequisites
- The
clayCLI on PATH, authenticated viaclay login(an OAuth session, not a Public API key) jq- An existing audience segment for the people entity type (this doc uses
audseg_0tm3lyrDBZzZgPaCsWx)
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. Create a source signal with non-default settings
clay signals create --type JobChange --name "TPC19 champions weekly" --schedule weekly \
--filter f97.json \
--input '{"entityType":"CONTACT","segmentIds":["audseg_0tm3lyrDBZzZgPaCsWx"],"lookBackTimeWindowInMonths":6}'
f97.json holds a filter requiring confidence >= 97 instead of the default 90. Real output, filtered with jq to the settings that matter here:
{"id":"td_0tmb2gerNf5UeWgg7Gu","name":"TPC19 champions weekly","sched":"weekly","filter":97,"lb":6}
2. Read what get returns for the inputs
clay signals get td_0tmb2fgbeg6dVh73MwS | jq -c '.signal.inputs'
This was run on a separate signal created with default settings. Real output:
{"type":"JobChange","lookBackTimeWindowInMonths":3,"entityType":"CONTACT","segmentIds":["audseg_0tm3lyrDBZzZgPaCsWx"]}
The object includes type and the defaulted lookBackTimeWindowInMonths, in addition to the two keys that were submitted.
3. Clone with --input alone
clay signals get td_0tmb2gerNf5UeWgg7Gu > src.json jq -c .signal.inputs src.json | clay signals create --type JobChange --input -
Real output, filtered to name, schedule, filter threshold, and look-back:
{"id":"td_0tmb2gfJi7opmgxwbYF","name":"Event: Job change","sched":"monthly","filter":90,"lb":6}
The targeting and the look-back of 6 months carried over. The name reverted to the type's default (Event: Job change), the schedule to monthly, and the filter threshold to 90.
4. Clone with the name, schedule, and filter copied
jq -c .signal.inputs src.json | clay signals create \ --type "$(jq -r .signal.type src.json)" \ --name "$(jq -r .name src.json) (copy)" \ --schedule "$(jq -r .schedule.periodUnit src.json)" \ --filter "$(jq -c .filter src.json)" \ --input -
Real output:
{"id":"td_0tmb2ggHTVHVz8GUrkY","name":"TPC19 champions weekly (copy)","sched":"weekly","filter":97,"lb":6,"runStatus":"Paused"}
5. Cloning with a mismatched type
clay signals get td_0tmb2fgbeg6dVh73MwS | jq -c .signal.inputs | clay signals create --type Promotion --input -
Real output (stderr, exit 2):
{"error":{"code":"validation_error","message":"--input: type is \"JobChange\", which disagrees with the signal type Promotion"}}
6. Confirm every schedule value round-trips
.schedule.periodUnit is accepted by --schedule for all five cadences. A signal created with each of --schedule daily|weekly|biweekly|monthly|quarterly stores:
{"periodAmount":1,"periodUnit":"daily"}
{"periodAmount":1,"periodUnit":"weekly"}
{"periodAmount":2,"periodUnit":"biweekly"}
{"periodAmount":1,"periodUnit":"monthly"}
{"periodAmount":1,"periodUnit":"quarterly"}
biweekly is stored with periodAmount: 2 and periodUnit: "biweekly", so copying periodUnit alone preserves it.
Verify the result
Confirm the full clone matches its source on name stem, schedule, filter, and look-back:
diff <(jq -S '{sched: .schedule.periodUnit, filter, lb: .signal.inputs.lookBackTimeWindowInMonths, seg: .input}' src.json) \
<(clay signals get td_0tmb2ggHTVHVz8GUrkY | jq -S '{sched: .schedule.periodUnit, filter, lb: .signal.inputs.lookBackTimeWindowInMonths, seg: .input}') && echo identical
Expected: the command prints identical, because schedule, filter, look-back, and targeting match between source and clone.
How it works
signals get splits a signal's state across several top-level fields: signal.inputs holds what it watches and its per-type settings, while name, schedule, and filter are separate fields. signals create --input accepts the same shape signal.inputs has, which is what makes the pipe work, but it only receives that one field. The remaining three arrive through their own flags, and each falls back to the type's default when omitted.
Common issues
The clone has the default name and a 90 confidence threshold
Piping signal.inputs alone leaves --name, --schedule, and --filter unset, so the new signal gets the type's defaults for each. Pass all three from the source when an exact copy is the goal.
Stdin and file input are both supported
--input - reads stdin and --input <path> reads a file. The result is the same for both.
A clone is created Paused
--activate is not copied from anything, so the clone is Paused and does not run or spend until it is resumed. Cloning from an Active source was not tested.
Next steps
- Use
clay signals updateto adjust the clone's schedule or filter afterward. - See the reconcile doc in this batch to manage a whole set of signals from a manifest instead of cloning one at a time.
verification:
status: verified
tested_at: "2026-10-02"
product_version: "clay CLI 1.8.0+71cb1bf09c7e"
command: "diff <(jq -S '{sched: .schedule.periodUnit, filter, lb: .signal.inputs.lookBackTimeWindowInMonths, seg: .input}' src.json) <(clay signals get <clone-id> | jq -S '{sched: .schedule.periodUnit, filter, lb: .signal.inputs.lookBackTimeWindowInMonths, seg: .input}') && echo identical"
expected_result: "The command prints identical, because schedule, filter, look-back, and targeting match between source and clone."