Generated banner for Kibana Dashboards
Generated logo for Kibana Dashboards

Kibana Dashboards

ElasticOfficialCreated by elastic

  • Skill
  • Tool integration
  • Cloud and DevOps
  • Apache-2.0
  • No issues found

From the author

Create and manage Kibana Dashboards and Lens visualizations. Use when you need to define dashboards and visualizations declaratively, version control them, automate their deployment, or improve layout, sections, controls, or chart design.
— the description by elastic

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/elastic/kibana-dashboards/download
  2. Unzip 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} 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, 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.

  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 intentDatasetMetric / axis pattern
    Simple count or aggregation on a saved data viewdata_source.type: "data_view_reference" with ref_idmetrics: [{ type: "primary", operation: "count" }] (or other aggregation operations)
    Ad-hoc index patterndata_source.type: "data_view_spec" with index_pattern and time_fieldSame aggregation operation fields
    ES|QL query (explicit or complex logic)data_source.type: "esql" with querymetrics: [{ 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.

    Write the ES|QL with the elasticsearch-esql skill. It covers schema discovery, time series data streams (TS, RATE for 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 days for ?_tstart and ?_tend, which only Kibana can supply.

    • Check the data's time span (STATS MIN(@timestamp), MAX(@timestamp)). Without a saved time_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.

  4. 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 grid coordinates. 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.

  5. Build a dashboard body when creating or updating dashboards. The request body is flat — title, panels, and optional description, time_range, and pinned_panels at 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" }). Supplying time_range persists 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-overview with 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 title because its label already names it, and it is a quarter-width, 5-row panel — both are design defaults, not stylistic choices. Prefer inline config properties over config.ref_id for portable dashboards. Read Dashboard API Reference for panel types and copy workflows.

  6. 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):

    {
      "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 and Chart Types Reference for xy, gauge, heatmap, and other chart schemas, and apply the same Chart Design Guidelines as for dashboard panels.

  7. Edit existing objects in place. PUT replaces the whole document, so GET first, change only what was asked, and send everything else back unchanged — panel ids, queries, filters, controls, and time_range included.

  8. 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.

    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.

  9. 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.

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.

SettingNeedsFallback on older clusters
options_list_control with values_source: "esql"9.5data_view_id and field_name; create a data view if none covers the index
background_chart: { type: "trend" } on an ES|QL metric9.5Omit background_chart; the metric shows the value only
styling: { areas: { fill: "gradient" } }9.6Omit 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 title and a description on every new dashboard, derived from what the queries measure.

  • Sections ({ title, collapsed, grid: { y }, panels } entries in panels) for roughly 6+ visualization panels, each holding only panels matching its purpose. 3–5 options-list controls in pinned_panels on 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:

    Panelwh
    Metric6, 8, or 125–6
    Gauge128
    XY, heatmap, tag cloud24 (48 for the primary time series)10
    Treemap / waffle / mosaic2410
    Pie / donut1210
    Markdown24–484–9
    Data table24–4812–16

    Never make a metric or gauge full-width. Use w values that divide 48. Fill rows left to right; a new row's y is the previous row's y + max(h); panels in a row share h; 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 a breakdown_by or more than one y column, and visibility: "hidden" on single-series charts. Use layout: { type: "grid" } only when the legend carries statistics. The API defaults visibility to hidden and layout to grid; it does not count the series. Areas (gradient fill) only up to three series, lines above that. Line charts take the elastic_line_optimized palette on breakdown_by.color. Use scale: "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_to without color, 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.direction

Metric 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 = 15000

Examples

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

  1. 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.

  2. 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

ErrorLikely causeFix
404 on GET after PUTWrong id or spaceConfirm id and retry GET kbn:/api/dashboards/{id}
400 validationES|QL column mismatchAlign metrics[].column / layer column with STATS aliases in the query
ES|QL panel saved as data viewWrong dataset typeUse data_source.type: "esql", not data_view_reference
Empty dashboard missing time filterOmitted time_rangeInclude { "from": "now-7d", "to": "now" } when a default range is required
XY chart failureMissing layer data_sourcePut data_source inside each layer, not only at the root
Metric value rendered greenapply_color_to without colorOmit both, or add a color config alongside apply_color_to: "value"
Metric tile is a solid colored blockbackground_chart combined with color stepsLens 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 lineFROM query without ?_tstart / ?_tend, or TS query with KEEP after STATSAdd the time filter, or drop the KEEP; see the Metric rules in Chart Design Guidelines
Multi-series xy chart shows no legendlegend.visibility omitted, so the API set it to hiddenSet legend: { position: "bottom", visibility: "visible" } on charts with a breakdown or several y columns
400 on color configCategorical mapping on a numeric column, or steps on a keyword columnMatch the coloring mode to the column type; see Chart Design Guidelines
400 on stepsNon-continuous ranges or missing gte/ltEvery step except the first needs gte, every step except the last needs lt, the last uses lte
400 on format.durationUnit name such as millisecondsUse the short unit codes ps, ns, us, ms, s, min, h, d, w, mo, y; to accepts auto
Control fails validationMissing data_view_idLook the data view up with GET kbn:/api/data_views, create one, or source the options from ES|QL instead
Panels overlap or leave gapsCoordinates not recomputedReflow 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/statuselastic kb system get-status
POST kbn:/api/saved_objects/_importelastic kb saved-objects post-saved-objects-import --file '<path.ndjson>' --overwrite
POST kbn:/api/saved_objects/_exportelastic kb saved-objects post-saved-objects-export --objects '[{"type":"<type>","id":"<id>"}]' (returns a JSON array, not NDJSON)
GET kbn:/api/dashboardselastic 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/visualizationselastic 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_viewselastic kb data-views get-all-data-views-default (to find a data_view_id for controls)
POST kbn:/api/data_views/data_viewelastic kb data-views create-data-view-default --input-file '<path.json>'
GET /_cat/indices, GET /{index}/_mapping, GET /{index}/_settings/index.mode, POST /_queryelastic 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

references/dashboard-design.md

Dashboard Design Guidelines

How to compose, structure, and lay out a dashboard so it reads well. These rules apply to every dashboard you create or update through PUT kbn:/api/dashboards/{id}. Chart-level rules (chart type choice, titles, units, legends, colors) live in Chart Design Guidelines.

Composition

A well-composed dashboard orders panels as summary metrics → trends → breakdowns and distributions:

  1. Consider a markdown panel when it adds value — to set context about what the dashboard monitors, data sources, or important notes. Not every dashboard needs one. Never use a markdown panel as a dashboard title.

  2. Lead with high-level metrics (metric or gauge panels): total counts, averages, key performance indicators that give an at-a-glance summary.

  3. Follow with time-series trends (xy line/area panels): how the key metrics change over time.

  4. Follow with breakdowns and distributions (xy bar, heatmap, tag cloud, partition charts, or ranking tables): top-N rankings, categorical splits, and density views. A table of top countries belongs here, not with summary metrics.

  5. Include as many panels as are valuable for the underlying data and user intent. Let the richness and diversity of the available fields drive the panel count instead of a fixed numeric target.

  6. Every panel should serve a clear purpose. Do not add panels just to fill space, but do not artificially limit the dashboard when more panels would provide genuine insight.

When the user's request is vague (for example "create a dashboard for my logs"), inspect the index mapping thoroughly (GET /{index}/_mapping) and compose a rich dashboard that covers the breadth of the available data — overview metrics, time-series trends, breakdowns, and distributions. Let the fields drive the panel count rather than defaulting to a minimal set.

Primary time series

Before writing panels, pick one or two primary time-series xy charts — the overview trend that matches the dashboard title or intent. On a new dashboard, give at least one and at most two of them legend statistics (avg/min/max shown in the legend). Skip this for categorical bar charts and for charts whose measure is already an AVG/MIN/MAX of a field. See Series statistics for how to express it.

Titles and descriptions

  • Every dashboard MUST have a non-empty title. For a new dashboard also set description. If an existing dashboard's title is empty or a placeholder such as "User Dashboard", invent a title from its contents.

  • Derive the dashboard title, description, section titles, panel titles, and markdown text from what the queries actually measure and the indices they read — not from the existing dashboard name or an assumed domain. Rename anything misleading.

  • Panel titles must be self-explanatory and exhaustive so axis titles become unnecessary. Never duplicate information across the panel title, axis titles, and metric labels. Charts that display their information within themselves (metric, gauge, tag cloud, waffle) get no panel title at all.

Sections

Sections are collapsible groups of panels. In the API a section is an entry of the top-level panels array with a title, collapsed, a grid holding only y, and its own nested panels. Each section's panels start again at y: 0, and the next section's outer y is the previous one plus 1:

"panels": [
  {
    "title": "Key metrics",
    "collapsed": false,
    "grid": { "y": 0 },
    "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" }]
        }
      }
    ]
  },
  {
    "title": "Trends",
    "collapsed": false,
    "grid": { "y": 1 },
    "panels": [
      { "type": "vis", "id": "requests-over-time", "grid": { "x": 0, "y": 0, "w": 48, "h": 10 }, "config": { "type": "xy", "...": "..." } }
    ]
  }
]

