> ## Documentation Index
> Fetch the complete documentation index at: https://docsv4.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](/api-reference/analytics/dashboard/run-analytics-query), instead of one endpoint per chart.

Dashboards are owned by their creator. Roles a dashboard is shared with can view, duplicate, and save it as a template, but only the owner can update or delete it.

## Key Features

<CardGroup cols={2}>
  <Card title="Read dashboards" icon="list" href="/api-reference/analytics/dashboard/read-dashboards">
    GET /dynamic-dashboards
  </Card>

  <Card title="Create dashboard" icon="plus" href="/api-reference/analytics/dashboard/create-dashboard">
    POST /dynamic-dashboard
  </Card>

  <Card title="Read widgets" icon="list" href="/api-reference/analytics/dashboard/read-widgets">
    GET /dynamic-dashboard-widgets
  </Card>

  <Card title="Run analytics query" icon="chart-column" href="/api-reference/analytics/dashboard/run-analytics-query">
    POST /analytics/query
  </Card>
</CardGroup>

## Endpoints

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

## 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, create custom widget | `add/dashboard` |
| Update dashboard, update custom widget | `edit/dashboard` |
| Delete dashboard, template, or custom widget | `delete/dashboard` |

Updating or deleting a dashboard you can see but do not own answers `403` with `dydsh-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.

* **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 (send `[]` for a private dashboard).
* **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.
</Note>

### Templates and Copies

* **Duplicate** copies any dashboard the caller can see: `description`, `hubScope`, `timeRange`, and `grid` are copied, the copy starts unshared (`sharedRoleIds: []`), 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. A refused `viz` or `spec` is `400 dydsh-014`, with the analytics rule in `error` (`code`, `message`, `field`).
* A built-in widget, a card's widget row, or another organization's widget is `404 dydsh-041`.

## 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, 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.
* **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`. `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](/api-reference/status-codes) - HTTP status codes and error format
* [Hub Overview](/api-reference/setting/hub-overview) - Hubs that scope analytics queries
* [Role Overview](/api-reference/setting/role-overview) - Roles a dashboard is shared with


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