# Dashboards (/docs/build/dashboards)



A dashboard is a page of analytics panels that your project code defines. Each panel reads one metric from Domino's catalog, so its numbers match `domino analytics query`. You deploy a dashboard in test, check it with `domino dashboards get`, and promote it to live like any other definition.

For the model behind this guide, read [Analytics](/docs/concepts/analytics). To choose metrics from a project's goals and explain a dashboard's numbers with an agent, see the [analytics reference](/docs/reference/analytics).

```mermaid
flowchart LR
    C["defineDashboard"] --> R["relay.json dashboards"]
    R --> D["domino deploy (test)"]
    D --> G["domino dashboards get"]
    D --> P["Promotion"]
    P --> L["Live"]
```

## Define a dashboard [#define-a-dashboard]

### 1. Choose the metrics [#1-choose-the-metrics]

Run `domino analytics metrics` to list the catalog. Each metric names the breakdowns, filters and charts it accepts, and whether it needs a point ledger.

### 2. Write the definition [#2-write-the-definition]

Default-export `defineDashboard` from a file anywhere in your project:

```ts
// dashboards/growth.ts
import { defineDashboard } from "@domino-sdk/relay/authoring"

export default defineDashboard({
  key: "growth",
  title: "Growth",
  description: "Sign-ups, quest completions and points.",
  defaultRange: "30d",
  position: 1,
  sections: [
    {
      panels: [
        { key: "sign-ups", title: "Sign-ups", metric: "members.signUps", chart: "number" },
        { key: "xp", title: "XP awarded", metric: "points.awarded", ledger: "xp", chart: "line" },
        {
          key: "completions",
          title: "Completions by quest",
          metric: "quests.completions",
          breakdown: "quest",
          chart: "bar",
          width: 4,
        },
      ],
    },
    {
      title: "VIPs",
      panels: [
        {
          key: "vip-rejections",
          title: "Why VIP attempts are rejected",
          metric: "quests.rejections",
          breakdown: "reason",
          filter: { group: "vip" },
          chart: "table",
        },
      ],
    },
  ],
})
```

The types only accept the breakdowns, filters and charts the panel's metric allows. A `ledger`, or a filter key, can be a key or a handle from `defineLedger`, `defineQuest` and the other `define…` functions.

### 3. List it in relay.json [#3-list-it-in-relayjson]

```json
"dashboards": [{ "entry": "dashboards/growth.ts" }]
```

Add `exportName` when the file exports several dashboards.

### 4. Deploy and check it [#4-deploy-and-check-it]

Run `domino deploy`. The preview lists each dashboard and whether publishing creates it, updates it, or leaves it `unchanged`. Then read it back:

```sh
domino dashboards get growth --range 7d
```

It prints every panel's numbers, or the panel's error, as an operator would see them. Add `--json` for the full read.

## What a dashboard holds [#what-a-dashboard-holds]

| Part      | Fields                                                                                                                                                |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Dashboard | `key`, `title`, optional `description`, optional `defaultRange` (`7d`, `30d`, `90d` or `all`; default `30d`), optional `position`, and `sections`     |
| Section   | Optional `title` and its `panels`, in order                                                                                                           |
| Panel     | `key`, `title`, optional `description`, `metric`, optional `breakdown`, optional `filter`, `ledger` for points metrics, `chart`, and optional `width` |

Panels sit on a 4-column grid. `width` is 1 to 4, and defaults by chart:

| Chart    | Breakdown   | Default width |
| -------- | ----------- | ------------- |
| `number` | Not allowed | 1             |
| `line`   | Optional    | 2             |
| `bar`    | Optional    | 2             |
| `table`  | Required    | 4             |

A broken-down `line` or `bar` panel reads the top 8 rows, with the rest summed into Other. A `table` reads up to 100 rows.

## What a deployment refuses [#what-a-deployment-refuses]

A deployment fails with a coded error, and publishes nothing, when a dashboard has:

| Code                                     | Cause                                                                                              |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `unknown-metric`                         | A metric the catalog doesn't have                                                                  |
| `breakdown-not-accepted`                 | A breakdown the metric doesn't take, or a `number` panel with a breakdown                          |
| `breakdown-required`                     | A `table` panel without a breakdown                                                                |
| `filter-not-accepted`                    | A filter the metric doesn't take                                                                   |
| `chart-not-accepted`                     | A chart the metric can't draw, such as a rate as `bar`                                             |
| `ledger-required`, `ledger-not-accepted` | A points metric without `ledger`, or `ledger` on another metric                                    |
| `unknown-reference`                      | A quest, tracker, ledger, offer, referral program or leaderboard key this environment doesn't have |
| `referral-program-required`              | A referral metric in a project with no referral program                                            |
| `duplicate-key`                          | Two dashboards with one key, or two panels with one key in a dashboard                             |
| `too-many-panels`                        | More than 24 panels in a dashboard                                                                 |
| `too-many-dashboards`                    | More than 20 dashboards that aren't archived in the project                                        |

