# Screen pop rules

> How screen pop rules match a delivered or established call, how priority is resolved, and what each action, render surface, and URL token does.

A **screen pop rule** takes a call that the CTI layer has just delivered to an
agent and opens a URL for that agent — a CRM contact record, an order lookup, an
internal page. Each rule stores the match conditions (ANI, DNIS, direction,
trunk), the event it fires on, an action, a render surface, and a URL template
whose `{{token}}` placeholders are substituted with the call's own values.

Rules live in the `screen_pop_rule` table, one set per org, and are managed from
**Admin → Screen pop** in the TeleQuick agent suite.

## How a rule is matched

When a call reaches the `delivered` or `established` state, the rule set for the
org is evaluated. A rule matches only if **all four** of its conditions hold for
that call:

| Condition       | Compared against        | Column          |
| --------------- | ----------------------- | --------------- |
| Caller number   | ANI                     | `ani_pattern`   |
| Dialed number   | DNIS                    | `dnis_pattern`  |
| Call direction  | `inbound` / `outbound`  | `direction`     |
| Trunk           | The call's trunk        | `trunk_pattern` |

In addition, the rule's `trigger_event` must equal the event being processed,
and the rule must be `active`. Inactive rules are never matched; they are kept
so that you can stage a rule before turning it on, and they are hidden from the
console list unless you check **Include inactive**.

Only one rule pops per event: **the first match wins**. The remaining rules are
not evaluated once a match is found.

## Pattern grammar and priority order

`ani_pattern`, `dnis_pattern`, and `trunk_pattern` are globs. `*` matches
anything, which makes `*` the "don't care" value for that field. All three
default to `*` in the form, and an empty input is stored as `*` rather than as
an empty string — a blank pattern field can never accidentally narrow a rule.

`direction` is not a glob. It is one of three literal values:

| Value      | Matches                    |
| ---------- | -------------------------- |
| `any`      | Inbound and outbound calls |
| `inbound`  | Inbound only               |
| `outbound` | Outbound only              |

`priority` is an integer between `0` and `10000` and decides which rule is
consulted first. Give every rule a distinct priority. Two rules that can match
the same call at the same priority leave the outcome dependent on the tie-break,
and the console shows you nothing but the `Prio` column — there is no visible
secondary ordering to reason about. The console lists rules with the priority as
the first column precisely so that the evaluation order is the thing you scan.

