dashboard-builder
GitHub指导Agent构建Maple可观测性仪表板。要求先通过测试工具验证数据查询有效性,再添加统计、表格或图表组件,确保可视化内容基于真实数据并处理空数据情况。
Trigger Scenarios
Install
npx skills add MapleTechLabs/maple --skill dashboard-builder -g -y
SKILL.md
Frontmatter
{
"name": "dashboard-builder",
"description": "Use when the user asks to build, add, edit, or fix Maple dashboards or dashboard widgets (stats, tables, charts, lists)."
}
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:
- Build the widget config mentally (endpoint, params, transform)
- Call test_widget_query with the exact same endpoint, params, and transform you plan to use
- 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.
- Briefly summarize the test results (e.g., "Tested errors_summary — found 42 errors at 2.1% error rate")
- 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:
{
"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:
- 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.
- 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.
- Do NOT change
id,layout, orvisualizationunless the fix explicitly requires it. Preservedisplay.titleand other display config that is not implicated by the problem. - 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.
- Call update_dashboard_widget with
dashboard_id,widget_id, and a complete correctedwidget_json(the full widget object as a JSON string), not just the changed fields. - 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.
- DO NOT narrate your tool calls or explain your investigation process in detail
- After adding widgets, confirm what was added in one sentence, with a link to the dashboard
Version History
- 6578edf Current 2026-07-31 11:20


