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.
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
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.
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. 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.
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.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.sharedRoleIdsmust hold at least one role of the caller’s organization: an empty array is400 dydsh-009, a repeated id is400 dydsh-002, and a role of another organization or an unknown id is400 dydsh-011. The same rules apply when an update sendssharedRoleIds. Duplicate is shared with the caller’s role. - 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 is shared with the caller’s role, 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 (layer-1 rules such asfilter_value_missingandfilter_value_invalid, plus the cost limits on a 31-day window, such asbucket_estimate_exceeded). Aspecwith its owntimeRangeis refused, because a widget always follows the dashboard time range. A refusedvizorspecis400 dydsh-014, with the analytics rule inerror(code,message,field) when the analytics validator refused it.- A Flow is optional. Without a
flowIdfilter a custom widget reads every task of your organization; with one or more (operatoreqwith a string, orinwith a list of strings) it reads only those Flows. - Each Flow
_idthat the storedspecdoes 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’sspec(or add a custom card) on create or update dashboard. Send the Flow_idin lower case, because analytics filters match it case-sensitively. - That refusal is
400 dydsh-014withouterror; the reason is inmessageonly. 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, anddefaultLayoutinto a new custom widget with thenameanddescriptionyou send; the card and the dashboard are not changed.nameis required (dydsh-001, at most 200 characters,dydsh-009),nameanddescriptionmust be strings (dydsh-016). A card that is not a custom query widget is400 dydsh-014, a dashboard you cannot see is404 dydsh-033, acardIdthat is no longer on the stored dashboard is404 dydsh-042, and a role withoutadd/dashboardgets403 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 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. - Teams: for a caller in a team, Task (Common field) and Task - 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
assigneeeqorinfilter (anexistsorrangefilter onassigneestill 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).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. When a team caller’s team members cannot be resolved, a Task card fails withteam_scope_unresolved(a per-itemerrorin a query,422in Get sample rows) instead of running without the team limit; retry the request.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