A complete dashboard built this way, with three sections, ES|QL panels, and ES|QL-sourced controls, is in assets/dashboard-esql.json, with a data-view counterpart in assets/dashboard-data-view.json.

When to use sections
  • Keep small dashboards flat when a single sequence of panels is easy to scan.

  • Use sections to separate summary metrics, trends, and breakdowns in that order. Each section must contain only panels matching its purpose: a "Key metrics" section contains summary metric/gauge panels, not time-series charts or ranking tables.

  • Use per-domain sections when they make the dashboard clearer; keep summary metrics → trends → breakdowns ordered within each domain.

  • Prefer sections for larger dashboards, especially when there are roughly 6 or more visualization panels or when the layout would otherwise feel long and hard to navigate.

  • Do not add sections only for decoration. Use them when they make the dashboard structure clearer.

  • Leave collapsed: false unless the user asks for collapsed sections; collapsed panels are not rendered until expanded.

Controls

Controls are interactive filters pinned above the dashboard that let users explore data without editing queries. They live in the root pinned_panels array of the dashboard body:

"pinned_panels": [
  {
    "type": "options_list_control",
    "width": "medium",
    "grow": true,
    "config": {
      "title": "Service",
      "data_view_id": "<data view id>",
      "field_name": "service.name"
    }
  }
]

