Custom signals
A custom signal is a named boolean SQL predicate. Once you create one, it behaves like a built-in column: reference it by name anywhere a WHERE clause is accepted, or use it as a signal name across the read and subscribe endpoints.
Where you can reference one
A custom signal is a filter, not a returnable column — you match with it, you don’t select it.
| Surface | Works? | How |
|---|---|---|
Scan q (POST /v2/scan) | Yes | Name it in the WHERE clause; it expands to its stored SQL at request time. |
Signal name — /v2/signals/{signal} match, /history, /events | Yes | Pass the custom’s name as the signal. It’s indistinguishable from a built-in. |
Ticker subscribe condition | Yes | Referenced in the condition, then frozen (see below). |
| Webhook triggers (scan / ticker / signal) | Yes | Expanded and frozen into the stored trigger at creation. |
Signal catalog (GET /v2/signals?kind=custom) | Yes | Listed alongside built-ins, with its expr returned. |
Scan select / fields / order | No | These return or sort columns; a custom is a boolean predicate — filter with it in q instead. |
Events grammar (/v2/events q / event_q) | No | That grammar is a fixed four-column namespace (ticker, ts, kind, payload); custom names aren’t resolved there. |
Signals can reference other signals
Compose small predicates into bigger ones — up to five levels deep.
A custom signal’s expression may reference other custom signals by name. References expand recursively at request time, up to five levels of nesting; a deeper chain — or a circular reference — returns 400 invalid_query.
Two guardrails are enforced when you create a signal, so a reference is always an unambiguous lookup: a signal can’t reference itself (rejected at create), and its name can’t collide with a built-in column name (returns 409 name_collision).
Subscriptions freeze the definition
A webhook captures the SQL as it was the moment you created it.
When a custom signal is used in a webhook trigger, its SQL is expanded and stored on the subscription at creation time. Editing the signal later does not retroactively change existing webhooks — they keep firing on the definition they were created with. Re-create the subscription to pick up a new definition.
Keep them single-ticker for cross-endpoint use
Cross-ticker references work in scan, but not on the signal endpoints.
Scan rewrites cross-ticker references (e.g. spy.close) as it expands your query, so a custom signal that compares one ticker against another works in a /v2/scan q. The /v2/signals family (match, history, events, subscribe) does not perform that rewrite, so a custom that uses cross-ticker references will return a 400 there. Keep a signal single-ticker if you want to use it everywhere.