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 answers403 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. Inwidgets[], 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.wfrom 2 togrid.columns,layout.hfrom 3 to 18 ornull(as tall as its content),layout.xfrom 0 to 48,layout.y0 or more.grid.columnsis 1-48, default 12. - Defaults on create:
hubScope{mode: "all", hubIds: []},timeRangenull,grid{columns: 12}, andsharedRoleIdsset 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) astemplateId. Built-in templates win over organization templates with the same name. An unknown name is404.
Updating Cards
There are no per-card endpoints. One update carries the wholewidgets 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, andwidgetsare applied; other keys are ignored. A body with none of them, including one that carries onlyupdatedTime, is400 dydsh-001. - When a widget’s
namechanges, the card’swidgetNamefollows. Echoing back an unchangedwidgetobject from a read is harmless, and two cards sharing onewidgetIdare split so each owns its own row. - Card widths are validated against
grid.columnsfrom the body, else the stored dashboard’s, else 12. - Deleting a dashboard has no
updatedTimeprecondition; 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, andgridare copied, the copy starts unshared (sharedRoleIds: []), every card gets a cloned widget row, andoriginrecords{kind: "duplicate", sourceId}. - Save as template stores a snapshot: each card keeps its
layoutand embeds its full widget definition, withoutcardId,widgetId, orwidgetName.descriptionfalls back to the dashboard’s.namemust 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 machinenameto send astemplateId, and a displaytitle) and organization rows (the typednameand anorigin). 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 thecustom 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.
vizmust be a chart type used by the built-in query widgets: currentlybar,donut,heatmap,kpi,line, andtable.specis validated the same way as Run analytics query. A refusedvizorspecis400 dydsh-014, with the analytics rule inerror(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 acolumns 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 absolutefrom/toasYYYY-MM-DDcalendar dates widened to whole days (2026-02-30, relative words or datetimes are422 time_range_invalid). Day boundaries follow the optional batchutcOffset(±HH:MM, default+07:00). An item’sspec.timeRangeoverrides the batchtimeRange. A window longer than 31 days (inclusive) is never trimmed; that item fails withtime_range_exceeded. - Hubs:
hubIdsomitted 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 is403 hub_not_allowedfor the whole request. - Filters: every filter needs a unique
id(element_id_missing,duplicate_id).eqtakes one scalar andina list of scalars;flowIdvalues are hub-independent ids (24-character hex) andassignee/createdBy/assignedBy/doneByvalues are emails. Anything else is422 filter_value_invalid. Matching is exact and case-sensitive. - Limits: 3 measures, 2 dimensions,
termslimit 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_detailis always listed and currently always unavailable, with the reason inavailability.reason. Physical index paths are never exposed. - Spec source: the spec is always taken from the request.
widgetIdis echoed and logged only, and results are not cached. With no time range at either level, the request is422 time_range_missing.resolvedTimeRangein the response is the window actually applied. - Errors: engine errors are never forwarded raw; the item reports
engine_error.mode: "debug"adds adebugblock 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.
Related Resources
- Status Codes - HTTP status codes and error format
- Hub Overview - Hubs that scope analytics queries
- Role Overview - Roles a dashboard is shared with