When building a new dashboard from scratch, proactively add 3–5 dropdown controls for the most useful categorical fields. Pick fields that appear in panel BY / WHERE clauses and prefer low-cardinality keyword fields (for example service.name, host.name, env, region, kubernetes.namespace, http.response.status_code). Avoid high-cardinality identifiers (trace IDs, request IDs, UUIDs).

Do not add controls to dashboards already scoped to a single entity (one host, one service, and so on).

Control types:

  • options_list_control — dropdown for categorical / keyword fields. The most common type (95% of cases).

  • range_slider_control — numeric range slider. Add sparingly, only when filtering by a numeric threshold is useful across multiple panels (for example latency, bytes, duration).

  • time_slider_control — global time sub-range picker. Add at most one per dashboard, only when time-range narrowing within the global window is useful. Its config can be {}.

Where the options come from. An options_list_control lists its values from a data view field or from an ES|QL query:

  • Data view: config.data_view_id and config.field_name. Look the data view up with GET kbn:/api/data_views; if none covers the index, create one with POST kbn:/api/data_views/data_view and a body such as { "data_view": { "title": "logs-*", "timeFieldName": "@timestamp" } }.

  • ES|QL (Kibana 9.5+): config.values_source: "esql" and config.esql_query. No data view is needed. End the query with STATS BY <field> so it returns one column of distinct values, and use the same field name the panel queries filter on. On 9.4 use the data view form above.

On a dashboard whose panels are all ES|QL, prefer the ES|QL source so the dashboard does not depend on a data view that may not exist.

"pinned_panels": [
  {
    "type": "options_list_control",
    "width": "medium",
    "grow": true,
    "config": {
      "title": "Host",
      "values_source": "esql",
      "esql_query": "FROM metrics-* | STATS BY host.name",
      "selected_options": []
    }
  }
]

Required fields per control:

  • type: one of the three above.

  • options_list_control: config.data_view_id and config.field_name, or config.values_source: "esql" and config.esql_query.

  • range_slider_control: config.data_view_id and config.field_name.

  • config.title (optional, options_list_control and range_slider_control only): human-readable label shown above the control (for example "Service").

Defaults: width: "medium", grow: true (fills available horizontal space). Override only if the user asks.

Removing controls: drop the entry from pinned_panels and PUT the full body back.

Panel layout

The dashboard uses a 48-column grid. On a 16:9 screen, roughly 20–24 rows are visible without scrolling. Aim for 8–12 panels above the fold.

Every panel requires grid: { x, y, w, h }. The origin (0, 0) is the top-left corner.