A practical layout is: specific rules (one customer's ANI, one DID) at low
priority numbers, and a catch-all `* → *` rule as the last resort at a much
higher number, with gaps in between so that you can insert rules later without
renumbering.

## Trigger events: delivered vs established

`trigger_event` pins the rule to exactly one point in the call's lifecycle.

| Value         | Fires when                          | Use it when |
| ------------- | ----------------------------------- | ----------- |
| `delivered`   | The call is ringing at the agent    | The agent should have the record on screen before they answer. |
| `established` | The call has been answered          | The pop should not happen for calls the agent never takes. |

A rule fires on one event, not both. If you want the record on screen at ring
time *and* a second surface after answer, create two rules with the same
patterns and different `trigger_event` values.

See [Routing events](/modalities/voice/cti/routing-events) for the definitions
of the `delivered` and `established` events themselves.

## Action kinds and render surfaces

Two separate fields describe what happens. `action` classifies the kind of
target; `render` names the surface the agent suite uses.

`action` is one of:

| Action        | Meaning |
| ------------- | ------- |
| `url`         | An external URL built from the template. |
| `iframe`      | An embedded document built from the template. |
| `internal`    | A destination inside the agent suite. |
| `crm_webhook` | A CRM integration target rather than a page the agent navigates to. |

`render` is one of `new_tab`, `iframe`, `modal`, or `side_panel`, and selects
where the agent suite puts the result — a new browser tab, an inline frame, a
modal dialog over the agent's current screen, or the suite's side panel.

`url_template` is validated the same way for every action value, so an
`internal` or `crm_webhook` rule is still stored with (and still requires) a
well-formed `http`/`https` template. `url_template` is nullable: if you clear
the field, `null` is stored.

`title` is an optional label — for example `Customer` — carried with the rule so
the render surface can be captioned. It is stored as `null` when left blank.

## URL template tokens and encoding

`url_template` is a plain `http` or `https` URL containing `{{token}}`
placeholders. The supported tokens are:

| Token           | Substituted with              |
| --------------- | ----------------------------- |
| `{{ani}}`       | The caller's number           |
| `{{dnis}}`      | The dialed number             |
| `{{callId}}`    | The call's id                 |
| `{{direction}}` | `inbound` or `outbound`       |
| `{{trunk}}`     | The call's trunk              |
| `{{agent}}`     | The agent the call was delivered to |

Substituted values are **URL-encoded server-side**. Do not pre-encode them and
do not wrap a token in your own escaping — a `+` in an E.164 ANI is handled for
you, and double-encoding is the usual cause of a CRM lookup that returns "no
such contact" for a number that plainly exists.

Whitespace inside the braces is tolerated: `{{ani}}` and `{{ ani }}` are both
recognised as tokens.

A typical template:

```
https://crm.example.com/contact?phone={{ani}}&call={{callId}}
```

Tokens may appear in the path as well as the query string, and a template with
no tokens at all is valid — it pops the same static page for every matching
call.

## Managing rules from the console

The screen is a thin CRUD surface over four procedures:

| Procedure           | Input                                                        | Notes |
| ------------------- | ------------------------------------------------------------ | ----- |
| `screenPop.list`    | `{ orgId, search?, includeInactive, page, perPage }`         | Returns `{ rows, total }`. The console requests 50 rows per page. |
| `screenPop.create`  | `{ orgId, row }`                                             | `row` carries every field except `id` and `active`. |
| `screenPop.update`  | `{ orgId, id, patch }`                                       | `patch` carries every editable field **including** `active`. |
| `screenPop.remove`  | `{ orgId, id }`                                              | Permanent. The console asks for confirmation first. |

The **Active** checkbox only appears when editing an existing rule, because
`create` does not accept it. To stage a rule as inactive, create it and then
open it again and clear **Active**.

The search box filters on rule name *and* pattern, so you can find every rule
that mentions a particular DID by pasting the number in. Typing in the search
box or toggling **Include inactive** resets you to the first page.

## Field reference

| Field           | Type                                                | Default in the form |
| --------------- | --------------------------------------------------- | ------------------- |
| `name`          | string, required                                    | empty               |
| `priority`      | integer `0`–`10000`                                 | `10`                |
| `ani_pattern`   | glob                                                | `*`                 |
| `dnis_pattern`  | glob                                                | `*`                 |
| `direction`     | `any` \| `inbound` \| `outbound`                    | `any`               |
| `trunk_pattern` | glob                                                | `*`                 |
| `trigger_event` | `delivered` \| `established`                        | `delivered`         |
| `action`        | `url` \| `iframe` \| `internal` \| `crm_webhook`    | `url`               |
| `url_template`  | `http`/`https` URL with tokens, nullable            | empty → `null`      |
| `render`        | `new_tab` \| `iframe` \| `modal` \| `side_panel`    | `new_tab`           |
| `title`         | string, nullable                                    | empty → `null`      |
| `active`        | boolean (edit only)                                 | —                   |

## Testing and common failures

Validation is *reveal-on-save*: the form does not mark anything red while you
type, and shows field errors only after you press **Save** with an invalid
value.

- **"Name is required."** `name` is trimmed before saving, so a name of only
  spaces is rejected.
- **"Priority must be a whole number."** Clearing the priority input leaves the
  controlled number field as `NaN`, not `0`. Retype a value. Values outside
  `0`–`10000` are rejected the same way.
- **Invalid URL template.** The template is validated *after* its `{{token}}`
  placeholders are substituted, so `https://crm.example.com/c?p={{ani}}` passes.
  A template with a scheme other than `http` or `https`, or one that is
  malformed once tokens are removed, fails.
- **The rule never pops.** Check, in order: the rule is `active`; its
  `trigger_event` matches the event you expect (a `delivered` rule will not fire
  on answer); `direction` is not narrower than the traffic; and no
  higher-precedence rule with a catch-all `* → *` pattern is matching first.
- **The wrong rule pops.** Two rules that both match at the same priority. Give
  them distinct priorities and re-test.
- **The CRM lookup misses.** Confirm you are not encoding token values yourself.
  Substitution is URL-encoded for you.

Save errors from the server are surfaced twice: inline under the form buttons
and as a toast.

## Related

- [Routing events](/modalities/voice/cti/routing-events) — the `delivered` and
  `established` events that rules trigger on
- [Telemetry](/platform/telemetry) — ANI, DNIS, trunk, and call id as they
  appear in CDRs
