> ## 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.

# Dimensions

> The per-tenant registry of custom dimensions that Behavioral Metrics can group and filter by.

## Overview

**Dimensions** are a per-tenant registry (`/data/dimensions`) of custom grouping keys -- things like a marketing campaign, a customer's region, or a journey stage -- that extend [Behavioral Metrics](/studio/behavioral-metrics) beyond the four built-in fixed dimensions (`offerId`, `channelId`, `creativeId`, `outcomeType`).

Once a dimension is registered and active, its `key` becomes a selectable entry in a metric's `groupByDimensions`, and its value is stamped onto every new `interaction_history` row so metrics can group and filter by it.

## Field Reference

| Field       | Type   | Required | Default  | Description                                                                                                                                                          |
| ----------- | ------ | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`      | string | Yes      | --       | Display name (1-100 characters)                                                                                                                                      |
| `key`       | string | Yes      | --       | The identifier used in a metric's `groupByDimensions` and in `dimensionMapping`. Must be a lowercase slug matching `^[a-z0-9_]+$` (letters, digits, underscore only) |
| `source`    | enum   | Yes      | --       | Where the value is read from: `request_attribute`, `customer_attribute`, `journey`, `decision`, `outcome`                                                            |
| `sourceKey` | string | Yes      | --       | The field to read within that source (1-200 characters). Meaning depends on `source` -- see below                                                                    |
| `type`      | enum   | No       | `string` | `string`, `number`, or `boolean`. Descriptive metadata for the registry UI -- every captured value is stored and compared as a string regardless of `type`           |
| `status`    | enum   | No       | `active` | `active` or `archived`. Only `active` dimensions are captured on new interactions or offered in the Behavioral Metrics dimension picker                              |

`name` + `key` are unique per tenant.

## Sources

Each dimension's `source` determines where its value comes from and how `sourceKey` is interpreted.

<Tabs>
  <Tab title="request_attribute">
    Reads from the `attributes`/`context` payload sent on the `/recommend` or `/respond` request. `sourceKey` is the attribute name.

    **Example:** track which marketing campaign drove a recommendation.

    ```json theme={null}
    {
      "name": "Campaign",
      "key": "campaign",
      "source": "request_attribute",
      "sourceKey": "campaign"
    }
    ```

    Given a request like `{ "attributes": { "campaign": "summer24" } }`, this dimension captures `campaign: "summer24"` on the resulting interaction.
  </Tab>

  <Tab title="customer_attribute">
    Reads from the customer's unified profile, point-in-time at capture. `sourceKey` is the profile field name. Resolves at `/api/v1/recommend`, `/api/v1/respond/bulk`, batch campaign delivery, and the pipeline `outcomes` node -- **not** on plain `/api/v1/respond`, which doesn't load the customer profile on that path.

    **Example:**

    ```json theme={null}
    {
      "name": "Region",
      "key": "region",
      "source": "customer_attribute",
      "sourceKey": "region"
    }
    ```

    <Note>Forward-only -- see [History honesty](#history-honesty) below.</Note>
  </Tab>

  <Tab title="journey">
    Reads the customer's current journey stage. `sourceKey` is the journey-context field (`stage`).

    **Example:**

    ```json theme={null}
    {
      "name": "Journey Stage",
      "key": "stage",
      "source": "journey",
      "sourceKey": "stage"
    }
    ```

    <Warning>Not currently captured anywhere -- the journey engine doesn't write to `interaction_history` yet, so a `journey`-sourced dimension resolves to nothing at every capture point today. It's forward-only by design, ready for when journeys start writing interaction history.</Warning>
  </Tab>

  <Tab title="decision">
    Reads a field off the offer being decided. `sourceKey` must be one of `offerId`, `channelId`, `creativeId`, `categoryId` -- any other value never resolves to anything. `offerId`, `channelId`, and `creativeId` resolve at every capture point; `categoryId` is only available at `/api/v1/recommend` time -- it isn't threaded through the `/respond`, batch-campaign, or pipeline `outcomes` capture paths.

    **Example:** an alternate, custom-named grouping on channel (in addition to the always-available fixed `channelId` dimension).

    ```json theme={null}
    {
      "name": "Channel Group",
      "key": "chan",
      "source": "decision",
      "sourceKey": "channelId"
    }
    ```
  </Tab>

  <Tab title="outcome">
    Captures the outcome key recorded for the interaction (e.g. `impression`, `click`, `convert`). `sourceKey` is not used to select a field -- the form requires a non-empty placeholder, but the captured value is always the interaction's outcome key.

    **Example:**

    ```json theme={null}
    {
      "name": "Outcome",
      "key": "oc",
      "source": "outcome",
      "sourceKey": "outcomeKey"
    }
    ```
  </Tab>
</Tabs>

## Capture

Active 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. A dimension whose value is missing, `null`, or empty for a given interaction is simply omitted from that row's `dimensions` object rather than written as an empty string.

Capture is best-effort: the tenant's active dimension list is cached briefly, and any failure to resolve dimensions never fails or blocks the write it's attached to -- the interaction is recorded with no dimensions instead.

Not every write point has the same context available, so a dimension can resolve at some but not others:

| Write point              | `request_attribute`                                  | `customer_attribute`                    | `decision` (offer/channel/creative) | `decision` (category) | `outcome`                              |
| ------------------------ | ---------------------------------------------------- | --------------------------------------- | ----------------------------------- | --------------------- | -------------------------------------- |
| `/api/v1/recommend`      | Yes (`context` + `attributes`)                       | Yes                                     | Yes                                 | Yes                   | No -- impressions carry no outcome key |
| `/api/v1/respond`        | Yes (`context` + `attributes`)                       | No -- profile isn't loaded on this path | Yes                                 | No                    | Yes                                    |
| `/api/v1/respond/bulk`   | Yes (per-item `context`)                             | Yes                                     | Yes                                 | No                    | Yes                                    |
| Pipeline `outcomes` node | Yes (response-file row)                              | Yes                                     | Yes                                 | No                    | Yes                                    |
| Batch campaign delivery  | No -- a scheduled run has no request-time attributes | Yes                                     | Yes                                 | No                    | Yes (always `impression`)              |

<Info>
  `categoryId` (a `decision`-source dimension) is only available at `/api/v1/recommend` time -- it isn't threaded through the `/respond`, batch-campaign, or pipeline `outcomes` capture paths. `offerId`, `channelId`, and `creativeId` resolve everywhere. `customer_attribute` and `journey` are covered in [History honesty](#history-honesty) below.
</Info>

## History honesty

Whether an *existing* interaction has a value for a dimension you just registered depends on the dimension's source:

* **`request_attribute`, `decision`, and `outcome`** dimensions can be backfilled, because their source values are already stored verbatim on the `interaction_history` row (`context`, the decision columns, `outcomeTypeKey`).
* **`customer_attribute`** dimensions are **forward-only** -- the profile is read live at capture time and never stored verbatim on the interaction row, so there's nothing to recover after the fact. It resolves at `/api/v1/recommend`, `/api/v1/respond/bulk`, batch campaign delivery, and the pipeline `outcomes` node, but not on plain `/api/v1/respond`.
* **`journey`** dimensions are forward-only too, and today resolve nowhere at all -- see the note on the [journey source](#sources) above.
* Older interactions (or any interaction written before a dimension's capture path existed) show as **`(unset)`** for that dimension, in the UI and at compute time, no matter what you do.

## Backfilling history

For the three backfillable sources, a one-time script retroactively stamps `dimensions` onto historical `interaction_history` rows so a newly-registered dimension works across existing data, not just interactions recorded after registration:

```bash theme={null}
npx tsx scripts/backfill-interaction-dimensions.ts --tenant <id> [--apply]
```

* **Dry run by default** -- without `--apply`, the script only reports how many rows *would* be updated per dimension; it mutates nothing.
* **`--apply`** performs the update.
* Only `request_attribute`, `decision`, and `outcome` dimensions are backfilled. `customer_attribute` and `journey` dimensions are skipped (nothing to backfill from -- see [History honesty](#history-honesty)). A `decision` dimension whose `sourceKey` isn't one of `offerId`/`channelId`/`creativeId`/`categoryId` is also skipped, with the reason reported.
* **Idempotent** -- an interaction row is only updated when the dimension's value is present on the row *and* that key isn't already stamped in `dimensions`, so re-running the script (e.g. after registering more dimensions) never overwrites a value captured live.

After registering a dimension whose history you want reflected in a metric, run the backfill (if backfillable) and then recompute the metric -- see [Behavioral Metrics → Automatic re-aggregation](/studio/behavioral-metrics#automatic-re-aggregation).

## Using a dimension

<Steps>
  <Step title="Register the dimension">
    Go to **Data > Dimensions**, click **+ New Dimension**, and set its name, key, source, and source key.
  </Step>

  <Step title="Add it to a metric">
    In [Behavioral Metrics](/studio/behavioral-metrics), add the dimension's `key` to a metric's `groupByDimensions` (up to 4 total, mixing fixed and custom keys). The metric must use `computeMode: "batch"` -- realtime metrics only support the four fixed dimensions.
  </Step>

  <Step title="(Optional) backfill history">
    If the dimension's source is `request_attribute`, `decision`, or `outcome` and you want existing history reflected, run the backfill script above.
  </Step>

  <Step title="Compute">
    Save the metric (an async recompute fires automatically) or trigger it manually via **Compute Now** / `POST /api/v1/behavioral-metrics/{id}/compute`.
  </Step>

  <Step title="Reference it in a rule">
    A [Decisioning Gate](/decisioning/qualification-rules) of type `metric_condition` can map the dimension by its registered key in `dimensionMapping`.
  </Step>
</Steps>

## API Reference

### List Dimensions

```bash theme={null}
GET /api/v1/dimension-definitions
```

Supports cursor-based pagination. Requires `admin`, `editor`, or `viewer` role.

### Create a Dimension

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

```json theme={null}
{
  "name": "Campaign",
  "key": "campaign",
  "source": "request_attribute",
  "sourceKey": "campaign",
  "type": "string"
}
```

Requires `admin` or `editor` role. Returns `409 Conflict` if a dimension with the same `key` already exists for the tenant.

### Update a Dimension

```bash theme={null}
PUT /api/v1/dimension-definitions
```

Pass the dimension `id` in the JSON body alongside the updated fields. Requires `admin` or `editor` role.

### Delete a Dimension

```bash theme={null}
DELETE /api/v1/dimension-definitions?id=<dimensionId>
```

Requires `admin` or `editor` role.

<Warning>
  Archiving or deleting a dimension stops it from being captured on new interactions and from being offered in the Behavioral Metrics dimension picker. Metrics that already reference the dimension's key keep computing against whatever history was already captured -- review dependent metrics before removing a dimension.
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="Behavioral Metrics" icon="chart-bar" href="/studio/behavioral-metrics">
    Group and filter metrics by the dimensions you register here.
  </Card>

  <Card title="Decisioning Gates" icon="filter" href="/decisioning/qualification-rules">
    Use a dimensioned metric in a `metric_condition` rule via `dimensionMapping`.
  </Card>
</CardGroup>
