Skip to main content

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, 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

Read dashboards

GET /dynamic-dashboards

Create dashboard

POST /dynamic-dashboard

Read widgets

GET /dynamic-dashboard-widgets

Run analytics query

POST /analytics/query

Endpoints

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. 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: 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.
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 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.
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.