assets/bar-chart-esql.json
{
"title": "Requests by Response Code",
"type": "xy",
"axis": {
"x": { "title": { "visible": false } },
"y": { "title": { "visible": false } }
},
"layers": [
{
"type": "bar",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | STATS count = COUNT() BY response.keyword"
},
"x": { "column": "response.keyword" },
"y": [{ "column": "count" }]
}
]
}
assets/dashboard-basic.json
{
"title": "Basic Dashboard",
"panels": [
{
"type": "markdown",
"id": "header-panel",
"grid": { "x": 0, "y": 0, "w": 48, "h": 4 },
"config": {
"content": "## Welcome to the Dashboard\n\nThis is a basic dashboard created via the Kibana Dashboards & Visualizations API."
}
}
],
"time_range": {
"from": "now-24h",
"to": "now"
}
}
assets/dashboard-with-visualizations.json
{
"title": "Dashboard with Visualization Panels",
"panels": [
{
"type": "markdown",
"id": "header",
"grid": {
"x": 0,
"y": 0,
"w": 48,
"h": 3
},
"config": {
"content": "# Log Analysis Dashboard"
}
},
{
"type": "vis",
"id": "total-count",
"grid": {
"x": 0,
"y": 3,
"w": 12,
"h": 8
},
"config": {
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | STATS count = COUNT()"
},
"metrics": [
{
"type": "primary",
"column": "count"
}
]
}
},
{
"type": "vis",
"id": "avg-bytes",
"grid": {
"x": 12,
"y": 3,
"w": 12,
"h": 8
},
"config": {
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | STATS avg_bytes = AVG(bytes)"
},
"metrics": [
{
"type": "primary",
"column": "avg_bytes"
}
]
}
},
{
"type": "vis",
"id": "response-codes",
"grid": {
"x": 24,
"y": 3,
"w": 24,
"h": 8
},
"config": {
"type": "xy",
"layers": [
{
"type": "bar",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | STATS count = COUNT() BY response = TO_STRING(response)"
},
"x": { "column": "response" },
"y": [{ "column": "count" }]
}
]
}
},
{
"type": "vis",
"id": "timeline",
"grid": {
"x": 0,
"y": 11,
"w": 48,
"h": 12
},
"config": {
"type": "xy",
"axis": {
"x": { "scale": "temporal", "domain": { "type": "fit", "rounding": false } }
},
"layers": [
{
"type": "line",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart | STATS count = COUNT() BY BUCKET(@timestamp, 75, ?_tstart, ?_tend)"
},
"x": { "column": "BUCKET(@timestamp, 75, ?_tstart, ?_tend)", "label": "@timestamp" },
"y": [{ "column": "count" }]
}
]
}
},
{
"type": "vis",
"id": "top-hosts",
"grid": {
"x": 0,
"y": 23,
"w": 24,
"h": 10
},
"config": {
"type": "xy",
"layers": [
{
"type": "bar",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | STATS count = COUNT() BY host = TO_STRING(host)"
},
"x": { "column": "host" },
"y": [{ "column": "count" }]
}
]
}
},
{
"type": "vis",
"id": "bytes-by-os",
"grid": {
"x": 24,
"y": 23,
"w": 24,
"h": 10
},
"config": {
"type": "data_table",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | STATS count = COUNT(), total_bytes = SUM(bytes) BY os = TO_STRING(machine.os)"
},
"rows": [{ "column": "os" }],
"metrics": [{ "column": "count" }, { "column": "total_bytes" }]
}
}
],
"time_range": {
"from": "now-7d",
"to": "now"
}
}
assets/datatable.json
{
"type": "data_table",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | STATS count = COUNT(), avg_bytes = AVG(bytes), total_bytes = SUM(bytes) BY host.keyword"
},
"rows": [{ "column": "host.keyword" }],
"metrics": [{ "column": "count" }, { "column": "avg_bytes" }, { "column": "total_bytes" }]
}
assets/demo-dashboard.json
{
"title": "Kibana as Code Demo Dashboard",
"panels": [
{
"type": "vis",
"id": "total-requests",
"grid": { "x": 0, "y": 0, "w": 12, "h": 5 },
"config": {
"title": "",
"type": "metric",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"metrics": [
{
"type": "primary",
"operation": "count",
"label": "Total Requests"
}
]
}
},
{
"type": "vis",
"id": "avg-bytes",
"grid": { "x": 12, "y": 0, "w": 12, "h": 5 },
"config": {
"title": "",
"type": "metric",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"metrics": [
{
"type": "primary",
"operation": "average",
"field": "bytes",
"label": "Average Bytes",
"format": { "type": "bytes", "decimals": 0 }
}
]
}
},
{
"type": "vis",
"id": "unique-ips",
"grid": { "x": 24, "y": 0, "w": 12, "h": 5 },
"config": {
"title": "",
"type": "metric",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"metrics": [
{
"type": "primary",
"operation": "unique_count",
"field": "clientip",
"label": "Unique Client IPs"
}
]
}
},
{
"type": "vis",
"id": "max-bytes-gauge",
"grid": { "x": 36, "y": 0, "w": 12, "h": 5 },
"config": {
"title": "Max Bytes",
"type": "gauge",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"metric": {
"operation": "max",
"field": "bytes",
"format": { "type": "bytes", "decimals": 0 }
}
}
},
{
"type": "vis",
"id": "requests-timeline",
"grid": { "x": 0, "y": 5, "w": 32, "h": 12 },
"config": {
"title": "Requests Over Time",
"type": "xy",
"axis": {
"x": { "title": { "visible": false } },
"y": { "title": { "visible": false } }
},
"layers": [
{
"type": "area",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"x": {
"operation": "date_histogram",
"field": "@timestamp"
},
"y": [{ "operation": "count" }]
}
]
}
},
{
"type": "vis",
"id": "response-codes",
"grid": { "x": 32, "y": 5, "w": 16, "h": 12 },
"config": {
"title": "Response Codes",
"type": "xy",
"axis": {
"x": { "title": { "visible": false } },
"y": { "title": { "visible": false } }
},
"layers": [
{
"type": "bar_horizontal",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"x": {
"operation": "terms",
"fields": ["response.keyword"],
"limit": 10
},
"y": [{ "operation": "count" }]
}
]
}
},
{
"type": "vis",
"id": "traffic-heatmap",
"grid": { "x": 0, "y": 17, "w": 24, "h": 12 },
"config": {
"title": "Traffic Heatmap",
"type": "heatmap",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"metric": { "operation": "count" },
"x": {
"operation": "date_histogram",
"field": "@timestamp"
},
"y": {
"operation": "terms",
"fields": ["response.keyword"],
"limit": 10
}
}
},
{
"type": "vis",
"id": "top-urls-table",
"grid": { "x": 24, "y": 17, "w": 24, "h": 12 },
"config": {
"title": "Top URLs",
"type": "data_table",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"metrics": [
{ "operation": "count" },
{ "operation": "sum", "field": "bytes" },
{ "operation": "average", "field": "bytes" }
],
"rows": [
{
"operation": "terms",
"fields": ["url.keyword"],
"limit": 15,
"rank_by": { "type": "metric", "metric_index": 0, "direction": "desc" }
}
]
}
},
{
"type": "vis",
"id": "top-hosts",
"grid": { "x": 0, "y": 29, "w": 24, "h": 10 },
"config": {
"title": "Top Hosts",
"type": "xy",
"axis": {
"x": { "title": { "visible": false } },
"y": { "title": { "visible": false } }
},
"layers": [
{
"type": "bar",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"x": {
"operation": "terms",
"fields": ["host.keyword"],
"limit": 10
},
"y": [{ "operation": "count" }]
}
]
}
},
{
"type": "vis",
"id": "traffic-by-country",
"grid": { "x": 24, "y": 29, "w": 24, "h": 10 },
"config": {
"title": "Traffic by Country",
"type": "xy",
"axis": {
"x": { "title": { "visible": false } },
"y": { "title": { "visible": false } }
},
"layers": [
{
"type": "bar",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"x": {
"operation": "terms",
"fields": ["geo.dest"],
"limit": 10
},
"y": [{ "operation": "sum", "field": "bytes" }]
}
]
}
}
],
"time_range": { "from": "now-30d", "to": "now" }
}
assets/ecommerce-analytics-dashboard.json
{
"title": "E-Commerce Analytics",
"panels": [
{
"type": "vis",
"id": "total-revenue",
"grid": {
"x": 0,
"y": 0,
"w": 12,
"h": 5
},
"config": {
"type": "metric",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"metrics": [
{
"type": "primary",
"operation": "sum",
"field": "taxful_total_price",
"label": "Total Revenue"
}
]
}
},
{
"type": "vis",
"id": "total-orders",
"grid": {
"x": 12,
"y": 0,
"w": 12,
"h": 5
},
"config": {
"type": "metric",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"metrics": [
{
"type": "primary",
"operation": "count",
"label": "Total Orders"
}
]
}
},
{
"type": "vis",
"id": "avg-order-value",
"grid": {
"x": 24,
"y": 0,
"w": 12,
"h": 5
},
"config": {
"type": "metric",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"metrics": [
{
"type": "primary",
"operation": "average",
"field": "taxful_total_price",
"label": "Avg Order Value"
}
]
}
},
{
"type": "vis",
"id": "total-products-sold",
"grid": {
"x": 36,
"y": 0,
"w": 12,
"h": 5
},
"config": {
"type": "metric",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"metrics": [
{
"type": "primary",
"operation": "sum",
"field": "total_quantity",
"label": "Products Sold"
}
]
}
},
{
"type": "vis",
"id": "revenue-timeline",
"grid": {
"x": 0,
"y": 5,
"w": 32,
"h": 10
},
"config": {
"title": "Daily Revenue Trend",
"type": "xy",
"axis": {
"x": {
"title": {
"visible": false
}
},
"y": {
"title": {
"visible": false
}
}
},
"layers": [
{
"type": "area",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"x": {
"operation": "date_histogram",
"field": "order_date"
},
"y": [
{
"operation": "sum",
"field": "taxful_total_price"
}
]
}
]
}
},
{
"type": "vis",
"id": "orders-by-day-of-week",
"grid": {
"x": 32,
"y": 5,
"w": 16,
"h": 10
},
"config": {
"title": "Orders by Day of Week",
"type": "xy",
"axis": {
"x": {
"title": {
"visible": false
}
},
"y": {
"title": {
"visible": false
}
}
},
"layers": [
{
"type": "bar_horizontal",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"x": {
"operation": "terms",
"fields": ["day_of_week"],
"limit": 7
},
"y": [
{
"operation": "count"
}
]
}
]
}
},
{
"type": "vis",
"id": "revenue-by-category",
"grid": {
"x": 0,
"y": 15,
"w": 16,
"h": 8
},
"config": {
"title": "Revenue by Category",
"type": "xy",
"axis": {
"x": {
"title": {
"visible": false
}
},
"y": {
"title": {
"visible": false
}
}
},
"layers": [
{
"type": "bar_horizontal",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"x": {
"operation": "terms",
"fields": ["category.keyword"],
"limit": 6
},
"y": [
{
"operation": "sum",
"field": "taxful_total_price"
}
]
}
]
}
},
{
"type": "vis",
"id": "top-manufacturers",
"grid": {
"x": 16,
"y": 15,
"w": 16,
"h": 8
},
"config": {
"title": "Top Manufacturers",
"type": "xy",
"axis": {
"x": {
"title": {
"visible": false
}
},
"y": {
"title": {
"visible": false
}
}
},
"layers": [
{
"type": "bar_horizontal",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"x": {
"operation": "terms",
"fields": ["manufacturer.keyword"],
"limit": 6
},
"y": [
{
"operation": "sum",
"field": "taxful_total_price"
}
]
}
]
}
},
{
"type": "vis",
"id": "revenue-by-gender",
"grid": {
"x": 32,
"y": 15,
"w": 16,
"h": 8
},
"config": {
"title": "Revenue by Gender",
"type": "xy",
"axis": {
"x": {
"title": {
"visible": false
}
},
"y": {
"title": {
"visible": false
}
}
},
"layers": [
{
"type": "bar_horizontal",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"x": {
"operation": "terms",
"fields": ["customer_gender"],
"limit": 5
},
"y": [
{
"operation": "sum",
"field": "taxful_total_price"
}
]
}
]
}
},
{
"type": "vis",
"id": "top-customers-table",
"grid": {
"x": 0,
"y": 23,
"w": 24,
"h": 12
},
"config": {
"title": "Top Customers by Spend",
"type": "data_table",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"metrics": [
{
"operation": "sum",
"field": "taxful_total_price"
},
{
"operation": "count"
},
{
"operation": "average",
"field": "taxful_total_price"
}
],
"rows": [
{
"operation": "terms",
"fields": ["customer_full_name.keyword"],
"limit": 10,
"rank_by": {
"type": "metric",
"metric_index": 0,
"direction": "desc"
}
}
]
}
},
{
"type": "vis",
"id": "top-countries-table",
"grid": {
"x": 24,
"y": 23,
"w": 24,
"h": 12
},
"config": {
"title": "Top Countries by Revenue",
"type": "data_table",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"metrics": [
{
"operation": "sum",
"field": "taxful_total_price"
},
{
"operation": "count"
},
{
"operation": "unique_count",
"field": "customer_id"
},
{
"operation": "average",
"field": "taxful_total_price"
}
],
"rows": [
{
"operation": "terms",
"fields": ["geoip.country_iso_code"],
"limit": 10,
"rank_by": {
"type": "metric",
"metric_index": 0,
"direction": "desc"
}
}
]
}
},
{
"type": "vis",
"id": "aov-trend",
"grid": {
"x": 0,
"y": 35,
"w": 24,
"h": 10
},
"config": {
"title": "Average Order Value Trend",
"type": "xy",
"axis": {
"x": {
"title": {
"visible": false
}
},
"y": {
"title": {
"visible": false
}
}
},
"layers": [
{
"type": "line",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"x": {
"operation": "date_histogram",
"field": "order_date"
},
"y": [
{
"operation": "average",
"field": "taxful_total_price"
}
]
}
]
}
},
{
"type": "vis",
"id": "revenue-by-continent",
"grid": {
"x": 24,
"y": 35,
"w": 24,
"h": 10
},
"config": {
"title": "Revenue by Continent",
"type": "xy",
"axis": {
"x": {
"title": {
"visible": false
}
},
"y": {
"title": {
"visible": false
}
}
},
"layers": [
{
"type": "bar_horizontal",
"data_source": {
"type": "data_view_spec",
"index_pattern": "kibana_sample_data_ecommerce",
"time_field": "order_date"
},
"x": {
"operation": "terms",
"fields": ["geoip.continent_name"],
"limit": 7
},
"y": [
{
"operation": "sum",
"field": "taxful_total_price"
}
]
}
]
}
}
],
"time_range": {
"from": "now-30d",
"to": "now"
}
}
assets/line-chart-timeseries.json
{
"title": "Requests Over Time",
"type": "xy",
"axis": {
"x": { "title": { "visible": false }, "scale": "temporal", "domain": { "type": "fit", "rounding": false } },
"y": { "title": { "visible": false } }
},
"layers": [
{
"type": "line",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart | STATS count = COUNT() BY BUCKET(@timestamp, 75, ?_tstart, ?_tend)"
},
"x": { "column": "BUCKET(@timestamp, 75, ?_tstart, ?_tend)", "label": "@timestamp" },
"y": [{ "column": "count" }]
}
]
}
assets/metric-esql.json
{
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | STATS avg_bytes = AVG(bytes)"
},
"metrics": [
{
"type": "primary",
"column": "avg_bytes"
}
]
}
references/chart-types-reference.md
# Chart Types Reference
Complete schema reference for each supported chart type via the Kibana dashboards & visualizations API.
**Supported Chart Types:**
- `metric` — Single metric value
- `xy` — Line, area, bar charts
- `gauge` — Gauge visualization
- `heatmap` — Heatmap charts
- `tag_cloud` — Tag/word cloud
- `data_table` — Data tables
- `region_map` — Region/choropleth maps
- `pie`, `treemap`, `mosaic`, `waffle` — Partition charts (use `pie` with `styling.donut_hole` for donuts: `"s"`, `"m"`,
or `"l"`)
## DataView Aggregation Operations
When using `data_view_reference` or `data_view_spec` datasets, the following operations are available:
| Operation | Description | Requires Field |
| -------------------- | ------------------------------------ | -------------- |
| `count` | Document count | No |
| `average` | Average value | Yes |
| `sum` | Sum of values | Yes |
| `max` | Maximum value | Yes |
| `min` | Minimum value | Yes |
| `unique_count` | Cardinality | Yes |
| `median` | Median value | Yes |
| `standard_deviation` | Standard deviation | Yes |
| `percentile` | Percentile (with `percentile` param) | Yes |
| `percentile_rank` | Percentile rank (with `rank` param) | Yes |
| `last_value` | Last value (with `time_field`) | Yes |
| `date_histogram` | Time buckets (for x-axis) | Yes |
| `terms` | Top values (for x-axis/breakdown) | Yes |
## Metric
Single metric value display. Uses a `metrics` (plural) array with `type: "primary"` or `type: "secondary"`.
**ES|QL:**
```json
{
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT()"
},
"metrics": [
{
"type": "primary",
"column": "count"
}
]
}
```
**dataView:**
```json
{
"type": "metric",
"data_source": { "type": "data_view_reference", "ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247" },
"metrics": [
{
"type": "primary",
"operation": "count",
"label": "Total Events"
}
]
}
```
**Metric Item Properties:**
| Property | Type | Required | Description |
| ----------- | ------ | -------- | --------------------------------------------------------------- |
| `type` | string | Yes | `"primary"` or `"secondary"` |
| `operation` | string | dataView | Aggregation name (for dataView only; not used with ES\|QL) |
| `column` | string | ES\|QL | ES\|QL column name |
| `field` | string | dataView | Field name (required for dataView aggregations needing a field) |
| `label` | string | No | Display label |
**Metric Styling:** Styling is configured at the **config root level** (sibling to `type`, `data_source`, `metrics`),
not inside `metrics[]`. Uses `primary` and `secondary` sub-objects:
```json
{
"type": "metric",
"data_source": { ... },
"metrics": [{ "type": "primary", "operation": "count" }],
"styling": {
"primary": {
"position": "bottom",
"labels": { "alignment": "left" },
"value": { "sizing": "auto", "alignment": "right" }
}
}
}
```
> **Tip:** For ES|QL metrics in dashboards, avoid redundant labels by leaving the panel `title` empty (`""`) and
> aliasing the column name in ES|QL with backticks (e.g. ``STATS `Total Requests` = COUNT()`` and setting
> `"column": "Total Requests"`).
## XY Charts
Line, area, and bar charts. For ES|QL, the `data_source` goes **inside each layer**.
### Bar Chart
```json
{
"type": "xy",
"layers": [
{
"type": "bar",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT() BY status"
},
"x": { "column": "status" },
"y": [{ "column": "count" }]
}
]
}
```
### Line Chart (Time Series)
```json
{
"type": "xy",
"axis": {
"x": { "scale": "temporal", "domain": { "type": "fit", "rounding": false } }
},
"layers": [
{
"type": "line",
"data_source": {
"type": "esql",
"query": "FROM logs | WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart | STATS count = COUNT() BY BUCKET(@timestamp, 75, ?_tstart, ?_tend)"
},
"x": { "column": "BUCKET(@timestamp, 75, ?_tstart, ?_tend)", "label": "@timestamp" },
"y": [{ "column": "count" }]
}
]
}
```
### Area Chart
```json
{
"type": "xy",
"axis": {
"x": { "scale": "temporal", "domain": { "type": "fit", "rounding": false } }
},
"layers": [
{
"type": "area",
"data_source": {
"type": "esql",
"query": "FROM metrics | WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart | STATS avg_cpu = AVG(cpu) BY BUCKET(@timestamp, 75, ?_tstart, ?_tend)"
},
"x": { "column": "BUCKET(@timestamp, 75, ?_tstart, ?_tend)", "label": "@timestamp" },
"y": [{ "column": "avg_cpu" }]
}
]
}
```
### Multiple Y-Axis Values
```json
{
"type": "xy",
"axis": {
"x": { "scale": "temporal", "domain": { "type": "fit", "rounding": false } }
},
"layers": [
{
"type": "line",
"data_source": {
"type": "esql",
"query": "FROM logs | WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart | STATS count = COUNT(), errors = COUNT(CASE(level == \"error\", 1, null)) BY BUCKET(@timestamp, 75, ?_tstart, ?_tend)"
},
"x": { "column": "BUCKET(@timestamp, 75, ?_tstart, ?_tend)", "label": "@timestamp" },
"y": [
{ "column": "count", "label": "Total" },
{ "column": "errors", "label": "Errors" }
]
}
]
}
```
### Split Series (Color by Field)
```json
{
"type": "xy",
"axis": {
"x": { "scale": "temporal", "domain": { "type": "fit", "rounding": false } }
},
"layers": [
{
"type": "line",
"data_source": {
"type": "esql",
"query": "FROM logs | WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart | STATS count = COUNT() BY BUCKET(@timestamp, 75, ?_tstart, ?_tend), host"
},
"x": { "column": "BUCKET(@timestamp, 75, ?_tstart, ?_tend)", "label": "@timestamp" },
"y": [{ "column": "count" }],
"breakdown_by": { "column": "host" }
}
]
}
```
**Layer Types:**
- `bar` — Vertical bars
- `bar_stacked` — Stacked bars
- `bar_percentage` — Percentage bars
- `bar_horizontal` — Horizontal bars
- `bar_horizontal_stacked` — Horizontal stacked bars
- `bar_horizontal_percentage` — Horizontal percentage bars
- `line` — Line chart
- `area` — Area chart
- `area_stacked` — Stacked area
- `area_percentage` — Percentage area
## Gauge
For ES|QL, reference the query output column directly. Do not pass `min`/`max`/`goal` for ES|QL gauges — the API injects
defaults. Do **not** include `operation` in `metric` — it is not a valid property for gauge and will be rejected.
```json
{
"type": "gauge",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS success_rate = COUNT(CASE(TO_INTEGER(status) == 200, 1, null)) * 100.0 / COUNT()"
},
"metric": { "column": "success_rate" }
}
```
**Gauge Properties:**
| Property | Type | Required | Description |
| --------------- | ------ | -------- | ------------------ |
| `metric.column` | string | Yes | ES\|QL column name |
## Heatmap
```json
{
"type": "heatmap",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | STATS count = COUNT() BY hour = DATE_EXTRACT(\"hour_of_day\", @timestamp), day = DATE_EXTRACT(\"day_of_week\", @timestamp)"
},
"x": { "column": "hour" },
"y": { "column": "day" },
"metric": { "column": "count" }
}
```
## Tag Cloud
Uses `tag_by` for the tag dimension and `metric` for the value.
```json
{
"type": "tag_cloud",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT() BY keyword"
},
"tag_by": { "column": "keyword" },
"metric": { "column": "count" }
}
```
## Datatable
For ES|QL, uses `metrics` and `rows` arrays. Each entry uses `{ column: "..." }`.
```json
{
"type": "data_table",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT(), avg_bytes = AVG(bytes) BY host"
},
"metrics": [{ "column": "count" }, { "column": "avg_bytes" }],
"rows": [{ "column": "host" }]
}
```
**For dataView**, the datatable uses aggregation operations:
```json
{
"type": "data_table",
"data_source": { "type": "data_view_reference", "ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247" },
"metrics": [{ "operation": "count" }, { "operation": "average", "field": "bytes" }],
"rows": [
{
"operation": "terms",
"fields": ["host.keyword"],
"limit": 15,
"rank_by": { "type": "metric", "metric_index": 0, "direction": "desc" }
}
]
}
```
## Partition (Pie, Treemap, Mosaic, Waffle)
Partition charts display parts of a whole. Uses a flat structure (no `layers`) with `metrics` for the slice sizes and
`group_by` for the rings or groupings. The schema is identical for all partition types—simply change `"type": "pie"` to
`"treemap"`, `"mosaic"`, or `"waffle"`. To create a donut, use `"type": "pie"` with `"styling": { "donut_hole": "m" }`.
Valid `donut_hole` values are `"none"`, `"s"`, `"m"`, or `"l"`.
**ES|QL Example:**
```json
{
"type": "pie",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT() BY os"
},
"metrics": [{ "column": "count" }],
"group_by": [{ "column": "os" }]
}
```
## Region Map
```json
{
"type": "region_map",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT() BY geo.country_iso_code"
},
"region": { "column": "geo.country_iso_code" },
"metric": { "column": "count" }
}
```
## Common Patterns
### Renaming Columns for Clarity
```json
{
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS total_events = COUNT(), error_count = COUNT(CASE(level == \"error\", 1, null)) | EVAL error_rate = ROUND(error_count * 100.0 / total_events, 2)"
},
"metrics": [{ "type": "primary", "column": "error_rate" }]
}
```
### Time Bucketing
**Auto buckets (Recommended):**
Do **not** reassign the BUCKET result. Use the full expression as both the `BY` clause and the `column` reference, with
a `label` for display:
```esql
WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart | STATS count = COUNT() BY BUCKET(@timestamp, 75, ?_tstart, ?_tend)
```
```json
"x": { "column": "BUCKET(@timestamp, 75, ?_tstart, ?_tend)", "label": "@timestamp" }
```
**Important:** Always set `"scale": "temporal"` on the x-axis for time series charts. Without it, Kibana treats the
bucket column as categorical text and renders unsorted, verbose timestamp strings instead of a proper multilevel time
axis.
```json
"axis": {
"x": { "scale": "temporal", "domain": { "type": "fit", "rounding": false } }
}
```
**Hourly buckets:**
```esql
STATS count = COUNT() BY bucket = DATE_TRUNC(1 hour, @timestamp)
```
**Daily buckets:**
```esql
STATS count = COUNT() BY bucket = DATE_TRUNC(1 day, @timestamp)
```
**5-minute buckets:**
```esql
STATS count = COUNT() BY bucket = DATE_TRUNC(5 minutes, @timestamp)
```
### Number Formatting
Use the `format` property on metrics, y-axis columns, and gauge metrics to display values with proper units.
| Format | Properties | Example Output |
| ---------- | ----------------------------------------------------------------- | -------------- |
| `bytes` | `{ "type": "bytes", "decimals": 0 }` | 5 KB, 19 KB |
| `bits` | `{ "type": "bits", "decimals": 1 }` | 40.2 kbit |
| `number` | `{ "type": "number", "decimals": 2, "compact": true }` | 5.75K |
| `percent` | `{ "type": "percent", "decimals": 1 }` | 42.5% |
| `duration` | `{ "type": "duration", "from": "milliseconds", "to": "seconds" }` | 1.5 s |
| `custom` | `{ "type": "custom", "pattern": "0,0.00" }` | 5,750.16 |
All formats accept an optional `"suffix"` (e.g., `" /s"` for rate displays).
**Percent formatting:** Two options depending on the value range. `"type": "percent"` expects a decimal fraction (0.425
→ 42.5%). `{ "type": "number", "decimals": 1, "suffix": "%" }` works when the value is already a whole-number percentage
(42.5 → 42.5%).
**dataView operation example:**
```json
{ "operation": "average", "field": "bytes", "format": { "type": "bytes", "decimals": 0 } }
```
**ES|QL column example:**
```json
{ "column": "avg_bytes", "format": { "type": "bytes", "decimals": 0 } }
```
**Gauge metric example:**
```json
"metric": { "operation": "max", "field": "bytes", "format": { "type": "bytes", "decimals": 0 } }
```
### Filtering in ES|QL
```esql
FROM logs
| WHERE @timestamp > NOW() - 24 hours AND level == "error"
| STATS count = COUNT() BY host
```
references/dashboard-api-reference.md
# Dashboard API Reference
The Dashboards API provides CRUD endpoints for managing Kibana dashboards.
## Prerequisites
**Version Requirement:** Kibana 9.4+ (SNAPSHOT)
## API Versioning
All requests require the `Elastic-Api-Version: 2023-10-31` header.
## Endpoints
### Get Dashboard
```http
GET kbn:/api/dashboards/{id}
```
**Response:**
```json
{
"id": "dashboardsApiTest",
"data": {
"title": "Dashboards API Test",
"panels": [
{
"grid": { "y": 0, "x": 0, "w": 9, "h": 4 },
"config": { "content": "## Dashboard from API!" },
"id": "ee5f034d-ae54-4ac5-b437-93ac5db0363a",
"type": "markdown"
}
],
"time_range": { "from": "now-2d", "to": "now" }
},
"meta": {
"managed": false,
"updated_at": "2025-12-08T17:33:48.177Z",
"updated_by": "user_id",
"version": "WzMwLDFd",
"created_at": "2025-12-08T17:33:48.177Z",
"created_by": "user_id"
},
"spaces": ["default"]
}
```
### Create Dashboard
```http
POST kbn:/api/dashboards
```
**Request Body:**
```json
{
"title": "My Dashboard",
"panels": [ ... ],
"time_range": { "from": "now-24h", "to": "now" }
}
```
The request body is flat — `title`, `panels`, and `time_range` go at the root level. Dashboard IDs are auto-generated by
the API. `POST` does not accept an `id` parameter. To create-or-update, use `PUT /api/dashboards/{id}`.
**Properties:**
| Property | Required | Description |
| ------------ | -------- | ------------------ |
| `title` | Yes | Dashboard title |
| `panels` | Yes | Array of panels |
| `time_range` | No | Default time range |
**Response** wraps the definition in a `data` envelope:
```json
{
"id": "auto-generated-id",
"data": { "title": "My Dashboard", "panels": [...] },
"meta": { ... },
"spaces": ["default"]
}
```
### Update Dashboard (Upsert)
`PUT` supports upsert — it creates the dashboard if it does not exist, or updates it if it does.
```http
PUT kbn:/api/dashboards/{id}
```
**Request Body:**
```json
{
"title": "Updated Title",
"panels": [ ... ],
"time_range": { "from": "now-7d", "to": "now" }
}
```
### Delete Dashboard
```http
DELETE kbn:/api/dashboards/{id}
```
**Response:** Empty on success.
## Space-Specific Endpoints
To operate on dashboards in a specific space, use the space-prefixed URL:
```http
GET kbn:/s/{space-id}/api/dashboards/{id}
POST kbn:/s/{space-id}/api/dashboards
PUT kbn:/s/{space-id}/api/dashboards/{id}
DELETE kbn:/s/{space-id}/api/dashboards/{id}
```
## Dashboard Request Body Schema
| Property | Type | Required | Description |
| ------------------ | ------ | -------- | -------------------------- |
| `title` | string | Yes | Dashboard title |
| `panels` | array | Yes | Array of panel definitions |
| `time_range` | object | No | Default time range |
| `description` | string | No | Dashboard description |
| `refresh_interval` | object | No | Auto-refresh settings |
### time_range Object
```json
{
"from": "now-24h",
"to": "now"
}
```
Supports relative (`now-1h`, `now-7d`) and absolute (`2024-01-01T00:00:00Z`) formats.
## Panel Schema
### Common Panel Properties
| Property | Type | Required | Description |
| -------- | ------ | -------- | ---------------------------- |
| `type` | string | Yes | Panel type (see below) |
| `id` | string | Yes | Unique panel identifier |
| `grid` | object | Yes | Grid position and size |
| `config` | object | Yes | Panel-specific configuration |
### grid Object
```json
{
"x": 0,
"y": 0,
"w": 24,
"h": 15
}
```
| Property | Description |
| -------- | ------------------------- |
| `x` | Column position (0-47) |
| `y` | Row position |
| `w` | Width in columns (max 48) |
| `h` | Height in rows |
## Panel Types
Panel types correspond to embeddable type identifiers registered in Kibana.
### markdown (Markdown Text)
```json
{
"type": "markdown",
"id": "unique-id",
"grid": { "x": 0, "y": 0, "w": 48, "h": 4 },
"config": {
"content": "## Markdown content here"
}
}
```
### vis (Visualization)
```json
{
"type": "vis",
"id": "unique-id",
"grid": { "x": 0, "y": 0, "w": 24, "h": 15 },
"config": {
"ref_id": "visualization-id"
}
}
```
Or with inline definition (recommended — properties are at config root, not nested under `attributes`):
```json
{
"type": "vis",
"id": "unique-id",
"grid": { "x": 0, "y": 0, "w": 24, "h": 15 },
"config": {
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT()"
},
"metrics": [{ "type": "primary", "column": "count" }]
}
}
```
### links (Dashboard Links)
> **Note:** The `links` panel type may not be available in all Kibana versions.
```json
{
"type": "links",
"id": "unique-id",
"grid": { "x": 0, "y": 0, "w": 8, "h": 4 },
"config": {
"links": [
{ "type": "dashboard", "destination": "dashboard-id-1" },
{ "type": "dashboard", "destination": "dashboard-id-2" }
]
}
}
```
### map (Maps)
> **Note:** The `map` panel type may not be available in all Kibana versions.
```json
{
"type": "map",
"id": "unique-id",
"grid": { "x": 0, "y": 0, "w": 24, "h": 18 },
"config": {
"ref_id": "map-object-id"
}
}
```
### discover_session (Saved Search)
```json
{
"type": "discover_session",
"id": "unique-id",
"grid": { "x": 0, "y": 0, "w": 48, "h": 15 },
"config": {
"ref_id": "saved-search-id"
}
}
```
## Example: Complete Dashboard
```json
{
"title": "Operations Dashboard",
"panels": [
{
"type": "markdown",
"id": "header",
"grid": { "x": 0, "y": 0, "w": 48, "h": 3 },
"config": { "content": "# Operations Overview" }
},
{
"type": "vis",
"id": "total-events",
"grid": { "x": 0, "y": 3, "w": 12, "h": 8 },
"config": {
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT()"
},
"metrics": [{ "type": "primary", "column": "count" }]
}
},
{
"type": "vis",
"id": "events-by-host",
"grid": { "x": 12, "y": 3, "w": 36, "h": 15 },
"config": {
"type": "xy",
"layers": [
{
"type": "bar",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT() BY host"
},
"x": { "column": "host" },
"y": [{ "column": "count" }]
}
]
}
}
],
"time_range": { "from": "now-24h", "to": "now" }
}
```
## Copying Dashboards
1. Call `GET kbn:/api/dashboards/{id}` on the source cluster.
2. Edit the exported definition (change title, panels, or id as needed).
3. Call `PUT kbn:/api/dashboards/{id}` on the destination cluster.
**Note:** Cannot move a dashboard with the same id between spaces in the same cluster without deleting the origin first.
## Common Issues
| Error | Solution |
| ----------------------- | ---------------------------------------------------------------------------- |
| "401 Unauthorized" | Verify credentials and Kibana privileges |
| "404 Not Found" | Verify dashboard/visualization ID exists |
| "409 Conflict" | Dashboard/viz with that ID already exists; delete first or use update |
| "id not allowed in PUT" | Remove `id` and `spaces` from update body |
| Schema validation error | For ES\|QL: ensure column names match query output; use `{ column: 'name' }` |
| ES\|QL column reference | ES\|QL uses `{ column: 'col' }` to reference query output columns |
| Metric chart structure | Metric chart requires `metrics` (plural) array: `[{ type: 'primary', ... }]` |
| Tagcloud bucketing | Tagcloud requires `tag_by` for the tag dimension |
| Datatable structure | ES\|QL data_table requires `metrics` + `rows` arrays |
| XY chart fails | Put `data_source` inside each layer (for both dataView and ES\|QL) |
| Heatmap property names | Heatmap uses `x`, `y`, `metric` for axes and value |
| XY axis config | Use `axis` (singular); `y` for left axis, `y2` for right axis |
| ref_id panels missing | Prefer inline definitions (properties in `config`) over `ref_id` |
## Testing from Dev Tools
Prefix requests with `kbn:` and include version header:
```text
GET kbn:/api/dashboards/my-dashboard
{
"headers": { "Elastic-Api-Version": "2023-10-31" }
}
POST kbn:/api/dashboards
{
"headers": { "Elastic-Api-Version": "2023-10-31" },
"body": { ... }
}
```
references/lens-api-reference.md
# Lens Visualizations API Reference
The Visualizations API provides CRUD endpoints for standalone Lens library items (Kibana 9.4+).
## Endpoints
### List Visualizations
```http
GET kbn:/api/visualizations?query=&page=&per_page=
```
| Parameter | Type | Description |
| ---------- | ------ | ------------------------------- |
| `query` | string | Search query |
| `page` | number | Page number (default: 1) |
| `per_page` | number | Results per page (default: 100) |
### Get Visualization
```http
GET kbn:/api/visualizations/{id}
```
### Create Visualization
```http
POST kbn:/api/visualizations
```
`POST` auto-generates an id. Send the definition without an id field.
### Update Visualization (Upsert)
```http
PUT kbn:/api/visualizations/{id}
```
`PUT` creates the visualization when the id does not exist.
### Delete Visualization
```http
DELETE kbn:/api/visualizations/{id}
```
## Response Envelope
Single-item responses wrap the definition:
```json
{
"id": "uuid",
"data": { "type": "metric", "data_source": { ... }, "metrics": [ ... ] },
"meta": {
"created_at": "ISO timestamp",
"updated_at": "ISO timestamp",
"managed": false
}
}
```
List responses return `{ "data": [ ... ], "meta": { "page", "per_page", "total" } }`.
## Common Properties
| Property | Type | Description |
| ----------------------- | ------- | ----------------------------------------- |
| `type` | string | Chart type (required) |
| `data_source` | object | Data source configuration (required) |
| `title` | string | Display title |
| `sampling` | number | Sampling rate 0–1 (default: 1) |
| `ignore_global_filters` | boolean | Ignore dashboard filters (default: false) |
## Dataset Configuration
### ES|QL Dataset
Use when the user requests ES|QL. The API persists a Lens saved object backed by a text-based ES|QL datasource.
```json
{
"data_source": {
"type": "esql",
"query": "FROM logs* | STATS count = COUNT()"
}
}
```
Reference result columns with `{ "column": "count" }` on metrics or layer axes — not `operation: "count"`.
### Data View Dataset
```json
{
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
}
}
```
Use aggregation operations such as `count`, `average`, `sum`, `terms`, and `date_histogram` on metrics and axes.
### Index Pattern Dataset
```json
{
"data_source": {
"type": "data_view_spec",
"index_pattern": "logs-*",
"time_field": "@timestamp"
}
}
```
## Metric Example (ES|QL)
```json
{
"type": "metric",
"title": "Total Requests",
"data_source": {
"type": "esql",
"query": "FROM logs* | STATS count = COUNT()"
},
"metrics": [{ "type": "primary", "column": "count" }]
}
```
## Error Responses
| Code | Meaning |
| ---- | ----------------------------------------- |
| 400 | Schema validation failure |
| 401 | Authentication required |
| 404 | Visualization not found |
| 409 | Conflict on create when id already exists |
SKILL.md
---
name: kibana-dashboards
description: >
Create and manage Kibana Dashboards and Lens visualizations. Use when you need to
define dashboards and visualizations declaratively, version control them, or automate
their deployment.
metadata:
author: elastic
version: 0.3.0
universal: true
compatibility: Kibana 9.4 or later (Dashboards and Visualizations APIs) with matching
Elasticsearch, self-managed, Elastic Cloud Hosted, or Elastic Cloud Serverless.
Requires the `elastic` CLI ≥ 0.3 with `stack kb` support (dedicated `dashboards`
and `visualizations` commands).
---
# Kibana Dashboards and Lens Visualizations
Create, update, and delete Kibana dashboards and standalone Lens visualizations using the Kibana 9.4+ Dashboards and
Visualizations APIs. Produce minimal, diffable JSON bodies; prefer inline panel definitions over library references; and
choose the correct dataset type (data view vs ES|QL) before writing metrics or chart layers.
<!-- begin-partial: preamble -->
## Environment Configuration
This skill executes Elasticsearch operations through the `elastic` CLI. If the
[`elastic` CLI](https://github.com/elastic/cli#configuration) is not installed, tell the user what it is needed for. Do
not guess credentials, call the HTTP API directly, or attempt other workarounds.
This skill references operations in HTTP-shorthand form (e.g., `GET /`, `GET /_cat/indices`, `GET /{index}/_mapping`,
`GET /{index}/_settings/index.mode`, `POST /_query`). The [Operations](#operations) table at the end of this document
maps each shorthand to the equivalent `elastic` CLI command — always use the CLI rather than calling the HTTP API
directly.
<!-- end-partial: preamble -->
## Prerequisites
**Version requirement:** Kibana 9.4+ (Dashboards and Visualizations APIs).
**ES|QL placement:**
- Standalone library charts: `PUT kbn:/api/visualizations/{id}` with `data_source.type: "esql"`.
- ES|QL panels embedded in a dashboard: inline `vis` panel `config` with `data_source.type: "esql"` via
`PUT kbn:/api/dashboards/{id}`.
- Do not use `data_source.type: "data_view_reference"` or index-pattern aggregations when the user explicitly requests
ES|QL — the persisted Lens state must use a text-based ES|QL datasource (`textBased` / `esql`), not a data-view count
operation.
## Process
1. **Verify Kibana connectivity.** Call `GET kbn:/api/status`. If the call fails, stop and surface the error — do not
guess endpoints or credentials. Read `version.number` to confirm the cluster meets the 9.4+ requirement.
2. **Classify the task.** Decide whether the user needs a **dashboard** (collection of panels, optional time range), a
**standalone Lens visualization** (library item referenced by id or used alone), or **both**. Determine whether a
deterministic saved-object id was supplied — when given, use upsert (`PUT`) with that id rather than `POST` (which
auto-generates ids).
3. **Choose the dataset type before building metrics or layers.**
| User intent | Dataset | Metric / axis pattern |
| ------------------------------------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Simple count or aggregation on a saved data view | `data_source.type: "data_view_reference"` with `ref_id` | `metrics: [{ type: "primary", operation: "count" }]` (or other aggregation operations) |
| Ad-hoc index pattern | `data_source.type: "data_view_spec"` with `index_pattern` and `time_field` | Same aggregation `operation` fields |
| ES\|QL query (explicit or complex logic) | `data_source.type: "esql"` with `query` | `metrics: [{ type: "primary", column: "<alias>" }]` or layer axes `{ column: "<alias>" }` — **never** `operation: "count"` on the metric |
Write the aggregation in the ES|QL query (`STATS count = COUNT()`), then reference the resulting column by name.
4. **Build a dashboard body when creating or updating dashboards.** The request body is flat — `title`, `panels`, and
optional `time_range` at the root. Do not wrap in `{ data: ... }` on write. Required fields:
- `title` — exact string the user requested.
- `panels` — array; use `[]` when the user asks for an empty dashboard (do not omit the key or invent panels).
- `time_range` — when the user specifies a default time filter, set `{ "from": "<expr>", "to": "<expr>" }` (for
example `{ "from": "now-7d", "to": "now" }`). Supplying `time_range` persists the dashboard time filter on open
(equivalent to enabling time restore in the UI).
**Upsert with a deterministic id:**
```json
{
"title": "Sales Overview",
"panels": [],
"time_range": { "from": "now-7d", "to": "now" }
}
```
Call `PUT kbn:/api/dashboards/eval-sales-overview` with the body above when the user supplies that id.
**Inline ES|QL metric panel example** (inside `panels`):
```json
{
"type": "vis",
"id": "total-requests",
"grid": { "x": 0, "y": 0, "w": 12, "h": 6 },
"config": {
"title": "Total Requests",
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM logs* | STATS count = COUNT()"
},
"metrics": [{ "type": "primary", "column": "count" }]
}
}
```
Prefer inline `config` properties over `config.ref_id` for portable dashboards. Read
[Dashboard API Reference](references/dashboard-api-reference.md) for panel types, grid layout, and copy workflows.
5. **Build a standalone Lens visualization when the user asks for a library chart.** Use the Visualizations API. Upsert
with `PUT kbn:/api/visualizations/{id}` when an id is supplied; otherwise `POST kbn:/api/visualizations` and report
the generated id from the response.
**ES|QL metric (total count from logs):**
```json
{
"type": "metric",
"title": "Total Requests",
"data_source": {
"type": "esql",
"query": "FROM logs* | STATS count = COUNT()"
},
"metrics": [{ "type": "primary", "column": "count" }]
}
```
Call `PUT kbn:/api/visualizations/eval-total-requests` when that id is required. The API persists a Lens saved object
whose datasource state uses ES|QL (`textBased` / `esql`), not an index-pattern aggregation.
Read [Lens API Reference](references/lens-api-reference.md) and
[Chart Types Reference](references/chart-types-reference.md) for xy, gauge, heatmap, and other chart schemas.
6. **Execute and confirm.** Perform the write with `PUT kbn:/api/dashboards/{id}` or `PUT kbn:/api/visualizations/{id}`
(or `POST` when no id is supplied). Confirm with `GET kbn:/api/dashboards/{id}` or
`GET kbn:/api/visualizations/{id}`. Report the id and title back to the user — do not claim success without a
successful read-back.
7. **List, export, or delete when requested.** Call `GET kbn:/api/dashboards` or `GET kbn:/api/visualizations` to
discover existing objects. Call `DELETE kbn:/api/dashboards/{id}` or `DELETE kbn:/api/visualizations/{id}` to remove
objects. For bulk export or import of saved objects, call `POST kbn:/api/saved_objects/_export` or
`POST kbn:/api/saved_objects/_import`.
## Dashboard grid
Dashboards use a **48-column** grid. On 16:9 screens, roughly **20–24 rows** fit above the fold — target **8–12 panels**
in that band.
| Width | Columns | Height (rows) | Use case |
| ------- | ------- | ------------- | ------------------------ |
| Full | 48 | 14–16 | Wide time series, tables |
| Half | 24 | 10–12 | Primary charts |
| Quarter | 12 | 5–6 | KPI metrics |
| Sixth | 8 | 4–5 | Dense metric rows |
**Grid packing:** When stacking rows, set the next panel's `y` to the previous panel's `y + h`. Panels sharing a row
should use the same `h`. Do not add markdown panels as dashboard titles — use descriptive chart titles instead.
## ES|QL patterns
**Time series bucket** (dashboard time picker injects `?_tstart` / `?_tend`):
```esql
FROM logs*
| WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart
| STATS count = COUNT() BY BUCKET(@timestamp, 75, ?_tstart, ?_tend)
```
Set `"scale": "temporal"` on the x-axis for time-series xy charts. See
[Chart Types Reference](references/chart-types-reference.md) for axis and layer details.
**Static reference values** — use `EVAL` in the query, then reference the column:
```esql
FROM logs* | STATS count = COUNT() | EVAL goal = 15000
```
## Examples
Example JSON definitions live under [assets/](assets/): `demo-dashboard.json`, `dashboard-with-visualizations.json`,
`metric-esql.json`, `bar-chart-esql.json`, `line-chart-timeseries.json`.
## Guidelines
1. **Match the user's id and title exactly** when supplied — do not substitute auto-generated ids.
2. **Honor empty panels** — when the user asks for `panels: []`, send an empty array; do not add placeholder panels.
3. **ES|QL when requested** — use `data_source.type: "esql"` and column references; never satisfy an ES|QL request with
`operation: "count"` on a data view.
4. **Minimal payloads** — omit derivable defaults; let the API inject styling and metadata.
5. **Confirm writes** — always read back with `GET` after create or update.
6. **Read references before complex charts** — metric and xy schemas differ between data view and ES|QL; consult
[Chart Types Reference](references/chart-types-reference.md) before generating partition or table charts.
## Common issues
| Error | Likely cause | Fix |
| ----------------------------------- | --------------------------- | ---------------------------------------------------------------------------- |
| 404 on GET after PUT | Wrong id or space | Confirm id and retry `GET kbn:/api/dashboards/{id}` |
| 400 validation | ES\|QL column mismatch | Align `metrics[].column` / layer `column` with `STATS` aliases in the query |
| ES\|QL panel saved as data view | Wrong dataset type | Use `data_source.type: "esql"`, not `data_view_reference` |
| Empty dashboard missing time filter | Omitted `time_range` | Include `{ "from": "now-7d", "to": "now" }` when a default range is required |
| XY chart failure | Missing layer `data_source` | Put `data_source` inside each layer, not only at the root |
## Operations
As of CLI v0.3.0 the Dashboards and Visualizations APIs have dedicated `elastic kb dashboards` and
`elastic kb visualizations` commands for listing, reading, updating, and deleting objects by id. The `create-*-redirect`
commands do not accept a request body yet, so to write a new object supply an id and use the `update-*-redirect` (PUT)
command, which carries the JSON body via `--input-file`. To author several objects at once, build a saved-object NDJSON
and import it with `post-saved-objects-import` (read it back with `post-saved-objects-export`).
| HTTP API (shorthand) | `elastic` CLI command |
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET kbn:/api/status` | `elastic kb system get-status` |
| `POST kbn:/api/saved_objects/_import` | `elastic kb saved-objects post-saved-objects-import --file '<path.ndjson>' --overwrite` |
| `POST kbn:/api/saved_objects/_export` | `elastic kb saved-objects post-saved-objects-export --objects '[{"type":"<type>","id":"<id>"}]'` |
| `GET kbn:/api/dashboards` | `elastic kb dashboards get-dashboards-redirect` |
| `GET kbn:/api/dashboards/{id}` | `elastic kb dashboards get-dashboard-redirect --id '<id>'` |
| `PUT kbn:/api/dashboards/{id}` | `elastic kb dashboards update-dashboard-redirect --id '<id>' --input-file '<path.json>'` |
| `DELETE kbn:/api/dashboards/{id}` | `elastic kb dashboards delete-dashboard-redirect --id '<id>'` |
| `POST kbn:/api/dashboards` (no id) | `create-dashboard-redirect` takes no body yet — supply an id and use `update-dashboard-redirect`, or author via `post-saved-objects-import` (type `dashboard`) |
| `GET kbn:/api/visualizations` | `elastic kb visualizations get-visualizations-redirect` |
| `GET kbn:/api/visualizations/{id}` | `elastic kb visualizations get-visualization-redirect --id '<id>'` |
| `PUT kbn:/api/visualizations/{id}` | `elastic kb visualizations update-visualization-redirect --id '<id>' --input-file '<path.json>'` |
| `DELETE kbn:/api/visualizations/{id}` | `elastic kb visualizations delete-visualization-redirect --id '<id>'` |
| `POST kbn:/api/visualizations` (no id) | `create-visualization-redirect` takes no body yet — supply an id and use `update-visualization-redirect`, or author via `post-saved-objects-import` (type `lens`) |