# 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

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

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

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.
