Manage custom dashboards with configurable widget grids. Dashboards can be organised into bases (group containers) using a self-referential parentId hierarchy. Bases appear as concertina drawers in the sidebar, with child dashboards nested inside. Each workspace can have multiple dashboards with drag-and-drop widget layouts, date-range filtering, multi-series charts, auto-refresh, global filters, and JSON import/export. Dashboards support 26 widget types spanning metrics, charts, tables, and content blocks. Widget data is computed server-side via the widget-data endpoint.
Workspace Provisioning
- When a new workspace is created, a default overview dashboard (type:
DASHBOARD) is automatically provisioned with 4 widgets: Recent Activity, Total Submissions, Active Matters, and Matters by Status. - A per-user task list (type:
TASK_LIST) is also provisioned for the workspace creator. Additional members receive their task list when added. - Provisioning is idempotent - safe to call multiple times.
- All dashboard pages resolve the active workspace from the user's current workspace preference (set by the workspace switch API).
Route Conventions
- All endpoints require authentication and appropriate workspace permissions.
- Mutation routes (POST, PUT, DELETE) require a valid security token and validate JSON payloads.
- The GET widget-data endpoint uses query-string encoding for read operations (no security token required).
- Errors follow
{ error, details? }.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/dashboards | List all dashboards for the org |
| POST | /api/dashboards | Create a new dashboard |
| GET | /api/dashboards/:id | Get dashboard details (including config) |
| PUT | /api/dashboards/:id | Update dashboard (name, config, isStarred, etc.) |
| DELETE | /api/dashboards/:id | Delete a dashboard (cannot delete default). Deleting a base cascade-deletes all child dashboards |
| GET | /api/dashboards/:id/widget-data?q=... | Fetch computed widget data (read-only, no token required) |
| POST | /api/dashboards/:id/widget-data | Fetch computed widget data (legacy) |
| GET | /api/dashboards/:id/sharing | Get sharing settings, collaborators, and workspace members |
| PUT | /api/dashboards/:id/sharing | Update sharing settings (visibility, workspace role, collaborators) |
Create Dashboard
Creates a new dashboard for the workspace. The name field is required.
POST /api/dashboards
Content-Type: application/json
// Create a regular dashboard inside a base
{
"name": "Sales Overview",
"description": "Key metrics and pipeline health",
"parentId": "clxyz..."
}
// Create a base (group container)
{
"name": "Analytics",
"settings": { "isBase": true, "icon": "layers" }
}
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Dashboard display name (1-255 chars) |
description | string | No | Short description (max 1000 chars) |
parentId | string | No | ID of a base dashboard to nest this dashboard under. Parent must exist in the same org and have settings.isBase: true |
settings | object | No | Dashboard settings. Set { isBase: true } to create a base (group container). Optional icon key for sidebar icon (database, folder, box, layers, archive, inbox, star, zap, hash, grid) |
Update Dashboard
Updates an existing dashboard. All fields are optional. The config field stores the full widget grid definition including auto-refresh and global filter settings.
PUT /api/dashboards/:id
Content-Type: application/json
{
"name": "Sales Overview",
"isStarred": true,
"config": {
"version": 1,
"widgets": [],
"autoRefreshInterval": 60,
"globalFilters": [
{ "field": "status", "operator": "eq", "value": "OPEN" }
]
}
}
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Dashboard display name |
description | string | No | Short description |
order | number | No | Sort order in sidebar (lower first) |
isStarred | boolean | No | Pin as favourite (starred dashboards sort first in sidebar) |
config | object | No | Full widget grid configuration (see Config Structure) |
parentId | string | null | No | Move dashboard into a base (ID) or set null to make it an orphan. Parent must have settings.isBase: true |
settings | object | No | Update settings (e.g. change base icon). Keys: isBase, icon |
Config Structure
The config field defines the dashboard widget grid, auto-refresh behaviour, and global filters. It is validated against a strict schema. You can export and import this JSON directly from the dashboard UI (Grid/JSON tabs).
{
"version": 1,
"autoRefreshInterval": 60,
"globalFilters": [
{ "field": "status", "operator": "eq", "value": "OPEN" }
],
"widgets": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "bar_chart",
"dataSource": {
"type": "matters",
"dateField": "createdAt",
"filters": [
{ "field": "status", "operator": "in", "value": ["OPEN", "ON_HOLD"] }
]
},
"aggregation": { "function": "count", "column": "status" },
"groupBy": { "column": "status" },
"secondaryGroupBy": { "column": "priority" },
"appearance": {
"title": "Matters by Status & Priority",
"subtitle": "Multi-series grouped chart",
"showLegend": true,
"showSparkline": false
},
"layout": { "x": 0, "y": 0, "w": 600, "h": 320, "minW": 300, "minH": 240 }
}
]
}
Top-Level Config Fields
| Field | Type | Description |
|---|---|---|
version | 1 | Schema version (must be 1) |
widgets | array | Array of widget definitions (max 50) |
autoRefreshInterval | number | Seconds between auto-refresh cycles. 0 = off. Typical: 30, 60, 300 |
globalFilters | array | Filters applied to all widgets whose data source supports the filter field |
Widget Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique widget identifier (UUID) |
type | string | Widget type (see Widget Types table) |
dataSource | object | Data source config with type, optional IDs, dateField, and filters |
aggregation | object | Aggregation function and target column |
groupBy | object | Primary grouping: column + optional date granularity, limit, sort |
secondaryGroupBy | object | Secondary grouping for multi-series charts (produces grouped/stacked bars, multi-line, etc.) |
appearance | object | Display settings (see Appearance Fields) |
layout | object | Pixel-based position: x, y (px offset), w, h (px size), minW, minH, z (z-index). Freeform canvas |
Widget Types
26 widget types are available, spanning metrics, charts, tables, and content blocks.
| Type | Category | Default Size | Description |
|---|---|---|---|
metric | Metric | 300x160 | Single large number with optional comparison delta and sparkline |
kpi_scorecard | Metric | 1200x160 | Multi-KPI row showing 2-4 metrics with individual sparklines and change deltas |
line_chart | Chart | 600x320 | Time-series or continuous data as connected points (supports multi-series) |
bar_chart | Chart | 600x320 | Vertical bars for categorical comparison (supports multi-series grouped bars) |
area_chart | Chart | 600x320 | Filled area under a line for volume trends (supports multi-series) |
pie_chart | Chart | 400x320 | Proportional slices with optional donut hole (innerRadius) |
stacked_bar_chart | Chart | 600x320 | Stacked bars showing composition within categories |
radar_chart | Chart | 400x320 | Spider/radar plot for multi-axis comparison |
scatter_chart | Chart | 600x320 | XY scatter plot for correlation analysis |
funnel_chart | Chart | 400x400 | Funnel stages showing progressive narrowing |
treemap_chart | Chart | 600x320 | Nested rectangles sized by value for hierarchical data |
gauge | Metric | 300x240 | Radial dial showing progress toward a target value |
sparkline | Metric | 300x160 | Compact inline trend line without axes |
heatmap | Chart | 600x320 | Color-coded matrix for density or intensity data |
waterfall_chart | Chart | 600x320 | Sequential additions and subtractions from an initial value |
combo_chart | Chart | 600x320 | Combined bar and line chart on dual axes |
table_grid | Table | 600x320 | Sortable, paginated data table (click column headers to sort, 10 rows/page) |
progress_bar | Metric | 300x160 | Horizontal bar showing completion percentage against target |
progress_ring | Metric | 300x240 | Circular ring showing completion percentage |
status_breakdown | Metric | 600x240 | Segmented bar with labeled status counts |
activity_feed | Content | 600x400 | Chronological list of recent actions and events |
text_block | Content | 400x240 | Static rich text or markdown content block |
countdown | Content | 300x160 | Countdown timer to a target date |
comparison | Metric | 300x160 | Side-by-side comparison of two metric values |
leaderboard | Table | 400x400 | Ranked list with values (e.g. top assignees, top clients) |
image_block | Content | 300x240 | Static image or logo display block |
Data Sources
Each widget specifies a dataSource object. All sources support dateField for time-based filtering and filters for field-level conditions.
| Source Type | Extra Field | Date Field Default | Description |
|---|---|---|---|
table | tableId | createdAt | Query rows from a data table. Filters apply to JSON data column fields |
matters | templateId | createdAt | Query matters. Supports status, priority, assignee, and custom field filters |
submissions | formId | submittedAt | Query form submissions. Supports status, form, and submission data filters |
audit_log | - | createdAt | Query audit log entries. Supports action and resource type filters |
accounting | connectionId | date | Query accounting data (invoices, contacts, accounts) via pipeline connection |
forms | - | createdAt | Query form list metadata. Columns: id, title, status, createdAt, submissionCount |
workflows | - | createdAt | Query workflow list. Columns: id, name, status, createdAt, lastRunAt |
agent_tasks | - | createdAt | Query AI agent task list. Columns: id, title, status, createdAt, completedAt |
files | - | createdAt | Query file records. Columns: id, name, mimeType, size, createdAt, folder |
documents | - | createdAt | Query knowledge base documents. Columns: id, title, createdAt, updatedAt, wordCount |
users | - | createdAt | Query workspace users. Columns: id, name, email, role, createdAt |
pipelines | - | lastSyncAt | Query pipeline connections. Columns: id, name, provider, status, lastSyncAt |
Filter Object
Filters can be applied at the data source level (per-widget) or at the config level (global filters affecting all compatible widgets).
{
"field": "status",
"operator": "in",
"value": ["OPEN", "ON_HOLD"]
}
| Operator | Description | Value Type |
|---|---|---|
eq / neq | Equal / not equal | string | number |
gt / lt / gte / lte | Numeric comparison | number |
in | Value is in list | string[] |
contains | Substring match (case-insensitive) | string |
Widget Data
Fetch computed data for a single widget. The primary endpoint is GET with a base64-encoded query parameter q. A legacy POST endpoint is also available.
GET (preferred)
GET /api/dashboards/:id/widget-data?q=<base64-encoded-json>
The q parameter is the base64 encoding of a JSON object:
{
"widgetType": "bar_chart",
"dataSource": { "type": "matters" },
"aggregation": { "function": "count", "column": "status" },
"groupBy": { "column": "status" },
"dateFrom": "2026-01-01T00:00:00.000Z",
"dateTo": "2026-02-24T23:59:59.999Z"
}
POST (legacy)
POST /api/dashboards/:id/widget-data
Content-Type: application/json
{
"widgetType": "bar_chart",
"dataSource": {
"type": "matters",
"filters": [
{ "field": "status", "operator": "in", "value": ["OPEN", "ON_HOLD"] }
]
},
"aggregation": { "function": "count", "column": "status" },
"groupBy": { "column": "status" },
"secondaryGroupBy": { "column": "priority" },
"dateFrom": "2026-01-01T00:00:00.000Z",
"dateTo": "2026-02-24T23:59:59.999Z",
"includeSparkline": true,
"maxRows": 100
}
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
widgetType | string | Yes | One of the 26 widget type identifiers |
dataSource | object | Yes | Data source with type, optional IDs, dateField, and filters |
aggregation | object | No | Aggregation function and column |
groupBy | object | No | Primary grouping column with optional dateGranularity, limit, sort |
secondaryGroupBy | object | No | Secondary grouping for multi-series output. Returns multiSeries data instead of flat series |
dateFrom | string | No | ISO 8601 start date for date-range filtering |
dateTo | string | No | ISO 8601 end date for date-range filtering |
includeSparkline | boolean | No | When true, response includes sparklineData array (last 7 data points) |
maxRows | number | No | Maximum rows to process (1-2000, default 2000) |
Response - Single Series
{
"data": {
"series": [
{ "label": "Open", "value": 42 },
{ "label": "In Progress", "value": 18 },
{ "label": "Closed", "value": 67 }
],
"summary": { "total": 127, "count": 3, "avg": 42.3 },
"sparklineData": [12, 15, 18, 22, 19, 24, 17]
}
}
Response - Multi-Series (with secondaryGroupBy)
{
"data": {
"series": [],
"multiSeries": {
"categories": ["Open", "In Progress", "Closed"],
"series": [
{ "name": "High", "data": [12, 5, 20] },
{ "name": "Medium", "data": [20, 8, 30] },
{ "name": "Low", "data": [10, 5, 17] }
]
},
"summary": { "total": 127, "count": 3 }
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
data.series | array | Array of { label, value } for single-series data |
data.multiSeries | object | Present when secondaryGroupBy is used. Contains categories and series arrays |
data.summary | object | Rollup: total (sum), count (groups), optional avg |
data.sparklineData | number[] | Last 7 data points for inline trend display (only when includeSparkline: true) |
Aggregation Functions
| Function | Description |
|---|---|
count | Count of records in each group |
sum | Sum of numeric column values |
avg | Average (mean) of numeric column values |
min | Minimum value in the column |
max | Maximum value in the column |
count_distinct | Count of unique values in the column |
Appearance Fields
The appearance object controls visual presentation. Only title is required; all other fields are optional and type-dependent.
| Field | Type | Used By | Description |
|---|---|---|---|
title | string | All | Widget title (required, 1-200 chars) |
subtitle | string | All | Secondary description (max 500 chars) |
showLegend | boolean | Charts | Show chart legend |
showDataLabels | boolean | Charts | Show values on chart elements |
showGrid | boolean | Charts | Show grid lines |
curved | boolean | Line, Area | Smooth curve interpolation |
stacked | boolean | Bar, Area | Stack series instead of grouping side by side |
innerRadius | number | Pie | Inner radius for donut variant (0-100) |
unit / prefix / suffix | string | Metric, Table | Value formatting: {prefix}{unit}{value}{suffix} |
target | number | Gauge, Progress | Target value for progress/gauge widgets |
targetDate | string | Countdown | ISO 8601 date for countdown timer |
showSparkline | boolean | Metric | Show inline sparkline trend beneath the main number |
scorecardMetrics | array | KPI Scorecard | 2-4 metrics: { label, column, aggregation } |
colorOverrides | string[] | Charts | Custom colour palette (CSS values or hex codes) |
thresholds | array | Gauge, Metric | Colour zones: { value, color: "success"|"warning"|"error" } |
comparisonLabel / comparisonValue | string / number | Comparison | Reference value for comparison widget |
markdown | string | Text Block | Markdown content (max 5000 chars) |
imageUrl | string | Image Block | HTTP(S) image URL |
Date Range Presets
The dashboard UI includes a persistent date range toolbar. When a range is selected, the client computes dateFrom and dateTo and passes them to each widget-data request.
| Preset | Description |
|---|---|
7d | Last 7 days |
30d | Last 30 days |
90d | Last 90 days |
this_month | Current calendar month |
this_quarter | Current fiscal quarter (Q1 Jan-Mar, Q2 Apr-Jun, etc.) |
ytd | Year to date (Jan 1 to today) |
custom | Custom date range with explicit from and to dates |
GroupBy Options
{
"column": "createdAt",
"dateGranularity": "week",
"limit": 20,
"sort": "asc"
}
| Field | Type | Description |
|---|---|---|
column | string | Field name to group by |
dateGranularity | string | Time bucketing: day, week, month, quarter, year |
limit | number | Max number of groups to return (1-100) |
sort | string | Sort direction: asc or desc |
JSON Import / Export
The dashboard editor supports a Grid/JSON tab switcher. In JSON mode you can view and edit the raw dashboard config directly. Changes are validated in real time.
- Export: Click "Export .json" to download the config as a file, or "Copy" to copy to clipboard.
- Import: Click the Import button and select a
.jsonfile. The file is validated before applying. - Programmatic: Build a dashboard config JSON object and submit it via
PUT /api/dashboards/:idwith theconfigfield.
{
"version": 1,
"widgets": [
{
"id": "my-metric-1",
"type": "metric",
"dataSource": { "type": "matters" },
"aggregation": { "function": "count", "column": "id" },
"appearance": {
"title": "Total Matters",
"showSparkline": true
},
"layout": { "x": 0, "y": 0, "w": 300, "h": 160 }
}
]
}
KPI Scorecard Example
The kpi_scorecard widget renders 2-4 metrics in a horizontal row, each with its own aggregation, sparkline, and change indicator.
{
"id": "scorecard-1",
"type": "kpi_scorecard",
"dataSource": { "type": "matters" },
"aggregation": { "function": "count", "column": "id" },
"appearance": {
"title": "Matter Summary",
"scorecardMetrics": [
{ "label": "Total", "column": "id", "aggregation": "count" },
{ "label": "Open", "column": "status_OPEN", "aggregation": "count" },
{ "label": "Completed", "column": "status_COMPLETED", "aggregation": "count" },
{ "label": "Avg Days", "column": "daysOpen", "aggregation": "avg" }
]
},
"layout": { "x": 0, "y": 0, "w": 1200, "h": 160, "minW": 600, "minH": 160 }
}
Canvas Config
Dashboards use a pixel-based freeform canvas. Widgets are positioned absolutely with x, y pixel coordinates and sized with w, h pixel dimensions. Canvas alignment guides appear automatically when dragging or resizing widgets, showing snap lines to neighbouring widget edges and centres for precise positioning.
| Setting | Type | Description |
|---|---|---|
snapToGrid | boolean | Snap widget positions to a 20px grid when dragging (default: true) |
zoom | number | Canvas zoom level (0.5 - 2.0, default: 1.0). Ctrl+scroll to adjust |
pan | object | Canvas pan offset: { x, y } in pixels. Middle-click drag to pan |
z | number | Per-widget z-index for layering order. Higher values render on top. Used in layout.z |
AI Dashboard Tools
The AI assistant includes 7 tools for programmatic dashboard management. These tools allow the AI to list, add, update, remove, and bulk-update widgets, as well as replace the entire dashboard config. Additional theme AI tools let the AI read, create, update, and delete custom user themes, enabling AI-driven theme generation and customisation.
| Tool | Description |
|---|---|
list_dashboard_widgets | List all widgets on a dashboard |
add_dashboard_widget | Add a new widget with full config (type, data source, appearance, layout) |
update_dashboard_widget | Update an existing widget by ID |
remove_dashboard_widget | Remove a widget from a dashboard |
bulk_update_dashboard | Batch update multiple widgets and settings in one call |
set_dashboard_config | Replace the entire dashboard config (widgets, filters, auto-refresh) |
get_dashboard_stats | Get workspace dashboard summary statistics |
See the AI Tools Reference for full parameter details.
Sharing and Permissions
Each dashboard has sharing settings stored in its settings.sharing JSON field. Sharing controls who can view or edit a dashboard, with two visibility modes and per-user collaborator overrides.
Get Sharing Settings
GET /api/dashboards/:id/sharing
{
"sharing": {
"visibility": "workspace",
"workspaceRole": "editor",
"collaborators": [
{ "userId": "clx...", "role": "editor" }
]
},
"canManage": true,
"members": [
{
"userId": "clx...",
"name": "Will Lilley",
"email": "will@example.com",
"collaboratorRole": "editor"
}
]
}
visibility is "workspace" or "restricted". workspaceRole is "viewer" or "editor" (default access for workspace members). canManage is true if the user is admin/owner or dashboard creator. collaboratorRole is null if the member is not a collaborator.
Update Sharing Settings
Only workspace admins/owners or the dashboard creator can update sharing settings.
PUT /api/dashboards/:id/sharing
Content-Type: application/json
{
"visibility": "restricted",
"workspaceRole": "viewer",
"collaborators": [
{ "userId": "clx...", "role": "editor" },
{ "userId": "clx...", "role": "viewer" }
]
}
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
visibility | string | Yes | workspace (all members) or restricted (collaborators only) |
workspaceRole | string | Yes | Default access for workspace members: viewer or editor |
collaborators | array | Yes | Array of { userId, role } objects. Role is viewer or editor |
Cross-Workspace Data (Oversight)
Oversight workspaces can query data from subordinate workspaces in dashboard widgets. Set the sourceWorkspaceId field in a widget's data source to target a subordinate workspace.
{
"type": "table",
"tableId": "clx...",
"sourceWorkspaceId": "clx..."
}
- The widget-data API verifies an active
OversightRelationshipbefore querying the subordinate workspace. - If no oversight relationship exists or it is inactive, the API returns
403. - The designer sidebar shows a "Workspace" picker above the Source picker when the current workspace has subordinate workspaces.
Autosave
Dashboard widget configuration (layout, data source, appearance, elements) is autosaved with a 2-second debounce. The save indicator shows a dot for unsaved changes, a spinner while saving, and a brief "Autosaved" label that fades out after 2 seconds. Autosave only tracks structural config changes - not live data refreshes or widget data updates.
Dashboard Comments
Each dashboard supports threaded comments via the DashboardComment model. Comments are scoped to a dashboard and user, with optional parentId for nested replies. Comments include a content text field, timestamps (createdAt, updatedAt), and cascade-delete when the parent dashboard is removed.
Delete Dashboard
Deletes a dashboard. The default dashboard cannot be deleted and will return a 400 error.
DELETE /api/dashboards/:id
Error response when deleting default dashboard:
{
"error": "Cannot delete the default dashboard"
}