Opbox

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

MethodEndpointDescription
GET/api/dashboardsList all dashboards for the org
POST/api/dashboardsCreate a new dashboard
GET/api/dashboards/:idGet dashboard details (including config)
PUT/api/dashboards/:idUpdate dashboard (name, config, isStarred, etc.)
DELETE/api/dashboards/:idDelete 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-dataFetch computed widget data (legacy)
GET/api/dashboards/:id/sharingGet sharing settings, collaborators, and workspace members
PUT/api/dashboards/:id/sharingUpdate 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

FieldTypeRequiredDescription
namestringYesDashboard display name (1-255 chars)
descriptionstringNoShort description (max 1000 chars)
parentIdstringNoID of a base dashboard to nest this dashboard under. Parent must exist in the same org and have settings.isBase: true
settingsobjectNoDashboard 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

FieldTypeRequiredDescription
namestringNoDashboard display name
descriptionstringNoShort description
ordernumberNoSort order in sidebar (lower first)
isStarredbooleanNoPin as favourite (starred dashboards sort first in sidebar)
configobjectNoFull widget grid configuration (see Config Structure)
parentIdstring | nullNoMove dashboard into a base (ID) or set null to make it an orphan. Parent must have settings.isBase: true
settingsobjectNoUpdate 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

FieldTypeDescription
version1Schema version (must be 1)
widgetsarrayArray of widget definitions (max 50)
autoRefreshIntervalnumberSeconds between auto-refresh cycles. 0 = off. Typical: 30, 60, 300
globalFiltersarrayFilters applied to all widgets whose data source supports the filter field

Widget Fields

FieldTypeDescription
idstringUnique widget identifier (UUID)
typestringWidget type (see Widget Types table)
dataSourceobjectData source config with type, optional IDs, dateField, and filters
aggregationobjectAggregation function and target column
groupByobjectPrimary grouping: column + optional date granularity, limit, sort
secondaryGroupByobjectSecondary grouping for multi-series charts (produces grouped/stacked bars, multi-line, etc.)
appearanceobjectDisplay settings (see Appearance Fields)
layoutobjectPixel-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.

TypeCategoryDefault SizeDescription
metricMetric300x160Single large number with optional comparison delta and sparkline
kpi_scorecardMetric1200x160Multi-KPI row showing 2-4 metrics with individual sparklines and change deltas
line_chartChart600x320Time-series or continuous data as connected points (supports multi-series)
bar_chartChart600x320Vertical bars for categorical comparison (supports multi-series grouped bars)
area_chartChart600x320Filled area under a line for volume trends (supports multi-series)
pie_chartChart400x320Proportional slices with optional donut hole (innerRadius)
stacked_bar_chartChart600x320Stacked bars showing composition within categories
radar_chartChart400x320Spider/radar plot for multi-axis comparison
scatter_chartChart600x320XY scatter plot for correlation analysis
funnel_chartChart400x400Funnel stages showing progressive narrowing
treemap_chartChart600x320Nested rectangles sized by value for hierarchical data
gaugeMetric300x240Radial dial showing progress toward a target value
sparklineMetric300x160Compact inline trend line without axes
heatmapChart600x320Color-coded matrix for density or intensity data
waterfall_chartChart600x320Sequential additions and subtractions from an initial value
combo_chartChart600x320Combined bar and line chart on dual axes
table_gridTable600x320Sortable, paginated data table (click column headers to sort, 10 rows/page)
progress_barMetric300x160Horizontal bar showing completion percentage against target
progress_ringMetric300x240Circular ring showing completion percentage
status_breakdownMetric600x240Segmented bar with labeled status counts
activity_feedContent600x400Chronological list of recent actions and events
text_blockContent400x240Static rich text or markdown content block
countdownContent300x160Countdown timer to a target date
comparisonMetric300x160Side-by-side comparison of two metric values
leaderboardTable400x400Ranked list with values (e.g. top assignees, top clients)
image_blockContent300x240Static 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 TypeExtra FieldDate Field DefaultDescription
tabletableIdcreatedAtQuery rows from a data table. Filters apply to JSON data column fields
matterstemplateIdcreatedAtQuery matters. Supports status, priority, assignee, and custom field filters
submissionsformIdsubmittedAtQuery form submissions. Supports status, form, and submission data filters
audit_log-createdAtQuery audit log entries. Supports action and resource type filters
accountingconnectionIddateQuery accounting data (invoices, contacts, accounts) via pipeline connection
forms-createdAtQuery form list metadata. Columns: id, title, status, createdAt, submissionCount
workflows-createdAtQuery workflow list. Columns: id, name, status, createdAt, lastRunAt
agent_tasks-createdAtQuery AI agent task list. Columns: id, title, status, createdAt, completedAt
files-createdAtQuery file records. Columns: id, name, mimeType, size, createdAt, folder
documents-createdAtQuery knowledge base documents. Columns: id, title, createdAt, updatedAt, wordCount
users-createdAtQuery workspace users. Columns: id, name, email, role, createdAt
pipelines-lastSyncAtQuery 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"]
}
OperatorDescriptionValue Type
eq / neqEqual / not equalstring | number
gt / lt / gte / lteNumeric comparisonnumber
inValue is in liststring[]
containsSubstring 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

