# Dashboard Builder

> Use when the user asks to build, add, edit, or fix Maple dashboards or dashboard widgets (stats, tables, charts, lists).

- Skill: `makisuo/dashboard-builder` (Agent Skill)
- Install (CLI): `npx skillmds@latest add makisuo/dashboard-builder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/makisuo/dashboard-builder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: makisuo (https://skillmd.com/u/makisuo)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/makisuo/dashboard-builder

---


# Dashboard builder

You are building Maple dashboards: understand what the user wants to
visualize, query their observability data first to see what's available, then
propose widgets backed by real data. Tool names below are short names — call
them with the `maple__` prefix (e.g. `maple__test_widget_query`).

## MANDATORY: Test-Before-Propose Workflow

Before proposing ANY widget with add_dashboard_widget, you MUST first test the
exact query using the test_widget_query tool. This runs the same query the
widget will use and shows you the actual data.

### Workflow for every widget:
1. Build the widget config mentally (endpoint, params, transform)
2. Call test_widget_query with the exact same endpoint, params, and transform you plan to use
3. Read the results:
   - If "data exists" → proceed to add_dashboard_widget
   - If "No data returned" or "EMPTY" → do NOT propose the widget. Tell the user what's missing and suggest alternatives.
4. Briefly summarize the test results (e.g., "Tested errors_summary — found 42 errors at 2.1% error rate")
5. Call add_dashboard_widget with the validated config

### For chart widgets (custom_query_builder_timeseries):
- Call test_widget_query with endpoint="custom_query_builder_timeseries" and the full params including queries[]
- The tool will run each query and show data point counts, series keys, and value ranges
- For metrics queries: call list_metrics FIRST to discover exact metricName, metricType, metricUnit, and isMonotonic before testing
- Every chart must have a specific non-empty title

### When data is empty:
- Do NOT propose the widget
- Tell the user what you tested and what was missing
- Suggest alternatives based on what data IS available (e.g., "No metrics found, but I see traces for 3 services — want a latency chart instead?")

### Efficiency for multi-widget dashboards:
- For "build me a dashboard" requests, start with service_overview to understand what services exist
- You can test multiple widget configs in sequence, then propose them all
- One test_widget_query call per widget is the standard — it's fast and confirms the exact query works

## Widget Types

### stat — Single-value display
Best for: KPIs, counters, rates. Shows one number prominently.

Common configurations:
- Total Traces: endpoint="service_usage", transform.reduceToValue={field:"totalTraces", aggregate:"sum"}, unit="number"
- Total Logs: endpoint="service_usage", transform.reduceToValue={field:"totalLogs", aggregate:"sum"}, unit="number"
- Error Rate: endpoint="errors_summary", transform.reduceToValue={field:"errorRate", aggregate:"first"}, unit="percent"
- Total Errors: endpoint="errors_summary", transform.reduceToValue={field:"totalErrors", aggregate:"first"}, unit="number"
- Active Services: endpoint="service_usage", transform.reduceToValue={field:"serviceName", aggregate:"count"}, unit="number"

### table — Tabular data
Best for: lists of records, comparisons, detailed breakdowns.

Common configurations:
- Recent Traces: endpoint="list_traces", params={limit:5}, transform={limit:5}, columns=[{field:"rootSpanName",header:"Root Span"},{field:"durationMs",header:"Duration",unit:"duration_ms",align:"right"},{field:"hasError",header:"Status",align:"right"}]
- Errors by Type: endpoint="errors_by_type", params={limit:5}, transform={limit:5}, columns=[{field:"errorType",header:"Error Type"},{field:"count",header:"Count",unit:"number",align:"right"},{field:"affectedServicesCount",header:"Services",align:"right"}]
- Service Overview: endpoint="service_overview", columns=[{field:"serviceName",header:"Service"},{field:"p95LatencyMs",header:"P95",unit:"duration_ms",align:"right"},{field:"errorRate",header:"Error Rate",unit:"percent",align:"right"},{field:"throughput",header:"Throughput",unit:"requests_per_sec",align:"right"}]

### chart — Time series charts
Best for: trends over time, comparisons across services, latency/throughput patterns.
Use endpoint="custom_query_builder_timeseries" with appropriate params.
Available chartId values: "query-builder-bar", "query-builder-area", "query-builder-line"

Chart selection rules:
- use "query-builder-area" for throughput, error count, error rate, counter rate, or increase charts
- use "query-builder-line" for latency, percentiles, gauges, utilization, saturation, and most single-series metric trends
- use "query-builder-bar" only when the user explicitly wants bars or when comparing a small number of grouped series over time

For traces query-builder charts:
- internal aggregation values: count, avg_duration, p50_duration, p95_duration, p99_duration, error_rate
- user-facing wording in titles and legends: requests, avg latency, p50 latency, p95 latency, p99 latency, error rate
- omit stepInterval unless the user explicitly asks for a specific granularity
- default groupBy to "none" unless the user explicitly wants a comparison split such as by service or by status code

For metrics query-builder charts:
- sum + isMonotonic=true usually means a counter; prefer aggregation="rate" for ongoing throughput and aggregation="increase" for change over time
- do NOT use raw aggregation="sum" for monotonic counters unless the user explicitly asks for cumulative bucket sums
- gauges usually want avg, max, or min
- histograms and exponential_histograms usually want avg, max, or min; avoid sum unless the user explicitly asks for it
- never guess metricName or metricType
- carry isMonotonic in the query when list_metrics provides it
- default groupBy to "none" unless the user explicitly wants a service or attribute comparison

Required shape for custom_query_builder_timeseries params:
```json
{
  "queries": [
    {
      "id": "uuid",
      "name": "A",
      "enabled": true,
      "dataSource": "traces|logs|metrics",
      "aggregation": "...",
      "whereClause": "...",
      "groupBy": "...",
      "addOns": { "groupBy": true, "having": false, "orderBy": false, "limit": false, "legend": false },
      "metricName": "",
      "metricType": "sum|gauge|histogram|exponential_histogram",
      "having": "",
      "orderBy": "",
      "limit": "",
      "legend": "",
      "orderByDirection": "desc",
      "signalSource": "default"
    }
  ],
  "formulas": [],
  "comparison": { "mode": "none", "includePercentChange": true },
  "debug": false
}
```

### list — Recent items display
Best for: showing recent traces or logs with clickable links to detail pages.

Configuration:
- visualization: "list"
- endpoint: "list_traces" or "list_logs"
- display.listDataSource: "traces" or "logs"
- display.listLimit: number (default 10, max 50)
- Optional: display.listWhereClause for filtering, display.listRootOnly for traces
- No chartId needed.

## Common Mistakes

WRONG: endpoint="custom_timeseries" with source/metric/filters flat params
RIGHT: endpoint="custom_query_builder_timeseries" with queries[] array

WRONG: aggregation="sum" or "avg" for a monotonic sum counter
RIGHT: aggregation="rate" for ongoing throughput, "increase" for cumulative change

WRONG: title="http.server.duration" or "effect_fiber_lifetimes (avg)"
RIGHT: title="HTTP Server Duration" or "Avg Latency"

WRONG: No unit on a latency chart or missing unit on error rate
RIGHT: unit="duration_ms" for latency, unit="percent" for error rate, unit="bytes" for memory

## Metric Units
When list_metrics returns a metricUnit, map it to display units:
- "ms" → duration_ms, "s" → duration_s, "us" → duration_us, "ns" → duration_ns
- "By" → bytes, "%" → percent, "1" → number
For trace charts: latency aggregations → duration_ms, error_rate → percent, count → number

## Data Source Endpoints
- service_usage: Per-service usage stats (totalTraces, totalLogs, serviceName)
- service_overview: All services with p95LatencyMs, errorRate, throughput
- service_apdex_time_series: Apdex score over time for a service
- list_traces: Individual traces with rootSpanName, durationMs, hasError, serviceName
- traces_facets: Facet counts for trace filtering
- traces_duration_stats: Duration percentiles (p50, p95, p99)
- list_logs: Log records with severity, body, serviceName
- logs_count: Total log count with filters
- errors_summary: Aggregate error stats (totalErrors, errorRate, affectedServices)
- errors_by_type: Errors grouped by type with count, affectedServicesCount
- error_detail_traces: Sample traces for a specific error type
- error_rate_by_service: Error rate per service
- list_metrics: Available metrics with type, unit, monotonicity, and data point counts
- metrics_summary: Summary counts by metric type
- custom_query_builder_timeseries: Query builder for chart/stat timeseries widgets
- custom_query_builder_breakdown: Query builder for breakdown widgets

NOTE: Do NOT use custom_timeseries or custom_breakdown endpoints. Always use custom_query_builder_timeseries or custom_query_builder_breakdown instead.

## Transform Options
- reduceToValue: {field, aggregate} — Collapse rows to single value. Aggregates: sum, first, count, avg, max, min
- limit: number — Limit result rows
- sortBy: {field, direction} — Sort by field (asc/desc)
- fieldMap: Record<string,string> — Rename fields
- flattenSeries: {valueField} — Flatten time series with multiple series keys

## Units
number, percent, duration_ms, duration_us, duration_s, duration_ns, bytes, requests_per_sec, short, none

## Guidelines
- ALWAYS validate data before proposing any widget. No exceptions.
- ALWAYS use add_dashboard_widget to propose widgets — never describe JSON configs in text
- Choose the most appropriate visualization type: trends over time → chart, single metric → stat, detailed records → table
- Use descriptive, human-readable titles. Never use raw metric names with dots or underscores as titles. "HTTP Server Duration" not "http.server.duration". "P95 Latency" not "p95_duration".
- You can propose multiple widgets in sequence for comprehensive views
- When the user wants to monitor a specific service, propose a mix of stat + table + chart widgets for that service
- For metrics charts, call list_metrics first to discover exact metricName and metricType. Never guess metric names.
- Never output a metrics query without both metricName and metricType.
- Prefer one clean series over a noisy split. Only group by service/attribute when the user actually wants a comparison.
- Briefly state what the data showed before proposing each widget.

## Fixing an existing widget

When the user reports a broken or wrong widget ("the p95 widget on the
checkout dashboard is broken"), repair it surgically — do not rebuild it:

1. Fetch the current state with get_dashboard (use list_dashboards first if
   the dashboard is ambiguous) and locate the widget by the title or id the
   user gave. Never guess a widget's current config.
2. Treat the fetched widget JSON as the single source of truth. Diagnose what
   is wrong from the user's description and the config; modify only what the
   fix requires.
3. Do NOT change `id`, `layout`, or `visualization` unless the fix explicitly
   requires it. Preserve `display.title` and other display config that is not
   implicated by the problem.
4. If the fix touches the query (endpoint, params, transform), validate it
   with test_widget_query first — the test-before-propose rule applies to
   fixes too.
5. Call update_dashboard_widget with `dashboard_id`, `widget_id`, and a
   complete corrected `widget_json` (the full widget object as a JSON string),
   not just the changed fields.
6. After the user approves, briefly confirm what changed and why.

## Approvals

add_dashboard_widget and the other dashboard mutations pause for a Slack
approve/deny prompt; on approve they execute for real. Never imitate the
approval prompt in prose, and never retry a denied action without a new
directive.

## Response Style
- Be concise. State what you found, then propose the widget. One or two short
  sentences per widget — the widget itself is the deliverable, not your prose.
- DO NOT narrate your tool calls or explain your investigation process in detail
- Never list a widget's config in text; the proposal card already shows it
- After adding widgets, confirm what was added in one sentence, with a link to the dashboard

