# Filter vs apply-to (/docs/rules/match-vs-groupby)



When you build a custom rule (or open a template to see how it works), the two settings that
matter most are the **IF** filter and the **APPLY TO** pool. Get these right and the rest is
just numbers.

## IF — which calls the rule watches [#if--which-calls-the-rule-watches]

The filter picks the subset of traffic the rule cares about. Rules can match on any of these
fields:

| Field            | What it matches                                                                                |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| `user_id`        | The end user your app identifies on the call.                                                  |
| `paid_plan`      | The plan string your app stamps on the call.                                                   |
| `model`          | The model that ran.                                                                            |
| `provider`       | The provider that ran it.                                                                      |
| `operation`      | The kind of call — chat, embedding, image, and so on.                                          |
| `session_id`     | Your app's conversation or session id.                                                         |
| `trace_id`       | One agent run.                                                                                 |
| `metadata.<key>` | Any custom tag your app attaches — `metadata.team`, `metadata.feature`, `metadata.department`. |

The engine accepts all of them. What you can *author* today is narrower, and it is worth being
precise about where the line falls:

|                                                                | Custom rule builder | Templates                                  | API |
| -------------------------------------------------------------- | ------------------- | ------------------------------------------ | --- |
| Match on `user_id`, `paid_plan`, `operation`, `metadata.<key>` | ✅                   | ✅ (`paid_plan`, `user_id`, a metadata key) | ✅   |
| Match on `session_id`                                          | —                   | ✅ (the session templates)                  | ✅   |
| Match on `model`, `provider`, `trace_id`                       | —                   | —                                          | ✅   |
| Operators **is**, **contains**, **exists**                     | ✅                   | ✅ (**is**, **exists**)                     | ✅   |
| Operators **is not**, **in**                                   | —                   | —                                          | ✅   |
| Composite **AND** / **OR** filters                             | —                   | —                                          | ✅   |

"API" means a direct call to the rules API with a management key — there is no dashboard or
template route to those rows yet. If you need one of them, build the rule through the API and it
will show up in the rules table like any other.

<Callout type="warn">
  **is** is an *exact&#x2A; match — case- and whitespace-sensitive. The dashboard says it plainly:
  &#x2A;"Values are compared case-sensitively, spaces included — `free` never matches `Free`."* The
  same is true of **APPLY TO**: pooling by `paid_plan` gives `free` and `Free` two separate
  budgets. Send one canonical spelling from your app (lowercase and trimmed is the safe
  convention) and use exactly that string in the rule.
</Callout>

### More than one condition [#more-than-one-condition]

A rule's filter can be a single condition or a composite one: an **AND** or **OR** group of up to
**20** conditions. Groups don't nest — one combinator, one flat list. The rules table renders
composite filters as `IF … AND …` / `IF … OR …`, and flags two degenerate shapes for you: an
empty AND matches *everything*, an empty OR matches *nothing*.

Composite filters are &#x2A;*API-only today.** The enforcement engine evaluates them, and the rules
table displays them, but neither the Custom rule builder nor any template will author one — the
builder sends a single condition. Create composites through the rules API.

## APPLY TO — who gets their own budget [#apply-to--who-gets-their-own-budget]

This decides how the limit is *pooled*:

| Apply to                                                  | Meaning                                              |
| --------------------------------------------------------- | ---------------------------------------------------- |
| **Everyone matching this rule*&#x2A; &#x2A;(shared pool)* | One budget shared across all matching traffic.       |
| **Each unique `user_id`*&#x2A; &#x2A;(individual pool)*   | Every user gets their own copy of the limit.         |
| **Each unique `<field>`*&#x2A; &#x2A;(individual pool)*   | Every distinct value of the field gets its own copy. |

You can group by any of the fields above, not just `user_id` — grouping by `metadata.team` gives
every team its own budget from one rule.

<Callout type="info">
  The filter and the pool are independent. The filter says *which* calls count; the pool says
  *how many separate budgets* the limit becomes.
</Callout>

## A worked example [#a-worked-example]

> Give **every** user on the **free*&#x2A; plan their own **$5 / month** cap.

* **IF** `paid_plan` **is** `free` — the rule only watches free-plan calls.
* **APPLY TO** each unique `user_id` — so the $5 is *per user*, not $5 shared across all of them.

Flip APPLY TO back to *everyone matching* and the same rule becomes "$5 / month shared across
the entire free tier" — a very different limit from the same filter.

## When the limit resets [#when-the-limit-resets]

The third setting that changes a rule's meaning is its reset period. There are three kinds, and
they behave differently.

| Reset period                                  | Set it with                                                              | When a paused pool comes back                                                                                                         |
| --------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Calendar** — hourly, daily, weekly, monthly | The **Resets every** dropdown in the builder and most templates          | At the start of the next period, aligned to the **UTC calendar**: a day starts at UTC midnight, a week on Monday, a month on the 1st. |
| **Sliding** — 5 to 1440 minutes               | The session and per-tag templates ("resets after N minutes of no spend") | Only after that many minutes with **no matching spend** for that pool.                                                                |
| **Lifetime**                                  | Choosing *Lifetime*, or setting a limit with no period                   | Never. The cap is permanent until you change the rule.                                                                                |

<Callout type="warn">
  A sliding window is an **inactivity** window, not a rolling "last N minutes" window. While a
  pool keeps spending with no quiet gap that long, its total keeps growing and its budget never
  resets. That fits something that genuinely goes idle — a chat session, one customer's burst of
  work. For a pool that is always busy, or a cap that must reset on a schedule, use a calendar
  period instead.
</Callout>

The rules table shows which one a rule uses: *per DAILY*, *resets 30m after last spend*, or
*LIFETIME*.

## Next [#next]

<Cards>
  <Card title="Actions" href="/docs/rules/actions" description="What happens when a pool crosses its limit — or on every call, if there's no limit." />

  <Card title="Identity" href="/docs/concepts/identity" description="How your app supplies user_id, paid_plan and custom tags." />

  <Card title="Multi-tenant budgets" href="/docs/recipes/multi-tenant-budgets" description="A per-customer budget recipe using these two settings." />
</Cards>
