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

# Dashboards

> Build custom dashboards with widgets to visualize and analyze your call data, metrics, and metadata.

export const CopyPageButton = () => {
  if (typeof window !== 'undefined') {
    setTimeout(function () {
      if (document.getElementById('ck-tools')) return;
      var anchor = document.getElementById('content-area') || document.querySelector('.mdx-content');
      if (!anchor) return;
      if (!document.getElementById('ck-style')) {
        var s = document.createElement('style');
        s.id = 'ck-style';
        s.textContent = '#ck-tools{position:absolute;top:6px;right:0;z-index:100;font-family:inherit;}' + '.ck-row{display:inline-flex;align-items:stretch;border:1px solid rgba(0,0,0,0.15);border-radius:8px;overflow:hidden;background:#fff;}' + ':root.dark .ck-row{background:rgba(255,255,255,0.06);border-color:rgba(255,255,255,0.12);}' + '.ck-btn{padding:5px 12px;border:none;background:none;cursor:pointer;font-size:13px;font-weight:500;font-family:inherit;color:#374151;}' + ':root.dark .ck-btn{color:#d1d5db;}' + '.ck-btn:hover{background:rgba(0,0,0,0.04);}' + ':root.dark .ck-btn:hover{background:rgba(255,255,255,0.06);}' + '.ck-chevron{padding:5px 8px;border:none;background:none;cursor:pointer;font-size:14px;font-family:inherit;color:#374151;}' + ':root.dark .ck-chevron{color:#d1d5db;}' + '.ck-chevron:hover{background:rgba(0,0,0,0.04);}' + ':root.dark .ck-chevron:hover{background:rgba(255,255,255,0.06);}' + '.ck-divider{width:1px;background:rgba(0,0,0,0.12);flex-shrink:0;}' + ':root.dark .ck-divider{background:rgba(255,255,255,0.12);}' + '.ck-dd{position:absolute;top:calc(100% + 4px);right:0;min-width:180px;background:#fff;border:1px solid rgba(0,0,0,0.12);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);padding:4px;display:none;z-index:200;}' + ':root.dark .ck-dd{background:#1f2937;border-color:rgba(255,255,255,0.1);box-shadow:0 4px 16px rgba(0,0,0,0.35);}' + '.ck-item{display:block;width:100%;padding:7px 12px;border:none;background:none;border-radius:6px;cursor:pointer;font-size:13px;font-family:inherit;text-align:left;color:#374151;}' + ':root.dark .ck-item{color:#d1d5db;}' + '.ck-item:hover{background:rgba(0,0,0,0.05);}' + ':root.dark .ck-item:hover{background:rgba(255,255,255,0.07);}';
        document.head.appendChild(s);
      }
      var wrap = document.createElement('div');
      wrap.id = 'ck-tools';
      var row = document.createElement('div');
      row.className = 'ck-row';
      var mainBtn = document.createElement('button');
      mainBtn.className = 'ck-btn';
      mainBtn.textContent = 'Copy page';
      var divider = document.createElement('span');
      divider.className = 'ck-divider';
      var chevron = document.createElement('button');
      chevron.className = 'ck-chevron';
      chevron.textContent = '▾';
      var dd = document.createElement('div');
      dd.className = 'ck-dd';
      function closeDD() {
        dd.style.display = 'none';
      }
      function openDD() {
        dd.style.display = 'block';
      }
      chevron.onclick = function (e) {
        e.stopPropagation();
        if (dd.style.display === 'block') {
          closeDD();
        } else {
          openDD();
        }
      };
      document.addEventListener('click', function (e) {
        if (!e.target.closest('#ck-tools')) {
          closeDD();
        }
      });
      document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape') {
          closeDD();
        }
      });
      function makeItem(label, fn) {
        var b = document.createElement('button');
        b.className = 'ck-item';
        b.textContent = label;
        b.onclick = function () {
          fn();
          closeDD();
        };
        return b;
      }
      function getMarkdown() {
        var walk = function (node) {
          if (!node) return '';
          if (node.nodeType === 3) return node.textContent || '';
          if (node.nodeType !== 1) return '';
          var tag = node.tagName.toLowerCase();
          var skip = ['script', 'style', 'svg', 'noscript', 'button', 'iframe'];
          if (skip.indexOf(tag) !== -1) return '';
          if (node.id === 'ck-tools') return '';
          var ch = Array.from(node.childNodes).map(walk).join('');
          if (tag === 'h1') return '\n# ' + ch.trim() + '\n\n';
          if (tag === 'h2') return '\n## ' + ch.trim() + '\n\n';
          if (tag === 'h3') return '\n### ' + ch.trim() + '\n\n';
          if (tag === 'p') return '\n' + ch.trim() + '\n\n';
          if (tag === 'pre') return '\n```\n' + node.textContent.trim() + '\n```\n\n';
          if (tag === 'li') return '- ' + ch.trim() + '\n';
          if (tag === 'code') return '`' + ch.trim() + '`';
          return ch;
        };
        var content = document.querySelector('.mdx-content') || document.getElementById('content-area') || document.body;
        return walk(content).replace(/\n\n\n+/g, '\n\n').trim();
      }
      function copyMd() {
        var md = getMarkdown();
        navigator.clipboard.writeText(md).then(function () {
          mainBtn.textContent = 'Copied!';
          setTimeout(function () {
            mainBtn.textContent = 'Copy page';
          }, 2000);
        });
      }
      function viewMd() {
        var md = getMarkdown();
        var safe = md.split('&').join('&amp;').split('<').join('&lt;').split('>').join('&gt;');
        var html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{font-family:monospace;max-width:860px;margin:40px auto;padding:0 24px;line-height:1.7;white-space:pre-wrap;word-wrap:break-word}</style></head><body>' + safe + '</body></html>';
        window.open(URL.createObjectURL(new Blob([html], {
          type: 'text/html'
        })), '_blank');
      }
      function openClaude() {
        var prompt = 'Can you read this Cekura docs page ' + window.location.href + ' so I can ask you questions?';
        window.open('https://claude.ai/new?q=' + encodeURIComponent(prompt), '_blank');
      }
      mainBtn.onclick = copyMd;
      dd.appendChild(makeItem('Copy page', copyMd));
      dd.appendChild(makeItem('View as Markdown', viewMd));
      dd.appendChild(makeItem('Open in Claude', openClaude));
      row.appendChild(mainBtn);
      row.appendChild(divider);
      row.appendChild(chevron);
      wrap.appendChild(row);
      wrap.appendChild(dd);
      anchor.style.position = 'relative';
      anchor.insertBefore(wrap, anchor.firstChild);
    }, 50);
  }
  return null;
};