Archived quests, ledgers and other resources are valid references: their history keeps counting. Groups are checked when a panel is read, not at deploy.

## Read a dashboard [#read-a-dashboard]

| Command                                       | Management client                      | Returns                                                                                                                            |
| --------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `domino dashboards list [--include-archived]` | `dashboards.list({ includeArchived })` | Each dashboard's key, title, description, position, archive time and last update, by position (those without one last), then title |
| `domino dashboards get KEY [--range R]`       | `dashboards.read({ key, range })`      | The definition, the range read, and each panel's result or error                                                                   |

Both need `read` permission. A read uses the dashboard's `defaultRange` unless you pass one. Each panel's result is what `domino analytics query` returns for the same metric, breakdown and filters.

A panel that fails carries its own `error` while the other panels still have results:

| Error             | Cause                                                                   |
| ----------------- | ----------------------------------------------------------------------- |
| `group-not-found` | The panel filters on a group this environment doesn't have              |
| `panel-timeout`   | The panel's query took too long; try a shorter range                    |
| `panel-failed`    | The panel couldn't be read for another reason; read the dashboard again |

## Change, archive and promote [#change-archive-and-promote]

* **Change it in code.** Deploying a changed definition publishes a new version. Deploying an identical one publishes nothing.
* **Removing it from relay.json doesn't delete it.** Archive it instead: `domino dashboards archive KEY --reason "…"`, or `dashboards.archive(key, { reason })` with the management client. Restore it with `unarchive`, unless the project already has 20 dashboards that aren't archived (`too-many-dashboards`). Both need `publish` permission, work in test only, and are recorded in the audit log with the reason.
* **An archived dashboard stays readable.** It leaves `dashboards list` unless you pass `--include-archived`, and deploying its code again leaves it archived.
* **Live gets dashboards by promotion.** A [promotion](/docs/concepts/promotion) carries each dashboard to live by key, archive state included. Groups aren't copied, so a panel filtered on a group live doesn't have reads `group-not-found` there.

## View dashboards in Console [#view-dashboards-in-console]

Open **Analytics** and pick a dashboard in the switcher beside the range. The switcher lists **Project analytics** first, then the dashboards that aren't archived, in `dashboards list` order. A dashboard has its own address, `/analytics/KEY`.

* **Range.** A dashboard opens at its `defaultRange`. Choose 7 days, 30 days, 90 days or All time to read it over another range.
* **Panels.** Sections and panels follow the definition on a 4-column grid at each panel's `width`, in one column on a narrow screen. A `number` panel shows the total and the change against the previous range, a `line` or `bar` panel the series (one line or stacked bar per row, top 8 and Other, when broken down), and a `table` panel each row's total and change. Archived quests, offers and programs are labelled **Archived**.
* **States.** A panel with nothing in the range says **No activity in this range*&#x2A;. A rate below its minimum shows &#x2A;*—**; hover it for the minimum. A panel that failed shows its error, with **Retry** for `panel-timeout` and `panel-failed`, and the other panels still show.
* **Archive.** In test, people with `publish` permission can choose **Archive dashboard** and give a reason. An archived dashboard's address still opens it, with an **Archived** banner and **Unarchive dashboard**. Live has neither action: dashboards reach live by promotion.

Dashboards change in code; Console can't edit them.

## Share dashboards with a sponsor [#share-dashboards-with-a-sponsor]

Give a sponsor, client or partner the **Viewer** role to show them the project's dashboards and nothing else. Viewers see no members, evidence, quests or settings, and dashboards show only totals, never individual members.

1. Invite them to your organization: **Settings → Access → Team → Manage members**.
2. Under **Project permissions**, choose them, check **Viewer**, and check the environments whose dashboards they see, usually **Live**.
3. To end their access on its own, such as 30 days after an event, pick the day under &#x2A;*Access ends (UTC, optional)**.
4. Choose **Save project access**.

When they sign in, Console shows them only **Analytics**, opened on the project's first dashboard, with the switcher listing the others. **Project analytics** isn't theirs: it can show individual members. A project with no dashboards in that environment shows **No dashboards yet**. After the end date their access stops, and the grant stays listed as **Expired**.

> [!NOTE] The viewer role is experimental
> It can change in a minor release.

> [!DEV]
> A viewer holds one permission, `dashboards`, which reads `dashboards.list` and `dashboards.read` and nothing else. Every other role, and any key with `read`, reads dashboards too. See [Give people access to a project](/docs/use/projects#give-people-access-to-a-project).
