---
title: "Custom cards on native statistics dashboards"
description: "How to write your own SQL (MySQL) query or Mongo aggregation pipeline, test it with the dashboard filters, save it as a card and place it on a custom dashboard; the Custom cards right, filter placeholders, limits."
---

# Custom cards on native statistics dashboards

When none of the ready-made native statistics cards shows the metric you need, you can calculate it with your own query: SQL against the instance MySQL database or an aggregation pipeline against Mongo. You check the query with **Test** before saving, then put the saved card on a [custom dashboard](/en/statistics/how-to/build-custom-dashboard.md) next to ready-made cards. Dashboard filters (period, channel, operator and so on) can be passed into the query.

## When to use this

- You need a metric that no ready-made card has: your own grouping, field or breakdown.
- An analyst needs a table with columns of their choice and CSV/XLSX export.
- You want to check an idea on the instance data quickly, without ordering a new card.

## What to know

- **The feature is internal.** Only the ConnectiveOne team enables it, and only for its own administrators; it is not enabled on client instances. It needs native statistics and custom dashboards.
- **A query only reads data.** One `SELECT` or `WITH` query is allowed. Data changes, several statements separated by `;`, system schemas (`mysql`, `information_schema`, `performance_schema`, `sys`) and functions such as `SLEEP` are rejected before the query runs. The database checks it a second time: the query runs in a read-only session.
- **Only the current instance database.** Tables of another database on the same server (`other_db.table`) cannot be read: such a query is rejected on test, on save and when the card is shown. Write tables of your database without a prefix or with the current database name. A table alias that matches the name of another database on the server is rejected too — give it another name.
- **Only visible characters in a MySQL query.** Control characters (except tab and line break), invisible characters (zero-width, BOM, text direction marks) and non-standard spaces (non-breaking, narrow, etc.) are not allowed anywhere in the query, not even inside quotes: the check and the database could read such a query differently. These characters often come in when text is copied from documents or messengers — retype the spaces. If a value needs such a character, build it with `CHAR()` (a non-breaking space is `CHAR(0xC2A0 USING utf8mb4)`) or pass it as a filter value.
- **`/*! … */` comments with a version number (`/*!50700 … */`), `/*M! … */` comments and a comment inside such a comment** are rejected when the server could read them differently depending on its version. Plain `--`, `#` and `/* … */` comments are allowed.
- **Mongo** queries are strict JSON `{"collection": "…", "pipeline": [ … ]}`. Write stages (`$out`, `$merge`), service stages, `$where`, `$function`, `$accumulator`, `system.*` collections and references to another database (`{"db": …, "coll": …}` in `$lookup`, `$unionWith`, `$graphLookup`) are not allowed.
- **Run limits:** up to 30 seconds per query and up to 10,000 result rows. A larger result shows the first 10,000 rows and is marked as truncated; an export of it ends with a note row saying the data was cut. The same limits apply to the test, to the card on a dashboard and to exports.
- **Card limits:** name up to 120 characters, query up to 20,000 characters, up to 20 filter bindings, up to 200 custom cards per instance.
- **One card, one source.** You cannot combine MySQL and Mongo in one card.
- **A card on a dashboard is a link.** Change the query and every dashboard with the card shows the new data. Delete the card and dashboards show a grey **Card unavailable** tile in its place.

## Access rights

An administrator grants the rights in **Settings → Roles → Statistics**.

| Right | What it gives |
|-------|---------------|
| **Custom dashboards → Save** | Put existing custom cards on dashboards and see the data of any of them |
| **Custom cards** (together with dashboards Save) | Create, test, edit and delete custom cards and see their queries |
| **Custom dashboards → View** | See the data of custom cards that are on saved dashboards. Viewers never see the query text |

The **Custom cards** right is off by default and has no effect without dashboards **Save**.

> **Before you grant Custom cards:** it lets the user read any non-system MySQL table or Mongo collection of the instance, with no restriction by role, skill group or operator. Grant it only to trusted internal users such as analysts.

<!-- screenshot: Settings → Roles → Statistics, the Custom cards row with one column -->

## Before you start

- [x] Native statistics, custom dashboards and custom cards are enabled on the instance.
- [x] Your role has **Save** in the **Custom dashboards** row and the **Custom cards** right.
- [x] You know the table or collection with the data and the names of its fields.

## Steps: create a card

1. Open **Menu → Statistics → Custom dashboards**, choose a dashboard and click **Edit**.
2. Click **+ Cards** and, in the **Custom cards** group, click **+ Custom card**.
3. Enter a name and choose the source (**MySQL** or **Mongo**) and the look: number, table, bars, line, area, pie or rows.
4. Write the query. A MySQL example, rooms by channel:

   ```sql
   SELECT kw_channel AS channel, COUNT(*) AS rooms
   FROM op_chat_rooms
   WHERE 1=1 [[AND {{period}}]]
   GROUP BY kw_channel
   ```

5. If the card must follow the dashboard filters, add bindings (see below). In the example, the **Date** filter is bound to the `period` placeholder and the `op_chat_rooms.createdAt` field.
6. Click **Test**. The query runs with the filters currently selected on the dashboard; the result appears below in the chosen look, with a line like «Done in … s · rows: N».
7. Click **Save**. The card appears in the **Custom cards** group and is added to a dashboard like a ready-made card.

<!-- screenshot: the custom card dialog — source, look, query, bindings, test result -->

