> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Behavioral Metrics

> Computed aggregations over interaction history -- use in contact policies, decisioning gates, and scoring.

<Note>
  **See also**: [Behavioral Metrics REST API reference](/api-reference/behavioral-metrics) for request/response shapes, status codes, and error semantics.
</Note>

## Overview

**Behavioral metrics** let you define computed aggregations over a customer's interaction history. They transform raw event data (impressions, clicks, conversions) into meaningful signals like "email frequency in the last 7 days" or "conversion rate for credit card offers." You use these metrics to power dynamic rules in contact policies, qualification, and scoring.

<Info>
  Behavioral Metrics is the platform's single behavioral-aggregation surface. The older **Summary Definitions** feature has been [retired](/studio/summary-definitions) — it was never wired to an aggregation engine. Behavioral Metrics materializes real per-customer `MetricValue` rows (realtime or batch) and is what [Decisioning Gates](/decisioning/qualification-rules) actually read via the `metric_condition` rule type.
</Info>

## Field Reference

| Field               | Type      | Required | Default    | Description                                                                                                                                                        |
| ------------------- | --------- | -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`              | string    | Yes      | --         | Display name (1-100 characters)                                                                                                                                    |
| `description`       | string    | No       | `""`       | Human-readable description (max 500 characters)                                                                                                                    |
| `aggregateFunction` | enum      | Yes      | --         | Aggregation method: `count`, `sum`, `avg`, `min`, `max`, `ratio`                                                                                                   |
| `sourceField`       | enum      | Yes      | --         | Outcome field to aggregate: `impressions`, `positive`, `negative`, `neutral`, `converts`, `totalValue`                                                             |
| `windowDays`        | number    | No       | `null`     | Rolling lookback window in days (1-3650, custom). Null means all-time                                                                                              |
| `groupByDimensions` | string\[] | No       | `[]`       | Grouping dimensions, up to **4**: the fixed keys `offerId`, `channelId`, `creativeId`, `outcomeType`, or any registered [custom dimension](#custom-dimensions) key |
| `filterConditions`  | object    | No       | --         | Nested filter with `and`/`or` logic and field-level operators (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `contains`)                                  |
| `computeMode`       | enum      | No       | `realtime` | How the metric is computed: `realtime` or `batch`                                                                                                                  |
| `batchIntervalMin`  | number    | No       | --         | Minutes between batch recomputations (5-1440, only used when `computeMode` is `batch`)                                                                             |
| `status`            | enum      | Auto     | `active`   | Metric status: `active`, `paused`, `archived`                                                                                                                      |

## Aggregation Functions

| Function | Description                       | Example                          |
| -------- | --------------------------------- | -------------------------------- |
| `count`  | Number of matching records        | "3 impressions in 7 days"        |
| `sum`    | Sum of the source field value     | "Total spend from conversions"   |
| `avg`    | Average of the source field value | "Average order value"            |
| `min`    | Minimum value                     | "Lowest offer amount clicked"    |
| `max`    | Maximum value                     | "Highest offer amount converted" |
| `ratio`  | Ratio of two event types          | "Click-to-impression ratio"      |

<Info>
  The `ratio` aggregation function requires a secondary event type. For example, a click-through rate metric would use `positive` (clicks) as the source field and compute the ratio against `impressions`.
</Info>

## Source Fields

The `sourceField` determines which column of the outcome/interaction record is aggregated:

| Source Field  | Description                                  |
| ------------- | -------------------------------------------- |
| `impressions` | Count of impression events                   |
| `positive`    | Positive outcomes (clicks, opens)            |
| `negative`    | Negative outcomes (dismissals, unsubscribes) |
| `neutral`     | Neutral outcomes (views without action)      |
| `converts`    | Conversion events                            |
| `totalValue`  | Monetary value associated with conversions   |

## Dimensions

Dimensions control how the metric is grouped. You can specify up to **4** dimensions, mixing the built-in fixed dimensions with any [custom dimension](#custom-dimensions) registered for your tenant:

| Dimension     | Description                                                 |
| ------------- | ----------------------------------------------------------- |
| `offerId`     | Group by specific offer                                     |
| `channelId`   | Group by delivery channel (email, push, sms, etc.)          |
| `creativeId`  | Group by creative variant                                   |
| `outcomeType` | Group by outcome type (impression, click, conversion, etc.) |

**Examples:**

* No dimensions: "Total clicks in 7 days" (single number per customer)
* `channelId` dimension: "Clicks per channel in 7 days" (one number per channel per customer)
* `channelId` + `offerId` dimensions: "Clicks per channel per offer in 7 days"
* `channelId` + a custom `campaign` dimension: "Clicks per channel per marketing campaign in 7 days"

## Custom dimensions

Beyond the four fixed dimensions above, a metric can group by any dimension defined in your tenant's **[Dimensions registry](/studio/dimensions)** (`/data/dimensions`) -- things like a marketing campaign, a customer's region, or a journey stage. Each registered dimension has a `source` that tells the platform where to read its value from:

| Source               | Where the value comes from                                                   |
| -------------------- | ---------------------------------------------------------------------------- |
| `request_attribute`  | The `attributes`/`context` payload on the `/recommend` or `/respond` request |
| `customer_attribute` | The customer's unified profile, read point-in-time                           |
| `journey`            | The customer's journey stage                                                 |
| `decision`           | The offer/channel/creative/category being decided                            |
| `outcome`            | The outcome key recorded for the interaction (e.g. `click`, `convert`)       |

See [Dimensions](/studio/dimensions) for the full registry field reference and worked examples for each source.

### Capture

Custom dimensions are resolved and stamped onto `interaction_history.dimensions` (JSONB) at every point an interaction row is written: **`/api/v1/recommend`**, **`/api/v1/respond`**, **`/api/v1/respond/bulk`**, the pipeline's **`outcomes`** node (third-party response-file ingestion), and **batch campaign** delivery. Capture is best-effort and never blocks or fails the write it's attached to -- if dimension resolution errors for any reason, the row is written with no dimensions rather than failing the request. Not every write point has the same context available (e.g. a `categoryId` decision-dimension only resolves at `/recommend`; `request_attribute` dimensions don't resolve on batch campaign delivery) -- see the full capture matrix in [Dimensions → Capture](/studio/dimensions#capture).

### How custom-dimension metrics compute

A metric that groups by only the four fixed dimensions computes the same way it always has -- from the `interaction_summaries` daily rollups. A metric that groups by **any** custom dimension key instead computes with a windowed `GROUP BY` directly over `interaction_history`, since custom dimension values only exist there (the daily summary rollups have no room for tenant-defined columns).

<Info>
  **`avg`/`min`/`max` on custom-dimension metrics use daily grain**, matching the fixed-dimension path: history rows are first pre-aggregated to one value per (customer, dimension combination, day), and only then is `avg`/`min`/`max` applied across days. This keeps a custom-dimension metric's semantics consistent with a fixed-dimension metric computed over the same data -- without it, `avg`/`min`/`max` would run per-event instead of per-day and the two paths would silently diverge. `count`, `sum`, and `ratio` are exact (no pre-aggregation step).
</Info>

### Automatic re-aggregation

Because a custom-dimension metric always recomputes from the `interaction_history` fact table rather than a fixed rollup, adding, removing, or changing a dimension is fully reflected across the metric's history-in-window the next time it recomputes -- there's no stale precomputed shape to migrate. Recompute happens automatically in three ways, on top of the [compute modes](#compute-modes) described above:

* **On save** -- creating or modifying a batch metric fires an async recompute immediately (fire-and-forget; a failure is logged, not surfaced, so a slow or failing recompute can never fail the save).
* **On schedule** -- the same `recompute-metrics` cron described in [Batch Mode](#batch-mode) recomputes custom-dimension metrics that are due, exactly like fixed-dimension ones.
* **On demand** -- the "Compute Now" action in the UI (`POST /api/v1/behavioral-metrics/{id}/compute`).

### History honesty

Whether a custom dimension has a value for an *older* interaction depends on its source:

* **`request_attribute`, `decision`, and `outcome`** dimensions can be backfilled onto existing `interaction_history` rows, because their source values (request context, offer/channel/creative/category, outcome key) are already stored verbatim on the row. A one-time backfill script is provided -- see [Dimensions → Backfilling history](/studio/dimensions#backfilling-history).
* **`customer_attribute`** dimensions are **forward-only**: the customer's profile is read *live* at capture time and isn't stored verbatim on the interaction row, so there's nothing to backfill. It resolves wherever that live profile lookup happens -- `/api/v1/recommend`, `/api/v1/respond/bulk`, batch campaign delivery, and the pipeline `outcomes` node -- but **not** on plain `/api/v1/respond`, which doesn't load the customer profile on that path.
* **`journey`** dimensions are also forward-only, and currently capture **no** value anywhere: the journey engine doesn't write to `interaction_history` yet, so a `journey`-sourced dimension shows **`(unset)`** on every interaction today, not just older ones. The source exists so journey-stage dimensions work once journeys write to interaction history.
* Interactions recorded before a `customer_attribute`/`journey` dimension had a working capture path (or before a backfill was run, for the backfillable sources) show as **`(unset)`** for that dimension in the UI and are grouped under an empty value at compute time.

### Realtime is not supported for custom dimensions

Creating or updating a metric with `computeMode: "realtime"` and a custom dimension in `groupByDimensions` is rejected with **`400`**. The realtime increment path updates a value in place from a fixed interaction context that has no custom-dimension values, so a realtime metric grouped by a custom key would silently accumulate everything under one key forever. Use `computeMode: "batch"` for any metric that groups by a custom dimension.

### Unregistered dimension keys

`groupByDimensions` accepts arbitrary strings at save time (a tenant's custom keys can't be enumerated in a static schema), so creating a metric with a typo'd or not-yet-registered custom key is not rejected at save. It fails at **compute** time instead: the history compute path validates every non-fixed key against the tenant's registered [Dimensions](/studio/dimensions), and an unregistered or invalid key returns **`400`** from the manual `POST /api/v1/behavioral-metrics/{id}/compute` endpoint. (The on-save and scheduled recomputes hit the same validation but only log the failure, since they run in the background rather than in response to a request.)

## Time Window

The `windowDays` parameter defines the rolling lookback period:

* Minimum: **1 day**
* Maximum: **3650 days** (custom windows are supported -- not just the preset periods used elsewhere in the platform)
* Set to `null` or omit for an **all-time** aggregation
* The window is always rolling -- it looks back from the current moment

<Warning>
  Behavioral metrics aggregate from the daily interaction-summary rollups, which are purged after `summaryRetentionDays` (see [Retention](/governance-security/retention)). A `windowDays` longer than your tenant's configured summary retention period will return **partial data** -- the source rows for the older part of the window have already been purged. Keep long-window metrics within your configured retention period, or extend retention if you need multi-year lookbacks.
</Warning>

## Compute Modes

Behavioral metrics support two compute modes that control freshness and performance characteristics. In both modes, a Decisioning Gate or contact policy reads the metric the same way — a lookup against the metric's precomputed value for that customer (and dimension key, if grouped). The modes differ only in **how that precomputed value gets refreshed**:

### Realtime Mode

When `computeMode` is `realtime`, the metric's value **increments live**: every time an impression or outcome event is recorded (via `/api/v1/respond` or bulk respond), the platform checks which realtime metrics match that event and updates their stored value in place -- a non-blocking, fire-and-forget update that runs alongside the event write. There is no separate recompute step and nothing to schedule.

* **Freshness:** Always current -- reflects events recorded moments ago
* **Performance cost:** A small write-path increment per matching event; no cost at decision/read time
* **Best for:** Low-volume metrics, critical frequency caps, metrics that need sub-minute accuracy

### Batch Mode

When `computeMode` is `batch`, the metric is recomputed on a schedule defined by `batchIntervalMin`, rather than incrementing on every event. The scheduled recompute is driven by the cron endpoint `GET /api/v1/cron/recompute-metrics` (CRON\_SECRET-authed, like other platform crons; recommended cadence: **every 5 minutes**). Each tick, the cron scans every tenant's active batch metrics and recomputes only the ones that are **due** -- a metric recomputes only when its last-computed timestamp is older than its own `batchIntervalMin`, so a 60-minute metric doesn't do work on every 5-minute tick. A manual "Compute Now" action is also available in the UI outside the schedule.

* **Freshness:** Stale by up to `batchIntervalMin` minutes
* **Performance cost:** Near-zero latency at decision time; recompute cost is paid on the cron tick, not per event
* **Best for:** High-volume metrics, long windows, metrics where slight staleness is acceptable

<Info>
  **Orphan cleanup on recompute:** if you change a batch metric's `groupByDimensions` (e.g. add or remove a grouping dimension), the next scheduled or manual recompute deletes any stale `MetricValue` rows still keyed on the old dimension shape. This prevents a Decisioning Gate from reading a stale per-dimension value left over from before the change. Realtime metrics don't need this step -- there's no bulk recompute, so a dimension change just changes which key future increments write to.
</Info>

| Compute Mode | Freshness                        | Decision-Time Cost        | Use Case                                     |
| ------------ | -------------------------------- | ------------------------- | -------------------------------------------- |
| `realtime`   | Instant (event-driven increment) | Near-zero (simple lookup) | Frequency caps, real-time suppression        |
| `batch`      | Up to `batchIntervalMin` stale   | Near-zero (simple lookup) | Lifetime value, long-window conversion rates |

<Info>
  The `batchIntervalMin` field is only used when `computeMode` is `batch`. Valid values are 5 to 1440 minutes (5 minutes to 24 hours).
</Info>

## How Metrics Feed into Rules

Behavioral metrics are referenced in two key places during the decision pipeline:

### In Decisioning Gates (metric\_condition)

A decisioning gate of type `metric_condition` evaluates a behavioral metric against a threshold to determine whether an offer qualifies for a customer. The metric-condition config specifies:

| Field              | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `metricId`         | string | ID of the behavioral metric to evaluate. In the Decisioning Gates editor, the **Metric** field is a dropdown populated from your Behavioral Metrics -- not a free-text ID.                                                                                                                                                                                                                                                                                                            |
| `operator`         | enum   | Comparison: `gt`, `gte`, `lt`, `lte`, `eq`                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `threshold`        | number | Value to compare the metric against                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `dimensionMapping` | object | For a metric grouped by dimension(s), maps each dimension to the value to look up at decision time. Use `$candidate.offerId` or `$candidate.channelId` to match the offer being evaluated, or a literal value. Omit or leave a dimension blank to match the un-dimensioned total. Decisioning Gates run before a creative is chosen, so a `creativeId` (or `outcomeType`) dimension needs a literal — see the [dimension-mapping notes](/decisioning/qualification-rules#rule-types). |

```json theme={null}
{
  "name": "Engagement Score Gate",
  "ruleType": "metric_condition",
  "stage": "eligibility",
  "config": {
    "metricId": "bm_response_rate_30d",
    "operator": "lt",
    "threshold": 0.1,
    "dimensionMapping": { "offerId": "$candidate.offerId" }
  }
}
```

`metric_condition` is authored at the **Eligibility** or **Fit Filters** stage (hard filter only) -- it does not participate in match-stage soft scoring. When the condition **triggers** (the operator comparison evaluates true), the offer is dropped from the candidate set. See [Decisioning Gates](/decisioning/qualification-rules) for the full rule-type and stage model.

### In Contact Policies

Contact policies reference metrics to enforce frequency caps and fatigue detection:

```json theme={null}
{
  "name": "Dynamic Email Cap",
  "ruleType": "frequency_cap",
  "config": {
    "metricKey": "email_impression_7d",
    "threshold": 3,
    "action": "suppress"
  }
}
```

When the metric value exceeds the threshold, the contact policy suppresses the offer on that channel.

## Limits

<Warning>
  Each tenant can create a maximum of **20 behavioral metrics**. This limit prevents excessive computation during decision time. Choose metrics that provide the most value for your decisioning rules.
</Warning>

## Creating a Behavioral Metric

<Steps>
  <Step title="Navigate to Behavioral Metrics">
    Go to **Studio > Behavioral Metrics** in the sidebar.
  </Step>

  <Step title="Click Create Metric">
    Click the **+ New Metric** button.
  </Step>

  <Step title="Name and describe">
    Enter a display name and description.
  </Step>

  <Step title="Select aggregation function">
    Choose from count, sum, avg, min, max, or ratio.
  </Step>

  <Step title="Select source field">
    Choose the outcome field to aggregate (impressions, positive, negative, neutral, converts, totalValue).
  </Step>

  <Step title="Set dimensions (optional)">
    Add up to 4 dimensions: `offerId`, `channelId`, `creativeId`, `outcomeType`, or any [custom dimension](/studio/dimensions) registered for your tenant. Realtime metrics may only use the four fixed dimensions -- pick `batch` mode for custom dimensions.
  </Step>

  <Step title="Set time window">
    Enter the rolling lookback window in days (1-3650, custom), or leave empty for all-time.
  </Step>

  <Step title="Choose compute mode">
    Select `realtime` for always-fresh values or `batch` for precomputed values with a refresh interval.
  </Step>

  <Step title="Add filter conditions (optional)">
    Define filter conditions to narrow which interaction records are included in the aggregation.
  </Step>

  <Step title="Save">
    Save the metric. It begins computing immediately from existing interaction history.
  </Step>
</Steps>

## Recipes

Common behavioral metric patterns for real-world use cases:

<Tabs>
  <Tab title="Frequency Cap">
    **Metric:** Count of impressions per channel in 7 days

    ```json theme={null}
    {
      "name": "Channel Impression Count (7d)",
      "aggregateFunction": "count",
      "sourceField": "impressions",
      "groupByDimensions": ["channelId"],
      "windowDays": 7,
      "computeMode": "realtime"
    }
    ```

    **Usage:** Create a contact policy that suppresses offers when `channel_impression_7d > 3` for any channel.
  </Tab>

  <Tab title="Conversion Cooldown">
    **Metric:** Count of conversions per offer in 30 days

    ```json theme={null}
    {
      "name": "Offer Conversion Count (30d)",
      "aggregateFunction": "count",
      "sourceField": "converts",
      "groupByDimensions": ["offerId"],
      "windowDays": 30,
      "computeMode": "realtime"
    }
    ```

    **Usage:** Suppress an offer for 30 days after a customer converts, preventing redundant recommendations.
  </Tab>

  <Tab title="Channel Fatigue">
    **Metric:** Count of dismiss events per channel in 14 days

    ```json theme={null}
    {
      "name": "Channel Dismiss Count (14d)",
      "aggregateFunction": "count",
      "sourceField": "negative",
      "groupByDimensions": ["channelId"],
      "windowDays": 14,
      "computeMode": "realtime"
    }
    ```

    **Usage:** If a customer dismisses 5+ offers on a channel in 14 days, reduce that channel's priority with a soft fit multiplier.
  </Tab>

  <Tab title="Response Rate Gate">
    **Metric:** Ratio of clicks to impressions in 30 days

    ```json theme={null}
    {
      "name": "Click-Through Rate (30d)",
      "aggregateFunction": "ratio",
      "sourceField": "positive",
      "groupByDimensions": [],
      "windowDays": 30,
      "computeMode": "batch",
      "batchIntervalMin": 60
    }
    ```

    **Usage:** Use as a soft decisioning gate -- customers with low CTR get a reduced score multiplier.
  </Tab>

  <Tab title="High-Value Targeting">
    **Metric:** Sum of conversion values in 90 days

    ```json theme={null}
    {
      "name": "Total Conversion Value (90d)",
      "aggregateFunction": "sum",
      "sourceField": "totalValue",
      "groupByDimensions": [],
      "windowDays": 90,
      "computeMode": "batch",
      "batchIntervalMin": 120
    }
    ```

    **Usage:** Boost scoring for high-value customers by using this metric in the ranking weighting.
  </Tab>

  <Tab title="Cross-Sell Detection">
    **Metric:** Count of conversions per offer in 60 days

    ```json theme={null}
    {
      "name": "Offer Conversion Count (60d)",
      "aggregateFunction": "count",
      "sourceField": "converts",
      "groupByDimensions": ["offerId"],
      "windowDays": 60,
      "computeMode": "batch",
      "batchIntervalMin": 60
    }
    ```

    **Usage:** Suppress offers in categories where the customer already converted recently, and boost cross-category offers.
  </Tab>
</Tabs>

## API Reference

### Create a Behavioral Metric

```bash theme={null}
POST /api/v1/behavioral-metrics
Content-Type: application/json
```

**Request body:**

```json theme={null}
{
  "name": "Email Impression Count (7d)",
  "description": "Number of email impressions in the last 7 days",
  "aggregateFunction": "count",
  "sourceField": "impressions",
  "groupByDimensions": ["channelId"],
  "windowDays": 7,
  "computeMode": "realtime"
}
```

**Response (201 Created):**

```json theme={null}
{
  "id": "bm_email_impression_7d",
  "name": "Email Impression Count (7d)",
  "description": "Number of email impressions in the last 7 days",
  "aggregateFunction": "count",
  "sourceField": "impressions",
  "groupByDimensions": ["channelId"],
  "windowDays": 7,
  "computeMode": "realtime",
  "batchIntervalMin": 0,
  "status": "active",
  "createdAt": "2026-03-10T14:30:00Z",
  "updatedAt": "2026-03-10T14:30:00Z"
}
```

### List Behavioral Metrics

```bash theme={null}
GET /api/v1/behavioral-metrics
```

### Update a Behavioral Metric

```bash theme={null}
PUT /api/v1/behavioral-metrics
```

Pass the metric `id` in the JSON body alongside the updated fields.

### Delete a Behavioral Metric

```bash theme={null}
DELETE /api/v1/behavioral-metrics?id=<metricId>
```

<Warning>
  Deleting a behavioral metric may break contact policies or decisioning gates that reference it. Review dependent rules before deleting.
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="Dimensions" icon="tags" href="/studio/dimensions">
    Register the custom dimensions your behavioral metrics can group and filter by.
  </Card>

  <Card title="Algorithms & Models" icon="brain" href="/ai-ml/algorithms">
    Use behavioral metrics as features in scoring models.
  </Card>

  <Card title="Dashboards" icon="chart-line" href="/operations-reporting/dashboards">
    Monitor metric values and trends in real time.
  </Card>
</CardGroup>
