Kibana Dashboards
ElasticOfficialCreated by elastic
- Skill
- Tool integration
- Cloud and DevOps
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured approach to creating, updating, and managing Kibana dashboards and standalone Lens visualizations. It leverages the Kibana 9.4+ Dashboards and Visualizations APIs to enable declarative definition and version control of monitoring assets. The skill emphasizes using the elastic CLI for all operations, ensuring that dashboard layouts, chart designs, and data source configurations follow established design guidelines for consistency and readability.
What it does
- Creates and updates Kibana dashboards and Lens visualizations via API.
- Supports declarative definition of dashboard layouts and panel configurations.
- Integrates with the elastic CLI for automated deployment and management.
- Provides guidance on ES|QL and data view dataset types for visualizations.
- Enforces design standards for charts, metrics, and dashboard organization.
- Facilitates version control of dashboard and visualization definitions.
When to use it
- When you need to automate the deployment of Kibana dashboards.
- When you want to manage dashboard and visualization definitions as code.
- When you need to ensure consistent chart design and layout across dashboards.
- When you are working with Kibana 9.4 or later and require programmatic object management.
Advantages
- Enables version control for monitoring assets.
- Reduces manual configuration effort through declarative API usage.
- Ensures adherence to standardized dashboard and chart design patterns.
- Supports complex data querying via ES|QL integration.
Usage examples
- “Create a new dashboard titled 'System Overview' with an empty panel list.”
- The agent uses the Kibana Dashboards API to create a new dashboard object with the specified title and an empty panels array.
- “Update the 'Sales Overview' dashboard to include a new metric panel using ES|QL.”
- The agent retrieves the existing dashboard, adds the new panel configuration with the specified ES|QL query, and updates the dashboard via the API.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/elastic/kibana-dashboards/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip kibana-dashboards.zip -d .agents/skills
Documentation by elastic
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; choose the correct dataset type (data view vs ES|QL) before writing metrics or chart layers; and follow the design guidance in Dashboard Design Guidelines and Chart Design Guidelines so the result reads like a well-made Kibana dashboard, not a schema dump.
<!-- begin-partial: preamble -->
Environment Configuration
This skill executes Elasticsearch operations through the elastic CLI. If the
elastic CLI 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 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}withdata_source.type: "esql".ES|QL panels embedded in a dashboard: inline
vispanelconfigwithdata_source.type: "esql"viaPUT 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
Verify Kibana connectivity. Call
GET kbn:/api/status. If the call fails, stop and surface the error — do not guess endpoints or credentials. Readversion.numberto confirm the cluster meets the 9.4+ requirement, and keep the version in mind: a few settings are marked 9.5+ or 9.6+ in Design essentials and have a fallback for older clusters. Never refuse a dashboard on 9.4 because of them.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 thanPOST(which auto-generates ids).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"withref_idmetrics: [{ type: "primary", operation: "count" }](or other aggregation operations)Ad-hoc index pattern data_source.type: "data_view_spec"withindex_patternandtime_fieldSame aggregation operationfieldsES|QL query (explicit or complex logic) data_source.type: "esql"withquerymetrics: [{ type: "primary", column: "<alias>" }]or layer axes{ column: "<alias>" }— neveroperation: "count"on the metricWrite the aggregation in the ES|QL query (
STATS count = COUNT()), then reference the resulting column by name.Write the ES|QL with the
elasticsearch-esqlskill. It covers schema discovery, time series data streams (TS,RATEfor counters,METRICS_INFO), and query execution. Two dashboard-specific points on top of it:Run every panel query before saving it, substituting fixed expressions such as
NOW() - 7 daysfor?_tstartand?_tend, which only Kibana can supply.Check the data's time span (
STATS MIN(@timestamp), MAX(@timestamp)). Without a savedtime_range, Kibana opens the dashboard on the last 15 minutes, so a dashboard over older or historical data renders empty panels. Tell the user which range shows data, or ask whether to persist one.
Design before you write. Read Dashboard Design Guidelines and Chart Design Guidelines and decide the composition, the chart type and grid size of each panel, the per-chart presentation settings, and the exact
gridcoordinates. The rules most often missed are summarized in Design essentials below. Choices you do not make deliberately fall back to those defaults; they are not optional polish.Build a dashboard body when creating or updating dashboards. The request body is flat —
title,panels, and optionaldescription,time_range, andpinned_panelsat the root. Do not wrap in{ data: ... }on write. Required fields:title— exact string the user requested; otherwise a title derived from what the panels measure. Never empty.description— set on every new dashboard.panels— array of panels and sections; 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" }). Supplyingtime_rangepersists the dashboard time filter on open (equivalent to enabling time restore in the UI). Do not invent a range the user did not ask for.pinned_panels— the controls chosen in the design step (see the Controls section of the design guidelines).
Upsert with a deterministic id:
{ "title": "Sales Overview", "panels": [], "time_range": { "from": "now-7d", "to": "now" } }Call
PUT kbn:/api/dashboards/eval-sales-overviewwith the body above when the user supplies that id.Inline ES|QL metric panel example (inside
panels):{ "type": "vis", "id": "total-requests", "grid": { "x": 0, "y": 0, "w": 12, "h": 5 }, "config": { "type": "metric", "data_source": { "type": "esql", "query": "FROM logs* | STATS `Total requests` = COUNT()" }, "metrics": [{ "type": "primary", "column": "Total requests" }] } }The metric has no
titlebecause its label already names it, and it is a quarter-width, 5-row panel — both are design defaults, not stylistic choices. Prefer inlineconfigproperties overconfig.ref_idfor portable dashboards. Read Dashboard API Reference for panel types and copy workflows.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; otherwisePOST kbn:/api/visualizationsand report the generated id from the response.ES|QL metric (total count from logs):
{ "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-requestswhen 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 and Chart Types Reference for xy, gauge, heatmap, and other chart schemas, and apply the same Chart Design Guidelines as for dashboard panels.
Edit existing objects in place.
PUTreplaces the whole document, soGETfirst, change only what was asked, and send everything else back unchanged — panelids, queries, filters, controls, andtime_rangeincluded.Execute and confirm. Perform the write with
PUT kbn:/api/dashboards/{id}orPUT kbn:/api/visualizations/{id}(orPOSTwhen no id is supplied). Confirm withGET kbn:/api/dashboards/{id}orGET kbn:/api/visualizations/{id}. Report the id and title back to the user — do not claim success without a successful read-back.A successful read-back proves the object persisted, not that it renders. The API normalizes the body and fills in defaults. Compare the returned config against what you sent and look for values you did not intend, for example
legend.visibility: "hidden"on a multi-series chart.List, export, or delete when requested. Call
GET kbn:/api/dashboardsorGET kbn:/api/visualizationsto discover existing objects. CallDELETE kbn:/api/dashboards/{id}orDELETE kbn:/api/visualizations/{id}to remove objects. For bulk export or import of saved objects, callPOST kbn:/api/saved_objects/_exportorPOST kbn:/api/saved_objects/_import.
Design essentials
The full rules live in the references; these are the ones most often missed. They apply to every dashboard and visualization you write.
Version-gated settings. Everything else in this skill works on Kibana 9.4. These three need a newer Kibana; below that version use the fallback instead of the setting.
| Setting | Needs | Fallback on older clusters |
|---|---|---|
options_list_control with values_source: "esql" | 9.5 | data_view_id and field_name; create a data view if none covers the index |
background_chart: { type: "trend" } on an ES|QL metric | 9.5 | Omit background_chart; the metric shows the value only |
styling: { areas: { fill: "gradient" } } | 9.6 | Omit styling.areas; areas render with a solid fill |
Dashboard (Dashboard Design Guidelines)
Order panels summary metrics → trends → breakdowns. Let the fields drive the panel count; every panel needs a purpose.
Non-empty
titleand adescriptionon every new dashboard, derived from what the queries measure.Sections (
{ title, collapsed, grid: { y }, panels }entries inpanels) for roughly 6+ visualization panels, each holding only panels matching its purpose. 3–5 options-list controls inpinned_panelson new dashboards; on an all-ES|QL dashboard, source their values from an ES|QL query (values_source: "esql", 9.5+) instead of a data view.48-column grid, 20–24 rows above the fold, 8–12 panels in that band. Sizes by chart type:
Panel whMetric 6, 8, or 12 5–6 Gauge 12 8 XY, heatmap, tag cloud 24 (48 for the primary time series) 10 Treemap / waffle / mosaic 24 10 Pie / donut 12 10 Markdown 24–48 4–9 Data table 24–48 12–16 Never make a metric or gauge full-width. Use
wvalues that divide 48. Fill rows left to right; a new row'syis the previous row'sy + max(h); panels in a row shareh;x + w ≤ 48. Kibana compacts panels upward, so blank rows do not survive — separate groups with sections. Section panels use section-relative coordinates and a section occupies one outer row.
Charts (Chart Design Guidelines)
Pick the chart type from the selection list; tag clouds only for short strings, pies for fewer than about 7 slices.
No panel title on metric, gauge, tag cloud, and waffle charts; elsewhere a self-explanatory title and no axis titles (
title: { visible: false }on both axes). Never repeat information across title, axes, and labels.Natural units via
format: percent, bytes, bits, duration whenever the column reveals one; plain counts unformatted.XY: set
legend: { position: "bottom", layout: { type: "list" }, visibility: "visible" }on every chart with abreakdown_byor more than oneycolumn, andvisibility: "hidden"on single-series charts. Uselayout: { type: "grid" }only when the legend carries statistics. The API defaultsvisibilitytohiddenandlayouttogrid; it does not count the series. Areas (gradient fill) only up to three series, lines above that. Line charts take theelastic_line_optimizedpalette onbreakdown_by.color. Usescale: "temporal"for time axes, and legend statistics only when asked for avg/min/max "in the legend" (a presentation setting, not an ES|QL aggregation).Metric: trend background or secondary metric when the query supports it; color the value, never the background, and only for bounded metrics (percentages, error rates) with 3 status bands. Never set
apply_color_towithoutcolor, and never combine a background chart with color: Lens then colors the background, not the value.Gauge: never invent
min/max/goal; default to four equal percentage bands.Heatmap, pie, partition charts, xy series: default palettes unless the user asks. Tables color values as badges only.
Color only where it carries meaning, always from the Kibana Palette Catalog, never invented hex values or legacy palettes; the same category keeps one color across all charts.
On edits, preserve explicit user choices and existing thresholds; defaults are preferences, not proof a setting is wrong.
ES|QL patterns
Time series bucket (dashboard time picker injects ?_tstart / ?_tend):
FROM logs*
| WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart
| STATS count = COUNT() BY BUCKET(@timestamp, 75, ?_tstart, ?_tend)See Chart Types Reference for axis and layer details.
Time series data stream (TSDS) xy chart. TS queries accept the same BUCKET pattern, so the chart binds the time
axis the same way:
TS metrics-otel-default
| WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart AND attributes.device != "lo"
| STATS `Throughput` = SUM(RATE(metrics.system.network.io)) BY BUCKET(@timestamp, 75, ?_tstart, ?_tend), attributes.directionMetric with trend background. The API copies the query into a hidden trendline layer and adds its own time bucket.
On a TS source do not add BUCKET yourself and do not KEEP after STATS; on a FROM source keep the ?_tstart /
?_tend filter, or no trendline is built. Derive values inside STATS:
TS metrics-otel-default
| WHERE attributes.state == "idle"
| STATS `CPU busy` = 1 - AVG(metrics.system.cpu.utilization)Static reference values — use EVAL in the query, then reference the column:
FROM logs* | STATS count = COUNT() | EVAL goal = 15000Examples
Two complete dashboard bodies live under assets/. Both use every version-gated setting, so they need Kibana
9.6 as written; on 9.5 drop styling.areas, and on 9.4 also replace the ES|QL-sourced controls and the trend background
with their fallbacks.
dashboard-esql.json— an ES|QL dashboard over the Kibana sample web logs that follows the design guidance: three sections, formatted metrics with a trend background and status coloring, a primary time series with legend statistics, breakdown bars, a table, and options-list controls whose values come from ES|QL queries.dashboard-data-view.json— the same dashboard shape built on the sample logs data view with aggregation operations instead of ES|QL: sections, data-view controls, a primary time series with legend statistics, a heatmap, horizontal bars, and a ranked table with labeled and formatted columns.
Single-chart schema examples for every chart type are in Chart Types Reference.
Guidelines
Minimal payloads, complete presentation — omit derivable metadata and schema defaults, but always set the presentation settings the design guidance calls for (formats, hidden axis titles, legend position and visibility, gradient areas, metric coloring, gauge bands), subject to the version-gated table in Design essentials. The API does not add these for you.
Read references before complex charts — metric and xy schemas differ between data view and ES|QL; consult Chart Types Reference 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 |
| Metric value rendered green | apply_color_to without color | Omit both, or add a color config alongside apply_color_to: "value" |
| Metric tile is a solid colored block | background_chart combined with color steps | Lens colors the background when a trend or bar is present; drop either the background chart or the color |
| Metric panel: "expected at most one time tbucket" | Trend background on a TS query that groups by BUCKET(@timestamp, ...) | Remove the BUCKET; the API appends TBUCKET(75) itself (an existing TBUCKET is kept) |
| Trend metric shows no trend line | FROM query without ?_tstart / ?_tend, or TS query with KEEP after STATS | Add the time filter, or drop the KEEP; see the Metric rules in Chart Design Guidelines |
| Multi-series xy chart shows no legend | legend.visibility omitted, so the API set it to hidden | Set legend: { position: "bottom", visibility: "visible" } on charts with a breakdown or several y columns |
| 400 on color config | Categorical mapping on a numeric column, or steps on a keyword column | Match the coloring mode to the column type; see Chart Design Guidelines |
400 on steps | Non-continuous ranges or missing gte/lt | Every step except the first needs gte, every step except the last needs lt, the last uses lte |
400 on format.duration | Unit name such as milliseconds | Use the short unit codes ps, ns, us, ms, s, min, h, d, w, mo, y; to accepts auto |
| Control fails validation | Missing data_view_id | Look the data view up with GET kbn:/api/data_views, create one, or source the options from ES|QL instead |
| Panels overlap or leave gaps | Coordinates not recomputed | Reflow with the positioning rules in the Dashboard Design Guidelines |
Operations
As of CLI v0.4.0 the Dashboards and Visualizations APIs have dedicated elastic kb dashboards and
elastic kb visualizations command groups. elastic kb is shorthand for elastic stack kb. Write commands take the
JSON body from --input-file '<path.json>' or --body '<json>'. upsert-* takes the id on --id and creates or
replaces the object. delete-* requires --yes. Every command accepts --dry-run, which validates flags and JSON
syntax only. It does not validate the panel schema. To author several objects at once, build a saved-object NDJSON file
and import it with post-saved-objects-import. Read it back with post-saved-objects-export. If the CLI rejects a
command name from this table, run elastic stack kb <group> --help and use the listed name.
| 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>"}]' (returns a JSON array, not NDJSON) |
GET kbn:/api/dashboards | elastic kb dashboards search-dashboards |
GET kbn:/api/dashboards/{id} | elastic kb dashboards get-dashboard --id '<id>' |
PUT kbn:/api/dashboards/{id} | elastic kb dashboards upsert-dashboard --id '<id>' --input-file '<path.json>' |
POST kbn:/api/dashboards (no id) | elastic kb dashboards create-dashboard --input-file '<path.json>' |
DELETE kbn:/api/dashboards/{id} | elastic kb dashboards delete-dashboard --id '<id>' --yes |
GET kbn:/api/visualizations | elastic kb visualizations search-visualizations |
GET kbn:/api/visualizations/{id} | elastic kb visualizations get-visualization --id '<id>' |
PUT kbn:/api/visualizations/{id} | elastic kb visualizations upsert-visualization --id '<id>' --input-file '<path.json>' |
POST kbn:/api/visualizations (no id) | elastic kb visualizations create-visualization --input-file '<path.json>' |
DELETE kbn:/api/visualizations/{id} | elastic kb visualizations delete-visualization --id '<id>' --yes |
GET kbn:/api/data_views | elastic kb data-views get-all-data-views-default (to find a data_view_id for controls) |
POST kbn:/api/data_views/data_view | elastic kb data-views create-data-view-default --input-file '<path.json>' |
GET /_cat/indices, GET /{index}/_mapping, GET /{index}/_settings/index.mode, POST /_query | elastic es cat indices --index '<pattern>', elastic es indices get-mapping --index '<index>', elastic es indices get-settings --index '<index>' --name index.mode, elastic es esql query --format tsv --query "<esql>" [--filter '<query dsl json>'] |
Files
- ATTRIBUTION.mdAdded by AutoGineer960 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md29.4 KiB
- assets/dashboard-data-view.json13.6 KiB
- assets/dashboard-esql.json14.2 KiB
- references/chart-design.md24.1 KiB
- references/chart-types-reference.md13.8 KiB
- references/color-palettes.md3.8 KiB
- references/dashboard-api-reference.md8.9 KiB
- references/dashboard-design.md14.2 KiB
- references/lens-api-reference.md3.2 KiB
references/chart-design.md
Chart Design Guidelines
What a good chart looks like, and how to express it in the visualization config JSON. These rules apply to every inline
vis panel config and every standalone visualization body. Chart Types Reference
documents the schema of each chart type; this document tells you which settings to choose. Palette names, ids, and hex
values are in the Kibana Palette Catalog.
Every section has two parts: Design (the preference, stated without JSON) and Config (how to carry it out). Do not invent a second design policy in the config: the config only implements the design.
Choosing a chart type
Choose the type that best fits the user's intent and the nature of the data being visualized:
metric— Displays a single numeric value, KPI, or aggregate statistic (count, sum, average) with an optional trend line. Choose for single numbers without ranges or targets.gauge— Displays a single metric within a range with optional min/max/goal bounds. Choose when showing progress toward a goal or performance against thresholds (for example "CPU usage as a gauge", "sales target progress").xy— Line, bar, or area charts with X and Y axes. Choose for time series, trends, comparisons across series, or distributions/histograms (for example "request count over time", "average CPU over time", "sales by region as a bar chart"). Avg/min/max in the legend is still xy, not a combination chart.heatmap— Colors a two-dimensional grid of x/y buckets by metric magnitude. Choose when both axes are buckets (categorical or time) and color should convey density or intensity (for example "errors by service and status code", "requests by hour of day and day of week").tag_cloud— Displays terms sized by frequency or value. Choose only when the terms are short strings (tags, status codes, country codes, browsers). Do not use for long text such as error messages, URLs, or log lines — use a table instead.region_map— Choropleth map coloring geographic boundaries (country, state, county) by a metric. Choose when the data has region identifiers that join to map boundaries and a map view is expected (for example "revenue by state on a map").data_table— Structured table with sortable columns. Choose when precise values, sortable columns, or multi-dimensional breakdowns matter more than visual patterns (for example "list top 20 hosts by CPU usage").pie— Pie or donut showing part-to-whole proportions as slices. Choose for percentage breakdowns with a limited number of categories, ideally fewer than 7 (for example "traffic distribution by browser as a donut").treemap— Nested rectangles where area encodes magnitude. Choose for size comparisons across many categories or hierarchical breakdowns (for example "disk usage by folder", "log volume by service and host").waffle— Grid of small squares where the filled share encodes a proportion. Choose for intuitive single-percentage displays that read easier than pie charts (for example "percentage of requests that are errors").mosaic— Tiled rectangles where area and position encode the joint distribution of two categorical dimensions. Choose for cross-tabulations (for example "request methods by status code", "error distribution across services and environments").
When a request fits none of these, pick the closest supported type and explain the substitution.
General rules (all chart types)
Design
Titles: omit the panel title when the chart already displays the information within itself (metric, gauge, tag cloud, and waffle charts show their value and label directly). When a title is needed, make it self-explanatory and exhaustive so axis titles become unnecessary. Never duplicate information across the title, axis titles, and metric labels.
Units: show values in their natural unit whenever the data has a well-known one — percentages for utilization and rates, bytes for storage, memory, and network volume, bits for network throughput, human-readable durations for latency and response times. Column names and the request often reveal the unit (for example "cpu", "percent", "bytes_in", "disk_used", "latency_ms"); apply it even when nobody asked. Plain counts, rates without a known scale, and ambiguous units stay unformatted.
Defaults are preferences, not proof that an existing setting is wrong. A gauge goal, a threshold, or an unusual color may be intentional; preserve explicit user choices and meaningful existing settings when editing.
Config
Titles: omit the
titlefield (or set it to"") to show no title. For ES|QL metrics, alias the column in the query so the metric label is readable (for exampleSTATS `Total Requests` = COUNT()).Number formats — set
formaton the bound column (metrics[],y[], gaugemetric, table columns):Data Format CPU / utilization percentages { "type": "percent", "decimals": 1, "compact": true }(expects a 0–1 fraction)Whole-number percentages { "type": "number", "decimals": 1, "suffix": "%" }(value already 0–100)Bytes { "type": "bytes", "decimals": 1 }Bits { "type": "bits", "decimals": 1 }Durations { "type": "duration", "from": "<source unit>", "to": "auto" }Large plain numbers (if needed) { "type": "number", "decimals": 0, "compact": true }The duration
fromunit must match how the field is stored: one ofps,ns,us,ms,s,min,h,d,w,mo,y(for example"ms"forlatency_ms).toisauto,auto-approximate, or one ofms,s,min,h,d,w,mo,y(no sub-millisecond targets). Do NOT apply a format to plain counts or ambiguous units.
Series statistics versus measures
Two different "average" requests — do not mix them:
Measure over time: "average
<field>over time" (for example average CPU). The query averages that field withAVG(<field>)in a singleSTATS ... BY <time bucket>, and the chart binds that column. That is not legend statistics.Legend statistics: "log volume over time, show avg/min/max" or "trend with avg, min, max" when no source field is being averaged. Those words are presentation: query only the measure over time (for example the count per bucket), do not add
AVG/MIN/MAXcolumns or a secondSTATSthat collapses the buckets, and instead setlegend.statisticson the xy chart withlegend.visibility: "visible".
Supported legend.statistics values: min, max, avg, median, range, last_value, last_non_null_value,
first_value, first_non_null_value, difference, difference_percentage, count, total, standard_deviation,
variance, distinct_count, current_and_last_value. Never invent statistic columns the query does not emit.
"legend": { "position": "bottom", "layout": { "type": "grid" }, "visibility": "visible", "statistics": ["avg", "min", "max"] }Metric
Design
No panel title: the primary metric label already names the panel.
A single number is fine. When the query results support it and the value benefits from context, add a trend background or a secondary metric instead of leaving a lone number on white.
Show a progress bar only when the value has a meaningful maximum.
A secondary trend or delta needs no label; label a secondary metric only when it is a distinct named measure.
Color the value, not the background, and only when it carries meaning. Clearly bounded metrics (percent, ratio, CPU/memory/disk utilization, error rate, success rate, SLO compliance) benefit from status bands in the same scale as the value; for adverse metrics such as error rate, higher is worse. Unbounded values (raw counts, bytes, durations, throughput, rates with unknown scale) stay uncolored.
A metric is either colored or has a background chart, not both. When a trend or progress bar background is present, Lens applies the color to the background instead of the value, so status bands turn the whole tile into a colored block. For a bounded metric, prefer the colored value and skip the trend; for an unbounded metric, use the trend and no color.
Config
Trend backgrounds (
background_chart: { "type": "trend" }on the primary metric) and secondary metrics (a secondmetrics[]entry with"type": "secondary") must bind columns the same ES|QL query returns. Never invent another index or field.Trend backgrounds on ES|QL metrics (Kibana 9.5+). On 9.4 the setting is accepted but no trendline layer is generated for ES|QL sources, so omit
background_chartthere. From 9.5 the API builds a hidden*_trendlinelayer by copying the query and adding a time bucket to theSTATS ... BYclause. It needs to know the time field, which it reads from the column compared to?_tstart/?_tendin the query, or assumes@timestampfor aTSsource. Rules that follow from this:FROMqueries must filter on the time field with?_tstart/?_tend, otherwise no trendline layer is created and the metric silently renders without a trend. The API appendsBUCKET(<time field>, 75, ?_tstart, ?_tend), or keeps an existingBUCKET(<time field>, ...)in theBYclause, and adds the bucket column to any laterKEEP.TSqueries getTBUCKET(75)appended to the firstSTATSafterTS, unless thatSTATSalready groups byTBUCKET(...), which is kept. ABUCKET(@timestamp, ...)is not recognized on aTSsource, so it ends up next to the appendedTBUCKETand Elasticsearch rejects the query with "expected at most one time tbucket". The API does not add the bucket column to a laterKEEPonTSqueries, so aKEEPafterSTATSdrops it and the trend is empty.Simplest shape for both sources:
source | WHERE <time filter> ... | STATS <alias> = <agg>with derived values computed insideSTATS, for exampleSTATS `CPU busy` = 1 - AVG(cpu.idle), and noKEEPafterwards.
Progress bar:
background_chart: { "type": "bar", "max_value": "<column>" }on the primary metric, only for meaningful progress-to-max.For trend/delta secondary metrics, hide the label with
styling.secondary.label.visible: falseand omitlabel.Coloring: set
"apply_color_to": "value"only together with acolorconfig; do not color the background unless the user asks. When not coloring, omit bothcolorandapply_color_to—apply_color_towithout a color makes Lens tint the value with a default green.Never combine
background_chart(trendorbar) withcolorsteps orapply_color_to. Lens ignoresapply_color_to: "value"when a background chart is present and colors the background instead. Pick one per metric.Bounded metrics: explicit 3-band
stepswith thresholds in the same unit and scale as the metric output (percent thresholds for percent values, fractions for 0–1 values). Prefer "Status", "Negative", "Positive", or "Temperature" when thresholds have semantic meaning; use a status/adverse palette for adverse metrics. Copy the 3-stop preview of the chosen palette from the catalog.Unbounded values:
color: { "type": "auto" }or no color.
{
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM metrics* | STATS `Error rate` = 100.0 * COUNT(CASE(status >= 500, 1, null)) / COUNT()"
},
"metrics": [
{
"type": "primary",
"column": "Error rate",
"format": { "type": "number", "decimals": 1, "suffix": "%" },
"apply_color_to": "value",
"color": {
"type": "dynamic",
"range": "absolute",
"steps": [
{ "gte": 0, "lt": 1, "color": "#24c292" },
{ "gte": 1, "lt": 5, "color": "#fcd883" },
{ "gte": 5, "lte": 100, "color": "#f6726a" }
]
}
}
]
}Gauge
Design
Gauge bounds and goals describe business targets. Never invent, infer, or backfill minimum, maximum, or goal values from the data or from units like bytes, requests, or rates; set them only when the user provides them, and keep existing ones on edits.
For new gauges, prefer four equal percentage color bands unless the user specifies different bands. When improving an existing dashboard, apply the four equal percentage bands unless the existing bands reflect explicit user thresholds or meaningful business ranges. A focused color or palette edit keeps the existing band count, thresholds, and percentage or absolute scale.
Config
Omit
min,max, andgoalunless the user supplied them or the existing configuration already has them.Default bands:
color: { "type": "dynamic", "range": "percentage", "steps": [...] }with 4 bands0 <= value < 25,25 <= value < 50,50 <= value < 75,75 <= value <= 100, colors copied from the 4-stop preview of the chosen palette. A palette-only edit changes only step colors: preserve the existing number of steps, threshold boundaries, andrange. Change existing bands only when the instruction requests it; explicit user thresholds take precedence over defaults.
"metric": {
"column": "cpu_pct",
"format": { "type": "percent", "decimals": 1, "compact": true },
"color": {
"type": "dynamic",
"range": "percentage",
"steps": [
{ "gte": 0, "lt": 25, "color": "#24c292" },
{ "gte": 25, "lt": 50, "color": "#aee8d2" },
{ "gte": 50, "lt": 75, "color": "#ffc9c2" },
{ "gte": 75, "lte": 100, "color": "#f6726a" }
]
}
}XY (line, area, bar)
Design
No axis titles: the panel title and column labels already convey meaning.
Area series use a gradient fill, never a solid fill.
Use areas only for one to three series. With more series, stacked or overlapping fills hide each other; use a line chart instead.
Place the legend outside the plot, at the bottom. Lay it out as a list, except when it carries legend statistics, where a grid keeps the numbers aligned in columns. Hide it when it only repeats what is visible (a single series); show it when there are several series or when it carries legend statistics.
Line charts use the "Elastic line optimized" palette, whose colors stay distinguishable as thin strokes. Other chart types keep the default palette. Add explicit per-series colors only when the user asks or when the same category must keep one color across charts.
Config
For horizontal bars, use
type: "bar_horizontal"withx= category column andy= metric column. Example: "top OS by count as horizontal bar" →type: "bar_horizontal", x: { column: "OS" }, y: [{ column: "Count" }]. Do NOT put the metric onx.Hide axis titles with
title: { "visible": false }on both the x and y axes; do not set axis title text.Area series:
styling: { "areas": { "fill": "gradient" } }(Kibana 9.6+; omitstyling.areason older clusters, which render a solid fill).Legend:
legend: { "position": "bottom", "layout": { "type": "list" } }with the default outside placement; do not setplacement. Use"layout": { "type": "grid" }only whenlegend.statisticsis set. The API defaultslayouttogrid, so set it explicitly on every xy chart. Always setlegend.visibilityexplicitly. Use"visible"when the layer has abreakdown_byor more than oneycolumn, and always when legend statistics are set. Use"hidden"for a single series whose name is already in the panel title or on the axis. The API fills an omittedvisibilitywith"hidden". It does not detect the series count the way the Lens editor does.Legend statistics: see Series statistics versus measures.
Time series: always
"scale": "temporal"on the x axis (see Chart Types Reference).Series count:
areaandarea_stackedlayers only when the query yields at most three series (oneycolumn times the breakdown cardinality). Above that, useline.Line palette: on
linelayers with abreakdown_by, setbreakdown_by.color: { "mode": "categorical", "palette": "elastic_line_optimized", "mapping": [] }. Onlinelayers with severalycolumns and no breakdown,y[].coloraccepts only a static color, so give each columncolor: { "type": "static", "color": "<hex>" }with consecutive hex values from theelastic_line_optimizedpreview in the palette catalog. Bar and area layers keep the default palette.Other coloring: omit explicit
colorproperties unless colors were requested; Lens applies its default palettes. Never introduce or switch to legacy palette ids (eui_amsterdam,kibana_v7_legacy,elastic_brand_2023).
{
"type": "xy",
"title": "Log volume over time",
"axis": {
"x": { "scale": "temporal", "domain": { "type": "fit", "rounding": false }, "title": { "visible": false } },
"y": { "title": { "visible": false } }
},
"legend": {
"position": "bottom",
"layout": { "type": "grid" },
"visibility": "visible",
"statistics": ["avg", "min", "max"]
},
"styling": { "areas": { "fill": "gradient" } },
"layers": [
{
"type": "area",
"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", "label": "Logs" }]
}
]
}Heatmap
Design
Keep the default "Temperature" palette that Lens binds to the data; use a custom palette or thresholds only when the user asks.
Config
Omit
metric.coloror usecolor: { "type": "auto" }; generate explicitsteps(5 bands, colors from the 5-stop preview) only when the user requests a custom palette or gives thresholds.
Tag cloud
Only for short strings (tags, status codes, country codes, browsers); never for long text. No panel title — the terms are the content.
No coloring rules beyond the general Color guidance.
Region map
Choose only when the data has region identifiers that join to map boundaries. No coloring rules beyond the general Color guidance.
Data table
Design
Color table values as badges, and only where color adds meaning (status, severity, magnitude). Do not color cell backgrounds or text unless the user asks.
Config
Column color settings go on the column entry in metrics[] or rows[]:
Prefer
"apply_color_to": "badge"; avoid cell background or text coloring unless the user asks.Numeric columns: when coloring is useful, use
"apply_color_to": "badge"withcolor: { "type": "auto" }so Lens computes stops from table data. Generate explicit 5-bandstepsonly when the user asks for a palette or thresholds.Categorical (keyword/text) columns: when coloring is useful, use
color: { "mode": "categorical", "palette": "<palette id>", "mapping": [] }so Lens assigns colors to actual values.NEVER apply categorical mapping to a numeric column or dynamic palette steps to a keyword column.
"metrics": [{ "column": "error_rate", "format": { "type": "number", "decimals": 1, "suffix": "%" }, "apply_color_to": "badge", "color": { "type": "auto" } }],
"rows": [{ "column": "service", "apply_color_to": "badge", "color": { "mode": "categorical", "palette": "default", "mapping": [] } }]Pie and donut
Design
Use the default palette; per-slice or custom colors only when the user asks.
Keep the category count small (ideally fewer than 7); above that, prefer a bar chart or table.
Config
Omit explicit
colorproperties; Lens applies its default palette. Add colors only when the user explicitly requests them.Donuts:
"styling": { "donut_hole": "m" }on apie.
Treemap, waffle, mosaic
Follow the general Color guidance: let Lens apply its default palette and add explicit colors only when the user asks or when a category must keep one color across charts.
Color
Design
Add color only when it adds meaning: status colors for meaningful thresholds, intensity colors for magnitude, and one consistent color for the same category wherever it appears across charts. Neutral data with no useful color meaning stays uncolored.
Choose palettes from the Kibana palette catalog, never invented colors or legacy palettes: "Status" for threshold bands, "Temperature" for intensity, "Complementary" for divergence, "Negative"/"Positive" for adverse/favorable values, "Cool"/"Warm"/"Gray" for neutral magnitude, and a categorical palette for distinct categories:
elastic_line_optimizedon line charts,defaultelsewhere,severitywhen categories are severities.Thresholds are data values in the metric's own unit and scale. When only the colors change, keep the existing thresholds.
Respect explicit user choices and meaningful existing color assignments. An off-palette color is not automatically wrong; do not assume an existing color was invented just because its history is unknown.
Cross-chart consistency is your job: when a category (a service, a status code) appears in several panels, give it the same color in every panel's config.
Config
Default policy
Prefer Lens defaults for unknown-scale data: use
color: { "type": "auto" }or omitcolorwhen Lens can calculate better thresholds at render time.Generate explicit numeric
stepsonly when the chart-specific rules above allow it (metric bounded values, gauge bands) or when the user asks for a custom palette or exact thresholds.The chart-specific coloring rules above override this policy where they differ.
Coloring mode — choose by column type
Numeric columns →
color: { "type": "auto" }by default;color: { "type": "dynamic", "range": "absolute" | "percentage", "steps": [...] }only when explicit steps are allowed.Keyword / text columns →
color: { "mode": "categorical", "palette": "<palette id>", "mapping": [] }.NEVER apply categorical mapping to a numeric column or dynamic palette steps to a keyword column.
NEVER use the deprecated
type: "legacy_dynamic".
Dynamic steps — mechanics for when the rules call for explicit steps
Pick exactly ONE gradient palette, following the design guidance on which palette fits which meaning.
Use the step count the chart type calls for: 3 for metrics, 4 for gauges, 5 for heatmaps and tables. For a recolor of an existing chart, keep its current step count and copy the preview with that many stops.
Every
steps[*].colorhex MUST come from the selected palette's preview line for that step count, exactly as written.Each step is
{ "gte": <lower, inclusive>, "lt": <upper, exclusive>, "color": "#hex" }; the last step useslteinstead oflt. Steps must be continuous, and every step except the first needsgte.Step thresholds are data values, not display labels; keep them in the same unit and scale as the metric column. For rates, do not assume per-second thresholds unless the ES|QL query computes per-second values.
Keep palette order by default; to reverse, reverse the
stepscolors yourself. There is noreversefield.
Categorical mapping — pick a palette by id
Set
color: { "mode": "categorical", "palette": "<palette id>", "mapping": [] }and let Lens auto-assign a distinct color per distinct value at render time.The
palettevalue MUST be one of the categorical palette ids in the catalog, verbatim (default,elastic_line_optimized,severity).Leave
mapping: []by default. Only define explicitmapping[]entries when the user names specific values to color, or when a category must keep one color across charts.When you do map explicit values, each entry is
{ "values": ["<value>"], "color": { "type": "color_code", "value": "#hex" } }, drawing the hex from one of the catalog palettes.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
The author says: 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.4 with `stack kb` support (dedicated `dashboards` and `visualizations` commands).
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- elastic
- Publisher
- Elastic (Official)
- Source
- elastic/agent-skills · skills/kibana/kibana-dashboards · commit 09d879d
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Also distributed in
- elastic/agent-skills
AutoGineer is not affiliated with or endorsed by elastic.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download kibana-dashboards.zip