Grid sizes by chart type

Use these sizes — do not make metric or gauge panels full-width:

PanelWidth wHeight hNotes
Metric6, 8, or 125–6Single-number panels — keep them small. Fit 4–8 per row.
Gauge128Up to 4 per row.
XY (line / area / bar)2410Use full-width (w: 48) only for the primary time series.
Heatmap2410Needs height for the color matrix.
Tag cloud2410
Pie / donut1210
Treemap / waffle / mosaic2410
Markdown24–484–9Size based on content length and layout needs — not always full-width.
Data table24–4812–16Prefer full-width so columns are readable.

Metric rows: 8 metrics → each w: 6, h: 5; 6 metrics → each w: 8, h: 5; 4 metrics → each w: 12, h: 5.

Prefer w values that divide 48 evenly: 6, 8, 12, 24, 48.

Grid packing rules
  • Automatic upward movement: Kibana moves each panel upward at its current x until another panel or the top of its grid blocks it. A larger y does not preserve empty space above a panel or guarantee row alignment. Use sections, not blank rows, to separate semantic groups.

  • Eliminate dead space: Always calculate the bottom edge (y + h) of every panel. When starting a new row or placing panels below a row, set the new row's y to previous row's y + max(h) across all panels in that row — do not use only one neighbor's y + h.

  • Align row heights: If multiple panels are placed side-by-side in a row (sharing the same y), they should have the exact same height (h). If they do not, you must fill the resulting empty vertical space before placing the next full-width panel.

Positioning rules

Always set x and y so panels tile with no gaps:

  1. Fill rows left to right. Start at x: 0. The next panel's x = previous panel's x + w. When a panel would exceed column 48, start a new row.

  2. New row y = previous row's y + max(h) of all panels in that row.

  3. Same h per row when possible, so rows align cleanly.

  4. Panels' x + w must never exceed 48.

  5. When updating a dashboard, inspect the existing panels' grid from the GET response. If there is empty space (a gap where a panel was removed, or unused columns beside a tall panel), place the new panel in that gap instead of appending below. Choose w and h to fit the available space.

  6. Markdown panels use an explicit grid like any other panel. Size based on content length (w: 24–48, h: 4–9) and account for their height when positioning subsequent panels.

Reflow after layout changes
  • After resizing, moving, or removing panels, choose final sizes first, then recalculate positions across the affected layout, including panels whose sizes did not change. Pack rows using the rules above; do not keep old coordinates that leave gaps. Reflow each section separately.

  • Update existing panels in place (keep their id). Before sending the PUT, check the planned coordinates for avoidable gaps, overlaps, and grid bounds.

Section grid rules
  • Each section has its own coordinate space. Panels nested under a section's panels are section-relative: each section starts at y: 0. The same 48-column grid and sizing guidance apply within each section.

  • A section occupies exactly one row in the outer dashboard grid. When placing anything after a section, the next outer y is section.grid.y + 1 (not the sum of the internal panel heights).

  • Internal panel heights affect layout inside the section only; they do not increase the section's outer-grid height.

  • When mixing top-level panels and sections, compute outer y sequentially: top-level panels advance by y + h, sections advance by y + 1.

  • Top-level panels and sections share the same outer grid. A top-level panel placed at the same y as a section collides with it and is pushed below the section. Because PUT replaces the whole panels array, insert a panel above a section by renumbering the outer y of the section and everything after it in the body you send.

Example: 4 KPI metrics + 2 time-series charts + 1 breakdown bar chart
metric  (x:0,  y:0,  w:12, h:5)
metric  (x:12, y:0,  w:12, h:5)
metric  (x:24, y:0,  w:12, h:5)
metric  (x:36, y:0,  w:12, h:5)
xy-line (x:0,  y:5,  w:24, h:10)
xy-line (x:24, y:5,  w:24, h:10)
xy-bar  (x:0,  y:15, w:48, h:10)
Example: the same dashboard with sections
section "Key metrics"   (outer y:0)
  metric  (x:0,  y:0, w:12, h:5) … metric (x:36, y:0, w:12, h:5)
section "Trends"        (outer y:1)
  xy-line (x:0,  y:0, w:24, h:10)   xy-line (x:24, y:0, w:24, h:10)
section "Breakdowns"    (outer y:2)
  xy-bar  (x:0,  y:0, w:48, h:10)

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