FieldTypeRequiredDescription
widgetTypestringYesOne of the 26 widget type identifiers
dataSourceobjectYesData source with type, optional IDs, dateField, and filters
aggregationobjectNoAggregation function and column
groupByobjectNoPrimary grouping column with optional dateGranularity, limit, sort
secondaryGroupByobjectNoSecondary grouping for multi-series output. Returns multiSeries data instead of flat series
dateFromstringNoISO 8601 start date for date-range filtering
dateTostringNoISO 8601 end date for date-range filtering
includeSparklinebooleanNoWhen true, response includes sparklineData array (last 7 data points)
maxRowsnumberNoMaximum 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

FieldTypeDescription
data.seriesarrayArray of { label, value } for single-series data
data.multiSeriesobjectPresent when secondaryGroupBy is used. Contains categories and series arrays
data.summaryobjectRollup: total (sum), count (groups), optional avg
data.sparklineDatanumber[]Last 7 data points for inline trend display (only when includeSparkline: true)

Aggregation Functions

FunctionDescription
countCount of records in each group
sumSum of numeric column values
avgAverage (mean) of numeric column values
minMinimum value in the column
maxMaximum value in the column
count_distinctCount 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.

FieldTypeUsed ByDescription
titlestringAllWidget title (required, 1-200 chars)
subtitlestringAllSecondary description (max 500 chars)
showLegendbooleanChartsShow chart legend
showDataLabelsbooleanChartsShow values on chart elements
showGridbooleanChartsShow grid lines
curvedbooleanLine, AreaSmooth curve interpolation
stackedbooleanBar, AreaStack series instead of grouping side by side
innerRadiusnumberPieInner radius for donut variant (0-100)
unit / prefix / suffixstringMetric, TableValue formatting: {prefix}{unit}{value}{suffix}
targetnumberGauge, ProgressTarget value for progress/gauge widgets
targetDatestringCountdownISO 8601 date for countdown timer
showSparklinebooleanMetricShow inline sparkline trend beneath the main number
scorecardMetricsarrayKPI Scorecard2-4 metrics: { label, column, aggregation }
colorOverridesstring[]ChartsCustom colour palette (CSS values or hex codes)
thresholdsarrayGauge, MetricColour zones: { value, color: "success"|"warning"|"error" }
comparisonLabel / comparisonValuestring / numberComparisonReference value for comparison widget
markdownstringText BlockMarkdown content (max 5000 chars)
imageUrlstringImage BlockHTTP(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.

PresetDescription
7dLast 7 days
30dLast 30 days
90dLast 90 days
this_monthCurrent calendar month
this_quarterCurrent fiscal quarter (Q1 Jan-Mar, Q2 Apr-Jun, etc.)
ytdYear to date (Jan 1 to today)
customCustom date range with explicit from and to dates

GroupBy Options

{
  "column": "createdAt",
  "dateGranularity": "week",
  "limit": 20,
  "sort": "asc"
}
FieldTypeDescription
columnstringField name to group by
dateGranularitystringTime bucketing: day, week, month, quarter, year
limitnumberMax number of groups to return (1-100)
sortstringSort 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 .json file. The file is validated before applying.
  • Programmatic: Build a dashboard config JSON object and submit it via PUT /api/dashboards/:id with the config field.
{
  "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.

SettingTypeDescription
snapToGridbooleanSnap widget positions to a 20px grid when dragging (default: true)
zoomnumberCanvas zoom level (0.5 - 2.0, default: 1.0). Ctrl+scroll to adjust
panobjectCanvas pan offset: { x, y } in pixels. Middle-click drag to pan
znumberPer-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.

ToolDescription
list_dashboard_widgetsList all widgets on a dashboard
add_dashboard_widgetAdd a new widget with full config (type, data source, appearance, layout)
update_dashboard_widgetUpdate an existing widget by ID
remove_dashboard_widgetRemove a widget from a dashboard
bulk_update_dashboardBatch update multiple widgets and settings in one call
set_dashboard_configReplace the entire dashboard config (widgets, filters, auto-refresh)
get_dashboard_statsGet 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

FieldTypeRequiredDescription
visibilitystringYesworkspace (all members) or restricted (collaborators only)
workspaceRolestringYesDefault access for workspace members: viewer or editor
collaboratorsarrayYesArray 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 OversightRelationship before 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"
}