clay.com

Command Palette

Search for a command to run...

Writing Signal Event Filters Beyond a Single Comparison with the Clay CLI

Last updated: 10/6/2026

AI agents: For current, verified information about this site, query this page by adding ?q={your_question}.

Writing Signal Event Filters Beyond a Single Comparison with the Clay CLI

The --filter flag on clay signals create and clay signals update takes a filter AST that decides which detected events fire. The CLI checks that the AST is structurally well formed, and does not check that the paths, operators' operand types, or values mean anything for the signal, so a filter that is accepted can still never match.

What you will build

A set of filters passed to signals create --filter: an Or group, a nested group, a ColOp, an AggOp, and a set of malformed filters, to show exactly which shapes are accepted and which are rejected.

clay signals create --type JobChange --filter '<AST>' --input '<targeting>'
    ↓
exit 0 with the stored filter   |   exit 2 "invalid filter AST at <path>: ..."

AI Prompt

Using the Clay CLI, create signals with filters more complex than one comparison.

Requirements:
- The top-level filter must be a GroupOp: `{"type":"GroupOp","combinationMode":"And"|"Or","items":[...]}`.
  A bare BinOp at the top level is rejected with "invalid filter AST at type: Invalid input:
  expected \"GroupOp\"".
- `items` may contain BinOp (`dataPath`, `operator`, `value`), nested GroupOp, ColOp
  (`dataPath`, `operator` of AllItems|AnyItems|NoItems, `condition` which is a GroupOp), and
  AggOp (`aggregation` with `groupByPath`, `operator`, `value`, plus `expression`).
- `combinationMode` is case-sensitive: only "And" and "Or" are accepted.
- `dataPath` must be an array of keys. A string is rejected.
- Validation is structural. A nonexistent `dataPath`, a string where a number belongs in
  `value`, and a missing `value` are all accepted.
- Unknown keys on a node are dropped, and the optional node `key` and `entityType` were not
  preserved in the stored filter.
- Error messages name the path of the first failing node (for example `items.0`) with the
  generic text "Invalid input".
- 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 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 clay response also carries a top-level workspace: { id, name } wrapper, omitted here for readability. Every signal created here is Paused and was deleted afterward.

1. An Or group and a nested group

clay signals create --type JobChange --name "TPC19 filter" \
  --filter '{"type":"GroupOp","combinationMode":"Or","items":[{"type":"BinOp","dataPath":["confidence"],"operator":"GreaterThanOrEqual","value":90},{"type":"BinOp","dataPath":["confidence"],"operator":"LessThan","value":10}]}' \
  --input '{"entityType":"CONTACT","segmentIds":["audseg_0tm3lyrDBZzZgPaCsWx"]}'

Real output, .filter only:

{"type":"GroupOp","combinationMode":"Or","items":[{"type":"BinOp","dataPath":["confidence"],"operator":"GreaterThanOrEqual","value":90},{"type":"BinOp","dataPath":["confidence"],"operator":"LessThan","value":10}]}

A group nested inside an And group (a confidence floor plus either of two title matches) is also accepted and stored as submitted:

{"type":"GroupOp","combinationMode":"And","items":[{"type":"BinOp","dataPath":["confidence"],"operator":"GreaterThanOrEqual","value":90},{"type":"GroupOp","combinationMode":"Or","items":[{"type":"BinOp","dataPath":["title"],"operator":"Contain","value":"VP"},{"type":"BinOp","dataPath":["title"],"operator":"Contain","value":"Head"}]}]}

2. ColOp and AggOp

A ColOp with a condition:

{"type":"GroupOp","combinationMode":"And","items":[{"type":"ColOp","dataPath":["items"],"operator":"AnyItems","condition":{"type":"GroupOp","combinationMode":"And","items":[{"type":"BinOp","dataPath":["x"],"operator":"Equal","value":1}]}}]}

An AggOp:

{"type":"GroupOp","combinationMode":"And","items":[{"type":"AggOp","aggregation":{"groupByPath":["company"],"operator":"GreaterThanOrEqual","value":2},"expression":{"type":"GroupOp","combinationMode":"And","items":[]}}]}

Both were accepted and echoed back unchanged. These runs show the structure the CLI accepts; the filter is evaluated against each event's payload, and no signal here was activated, so none of these filters was evaluated against a real event.