<CopyPageButton />

<iframe src="https://www.youtube.com/embed/D76sVAbvsfc" title="Observability Overview in Cekura" frameborder="0" className="w-full aspect-video rounded-xl" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen />

## Overview

Dashboards let you create custom visualizations of your call data. Each dashboard contains **widgets** — individual charts that plot a specific field using filters, chart types, and aggregation functions.

Key concepts:

* **Dashboard** — a container that holds widgets and defines shared filters applied to all widgets within it.
* **Widget** — a single chart that plots one field (e.g. call duration, success rate, metric scores).
* **Filters** — JSON-based query conditions applied at the dashboard level, widget level, or both.

## Using dashboards in the app

Dashboards live under **Dashboards** in the sidebar. The rest of this guide
covers the underlying configuration and API; this section walks through the UI.

### Creating a dashboard

The first time you open Dashboards you'll see an empty state. Click **Create
dashboard**, give it a name, and you'll land on the new (empty) dashboard ready
for widgets.

### Dashboard controls

Every dashboard has a control bar:

* **Date range** — pick the window all widgets plot, up to the last 30 days
  (see [Time range](#time-range)).
* **Agent filter** — scope every widget to one or more agents, or leave it on
  *All agents*.
* **Add widget**, **Refresh**, and an **Edit** toggle.
* A **⋮** menu to rename, duplicate, or delete the dashboard, or include it in
  the daily report email.

### Adding a widget

Click **Add widget** to open the widget editor:

1. **Name** the widget.
2. Choose what to plot — a **call data field** (success, duration,
   `call_ended_reason`, a custom metadata key, …) or a **metric**.
3. Pick a **chart type** (line, bar, pie, list, or stat).
4. For time-series charts, choose a **time period** (hour/day/week/month) and an
   **aggregation** (count, sum, avg, min, max, …).
5. Optionally **group by** a dimension and add **widget-level filters**.
6. Save. The widget appears on the grid.

### Arranging widgets

Toggle **Edit** to rearrange the grid: drag widgets to reorder them, edit,
duplicate, or remove them, then **Save**. **Undo** reverts unsaved changes.

### Exploring data

Hover any chart for exact values. **Click a data point** to open a side sheet
of the call logs behind it, filtered to that bucket and group — a fast way to go
from a spike on the chart to the individual calls that caused it.

### Creating an alert from a widget

For any widget that plots a **metric**, you can set up an
[alert](/documentation/guides/observability/alerts) on that metric without
leaving the dashboard. Open the widget's **⋮** menu and choose **Create alert on
this metric** — the New alert drawer opens with the widget's metric already
selected, so you just pick the alert type and Slack delivery and save.

<Note>
  This option only appears on metric-based widgets (those plotting
  `metric_evaluations.value` with a metric selected). Widgets built on call-data
  fields like `success` or `duration` don't show it.
</Note>

## Time range

A dashboard's **date range** controls the window every widget plots, and it is
capped at **30 days**. This limit applies both to the dashboard UI and to the
widget data API endpoints (`GET /dashboards/widgets/{id}/data/` and
`POST /dashboards/widgets/preview_data/`):

* You can select any window from **the last 5 minutes up to the last 30 days**,
  using the quick ranges (Last 1 hour, Last 24 hours, Last 7 days, Last 30 days,
  …) or a custom from/to range.
* A custom range **cannot span more than 30 days**, and its **start cannot be
  earlier than 30 days ago**. Any request whose start timestamp (`timestamp_gte`)
  is older than 30 days returns `400 {"detail": "Timestamp filter value is too
  far in the past. Maximum allowed range is 30 days."}` — regardless of the
  window width. Splitting a longer period into ≤30-day sub-ranges does **not**
  bypass the limit if any sub-range's start is older than 30 days.

<Note>
  This limit is by design: long-range aggregation queries impose significant
  server load. It applies to **both the UI and the widget data API endpoints** —
  not just the interactive dashboard view. There is currently no API endpoint
  that returns server-side aggregated call metrics (call volume per day, enum
  breakdowns, metric counts) for date ranges older than 30 days. For historical
  analysis beyond 30 days, use the call logs API
  (`GET /observability/v1/call-logs/`) with arbitrary `timestamp` filters and
  aggregate client-side.
</Note>

This range is part of the dashboard's saved [filters](#filters) — it persists
with the dashboard and is applied to every widget. The per-widget **time period**
(hour/day/week/month) chosen in the widget editor is separate: it only controls
the bucket size used to aggregate data *within* the selected range.

## Filters

Filters are used on both dashboards and widgets to scope the data being visualized. Dashboard-level filters apply to all widgets, and widget-level filters apply only to that widget. When both are present, they are combined with AND logic.

### Filter Structure

A filter is a JSON object. It can be a single condition or a group of conditions combined with a logical operator.

**Single condition:**

```json theme={null}
{
  "field": "success",
  "op": "eq",
  "value": true
}
```

**Grouped conditions:**

```json theme={null}
{
  "operator": "and",
  "conditions": [
    { "field": "success", "op": "eq", "value": true },
    { "field": "duration", "op": "gte", "value": 60 }
  ]
}
```

Groups can be nested to build complex queries:

```json theme={null}
{
  "operator": "and",
  "conditions": [
    { "field": "success", "op": "eq", "value": true },
    {
      "operator": "or",
      "conditions": [
        { "field": "duration", "op": "gte", "value": 120 },
        { "field": "call_ended_reason", "op": "contains", "value": "timeout" }
      ]
    }
  ]
}
```

### Logical Operators

| Operator   | Description                                                                                                         |
| ---------- | ------------------------------------------------------------------------------------------------------------------- |
| `and`      | All conditions must match                                                                                           |
| `or`       | At least one condition must match                                                                                   |
| `same_row` | All conditions must match on the **same** related row (see [Metric Evaluation Filters](#metric-evaluation-filters)) |

### Comparison Operators

| Operator     | Description                      |
| ------------ | -------------------------------- |
| `eq`         | Equals                           |
| `neq`        | Not equals                       |
| `gt`         | Greater than                     |
| `gte`        | Greater than or equal to         |
| `lt`         | Less than                        |
| `lte`        | Less than or equal to            |
| `contains`   | Case-insensitive substring match |
| `startswith` | Case-insensitive prefix match    |
| `endswith`   | Case-insensitive suffix match    |
| `in`         | Value is in the provided list    |
| `isnull`     | Field is null                    |
| `regex`      | Regex pattern match              |

### Supported Filter Fields

#### Call Log Fields

| Field               | Type     | Supported Operators                         |
| ------------------- | -------- | ------------------------------------------- |
| `id`                | integer  | `eq`, `in`, `gt`, `gte`, `lt`, `lte`        |
| `success`           | boolean  | `eq`, `neq`, `isnull`                       |
| `duration`          | number   | `eq`, `gt`, `gte`, `lt`, `lte`              |
| `customer_number`   | string   | `eq`, `neq`, `contains`, `startswith`, `in` |
| `call_ended_reason` | string   | `eq`, `neq`, `contains`, `in`               |
| `dropoff_point`     | string   | `eq`, `neq`, `contains`, `in`, `isnull`     |
| `topic`             | string   | `eq`, `neq`, `contains`, `in`, `isnull`     |
| `timestamp`         | datetime | `eq`, `gt`, `gte`, `lt`, `lte`              |
| `is_reviewed`       | boolean  | `eq`                                        |

#### Related Model Fields

| Field              | Type    | Description |
| ------------------ | ------- | ----------- |
| `agent.id`         | integer | Agent ID    |
| `agent.agent_name` | string  | Agent name  |

#### Metadata Fields

You can filter on top-level metadata keys using dot notation:

```json theme={null}
{ "field": "metadata.org_name", "op": "eq", "value": "Acme Corp" }
```

```json theme={null}
{ "field": "metadata.region", "op": "in", "value": ["US", "EU"] }
```

<Note>
  Only top-level metadata keys are supported. Nested keys like `metadata.address.city` are not allowed.
</Note>

#### Metric Evaluation Fields

| Field                            | Type    | Description      |
| -------------------------------- | ------- | ---------------- |
| `metric_evaluations.value`       | dynamic | Evaluation value |
| `metric_evaluations.metric.id`   | integer | Metric ID        |
| `metric_evaluations.metric.name` | string  | Metric name      |

<Warning>
  When filtering on multiple metric evaluation fields (e.g. metric name AND value), use the `same_row` operator to ensure conditions apply to the same evaluation row:

  ```json theme={null}
  {
    "operator": "same_row",
    "conditions": [
      { "field": "metric_evaluations.metric.name", "op": "eq", "value": "accuracy" },
      { "field": "metric_evaluations.value", "op": "gte", "value": 0.9 }
    ]
  }
  ```

  Without `same_row`, an `and` operator could match a call where one evaluation has the right metric name and a *different* evaluation has the right value.
</Warning>

### Relative Datetime Values

Datetime fields support relative values for dynamic time-based filtering:

| Value      | Description                                                                              |
| ---------- | ---------------------------------------------------------------------------------------- |
| `now`      | Current UTC time                                                                         |
| `now-1d`   | 1 day ago                                                                                |
| `now-7d`   | 7 days ago                                                                               |
| `now-2h`   | 2 hours ago                                                                              |
| `now-30m`  | 30 minutes ago                                                                           |
| `today`    | Start of today (00:00:00 UTC) for `gte`/`gt`, end of today (23:59:59 UTC) for `lte`/`lt` |
| `today-7d` | 7 days before today                                                                      |

Supported time units: `s` (seconds), `m` (minutes), `h` (hours), `d` (days), `w` (weeks), `M` (months), `y` (years).

**Example — calls from the last 7 days:**

```json theme={null}
{
  "operator": "and",
  "conditions": [
    { "field": "timestamp", "op": "gte", "value": "today-7d" },
    { "field": "timestamp", "op": "lte", "value": "now" }
  ]
}
```

## Widgets

A widget plots a single field as a chart. You configure it with a field, chart type, and optional aggregation and time period.

### Supported Fields

| Field                      | Data Type | Description                                           |
| -------------------------- | --------- | ----------------------------------------------------- |
| `duration`                 | numeric   | Call duration in seconds                              |
| `success`                  | boolean   | Whether the call was successful                       |
| `is_reviewed`              | boolean   | Whether the call has been reviewed                    |
| `call_ended_reason`        | string    | Reason the call ended                                 |
| `dropoff_point`            | string    | Point at which the caller dropped off                 |
| `topic`                    | string    | Call topic                                            |
| `agent_id`                 | numeric   | Agent identifier                                      |
| `metric_evaluations.value` | dynamic   | Metric evaluation value (requires selecting a metric) |
| `metadata.*`               | string    | Any top-level metadata key (e.g. `metadata.org_name`) |

### Chart Types

| Chart Type | Description                              | Best For                                         |
| ---------- | ---------------------------------------- | ------------------------------------------------ |
| `line`     | Individual data points plotted over time | Viewing trends for numeric or datetime fields    |
| `bar`      | Data aggregated into time buckets        | Comparing aggregated values across time periods  |
| `pie`      | Distribution of categorical values       | Showing proportions for boolean or string fields |

### Aggregation Functions

Used with `bar` charts to aggregate values within each time bucket.

| Function | Description       | Applicable Data Types |
| -------- | ----------------- | --------------------- |
| `count`  | Count of records  | All types             |
| `sum`    | Sum of values     | Numeric               |
| `avg`    | Average of values | Numeric, Boolean      |
| `min`    | Minimum value     | Numeric, Datetime     |
| `max`    | Maximum value     | Numeric, Datetime     |

### Time Periods

Used with `bar` charts to define the time bucket size.

| Period  | Description    |
| ------- | -------------- |
| `hour`  | Group by hour  |
| `day`   | Group by day   |
| `week`  | Group by week  |
| `month` | Group by month |

### Valid Combinations

Not all field + chart type + aggregation combinations are valid. The rules depend on the field's data type:

| Data Type | Allowed Chart Types | Allowed Aggregations                |
| --------- | ------------------- | ----------------------------------- |
| Numeric   | `line`, `bar`       | `count`, `sum`, `avg`, `min`, `max` |
| Boolean   | `pie`, `bar`        | `count`, `avg`                      |
| String    | `pie`, `bar`        | `count`                             |
| Datetime  | `line`, `bar`       | `count`, `min`, `max`               |

<Note>
  The `metric_evaluations.value` field is dynamic — its data type and allowed chart types depend on the selected metric's evaluation type. For example, an `ENUM` metric only supports `pie` charts, while a `NUMERIC` metric supports `line`, `bar`, and `pie`.
</Note>

### Chart Output Formats

**Line chart** — individual data points:

```json theme={null}
[
  { "id": 1, "timestamp": "2025-11-03T00:00:00Z", "value": 45 },
  { "id": 2, "timestamp": "2025-11-03T01:00:00Z", "value": 62 }
]
```

**Bar chart** — aggregated time buckets:

```json theme={null}
[
  { "time_interval": "2025-11-03T00:00:00Z", "value": 4.5, "sample_count": 10 },
  { "time_interval": "2025-11-04T00:00:00Z", "value": 3.2, "sample_count": 8 }
]
```

**Pie chart** — value distribution (e.g. `call_ended_reason` with `pie` chart):

```json theme={null}
[
  { "label": "customer_ended_call", "value": 320, "percentage": 45.7 },
  { "label": "agent_ended_call", "value": 210, "percentage": 30.0 },
  { "label": "timeout", "value": 105, "percentage": 15.0 },
  { "label": "error", "value": 65, "percentage": 9.3 }
]
```

## Widget Data

Use the [Get Widget Data](/api-reference/dashboards/get-widget-data) endpoint to fetch the plot data for a saved widget. You can optionally override the widget's filters by passing a `filters` query parameter as a JSON string.

Use [Preview Widget Data](/api-reference/dashboards/preview-widget-data) to test a widget configuration before saving it — pass the full widget definition in the request body along with optional `dashboard_filters` to preview what the chart would look like.

### How Filters Are Applied

When fetching widget data, filters are merged in this order:

1. **Dashboard filters** — applied to all widgets in the dashboard.
2. **Widget filters** — applied to this specific widget only.
3. **Override filters** (optional) — passed at request time via the `filters` parameter.

If both dashboard and widget filters are present, they are combined using AND logic:

```json theme={null}
{
  "operator": "and",
  "conditions": [
    { /* dashboard filters */ },
    { /* widget filters */ }
  ]
}
```

## Metadata in Widgets

You can visualize any top-level metadata key as a widget field using the `metadata.*` prefix. For example, to chart the distribution of a custom `region` field you set on your calls:

* **Field:** `metadata.region`
* **Chart type:** `pie`

Metadata fields are treated as strings, so they support `pie` and `bar` chart types with `count` aggregation.

<Note>
  Only top-level metadata keys are supported for widget fields. Nested metadata keys are not supported.
</Note>

## API Reference

* [List Dashboards](/api-reference/dashboards/list-dashboards)
* [Create Dashboard](/api-reference/dashboards/create-dashboard)
* [Get Dashboard](/api-reference/dashboards/get-dashboard)
* [Update Dashboard](/api-reference/dashboards/update-dashboard)
* [Delete Dashboard](/api-reference/dashboards/delete-dashboard)
* [Duplicate Dashboard](/api-reference/dashboards/duplicate-dashboard)
* [List Widgets](/api-reference/dashboards/list-widgets)
* [Create Widget](/api-reference/dashboards/create-widget)
* [Get Widget](/api-reference/dashboards/get-widget)
* [Update Widget](/api-reference/dashboards/update-widget)
* [Delete Widget](/api-reference/dashboards/delete-widget)
* [Get Widget Data](/api-reference/dashboards/get-widget-data)
* [Preview Widget Data](/api-reference/dashboards/preview-widget-data)
