Build, repair, or review Maple dashboard widgets via the MCP. Triggers on phrases like 'create_dashboard', 'add_dashboard_widget', 'update_dashboard_widget', 'dashboard widget JSON', 'QueryDraft', 'trace dashboard widget', 'Invalid input for getQueryBuilderTimeseries', or any session that submits raw widget JSON to the maple MCP. Covers the source-discriminated QueryDraft shape, the custom whereClause grammar, valid aggregations per data source, groupBy prefix conventions, the stat-widget `reduceToValue` transform, hiding auxiliary series on formula charts, and the verification step (MCP success ≠ query success).
التثبيت
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
Build, repair, or review Maple dashboard widgets via the MCP. Triggers on phrases like 'create_dashboard', 'add_dashboard_widget', 'update_dashboard_widget', 'dashboard widget JSON', 'QueryDraft', 'trace dashboard widget', 'Invalid input for getQueryBuilderTimeseries', or any session that submits raw widget JSON to the maple MCP. Covers the source-discriminated QueryDraft shape, the custom whereClause grammar, valid aggregations per data source, groupBy prefix conventions, the stat-widget `reduceToValue` transform, hiding auxiliary series on formula charts, and the verification step (MCP success ≠ query success).
Maple dashboard widgets via MCP
When to use this skill
When you are constructing raw widget JSON for any of:
mcp__maple__create_dashboard with a dashboard_json payload
mcp__maple__add_dashboard_widget
mcp__maple__update_dashboard_widget
If you are creating a fresh dashboard, prefer the simplified widgets array on create_dashboard (SimpleWidgetSpec at apps/api/src/mcp/tools/create-dashboard.ts:94). It side-steps every trap below — fill in title, source, metric, optional group_by, optional service_name, and the tool builds the full shape for you. Raw JSON is for cases the simplified spec can't express (multi-query charts, formulas, hidden series, non-default transforms).
Trap 1 — Query drafts are source-discriminated
The query draft schema (packages/domain/src/http/query-engine.ts) is a union discriminated on dataSource. The metric-only fields — metricName, metricType, isMonotonic, signalSource — exist only on dataSource: "metrics" queries.
For dataSource: "traces" or "logs": do not include metricName, metricType, isMonotonic, or signalSource. They are not part of the trace/log query shape.
For dataSource: "metrics": include metricName (the metric to query), metricType, and optionally isMonotonic / signalSource.
Never add metricName/metricType/isMonotonic/signalSource to a trace or log query — they belong solely to dataSource: "metrics" queries. (Older stored dashboards were migrated to drop them; new widgets must not reintroduce them.)
Clauses join with AND (case-insensitive). No OR, no parentheses.
Keys are normalized to lowercase by the parser.
Quoted values use double quotes.
There is no IS NULL / IS NOT NULL. To require an attribute be present, use <key> exists; to require it absent, <key> !exists. This is the single most common mistake.
Attribute filters work directly. On dataSource: "traces" you can filter by any span/resource attribute — query.context = "tracesList", error.type != "Timeout", db.system = "clickhouse". Bare keys outside the small structured allowlist (service.name, span.name, deployment.environment, deployment.commit_sha, root_only, has_error) are auto-treated as span attributes; you can also write them explicitly as attr.<key> / resource.<key>. Cap: 5 attr.* + 5 resource.* filters per query.
Unhonored clauses are now rejected at write time.add_dashboard_widget / update_dashboard_widget / replace_dashboard_widgets run the builder before persisting and FAIL (nothing saved) if a clause can't be honored — e.g. exceeding the attr cap, or an unsupported logs/metrics filter key. (This used to be a silent drop.)
Wrong:
service.name = "ingest" AND maple.signal IS NOT NULL
Right:
service.name = "ingest" AND maple.signal exists
Minimum-viable trace chart widget JSON
Use as a template. Fill whereClause, groupBy, aggregation, display.title, display.unit, layout:
{"id":"w0","visualization":"chart","dataSource":{"endpoint":"custom_query_builder_timeseries","params":{"queries":[{"id":"q-w0","name":"A","enabled":true,"hidden":false,"dataSource":"traces","whereClause":"service.name = \"ingest\" AND maple.signal exists","aggregation":"count","stepInterval":"","orderByDirection":"desc","addOns":{"groupBy":true,"having":false,"orderBy":false,"limit":false,"legend":false},"groupBy":["maple.signal"],"having":"","orderBy":"","limit":"","legend":""}],"formulas":[],"comparison":{"mode":"none","includePercentChange":true},"debug":false}},"display":{"title":"Requests by Signal","chartId":"query-builder-bar","chartPresentation":{"legend":"visible"},"stacked":true,"curveType":"linear","unit":"number"},"layout":{"x":0,"y":0,"w":6,"h":4}}
Valid aggregate values: "sum" | "first" | "count" | "avg" | "max" | "min". No "last". Without reduceToValue, the series array passes through to the renderer and the stat shows [object Object],....
Gauge widget delta
visualization: "gauge" renders the same scalar as a stat, but on a 180° radial arc. It needs the same reduceToValue transform as a stat widget. Add gauge presentation under display:
display.thresholds color the arc (the highest threshold ≤ the value wins) and place tick marks. gauge.min/max default to 0/100. Arc color falls back to var(--chart-1) when no threshold matches.
Threshold lines on time-series charts
display.thresholds also works on chart widgets — each entry draws a dashed horizontal ReferenceLine across line/area/bar charts, with an optional label. Reuse it to mark SLO/alert boundaries.
rate / sum / increase are invalid for traces — a common mistake when porting metrics widgets to traces.
groupBy prefix conventions
The query-builder groupBy accepts a small literal allowlist per source; everything else
MUST be prefixed attr.<key>. A token that isn't recognized and isn't attr.-prefixed is
dropped — and now causes the widget mutation tools to reject the write rather than silently
breaking the breakdown.
traces: bare literals are only service / service.name, span / span.name, status / status.code, http.method, none / all. Every other attribute needs the attr. prefix — attr.maple.signal, attr.error.type, attr.http.response.status_code, attr.maple.org_id, etc. (Writing error.type bare in groupBy does NOT work — use attr.error.type.)
logs: bare literals only — service / service.name, severity, none. Logs do not support attr.* group by.
metrics: bare service / none; everything else uses attr. — attr.signal, attr.status, attr.org_id.
If a groupBy on an attribute finds zero distinct values, the chart collapses to a single
all series. inspect_chart_data now flags this as EMPTY_GROUPING (verdict broken) instead
of silently showing the ungrouped total.
display.unit is mandatory
Always set display.unit on chart and stat widgets. The default is "number". Pick more specific where applicable:
duration_ms for latency aggregations (avg_duration, p50_duration, p95_duration, p99_duration)
percent for error_rate
number for count
bytes / GB for size aggregations
Hiding auxiliary queries on charts with formulas
When a chart uses formulas and the auxiliary queries shouldn't render on their own, query.hidden: true is not enough on its own for the raw-JSON path — that flag is only consumed by the UI builder to generate the actual transform. For raw JSON, pair it with dataSource.transform.hideSeries.baseNames:
baseNames matches each hidden query's legend || name. Otherwise the auxiliary series render at full scale and skew percent-axis charts to absurd values (raw counts showing as "1200%").
Verification — MCP success ≠ query success
The widget mutation tools now do two things automatically that used to be silent gaps:
Pre-persist rejection: unhonored where-clause/groupBy clauses fail the write (nothing saved) — so a IS NULL, a 6th attr filter, or an unsupported logs/metrics key surfaces as an error instead of degrading to wrong/empty data.
Auto-validation: the response includes an inspect_chart_data summary (verdict + flags). inspect_chart_data now also evaluates formulas[] (so formula/hit-rate widgets verify end-to-end) and applies reduceToValue with the same first-numeric-field fallback the renderer uses (so stat-tile reducedValue reflects what renders). SUSPICIOUS_GAP is informational only — sparse/bursty data no longer downgrades the verdict.
Still confirm after submitting:
Read the returned validation summary; if verdict is suspicious/broken, fix and resubmit.
Call mcp__maple__inspect_chart_data for a deeper look, ormcp__maple__get_dashboard to read back the stored JSON, or load the dashboard URL.
Verdict flags worth knowing: EMPTY_GROUPING (groupBy found zero distinct values → one all series), METRIC_NOT_FOUND (a metrics widget references a metric name not in the warehouse — distinct from a real metric with no recent data), BUILDER_WARNINGS (a clause the engine couldn't honor).
If you see Invalid input for getQueryBuilderTimeseries, the culprit is almost always Trap 1 (metric fields on a trace/log query) or a malformed query draft.
Rebuilding many widgets at once
To replace a dashboard's whole widget list in one validated, atomic write, use
mcp__maple__replace_dashboard_widgets (widgets_json = a JSON array of widget objects, same
shape as widgets[] from get_dashboard). id and layout are optional per widget
(auto-generated / auto-placed). Every widget is validated before anything persists — if one
widget's query can't be honored, nothing is saved. This is the safe middle ground between N
incremental add_dashboard_widget calls and the corruption-prone full dashboard_json replace.
Quick checklist before submitting widget JSON
Trace/log queries omit metricName/metricType/isMonotonic/signalSource; metrics queries include metricName + metricType.
whereClause uses only =, !=, >, <, >=, <=, contains, !contains, exists, !exists, joined by AND (no SQL IS NULL).
aggregation is valid for the chosen dataSource (no rate/sum on traces).
groupBy uses the right prefix: bare only for the per-source allowlist (traces: service/span/status/http.method); every other attribute needs attr.<key> (traces & metrics). Logs: bare service/severity only.
display.unit is set (and matches the aggregation — duration_ms, percent, etc.).
Stat widgets include dataSource.transform.reduceToValue.
Formula charts with hidden queries include dataSource.transform.hideSeries.baseNames.
After submitting, read the auto-validation summary; verify suspicious/broken widgets with inspect_chart_data or by loading the dashboard.