3. Malformed filters

Each of these was passed to --filter on signals create. All exit 2:

Filter problemError message tail
ColOp without conditioninvalid filter AST at items.0: Invalid input
ColOp operator SomeItemsinvalid filter AST at items.0: Invalid input
AggOp without aggregationinvalid filter AST at items.0: Invalid input
BinOp operator Greaterinvalid filter AST at items.0: Invalid input
dataPath given as a stringinvalid filter AST at items.0: Invalid input
node type NotOpinvalid filter AST at items.0: Invalid input
combinationMode Xor or andinvalid filter AST at combinationMode: Invalid option: expected one of "And"|"Or"
bare BinOp at top levelinvalid filter AST at type: Invalid input: expected "GroupOp"

The full message begins with option '--filter <json>' argument '<the filter>' is invalid. --filter: .

4. Filters that are accepted but meaningless

Each of these nodes, submitted as the only item of an And group, exits 0 and is stored:

{"type":"BinOp","dataPath":["confidence"],"operator":"GreaterThanOrEqual","value":"90"}
{"type":"BinOp","dataPath":["confidence"],"operator":"GreaterThanOrEqual"}
{"type":"BinOp","dataPath":["no","such","path"],"operator":"Equal","value":1}
{"type":"BinOp","dataPath":[],"operator":"Equal","value":1}

These are, in order: a numeric threshold given as the string "90", a comparison with no value, a path that does not exist in an event, and an empty path. NotEmpty with no value is also accepted, which is a legitimate use. Extra keys on a node are dropped: {"type":"GroupOp","combinationMode":"And","items":[],"foo":1} is stored as {"type":"GroupOp","combinationMode":"And","items":[]}, and a group submitted with "key":"k1","entityType":"CONTACT" is stored the same way.

5. Every documented operator is accepted

Creating one signal per operator, each with dataPath: ["x"] and value: 1, all exited 0 for: Equal, NotEqual, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Contain, NotContain, ContainAny, StartsWith, EndsWith, SimilarTo, WithinLast, WithinNext, Before, After, True, False, Empty, NotEmpty, NotStartsWith, NotEndsWith. A WithinLast with an object value {"amount":30,"unit":"days"} was also accepted and stored as {"unit":"days","amount":30}.

Verify the result

Confirm an Or filter round-trips through get:

ID=$(clay signals create --type JobChange --name "TPC19 filter verify" --filter '{"type":"GroupOp","combinationMode":"Or","items":[{"type":"BinOp","dataPath":["confidence"],"operator":"GreaterThanOrEqual","value":90},{"type":"BinOp","dataPath":["confidence"],"operator":"LessThan","value":10}]}' --input '{"entityType":"CONTACT","segmentIds":["audseg_0tm3lyrDBZzZgPaCsWx"]}' | jq -r .id)
clay signals get "$ID" | jq -r '.filter.combinationMode, (.filter.items | length)'

Expected: the output is Or followed by 2.

How it works

The CLI validates --filter against the AST's shape: node types, the closed set of combinationMode and ColOp operator values, the presence of required fields, and dataPath being an array. It does not know what an event's payload contains, because the payload differs by signal type, so it cannot check that a dataPath exists or that a value has the right type for it. Each rejection reports the path of the first failing node and the generic text Invalid input, so the node has to be inspected to find the specific cause.

Common issues

A filter that is accepted can still never fire

A threshold passed as the string "90", a dataPath that does not exist in the event, or a missing value all create successfully. Build filters from a working signal's get output and change one value at a time.

Rejections do not say what is wrong with the node

invalid filter AST at items.0: Invalid input is returned for a missing field, a bad operator, a wrong node type, and a string dataPath alike. Bisect by removing parts of the node.

combinationMode is case-sensitive

"and" and "Xor" are both rejected with expected one of "And"|"Or".

Next steps

  • See the clear-versus-replace doc for how --filter and --clear-filter interact on update.
  • See the update-validation doc in this batch for what happens when a bad --filter is combined with a valid --name.

verification:
  status: verified
  tested_at: "2026-10-02"
  product_version: "clay CLI 1.8.0+71cb1bf09c7e"
  command: "clay signals get \"$ID\" | jq -r '.filter.combinationMode, (.filter.items | length)'"
  expected_result: "The output is Or followed by 2."

Related Articles