references/json-schema.md
# Dashboard + panel JSON schema
## Dashboard root
```json
{
"title": "My Dashboard",
"uid": "my-dashboard-v1",
"tags": ["service", "production"],
"time": { "from": "now-1h", "to": "now" },
"refresh": "30s",
"timezone": "browser",
"schemaVersion": 41,
"templating": { "list": [] },
"annotations": { "list": [] },
"panels": []
}
```
- `uid` — stable identifier; keep short
- `schemaVersion` — `41` for Grafana 11+
- `time.from` / `to` — relative (`now-1h`) or absolute ISO
- `refresh` — `"30s"`, `"1m"`, `"5m"`, `""` (off)
## Panel
```json
{
"id": 1,
"type": "timeseries",
"title": "Request Rate",
"gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 },
"datasource": { "type": "prometheus", "uid": "${datasource}" },
"targets": [{
"expr": "sum(rate(http_requests_total{job=\"$job\"}[5m])) by (status_code)",
"legendFormat": "{{status_code}}",
"refId": "A"
}],
"fieldConfig": {
"defaults": {
"unit": "reqps",
"thresholds": { "mode": "absolute", "steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 1000 },
{ "color": "red", "value": 5000 }
]}
},
"overrides": []
},
"options": {
"legend": { "calcs": ["mean","max","last"], "displayMode": "table", "placement": "bottom" },
"tooltip": { "mode": "multi", "sort": "desc" }
}
}
```
`gridPos`: 24-column grid. Widths: full=24, half=12, third=8, quarter=6. Height 1 unit ≈ 30 px.
## Panel types
| Panel | Use case |
|---|---|
| **Time series** | Any metric over time |
| **Stat** | Single value + sparkline |
| **Gauge** | % or value against min/max |
| **Bar gauge** | Side-by-side comparison |
| **Table** | Multi-column data |
| **Heatmap** | Distribution over time |
| **Logs** | Loki log streams |
| **Traces** | Tempo trace search |
| **Text** | Markdown docs |
| **Candlestick** | OHLC / min-max-avg |
| **Node graph** | Service dependency graph |
## Useful units
```
reqps requests/sec
ops ops/sec
Bps bytes/sec
percentunit 0.0-1.0 as %
bytes bytes (auto-scales)
decbytes decimal bytes (1 KB = 1000 B)
ms / s milliseconds / seconds
dtdurationms 1h 2m 3s
short compact (1.2k, 3.4M)
none raw number
```
## Template variables
```json
{ "name":"job", "type":"query", "datasource":{"type":"prometheus","uid":"prometheus"},
"query":{"query":"label_values(up, job)","refId":"A"},
"refresh":2, "includeAll":true, "multi":true, "label":"Service" }
{ "name":"cluster", "type":"constant", "query":"production", "label":"Cluster" }
{ "name":"datasource", "type":"datasource", "pluginId":"prometheus",
"includeAll":false, "label":"Prometheus" }
```
Multi-value variables expand to a regex OR: `$job=["api","worker"]` → `job=~"api|worker"`.
Chained: `label_values(kube_pod_info{namespace=\"$namespace\"}, pod)`.
## Transformations
```json
"transformations": [
{ "id": "merge", "options": {} },
{ "id": "organize", "options": {
"renameByName": { "Value #A": "Request Rate", "Value #B": "Error Rate" },
"excludeByName": { "Time": true }
}},
{ "id": "calculateField", "options": {
"alias": "Error %", "mode": "reduceRow",
"reduce": { "reducer": "last" },
"binary": { "left": "errors", "right": "total", "operator": "/" }
}},
{ "id": "filterByValue", "options": {
"filters": [{ "fieldName":"Error %", "config":{ "id":"greater", "options":{ "value":0.01 }}}],
"type": "include", "match": "any"
}}
]
```
Common IDs: `merge`, `organize`, `rename`, `calculateField`, `filterByValue`, `groupBy`, `sortBy`, `limit`, `labelsToFields`, `seriesToRows`, `partitionByValues`.
## Links & annotations
```json
"links": [
{ "title":"Go to details", "url":"/d/details?var-service=${__field.labels.service}", "targetBlank":false },
{ "title":"Runbook", "url":"https://wiki.example.com/runbook/${job}", "icon":"external link",
"targetBlank":true, "type":"link" }
]
```
Built-in vars: `${__value.raw}`, `${__field.labels.job}`, `${__url.params}`, `${__from}` / `${__to}` (Unix ms).
Loki annotation:
```json
{ "datasource":{"type":"loki","uid":"loki"},
"expr":"{job=\"deployments\"} |= \"deployed\"",
"name":"Deployments", "iconColor":"blue",
"titleFormat":"{{service}} deployed", "textFormat":"{{version}} by {{author}}" }
```
Prometheus annotation:
```json
{ "datasource":{"type":"prometheus","uid":"prometheus"},
"expr":"changes(kube_deployment_status_observed_generation{namespace=\"production\"}[5m]) > 0",
"step":"60s", "name":"Deployments", "iconColor":"blue",
"titleFormat":"Deploy: {{deployment}}" }
```
SKILL.md
---
name: dashboarding
license: Apache-2.0
description: Build, modify, and ship Grafana dashboards as JSON via the HTTP API — panel types (timeseries / stat / gauge / table / heatmap / logs / traces / node-graph), `gridPos` 24-column layout, units, thresholds, template + datasource + chained variables, transformations (`organize` / `calculateField` / `filterByValue`), panel + dashboard links with `${__field.labels.x}` / `${__from}`, and Loki/Prometheus annotations. Use when scripting dashboard creation, writing the dashboard JSON for a new service, adding a `$job` dropdown variable, computing an "Error %" column with a transformation, overlaying deploys as annotations, or pushing a dashboard via `POST /api/dashboards/db` — even when the user says "create a dashboard for this metric", "add a service dropdown", "show errors as percentage", "overlay our deploys", or "export the dashboard JSON" without naming the API or schema. After every API push, verify with the returned `version` plus a GET on the dashboard UID.
---
# Grafana Dashboard Authoring
> **Docs**: https://grafana.com/docs/grafana/latest/dashboards/
Dashboards are JSON. Author once, push via API, share by `uid`.
## Prerequisites
- Grafana stack (OSS, Enterprise, or Cloud) reachable from your machine
- API token with `dashboards:write` (`Authorization: Bearer <token>`)
- `jq` for inspecting responses
- The JSON-schema cheat sheet in [`references/json-schema.md`](references/json-schema.md)
## Common Workflows
### 1. Push a new dashboard via the API + verify
```bash
# 1. Build the payload — wrap the dashboard JSON, set folder, mark overwrite
cat > /tmp/dash.json <<'JSON'
{
"dashboard": {
"uid": "demo-svc-v1",
"title": "Demo Service",
"schemaVersion": 41,
"tags": ["demo"],
"time": { "from": "now-1h", "to": "now" },
"templating": { "list": [] },
"panels": [{
"id": 1, "type": "timeseries", "title": "Request Rate",
"gridPos": { "x": 0, "y": 0, "w": 24, "h": 8 },
"datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [{
"expr": "sum(rate(http_requests_total[5m])) by (status_code)",
"legendFormat": "{{status_code}}", "refId": "A"
}],
"fieldConfig": { "defaults": { "unit": "reqps" }, "overrides": [] }
}]
},
"folderUid": "",
"overwrite": true,
"message": "initial push"
}
JSON
# 2. Validate the JSON BEFORE you send it (catches trailing-comma typos)
jq empty /tmp/dash.json && echo "json ok"
# 3. POST
RESP=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"$GRAFANA/api/dashboards/db" -d @/tmp/dash.json)
echo "$RESP" | jq '{status, uid, url, version}'
# Expect: status="success", url="/d/demo-svc-v1/...", version=1 (incremented on each push)
# 4. Verify the round-trip — read it back and confirm one panel + the expected title
curl -s -H "Authorization: Bearer $TOKEN" \
"$GRAFANA/api/dashboards/uid/demo-svc-v1" \
| jq '{title: .dashboard.title, panels: (.dashboard.panels | length)}'
# Expect: {"title":"Demo Service","panels":1}
# 5. Open the dashboard in a browser — confirm the panel renders with data.
```
### 2. Add a `$job` template variable to an existing dashboard
```bash
# 1. Fetch existing dashboard
curl -s -H "Authorization: Bearer $TOKEN" \
"$GRAFANA/api/dashboards/uid/demo-svc-v1" > /tmp/dash.json
# 2. Edit templating.list — append:
# { "name":"job", "type":"query",
# "datasource":{"type":"prometheus","uid":"prometheus"},
# "query":{"query":"label_values(up, job)","refId":"A"},
# "refresh":2, "includeAll":true, "multi":true, "label":"Service" }
# (Use jq, an editor, or the Grafana UI — schema in references/json-schema.md.)
# 3. Update the panel expr to use the variable: rate(http_requests_total{job=~"$job"}[5m])
# 4. POST it back with overwrite: true. Verify the variable appears in the UI dropdown.
```
### 3. Compute an "Error %" column with a transformation
```json
{
"id": "calculateField",
"options": {
"alias": "Error %", "mode": "reduceRow",
"reduce": { "reducer": "last" },
"binary": { "left": "errors", "right": "total", "operator": "/" }
}
}
```
Add this to the panel's `transformations: []`. Verify in the UI panel inspector — the new field should appear and update with the variable selection.
Full schema (panels, units, all transformations, annotations, links): [`references/json-schema.md`](references/json-schema.md).
## API reference
```bash
# Get
curl -s -H "Authorization: Bearer $TOKEN" \
"$GRAFANA/api/dashboards/uid/<uid>" | jq '.dashboard'
# Search
curl -s -H "Authorization: Bearer $TOKEN" \
"$GRAFANA/api/search?query=kubernetes&type=dash-db" | jq '.[] | {uid,title,folderTitle}'
# Create folder
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" "$GRAFANA/api/folders" \
-d '{"uid":"platform-team","title":"Platform Team"}'
```
For dashboards embedded in app plugins, use `@grafana/scenes` (skill `grafana-o11y:grafana-scenes`).
## Resources
- [Dashboard JSON model](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/view-dashboard-json-model/)
- [HTTP API — dashboards](https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/)
- [Panel types](https://grafana.com/docs/grafana/latest/panels-visualizations/)
- [Variables](https://grafana.com/docs/grafana/latest/dashboards/variables/)
- [Transformations](https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/)