<!-- video: creating a MySQL card, testing it, a rejected query — added by the demo -->

A Mongo example, dialogs by channel:

```json
{"collection": "statistics",
 "pipeline": [
   {"$match": "{{period}}"},
   {"$group": {"_id": "$channel", "dialogs": {"$sum": 1}}},
   {"$project": {"_id": 0, "channel": "$_id", "dialogs": 1}}
 ]}
```

Here the **Date** filter is bound to `period` and the `createdAt` field. Write dates in the query as `{"$date": "2026-10-01T00:00:00Z"}`.

## Dashboard filters in the query

A filter gets into the query only through a binding: **filter → placeholder → field**. Write the placeholder in the query as `{{name}}` (lowercase Latin letters, digits, `_`). The field is `column` or `table.column` for MySQL and a field path without `$` for Mongo.

| Filter | MySQL: `{{tag}}` becomes | Mongo: `"{{tag}}"` becomes |
|--------|----------------------------|------------------------------|
| Date (period) | `field >= start AND field < end + 1 day` | `{field: {$gte: …, $lt: …}}` |
| One value | `field = value` | `{field: value}` |
| Several values | `field IN (…)` | `{field: {$in: […]}}` |
| Filter not selected | `1=1`; a `[[ … ]]` block disappears completely | `{}` |

- **A `[[ … ]]` block** is an optional part of the query: when its filter is not selected, the block is removed. So `WHERE 1=1 [[AND {{period}}]]` works with and without a period.
- **Filter values are escaped before they are substituted** into the query, so quotes or special characters in a value cannot break it.
- **Write `[[` and `]]` only in the query itself**, not inside a quoted string, a backtick name or a comment. Otherwise the query is rejected.
- **A placeholder without a binding** is an «unknown placeholder» error.
- **Time zone.** For MySQL, the period bounds are calendar dates as they are stored in the table; the **Time zone** selector does not shift them, the same as in ready-made SQL cards of native statistics. For Mongo, the bounds are calculated in the time zone selected in the panel.
- A card without bindings ignores the dashboard filters and always shows the same result.

## How the result becomes a chart

There are no axis settings; the look follows the types and the order of the result columns:

- **bars, line, area, rows** — a category or a date and numbers. The first non-numeric column becomes the category and the numeric columns become the values, wherever they stand in the result: in a Mongo `$project` a number field may come before the label. When all columns are numeric, or all are text, the first column is the category. For stacked bars return three columns: date, category, value;
- **pie** — two columns: a label and a number, in any order;
- **number** — the first numeric column of the first row;
- **table** — all columns, with CSV/XLSX export.

Give columns clear aliases with `AS`: they become the headers. Two columns with the same name are an error.

## Edit or delete a card

1. In dashboard edit mode, open **+ Cards** → **Custom cards**.
2. Click the pencil next to a card to edit it, or **✕** to delete it. Before deleting, the system shows how many dashboards use the card.

If someone has just changed the same card, your save fails: reload the card and repeat your changes.

## How to check it worked

- The test shows a result and a line like «Done in … s · rows: N».
- Compare the number with the same query in the database for the same dates.
- Change the period on the dashboard and click **Apply**: a card bound to the date changes its number, a card without bindings does not.

## Troubleshooting

| What you see | What to do |
|--------------|------------|
| «Query rejected: only one SELECT or WITH query is allowed» | Keep one query that starts with `SELECT` or `WITH`. A semicolon is allowed only at the very end |
| «Query rejected: forbidden word / function / schema …» | The query changes data, locks tables or reads system schemas. Rewrite it as a plain read |
| «Query rejected: query to another database: …» | The query reads a table of another database on the server, or an alias matches the name of another database. Keep only tables of the current database, rename the alias |
| «Query rejected: could not check which databases the query addresses» | The database did not answer the check. Try again; if it repeats, contact support |
| «Query rejected: forbidden character U+…» | The query has an invisible character or a non-standard space (its code is in the message). Retype that part; build a value with such a character with `CHAR()` or pass it as a filter value |
| «Query rejected: a /*!…*/ comment with a version…» | Remove the version number from the `/*! … */` comment, or the comment itself |
| «Query rejected: colon outside a string: :name» | A colon before a word is reserved. Put the text in quotes or remove the colon |
| «Query rejected: unknown placeholder …» | Add a binding for the placeholder or remove it from the query |
| «Query rejected: forbidden stage: $out» (Mongo) | Remove the write stage. A card only reads data |
| «Query failed after … s: Unknown column …» | The query itself has an error: check the table and column names |
| «Several columns are named …» | Give the columns distinct aliases with `AS` |
| The query was stopped on time | Narrow the period, filter by an indexed field or aggregate the data |
| No **Custom cards** group in **+ Cards** | The feature is off on the instance, or you have no dashboards **Save** |
| No **+ Custom card** button, pencil or ✕ | Your role has no **Custom cards** right. You can still place existing cards |
| A grey **Card unavailable** tile on a dashboard | The card was deleted or custom cards are off on the instance. In edit mode remove the tile with ✕ |

## Related

- [Custom dashboards in native statistics](/en/statistics/how-to/build-custom-dashboard.md)
- [How to apply report filters](/en/statistics/how-to/apply-report-filters.md)
- [How to export statistics data](/en/statistics/how-to/export-statistics-data.md)
