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

# Dashboard

> Dashboard lets a user compose their own dashboard from cards, templates, and a widget catalogue

## What is Dashboard?

Dashboard lets a user compose their own dashboard from any number of cards, start from a template, duplicate a dashboard, share it with roles, and save it back as an organization template. Each card shows one widget: a chart type (`viz`) plus a query spec (`spec`). The numbers behind every card come from one generic aggregation endpoint, [Run analytics query](https://docsy3.mile.app/api-reference/analytics/dashboard/run-analytics-query.md), instead of one endpoint per chart.

A dashboard is visible to the roles in its `sharedRoleIds`, which is never empty: there is no private dashboard. Rights to change it follow the caller's role permission, never who created it; `createdBy` is the audit trail only. Visibility follows `sharedRoleIds` alone, so an update that drops the caller's own role hides the dashboard from the caller, creator included.

## Key Features

> [card] [Read dashboards](https://docsy3.mile.app/api-reference/analytics/dashboard/read-dashboards.md)
>
> GET /dynamic-dashboards

> [card] [Create dashboard](https://docsy3.mile.app/api-reference/analytics/dashboard/create-dashboard.md)
>
> POST /dynamic-dashboard

> [card] [Read widgets](https://docsy3.mile.app/api-reference/analytics/dashboard/read-widgets.md)
>
> GET /dynamic-dashboard-widgets

> [card] [Run analytics query](https://docsy3.mile.app/api-reference/analytics/dashboard/run-analytics-query.md)
>
> POST /analytics/query

## Endpoints

| Method | Path | Description |
| - | - | - |
| GET | `/dynamic-dashboards` | [Read dashboards](https://docsy3.mile.app/api-reference/analytics/dashboard/read-dashboards.md) visible to the caller. |
| POST | `/dynamic-dashboard` | [Create dashboard](https://docsy3.mile.app/api-reference/analytics/dashboard/create-dashboard.md), blank or from a template. |
| GET | `/dynamic-dashboard/{id}` | [Read dashboard by ID](https://docsy3.mile.app/api-reference/analytics/dashboard/read-dashboard-by-id.md), with widget definitions. |
| PATCH | `/dynamic-dashboard/{id}` | [Update partially dashboard by ID](https://docsy3.mile.app/api-reference/analytics/dashboard/update-partially-dashboard-by-id.md). Needs `edit/dashboard`. |
| DELETE | `/dynamic-dashboard/{id}` | [Delete dashboard by ID](https://docsy3.mile.app/api-reference/analytics/dashboard/delete-dashboard-by-id.md). Needs `delete/dashboard`. |
| POST | `/dynamic-dashboard/{id}/duplicate` | [Duplicate dashboard by ID](https://docsy3.mile.app/api-reference/analytics/dashboard/duplicate-dashboard-by-id.md). |
| POST | `/dynamic-dashboard/{id}/save-as-template` | [Save dashboard as template](https://docsy3.mile.app/api-reference/analytics/dashboard/save-dashboard-as-template.md). |
| POST | `/dynamic-dashboard/{id}/card/{cardId}/save-as-widget-template` | [Save card as widget template](https://docsy3.mile.app/api-reference/analytics/dashboard/save-card-as-widget-template.md). Needs `add/dashboard`. |
| GET | `/dynamic-dashboard-templates` | [Read dashboard templates](https://docsy3.mile.app/api-reference/analytics/dashboard/read-dashboard-templates.md). |
| DELETE | `/dynamic-dashboard-template/{id}` | [Delete dashboard template by ID](https://docsy3.mile.app/api-reference/analytics/dashboard/delete-dashboard-template-by-id.md). |
| GET | `/dynamic-dashboard-widgets` | [Read widgets](https://docsy3.mile.app/api-reference/analytics/dashboard/read-widgets.md): built-in plus custom widgets. |
| POST | `/dynamic-dashboard-widget` | [Create custom widget](https://docsy3.mile.app/api-reference/analytics/dashboard/create-custom-widget.md). |
| PATCH | `/dynamic-dashboard-widget/{id}` | [Update partially custom widget by ID](https://docsy3.mile.app/api-reference/analytics/dashboard/update-partially-custom-widget-by-id.md). |
| DELETE | `/dynamic-dashboard-widget/{id}` | [Delete custom widget by ID](https://docsy3.mile.app/api-reference/analytics/dashboard/delete-custom-widget-by-id.md). |
| POST | `/analytics/query` | [Run analytics query](https://docsy3.mile.app/api-reference/analytics/dashboard/run-analytics-query.md) for up to 12 cards. |
| GET | `/analytics/catalog` | [Read analytics catalog](https://docsy3.mile.app/api-reference/analytics/dashboard/read-analytics-catalog.md): data sources, fields, and limits. |
| GET | `/analytics/field-metadata` | [Read analytics field metadata](https://docsy3.mile.app/api-reference/analytics/dashboard/read-analytics-field-metadata.md). |

## Permissions

Dashboard reuses the dashboard permission family; widgets, templates, and analytics have no permission of their own. A call made without the permission answers `403` with the `sys-035` code.

| Endpoint | Permission |
| - | - |
| Read dashboards, templates, widgets, and every `/analytics` endpoint | `view/dashboard` |
| Create, duplicate, save as template, save a card as a widget template, create custom widget | `add/dashboard` |
| Update dashboard, update custom widget | `edit/dashboard` |
| Delete dashboard, template, or custom widget | `delete/dashboard` |

A role without the route's permission answers `403` with `sys-035`. A dashboard that does not exist, belongs to another organization, or is not visible to you answers `404` with `dydsh-033`; the three cases are indistinguishable on purpose.

## Cards and Widgets

Every card owns its own widget row (copy-on-create), so renaming or editing a card never changes the catalogue, a custom widget, or another dashboard. In `widgets[]`, each card identifies its widget in exactly one of three ways:

| Card sends | Meaning |
| - | - |
| `widgetId` | A widget `_id` from Read widgets or one the card already owns. |
| `widgetName` only | Resolved against the built-in widgets only. No match is `400 dydsh-037`; several matches is `400 dydsh-013`. |
| `widget` object only | A full definition, for example a card from a template draft. `widget.name` and `widget.viz` are required. |

A card that sends none of the three is `400 dydsh-013`, and `overrides` is refused with `400 dydsh-003`. There is no limit on the number of cards.

A card added from a custom widget (widget template) by `widgetId` always gets its own widget row on save, so editing the card never changes the widget template. The app's **Create widget** saves the widget template first with Create custom widget, then adds a card by its `widgetId`. A card definition never becomes a widget template: an `isCustom` key in it is ignored.

* **Layout:** `layout.w` from 2 to `grid.columns`, `layout.h` from 3 to 18 or `null` (as tall as its content), `layout.x` from 0 to 48, `layout.y` 0 or more. `grid.columns` is 1-48, default 12.
* **Defaults on create:** `hubScope` `{mode: "all", hubIds: []}`, `timeRange` `null`, `grid` `{columns: 12}`, and `sharedRoleIds` set to the creator's role when the key is absent. `sharedRoleIds` must hold at least one role of the caller's organization: an empty array is `400 dydsh-009`, a repeated id is `400 dydsh-002`, and a role of another organization or an unknown id is `400 dydsh-011`. The same rules apply when an update sends `sharedRoleIds`. Duplicate is shared with the caller's role.
* **From a template:** send the template's `name` (not its `_id`) as `templateId`. Built-in templates win over organization templates with the same name. An unknown name is `404`.

### Updating Cards

There are no per-card endpoints. One update carries the whole `widgets` array, in order; a card left out is removed. Keep each stored `cardId`; a missing, unknown, or repeated `cardId` gets a fresh one.

Beside a `widgetId`, a card may send `widget` with `name`, `description`, `content.body` (text and banner widgets only), `viz`, and `spec` to edit its own widget row. Any other key is refused with `400 dydsh-003`. `viz` and `spec` can only be edited on a query widget; on a composite, text, or banner widget they are refused with `400 dydsh-014`.

* Only `name`, `description`, `sharedRoleIds`, `hubScope`, `timeRange`, `grid`, and `widgets` are applied; other keys are ignored. A body with none of them, including one that carries only `updatedTime`, is `400 dydsh-001`.
* When a widget's `name` changes, the card's `widgetName` follows. Echoing back an unchanged `widget` object from a read is harmless, and two cards sharing one `widgetId` are split so each owns its own row.
* Card widths are validated against `grid.columns` from the body, else the stored dashboard's, else 12.
* Deleting a dashboard has no `updatedTime` precondition; the response echoes the deleted dashboard.

> [!NOTE]
>
> Send the `updatedTime` from your last read with every update. If someone saved in between, nothing is written and the API answers `409` with `dydsh-039` and the current dashboard in `data`, so you can reload without another request.

### Templates and Copies

* **Duplicate** copies any dashboard the caller can see: `description`, `hubScope`, `timeRange`, and `grid` are copied, the copy is shared with the caller's role, every card gets a cloned widget row, and `origin` records `{kind: "duplicate", sourceId}`.
* **Save as template** stores a snapshot: each card keeps its `layout` and embeds its full widget definition, without `cardId`, `widgetId`, or `widgetName`. `description` falls back to the dashboard's. `name` must be unique among your organization's templates (`400 dydsh-002`); a built-in template's name may be reused.
* **Read dashboard templates** returns built-in rows (`organizationId: "system"`, a machine `name` to send as `templateId`, and a display `title`) and organization rows (the typed `name` and an `origin`). Text is literal data and is never translated.
* A card whose widget row cannot be found refuses a duplicate or a save as template with `400 dydsh-037`. Template name uniqueness is checked in code, so two exactly concurrent saves can both succeed. Deleting a dashboard also deletes the widget rows its cards own; built-in widgets are never touched.

### Custom Widgets

A custom widget is a widget your organization builds from scratch. It appears in the `custom` category of Read widgets with `isCustom: true`. A card that uses it gets its own copy, so updating or deleting a custom widget never changes existing cards.

* `viz` must be a chart type used by the built-in query widgets: currently `bar`, `donut`, `heatmap`, `kpi`, `line`, and `table`.
* `spec` is validated the same way as Run analytics query (layer-1 rules such as `filter_value_missing` and `filter_value_invalid`, plus the cost limits on a 31-day window, such as `bucket_estimate_exceeded`). A `spec` with its own `timeRange` is refused, because a widget always follows the dashboard time range. A refused `viz` or `spec` is `400 dydsh-014`, with the analytics rule in `error` (`code`, `message`, `field`) when the analytics validator refused it.
* A Flow is optional. Without a `flowId` filter a custom widget reads every task of your organization; with one or more (`operator` `eq` with a string, or `in` with a list of strings) it reads only those Flows.
* Each Flow `_id` that the stored `spec` does not already carry must be a live (not deleted) Flow of your organization. Ids already stored are not checked again, so a card or widget whose Flow was deleted can still be saved, and removing a Flow is never refused. This applies when you create or update a custom widget, and when you send a custom card's `spec` (or add a custom card) on create or update dashboard. Send the Flow `_id` in lower case, because analytics filters match it case-sensitively.
* That refusal is `400 dydsh-014` without `error`; the reason is in `message` only. Built-in widgets and edits to a built-in card are not checked for a Flow.
* Updating or deleting a widget that is unknown, built-in, a card's widget row, or another organization's is `404 dydsh-041` ("This widget no longer exists.").
* **Save a card as a widget template:** in view mode, Save card as widget template copies a custom query card's `viz`, `spec`, and `defaultLayout` into a new custom widget with the `name` and `description` you send; the card and the dashboard are not changed. `name` is required (`dydsh-001`, at most 200 characters, `dydsh-009`), `name` and `description` must be strings (`dydsh-016`). A card that is not a custom query widget is `400 dydsh-014`, a dashboard you cannot see is `404 dydsh-033`, a `cardId` that is no longer on the stored dashboard is `404 dydsh-042`, and a role without `add/dashboard` gets `403 sys-035`. In edit mode the app sends the draft card's content to Create custom widget (`POST /dynamic-dashboard-widget`) instead and does not save the dashboard.

## Analytics Query

Run analytics query takes a batch of 1 to **12** items, each with a declarative query spec. The server validates every spec, applies the caller's organization, hub, team, and soft-delete scope from the session, runs the specs read-only, and returns a `columns` and `rows` table per item. A dashboard with more than 12 cards sends several requests.

* **Time range:** presets `today`, `yesterday`, `last_7d`, `last_30d` (organization calendar, `Asia/Jakarta`), or absolute `from`/`to` as `YYYY-MM-DD` calendar dates widened to whole days (`2026-02-30`, relative words or datetimes are `422 time_range_invalid`). Day boundaries follow the optional batch `utcOffset` (`±HH:MM`, default `+07:00`). An item's `spec.timeRange` overrides the batch `timeRange`. A window longer than **31 days** (inclusive) is never trimmed; that item fails with `time_range_exceeded`.
* **Hubs:** `hubIds` omitted or `[]` covers every hub the caller belongs to; a caller with no hub sees no rows. Ids must be 24-character hex (`422 hub_ids_invalid`). A hub outside the caller's list is `403 hub_not_allowed` for the whole request.
* **Teams:** for a caller in a team, Task (Common field) and Task - {Flow} count only tasks assigned to members of the caller's teams (users without a team count as members) or with no assignee, as in the task list, unless the spec names assignees itself with an `assignee` `eq` or `in` filter (an `exists` or `range` filter on `assignee` still applies the team limit); when no member is found, only the caller's own tasks and tasks with no assignee. The User source counts only users of the caller's teams plus users without a team. Data sources are not limited by team. An owner and a caller without a team are not limited. Team and hub scope both apply, and the request carries no team parameter.
* **Filters:** every filter needs a unique `id` (`element_id_missing`, `duplicate_id`). `eq` takes one scalar and `in` a list of scalars; `flowId` values are hub-independent ids (24-character hex) and `assignee` / `createdBy` / `assignedBy` / `doneBy` values are emails. Anything else is `422 filter_value_invalid`. Matching is exact and case-sensitive.
* **Limits:** 3 measures, 2 dimensions, `terms` limit 1-1000, 5000 estimated buckets, 1000 rows, 10 seconds per query, 20 seconds per batch, 60 requests per minute per user (`429 sys-036`). Read analytics catalog serves the same limits.
* **Catalog:** without `dataSource`, Read analytics catalog lists the sources (`task`, `user`, `task_detail`) as `{id, kind, label, requiresBigData, availability}`; `task_detail` is always listed and currently always unavailable, with the reason in `availability.reason`. Physical index paths are never exposed.
* **Spec source:** the spec is always taken from the request. `widgetId` is echoed and logged only, and results are not cached. With no time range at either level, the request is `422 time_range_missing`. `resolvedTimeRange` in the response is the window actually applied.
* **Errors:** engine errors are never forwarded raw; the item reports `engine_error`. When a team caller's team members cannot be resolved, a Task card fails with `team_scope_unresolved` (a per-item `error` in a query, `422` in Get sample rows) instead of running without the team limit; retry the request. `mode: "debug"` adds a `debug` block only for internal accounts and is ignored for anyone else.

| Situation | HTTP | Scope |
| - | - | - |
| Malformed batch or spec (unknown key or field, bad operator, too many measures) | 422 | Whole request; nothing runs |
| Requested hub not allowed | 403 | Whole request; nothing runs |
| Cost limit (31-day window, buckets, rows) or engine failure | 200 | That item: `status: "error"` |
| Data source needs a license the organization lacks | 200 | That item: `status: "no_permission"` |
| Batch time budget spent | 200 | Remaining items: `status: "timeout"` |
| Query ran and found nothing | 200 | That item: `status: "no_data"` |

Composite widgets (User by Status) are sent as two ordinary items with ids `{cardId}#active` and `{cardId}#total`; Inactive is Total minus Active. A heatmap spec groups `doneCoordinate` with a single `geoGrid` dimension and returns `lat`, `lng`, and `count` rows.

## Related Resources

* [Status Codes](https://docsy3.mile.app/api-reference/status-codes.md) - HTTP status codes and error format
* [Hub Overview](https://docsy3.mile.app/api-reference/setting/hub-overview.md) - Hubs that scope analytics queries
* [Role Overview](https://docsy3.mile.app/api-reference/setting/role-overview.md) - Roles a dashboard is shared with


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.