Skip to main content

maple-dashboard-widgets

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', 'panel_type', 'QueryDraft', or any session that submits widget JSON to the maple MCP. Covers the panel-type table, the kind-discriminated data source, the percent vs percent_100 unit rule, valid aggregations and group-by tokens per source, the custom whereClause grammar, the scalar reduceToValue transform, and the verification step (MCP success != chart correctness).

Source facts

Repository
MapleTechLabs/maple
Last source activity
September 25, 2026 at 17:54
Detected SKILL.md language
English
Stars
1,838
Forks
119

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
maple-dashboard-widgets
description
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', 'panel_type', 'QueryDraft', or any session that submits widget JSON to the maple MCP. Covers the panel-type table, the kind-discriminated data source, the percent vs percent_100 unit rule, valid aggregations and group-by tokens per source, the custom whereClause grammar, the scalar reduceToValue transform, and the verification step (MCP success != chart correctness).
# Maple dashboard widgets via MCP Everything below is generated from the live widget schema by `bun run --cwd apps/ai mcp:docs`. **Do not edit this file by hand.** Edit `apps/ai/src/mcp/lib/dashboard-schema-doc.ts` and regenerate. The same module backs the `describe_dashboard_schema` MCP tool, so an agent at runtime and a reader here see one truth. ## When to use this skill When constructing widget JSON for `mcp__maple__create_dashboard`, `mcp__maple__add_dashboard_widget`, `mcp__maple__update_dashboard_widget` or `mcp__maple__replace_dashboard_widgets`. For a brand-new dashboard, prefer the simplified `widgets` array on `create_dashboard` (`{ title, source, metric, group_by?, service_name?, unit? }`). It fills in the traps below. Reach for raw JSON when the simplified spec can't express what you need: multi-query charts, formulas, hidden series, non-default transforms. ## The three silent failures 1. **A data source is a `kind`-discriminated union.** `{ "endpoint": …, "params": … }` is the retired v2 shape and will not decode. 2. **`percent` means a 0–1 fraction; `percent_100` means 0–100.** Inverted from Grafana. 3. **`groupBy` is ignored unless `addOns.groupBy` is `true`.** No error, just an ungrouped total. ## Verification MCP success is not chart correctness. The mutation tools reject queries the engine can't honor and widgets that cannot render, and return an automatic `inspect_chart_data` summary for everything else. Read the verdict: `suspicious` or `broken` means fix and resubmit. ## Panel types `panel_type` is the whole answer to “what kind of widget is this”. Pass it and the persisted `visualization`, the `display.chartId` and the raw-SQL display type are all derived for you. The legacy `visualization` parameter is still accepted, but it collapses line/bar/area into `chart` and then needs a `display.chartId` to tell them apart. The two columns below are what `panel_type` resolves to, and are what you write directly when authoring an assembled widget rather than calling `add_dashboard_widget`. A panel whose `chartId` is `-` takes none: the `visualization` alone identifies it. | panel_type | Label | `visualization` | `display.chartId` | Raw-SQL type | Default w×h | Requirements | |---|---|---|---|---|---|---| | `line` | Line | `chart` | `query-builder-line` | `line` | 4×6 | - | | `bar` | Bar | `chart` | `query-builder-bar` | `bar` | 4×6 | - | | `hbar` | Horizontal Bar | `hbar` | `query-builder-hbar` | `hbar` | 4×6 | needs a group-by | | `area` | Area | `chart` | `query-builder-area` | `area` | 4×6 | - | | `pie` | Pie | `pie` | `query-builder-pie` | `pie` | 4×6 | needs a group-by | | `stat` | Stat | `stat` | - | `stat` | 3×4 | needs `transform.reduceToValue` | | `gauge` | Gauge | `gauge` | - | `stat` | 4×6 | needs `transform.reduceToValue` | | `table` | Table | `table` | - | `table` | 6×5 | - | | `list` | List | `list` | - | - | 6×5 | **no raw-SQL support** | | `histogram` | Histogram | `histogram` | `query-builder-histogram` | `histogram` | 4×6 | - | | `heatmap` | Heatmap | `heatmap` | `query-builder-heatmap` | `heatmap` | 4×6 | needs a group-by | | `funnel` | Funnel | `funnel` | `query-builder-funnel` | `funnel` | 4×6 | needs a group-by | | `paths` | Paths | `paths` | `query-builder-paths` | - | 6×5 | **no raw-SQL support** | | `markdown` | Note | `markdown` | - | - | 4×5 | **no raw-SQL support** | ### Choosing one - **line / area / bar**: a value over time. `area` and `bar` accept `display.stacked`; `line` does not. - **hbar**: a ranked “top N by volume”. Each row is labelled with its share of the **total**. - **funnel**: sequential stages with a drop-off. Labels each bar as a share of the *largest*, so an unranked breakdown of four equal things reads “100%” four times. Use `hbar` for that. `display.funnel.variant: "dropoff"` draws the same funnel as one column per step with the loss between steps, the median time between them, and where the leavers went. - **paths**: what people do in the steps after (or before) one event or page, as a flow. Defined by `display.paths` alone; no query set. - **pie**: composition, few slices. Collapses a long tail into “Other”. - **stat / gauge**: one number. A gauge adds an arc; set `display.gauge.min`/`max` to match the unit. - **table**: rows and columns; set `display.columns` for headers and per-column units. - **list**: recent traces/logs. Configured by `display.listDataSource`, never by SQL. - **heatmap / histogram**: a distribution. A histogram over traces can bucket raw values client-side. - **markdown**: a static note. Takes no query at all. ## Data sources A widget's `dataSource` is a discriminated union over `kind`: `query`, `raw_sql`, `route`, `static`. Every arm requires its `kind`. > **If you have seen `{ "endpoint": …, "params": … }` anywhere, that is the retired v2 shape > and it will not decode.** A `query` source spreads `queries`/`formulas` at the TOP LEVEL, > not under `params`, and requires `resultShape`. ### `kind: "query"`: the query builder `resultShape` is required and is one of `timeseries` (a value over time), `breakdown` (one row per group) or `list` (raw rows). Optional: `formulas`, `comparison`, `limit`, `defaultLimit`, `columns`, `transform`. ```json { "kind": "query", "resultShape": "timeseries", "queries": [ { "id": "q1", "name": "A", "enabled": true, "whereClause": "service.name = \"api\"", "aggregation": "error_rate", "stepInterval": "", "orderByDirection": "desc", "addOns": { "groupBy": true, "having": false, "orderBy": false, "limit": false, "legend": false }, "groupBy": [ "service.name" ], "having": "", "orderBy": "", "limit": "", "legend": "", "dataSource": "traces" } ] } ``` ### `kind: "raw_sql"`: your own ClickHouse SQL ```json { "kind": "raw_sql", "sql": "SELECT count() AS value FROM logs WHERE $__orgFilter", "displayType": "stat" } ``` ### `kind: "static"`: a markdown note, no request ```json { "kind": "static" } ``` ### `kind: "route"`: a curated built-in panel `{ "kind": "route", "endpoint": "service_overview", "params": { … } }`. These back the prebuilt panels; you rarely author one by hand. ### Scalar panels need a reduction A `stat` or `gauge` reads `data[0].value`. Without `transform.reduceToValue` it renders `[object Object]`. `add_dashboard_widget` injects `{ field: "value", aggregate: "first" }` when you omit it; set it explicitly to choose a different reducer. Valid aggregates: `sum`, `first`, `count`, `avg`, `max`, `min`: **there is no `last`**. Which one depends on what the query returns, because the query is bucketed over time and the reducer collapses those buckets into one number: - **A rate or a count** (`count`, a metrics `rate`) → `sum`, for a window total. - **A latency percentile or an average** (`p95_duration`, `avg_duration`, a gauge metric) → `avg` for the typical value over the window, or `max` for the worst bucket. **Not** `sum`: adding percentiles together is meaningless, and it is the common wrong choice. - **A current reading**, where only the newest bucket matters → `first`. ```json { "kind": "query", "resultShape": "timeseries", "queries": [ { "id": "q1", "name": "A", "enabled": true, "whereClause": "", "aggregation": "count", "stepInterval": "", "orderByDirection": "desc", "addOns": { "groupBy": false, "having": false, "orderBy": false, "limit": false, "legend": false }, "groupBy": [], "having": "", "orderBy": "", "limit": "", "legend": "", "dataSource": "traces" } ], "transform": { "reduceToValue": { "field": "value", "aggregate": "sum" } } } ``` ### The breakdown shape `resultShape: "breakdown"` returns one row per group instead of a series over time, the shape `pie`, `hbar`, `funnel` and `heatmap` need. It requires a group-by, and `limit` caps the rows (honoured for 1–100). ```json { "kind": "query", "resultShape": "breakdown", "queries": [ { "id": "q1", "name": "A", "enabled": true, "whereClause": "", "aggregation": "count", "stepInterval": "", "orderByDirection": "desc", "addOns": { "groupBy": true, "having": false, "orderBy": false, "limit": false, "legend": false }, "groupBy": [ "service.name" ], "having": "", "orderBy": "", "limit": "", "legend": "", "dataSource": "traces" } ], "limit": 10 } ``` ### Product-event funnels (`panel_type: "funnel"` + `display.funnel.steps`) A funnel widget has two modes. Without `display.funnel.steps` it draws a group-by breakdown as descending stages (the shape above). With them it is a **conversion funnel over product events** (page views, `track()` events and server-side events, stitched per person), and the query set is not used at all. Set the definition on `display_json.funnel` and `add_dashboard_widget` derives the data source (`kind: "route"`, `endpoint: "product_events_funnel"`) for you; do not pass `data_source_json`. - `steps`: 1–10, in order. `{ kind: "event", eventName, attributeEquals? }`, `{ kind: "page", pagePath, host? }`, or (**step 1 only**) `{ kind: "session", dimension, value }` with `dimension` one of `referrerHost`, `utmSource`, `utmMedium`, `utmCampaign`, `country`, `host`. - `keyBy`: `person` (default; user id, else the visitor's linked user, else the visitor), `visitor`, `user`, or `session`. - `windowSeconds`: the whole chain must complete within this many seconds of step 1 (default 86400). Must be positive. - `breakdownBy`: split the funnel by a session dimension (`referrerHost`, `utmSource`, `utmMedium`, `utmCampaign`, `country`, `host`) or `attribute:<key>`; the widget draws one bar per group per step for the top 6 groups by step-1 count. - `filters`: the population filter: only persons with a session matching these dimensions take part. `{ host?, pagePath?, referrerHost?, country?, deviceType?, browserName?, osName?, language?, utmSource?, utmMedium?, utmCampaign?, visitorType?: "new" | "returning" }`. They are spread FLAT into the derived route params. The step count, the step-1-only session rule and a positive `windowSeconds` are enforced on write: a definition that breaks one of them is rejected rather than saved, because the query engine would reject it again on every render. Use `list_product_events` to see which event names exist, and `query_funnel` to try a definition before pinning it to a board. ```json { "id": "w-signup-funnel", "visualization": "funnel", "dataSource": { "kind": "route", "endpoint": "product_events_funnel",
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub