
Kibana Dashboards
- 2 installs
- 31 repo stars
- Updated May 28, 2026
- elastic/cursor-plugins
This is a copy of kibana-dashboards by elastic - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
kibana-dashboards is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- kibana-dashboards
- AI & Agent Building
- AI-coding skill
Kibana Dashboards by the numbers
- 2 all-time installs (skills.sh)
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/elastic/cursor-plugins --skill kibana-dashboardsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 31 |
| Last updated | May 28, 2026 |
| Repository | elastic/cursor-plugins ↗ |
What it does
Helps with ai & agent building tasks.
Files
Kibana Dashboards and Visualizations
Overview
The Kibana dashboards and visualizations APIs provide a declarative, Git-friendly format for defining dashboards and visualizations. Definitions are minimal, diffable, and suitable for version control and LLM-assisted generation.
Key Benefits:
- Minimal payloads (no implementation details or derivable properties)
- Easy to diff in Git
- Consistent patterns for GitOps workflows
- Designed for LLM one-shot generation
- Robust validation via OpenAPI spec
Version Requirement: Kibana 9.4+ (SNAPSHOT)
Important Caveats
ES|QL Visualizations: ES|QL-based visualizations cannot be created via /api/visualizations. They must be createdas inline panels within dashboards using the Dashboard API.
>
Inline vs Saved Object References: When embedding visualization panels in dashboards, prefer inline definitions
over ref_id references. Inline definitions are more reliable and self-contained.Quick Start
Environment Configuration
Kibana connection is configured via environment variables. Run node scripts/kibana-dashboards.js test to verify the connection. If the test fails, suggest these setup options to the user, then stop. Do not try to explore further until a successful connection test.
Option 1: Elastic Cloud (recommended for production)
export KIBANA_CLOUD_ID="deployment-name:base64encodedcloudid"
export KIBANA_API_KEY="base64encodedapikey"Option 2: Direct URL with API Key
export KIBANA_URL="https://your-kibana:5601"
export KIBANA_API_KEY="base64encodedapikey"Option 3: Basic Authentication
export KIBANA_URL="https://your-kibana:5601"
export KIBANA_USERNAME="elastic"
export KIBANA_PASSWORD="changeme"Option 4: Local Development with start-local
Use start-local to spin up Elasticsearch/Kibana locally, then source the generated .env:
curl -fsSL https://elastic.co/start-local | sh
source elastic-start-local/.env
export KIBANA_URL="$KB_LOCAL_URL"
export KIBANA_USERNAME="elastic"
export KIBANA_PASSWORD="$ES_LOCAL_PASSWORD"Then run node scripts/kibana-dashboards.js test to verify the connection.
Optional: Skip TLS verification (development only)
export KIBANA_INSECURE="true"Basic Workflow
# Test connection and API availability
node scripts/kibana-dashboards.js test
# Dashboard operations
node scripts/kibana-dashboards.js dashboard get <id>
echo '<json>' | node scripts/kibana-dashboards.js dashboard create -
echo '<json>' | node scripts/kibana-dashboards.js dashboard update <id> -
node scripts/kibana-dashboards.js dashboard delete <id>
echo '<json>' | node scripts/kibana-dashboards.js dashboard upsert <id> -
# Visualization operations (standalone saved objects)
node scripts/kibana-dashboards.js vis list
node scripts/kibana-dashboards.js vis get <id>
echo '<json>' | node scripts/kibana-dashboards.js vis create -
echo '<json>' | node scripts/kibana-dashboards.js vis update <id> -
node scripts/kibana-dashboards.js vis delete <id>
echo '<json>' | node scripts/kibana-dashboards.js vis upsert <id> -Dashboards API
Dashboard Definition Structure
The API expects a flat request body with title and panels at the root level. The response wraps these in a data envelope alongside id, meta, and spaces.
{
"title": "My Dashboard",
"panels": [ ... ],
"time_range": {
"from": "now-24h",
"to": "now"
}
}Note: Dashboard IDs are auto-generated by the API. The script also accepts the legacy wrapped format
{ id?, data: { title, panels }, spaces? } and unwraps it automatically.Dashboard with Inline Visualization Panels (Recommended)
Use inline definitions (properties directly in config) for self-contained, portable dashboards:
{
"title": "My Dashboard",
"panels": [
{
"type": "vis",
"id": "metric-panel",
"grid": { "x": 0, "y": 0, "w": 12, "h": 6 },
"config": {
"title": "",
"type": "metric",
"data_source": { "type": "esql", "query": "FROM logs | STATS total = COUNT(*)" },
"metrics": [{ "type": "primary", "column": "total", "label": "Total Count" }]
}
},
{
"type": "vis",
"id": "chart-panel",
"grid": { "x": 12, "y": 0, "w": 36, "h": 8 },
"config": {
"title": "Events Over Time",
"type": "xy",
"axis": {
"x": { "scale": "temporal", "domain": { "type": "fit", "rounding": false } }
},
"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" }]
}
]
}
}
],
"time_range": { "from": "now-24h", "to": "now" }
}Dashboard Grid System
Dashboards use a 48-column, infinite-row grid. On 16:9 screens, approximately 20-24 rows are visible without scrolling. Design for density—place primary KPIs and key trends above the fold.
| Width | Columns | Height | Rows | Use Case |
|---|---|---|---|---|
| Full | 48 | Large | 14-16 | Wide time series, tables |
| Half | 24 | Standard | 10-12 | Primary charts |
| Quarter | 12 | Compact | 5-6 | KPI metrics |
| Sixth | 8 | Minimal | 4-5 | Dense metric rows |
Target: 8-12 panels above the fold. Use descriptive panel titles on the charts themselves instead of adding
markdown headers.
Grid Packing Rules:
- Eliminate Dead Space: Always calculate the bottom edge (
y + h) of every panel. When starting a new row or
placing a panel below another, its y coordinate must exactly match the y + h of the panel immediately above it.
- Align Row Heights: If multiple panels are placed side-by-side in a row (e.g., sharing the same
ycoordinate),
they should generally 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.
Panel Schema
{
"type": "vis",
"id": "unique-panel-id",
"grid": { "x": 0, "y": 0, "w": 24, "h": 15 },
"config": { ... }
}| Property | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Embeddable type (e.g., vis, markdown, map) |
id | string | No | Unique panel ID (auto-generated if omitted) |
grid | object | Yes | Position and size (x, y, w, h) |
config | object | Yes | Panel-specific configuration |
Visualizations API
Supported Chart Types
| Type | Description | ES\|QL Support | | ------------------------------------ | --------------------------- | -------------- | | metric | Single metric value display | Yes | | xy | Line, area, bar charts | Yes | | gauge | Gauge visualizations | Yes | | heatmap | Heatmap charts | Yes | | tag_cloud | Tag/word cloud | Yes | | data_table | Data tables | Yes | | region_map | Region/choropleth maps | Yes | | pie, treemap, mosaic, waffle | Partition charts | Yes |
Note: To create donut charts, usepiewithdonut_holeset to"s","m", or"l"(small, medium, large
hole). Use "none" for a solid pie.Dataset Types
There are three dataset types supported in the Visualizations API. Each uses different patterns for specifying metrics and dimensions.
Data View Dataset
Use data_view_reference with aggregation operations. Kibana performs the aggregations automatically.
{
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
}
}Available operations: count, average, sum, max, min, unique_count, median, standard_deviation, percentile, percentile_rank, last_value, date_histogram, terms. See Chart Types Reference for details.
ES|QL Dataset
Use esql with a query string. Reference the output columns using { column: 'column_name' }.
{
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT(), avg_bytes = AVG(bytes) BY host"
}
}ES|QL Column Reference Pattern:
{ "column": "count" }Key Difference: With ES|QL, you write the aggregation in the query itself, then reference the resulting columns.
With data view, you specify the aggregation operation and Kibana performs it.
>
Important: ES|QL visualizations cannot be created via /api/visualizations. They must be created as inline panelsin dashboards via the Dashboard API.
Index Dataset
Use index for ad-hoc index patterns without a saved data view:
{
"data_source": {
"type": "data_view_spec",
"index_pattern": "logs-*",
"time_field": "@timestamp"
}
}Examples
For detailed schemas and all chart type options, see Chart Types Reference.
Metric (Data View):
{
"type": "metric",
"data_source": { "type": "data_view_reference", "ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247" },
"metrics": [{ "type": "primary", "operation": "count", "label": "Total Requests" }]
}Metric (ES|QL):
{
"type": "metric",
"data_source": { "type": "esql", "query": "FROM logs | STATS count = COUNT()" },
"metrics": [{ "type": "primary", "column": "count", "label": "Total Requests" }]
}XY Bar Chart (Data View):
{
"title": "Top Hosts",
"type": "xy",
"axis": { "x": { "title": { "visible": false } }, "y": { "anchor": "start", "title": { "visible": false } } },
"layers": [
{
"type": "bar_horizontal",
"data_source": { "type": "data_view_reference", "ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247" },
"x": { "operation": "terms", "fields": ["host.keyword"], "limit": 10 },
"y": [{ "operation": "count" }]
}
]
}XY Time Series (ES|QL):
{
"title": "Requests Over Time",
"type": "xy",
"axis": {
"x": { "title": { "visible": false }, "scale": "temporal", "domain": { "type": "fit", "rounding": false } },
"y": { "anchor": "start", "title": { "visible": 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" }]
}
]
}Tip: Always hide axis titles when the panel title is descriptive. Use bar_horizontal for categorical data withlong labels. Use axis for axis configuration.Full Documentation
- Dashboard API Reference — Dashboard endpoints and schemas
- Visualizations API Reference — Visualization endpoints
- Chart Types Reference — Detailed schemas for each chart type
- Example Definitions — Ready-to-use definitions
Key Example Files
See assets/ for ready-to-use definitions: demo-dashboard.json, dashboard-with-visualizations.json, metric-esql.json, bar-chart-esql.json, line-chart-timeseries.json.
Common Issues
| Error | Solution |
|---|---|
| "401 Unauthorized" | Check KIBANA_USERNAME/PASSWORD or KIBANA_API_KEY |
| "404 Not Found" | Verify dashboard/visualization ID exists |
| "409 Conflict" | Dashboard/viz already exists; delete first or use update |
| Schema validation error | Ensure column names match query output; use { column: 'name' } for ES\ |
| Metric chart structure | Requires metrics array: [{ type: 'primary', ... }] |
| XY chart fails | Put data_source inside each layer, use axis (singular) |
| ref_id panels missing | Prefer inline definitions (properties in config) over ref_id |
Guidelines
1. Design for density — Operational dashboards must show 8-12 panels above the fold (within the first 24 rows). Use compact panel heights: metrics MUST be h=4 to h=6, and charts MUST be h=8 to h=12. 2. Never use Markdown for titles/headers — Do NOT add markdown panels to act as dashboard titles or section dividers. This wastes critical vertical space. Use descriptive panel titles on the charts themselves. 3. Prioritize above the fold — Primary KPIs and key trends must be placed at y=0. Deep-dives and data tables should be placed below the charts. 4. Use descriptive chart titles, hide axis titles — Write titles that explain what the chart shows (e.g., "Requests by Response Code"). A good panel title makes axis titles redundant. Always set axis.x.title.visible: false and axis.y.title.visible: false.
5. Choose the right dataset type — Use data_view_reference for simple aggregations, esql for complex queries 6. Inline definitions — Prefer inline properties in config over config.ref_id for portable dashboards 7. Test connection first — Run node scripts/kibana-dashboards.js test before creating resources 8. Get existing examples — Use vis get <id> to see the exact schema for different chart types (the CLI subcommand is vis) 9. Avoid redundant metric labels — For ES|QL metrics, avoid using both a panel title and an inner metric label, as it wastes space. Set the panel title to "" and configure the human-readable label by aliasing the ES|QL column name using backticks (e.g., `STATS Total Requests = COUNT() and "column": "Total Requests"). 10. **Format numbers with units** — Use the format property on metrics and y-axis columns to display proper units instead of raw numbers. Types: bytes, bits, number, percent, duration, custom. Example: "format": { "type": "bytes", "decimals": 0 }`. See Chart Types Reference for the full format table.
Schema Differences: Data View vs ES|QL
| Aspect | Data View | ES\|QL | | ------------------- | ----------------------------------------------------- | ------------------------------------------------- | | Dataset | { type: 'data_view_reference', ref_id: '...' } | { type: 'esql', query: '...' } | | Metric chart | metrics: [{ type: 'primary', operation: 'count' }] | metrics: [{ type: 'primary', column: 'col' }] | | XY columns | { operation: 'terms', fields: ['host'], limit: 10 } | { column: 'host' } | | Static values | { operation: 'static_value', value: 100 } | Use EVAL in query (see below) | | XY data_source | Inside each layer | Inside each layer | | Tagcloud | tag_by: { operation: 'terms', ... } | tag_by: { column: '...' } | | Datatable props | metrics, rows arrays | metrics, rows arrays with { column: '...' } |
Key Pattern: ES|QL uses { column: 'column_name' } to reference columns from the query result. The aggregationhappens in the ES|QL query itself. Use data_source for all data source configuration.>
Data source types: Usedata_view_reference(withref_id) for saved data views,data_view_spec(with
index_pattern) for ad-hoc index patterns, andesqlfor ES|QL queries.
ES|QL: Time Bucketing
Use BUCKET(@timestamp, n, ?_tstart, ?_tend) for time series charts. The numeric argument is the target number of buckets. Kibana injects ?_tstart/?_tend automatically. Do not reassign the result — use the full expression BUCKET(@timestamp, 75, ?_tstart, ?_tend) as both the BY clause and the column reference. Set "label" to provide a friendly display name:
"x": { "column": "BUCKET(@timestamp, 75, ?_tstart, ?_tend)", "label": "@timestamp" }Important: To get a proper multilevel time axis (e.g., "9th / April 2026 / 10th") instead of raw timestamp labels, you must set "scale": "temporal" on the x-axis:
"axis": {
"x": { "scale": "temporal", "domain": { "type": "fit", "rounding": false } }
}Without "scale": "temporal", Kibana treats the bucket column as categorical text and renders unsorted, verbose timestamp strings.
FROM logs | WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart | STATS count = COUNT(*) BY BUCKET(@timestamp, 75, ?_tstart, ?_tend)Note:BUCKET(@timestamp, n, ?_tstart, ?_tend)requires aWHEREclause with?_tstart/?_tendbounds (Kibana
injects these). Alternatively, use BUCKET(@timestamp, 1 hour) with a fixed duration — this does not requireparameters but won't auto-scale.
ES|QL: Extracting Date Parts
Use DATE_EXTRACT(part, date) with ES|QL part names (not SQL keywords). The part string must be double-quoted. Common parts: "hour_of_day", "day_of_week", "day_of_month", "month_of_year", "year", "day_of_year".
FROM logs | STATS count = COUNT() BY hour = DATE_EXTRACT("hour_of_day", @timestamp), day = DATE_EXTRACT("day_of_week", @timestamp)ES|QL: Creating Static/Constant Values
ES|QL does not support static_value operations. Instead, create constant columns using EVAL:
FROM logs | STATS count = COUNT() | EVAL max_value = 20000, goal = 15000Then reference with { "column": "max_value" }. For dynamic reference values, use aggregation functions like PERCENTILE() or MAX() in the query.
Design Principles
The APIs follow these principles:
1. Minimal definitions — Only required properties; defaults are injected 2. No implementation details — No internal state or machine IDs 3. Flat structure — Shallow nesting for easy diffing 4. Semantic names — Clear, readable property names 5. Git-friendly — Easy to track changes in version control 6. LLM-optimized — Compact format suitable for one-shot generation
{
"title": "Requests by Response Code",
"type": "xy",
"axis": {
"x": { "title": { "visible": false } },
"y": { "anchor": "start", "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" }]
}
]
}
{
"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"
}
}
{
"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"
}
}
{
"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" }]
}
{
"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": { "anchor": "start", "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": { "anchor": "start", "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": { "anchor": "start", "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": { "anchor": "start", "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" }
}
{
"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": {
"anchor": "start",
"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": {
"anchor": "start",
"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": {
"anchor": "start",
"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": {
"anchor": "start",
"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": {
"anchor": "start",
"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": {
"anchor": "start",
"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": {
"anchor": "start",
"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"
}
}
{
"title": "Requests Over Time",
"type": "xy",
"axis": {
"x": { "title": { "visible": false }, "scale": "temporal", "domain": { "type": "fit", "rounding": false } },
"y": { "anchor": "start", "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" }]
}
]
}
{
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM kibana_sample_data_logs | STATS avg_bytes = AVG(bytes)"
},
"metrics": [
{
"type": "primary",
"column": "avg_bytes"
}
]
}
{
"name": "kibana-dashboards",
"version": "1.0.0",
"type": "module",
"description": "CRUD operations for Kibana dashboards and visualizations",
"author": "elastic",
"license": "tbd",
"dependencies": {}
}
Chart Types Reference
Complete schema reference for each supported chart type via the Kibana dashboards & visualizations API.
Supported Chart Types:
metric— Single metric valuexy— Line, area, bar chartsgauge— Gauge visualizationheatmap— Heatmap chartstag_cloud— Tag/word clouddata_table— Data tablesregion_map— Region/choropleth mapspie,treemap,mosaic,waffle— Partition charts (usepiewithdonut_holefor 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:
{
"type": "metric",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT()"
},
"metrics": [
{
"type": "primary",
"column": "count"
}
]
}dataView:
{
"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\ |
column | string | ES\ | QL |
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:
{
"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 paneltitleempty ("") and
aliasing the column name in ES|QL with backticks (e.g. `STATSTotal 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
{
"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)
{
"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
{
"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
{
"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)
{
"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 barsbar_stacked— Stacked barsbar_percentage— Percentage barsbar_horizontal— Horizontal barsbar_horizontal_stacked— Horizontal stacked barsbar_horizontal_percentage— Horizontal percentage barsline— Line chartarea— Area chartarea_stacked— Stacked areaarea_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.
{
"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\ |
Heatmap
{
"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.
{
"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: "..." }.
{
"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:
{
"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 "donut_hole" set to "s", "m", or "l".
ES|QL Example:
{
"type": "pie",
"data_source": {
"type": "esql",
"query": "FROM logs | STATS count = COUNT() BY os"
},
"metrics": [{ "column": "count" }],
"group_by": [{ "column": "os" }]
}Region Map
{
"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
{
"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:
WHERE @timestamp <= ?_tend AND @timestamp > ?_tstart | STATS count = COUNT() BY BUCKET(@timestamp, 75, ?_tstart, ?_tend)"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.
"axis": {
"x": { "scale": "temporal", "domain": { "type": "fit", "rounding": false } }
}Hourly buckets:
STATS count = COUNT() BY bucket = DATE_TRUNC(1 hour, @timestamp)Daily buckets:
STATS count = COUNT() BY bucket = DATE_TRUNC(1 day, @timestamp)5-minute buckets:
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:
{ "operation": "average", "field": "bytes", "format": { "type": "bytes", "decimals": 0 } }ES|QL column example:
{ "column": "avg_bytes", "format": { "type": "bytes", "decimals": 0 } }Gauge metric example:
"metric": { "operation": "max", "field": "bytes", "format": { "type": "bytes", "decimals": 0 } }Filtering in ES|QL
FROM logs
| WHERE @timestamp > NOW() - 24 hours AND level == "error"
| STATS count = COUNT() BY hostDashboard 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
GET /api/dashboards/:id
Header: Elastic-Api-Version: 2023-10-31Response:
{
"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
POST /api/dashboards
Header: Elastic-Api-Version: 2023-10-31Request Body:
{
"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:
{
"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.
PUT /api/dashboards/:id
Header: Elastic-Api-Version: 2023-10-31Request Body:
{
"title": "Updated Title",
"panels": [ ... ],
"time_range": { "from": "now-7d", "to": "now" }
}Delete Dashboard
DELETE /api/dashboards/:id
Header: Elastic-Api-Version: 2023-10-31Response: Empty on success.
Space-Specific Endpoints
To operate on dashboards in a specific space, use the space-prefixed URL:
GET /s/{space-id}/api/dashboards/:id
POST /s/{space-id}/api/dashboards
PUT /s/{space-id}/api/dashboards/:id
DELETE /s/{space-id}/api/dashboards/:id
Header: Elastic-Api-Version: 2023-10-31Dashboard 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
{
"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
{
"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)
{
"type": "markdown",
"id": "unique-id",
"grid": { "x": 0, "y": 0, "w": 48, "h": 4 },
"config": {
"content": "## Markdown content here"
}
}vis (Visualization)
{
"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):
{
"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.{
"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.{
"type": "map",
"id": "unique-id",
"grid": { "x": 0, "y": 0, "w": 24, "h": 18 },
"config": {
"ref_id": "map-object-id"
}
}discover_session (Saved Search)
{
"type": "discover_session",
"id": "unique-id",
"grid": { "x": 0, "y": 0, "w": 48, "h": 15 },
"config": {
"ref_id": "saved-search-id"
}
}Example: Complete Dashboard
{
"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
Same Cluster, Different Space
# Get dashboard
node scripts/kibana-dashboards.js dashboard get my-dashboard > dashboard.json
# Edit dashboard.json: change id and spaces
# Then create in new space
node scripts/kibana-dashboards.js dashboard create dashboard.jsonDifferent Cluster
1. Get dashboard from source cluster 2. Submit to destination cluster's API
Note: Cannot move a dashboard with the same ID between spaces in the same cluster. Must delete from origin first, then create in destination.
Copy Dashboard Between Spaces/Clusters
# 1. Get dashboard from source
node scripts/kibana-dashboards.js dashboard get source-dashboard > dashboard.json
# 2. Edit dashboard.json to change id and/or spaces
# 3. Create on destination
node scripts/kibana-dashboards.js dashboard create dashboard.jsonCommon Issues
| Error | Solution |
|---|---|
| "401 Unauthorized" | Check KIBANA_USERNAME/PASSWORD or KIBANA_API_KEY |
| "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\ |
| ES\ | QL column reference |
| Metric chart structure | Metric chart requires metrics (plural) array: [{ type: 'primary', ... }] |
| Tagcloud bucketing | Tagcloud requires tag_by for the tag dimension |
| Datatable structure | ES\ |
| XY chart fails | Put data_source inside each layer (for both dataView and ES\ |
| Heatmap property names | Heatmap uses x, y, metric for axes and value |
| XY axis config | Use axis (singular); y with anchor: "start" for left 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:
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": { ... }
}Visualizations API Reference
The Visualizations API provides CRUD endpoints for managing standalone visualizations using dataView datasets.
ES|QL visualizations cannot be created or updated via this API. Use inline panels in the Dashboard API instead.
See Chart Types Reference for ES|QL panel schemas.
Endpoints
List Visualizations
GET /api/visualizations?query=&page=&per_page=
Header: Elastic-Api-Version: 2023-10-31| Parameter | Type | Description |
|---|---|---|
query | string | Search query |
searchFields | string | Fields to search (e.g., title) |
page | number | Page number (default: 1) |
per_page | number | Results per page (default: 100) |
Get Visualization
GET /api/visualizations/:id
Header: Elastic-Api-Version: 2023-10-31Create Visualization
POST /api/visualizations
Header: Elastic-Api-Version: 2023-10-31POST does not accept an id parameter. The API auto-generates one.
{
"type": "metric",
"data_source": {
"type": "data_view_reference",
"ref_id": "90943e30-9a47-11e8-b64d-95841ca0b247"
},
"metrics": [{ "type": "primary", "operation": "count" }]
}Update Visualization (Upsert)
PUT supports upsert — creates the visualization if it does not exist, or updates it if it does.
PUT /api/visualizations/:id
Header: Elastic-Api-Version: 2023-10-31Delete Visualization
DELETE /api/visualizations/:id
Header: Elastic-Api-Version: 2023-10-31Response Envelope
{
"id": "uuid",
"data": {
/* visualization definition */
},
"meta": {
"created_at": "ISO timestamp",
"updated_at": "ISO timestamp",
"created_by": "user_id",
"updated_by": "user_id",
"managed": false
}
}Search results wrap items in a data array with pagination in meta (page, per_page, total).
Common Properties
| Property | Type | Description |
|---|---|---|
type | string | Chart type (required) |
data_source | object | Data source configuration (required) |
sampling | number | Sampling rate 0-1 (default: 1) |
ignore_global_filters | boolean | Ignore dashboard filters (default: false) |
The API injects sensible defaults for omitted properties — clients can send minimal payloads.
#!/usr/bin/env node
/**
* CRUD operations for Kibana Dashboards and Visualizations using the API.
*
* Usage:
* ./kibana-dashboards.js dashboard get <id> - Get dashboard definition
* ./kibana-dashboards.js dashboard create <file|-> - Create dashboard
* ./kibana-dashboards.js dashboard update <id> <file|-> - Update dashboard
* ./kibana-dashboards.js dashboard upsert <id> <file|-> - Create or update dashboard
* ./kibana-dashboards.js dashboard delete <id> - Delete dashboard
*
* ./kibana-dashboards.js vis list [search] - List Visualizations
* ./kibana-dashboards.js vis get <id> - Get Visualization
* ./kibana-dashboards.js vis create <file|-> - Create Visualization
* ./kibana-dashboards.js vis update <id> <file|-> - Update Visualization
* ./kibana-dashboards.js vis upsert <id> <file|-> - Create or update Visualization
* ./kibana-dashboards.js vis delete <id> - Delete Visualization
*
* ./kibana-dashboards.js test - Test Kibana connection
*/
// =============================================================================
// Stdin Reading
// =============================================================================
async function readStdin() {
return new Promise((resolve, reject) => {
let data = "";
process.stdin.setEncoding("utf8");
if (process.stdin.isTTY) {
reject(new Error("No input provided via stdin. Use a file path or pipe JSON input."));
return;
}
process.stdin.on("readable", () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
data += chunk;
}
});
process.stdin.on("end", () => {
resolve(data);
});
process.stdin.on("error", (err) => {
reject(err);
});
});
}
// =============================================================================
// File System Helpers
// =============================================================================
import { readFileSync, existsSync } from "fs";
async function loadSpec(filePathOrStdin) {
let content;
if (filePathOrStdin === "-" || filePathOrStdin === "--stdin") {
content = await readStdin();
} else {
if (!existsSync(filePathOrStdin)) {
throw new Error(`File not found: ${filePathOrStdin}`);
}
content = readFileSync(filePathOrStdin, "utf-8");
}
return JSON.parse(content);
}
// =============================================================================
// Kibana Client Setup
// =============================================================================
function getKibanaConfig() {
const cloudId = process.env.KIBANA_CLOUD_ID || process.env.ELASTICSEARCH_CLOUD_ID;
let url = process.env.KIBANA_URL;
if (!url && cloudId) {
try {
const parts = cloudId.split(":");
if (parts.length === 2) {
const decoded = Buffer.from(parts[1], "base64").toString("utf8");
const decodedParts = decoded.split("$");
if (decodedParts.length >= 3 && decodedParts[2]) {
const domain = decodedParts[0];
const kibanaUuid = decodedParts[2];
let host = domain;
let port = "";
if (domain.includes(":")) {
const splitDomain = domain.split(":");
host = splitDomain[0];
port = `:${splitDomain[1]}`;
} else {
port = ":443";
}
url = `https://${kibanaUuid}.${host}${port}`;
}
}
} catch (e) {
console.error("Error parsing Cloud ID:", e.message);
}
}
const username = process.env.KIBANA_USERNAME || process.env.ELASTICSEARCH_USERNAME;
const password = process.env.KIBANA_PASSWORD || process.env.ELASTICSEARCH_PASSWORD;
const apiKey = process.env.KIBANA_API_KEY || process.env.ELASTICSEARCH_API_KEY;
const spaceId = process.env.KIBANA_SPACE_ID;
const insecure = process.env.KIBANA_INSECURE === "true";
if (!url) {
console.error("Error: No Kibana connection configured.");
console.error("");
console.error("Set one of these environment variable combinations:");
console.error(" 1. Elastic Cloud: KIBANA_CLOUD_ID + KIBANA_API_KEY");
console.error(" 2. URL + API Key: KIBANA_URL + KIBANA_API_KEY");
console.error(" 3. Basic Auth: KIBANA_URL + KIBANA_USERNAME + KIBANA_PASSWORD");
console.error("");
console.error("For local development, use start-local to run Elasticsearch and Kibana via Docker:");
console.error(" https://github.com/elastic/start-local");
console.error("");
console.error(" curl -fsSL https://elastic.co/start-local | sh");
console.error("");
console.error("Then configure the environment:");
console.error(" source elastic-start-local/.env");
console.error(' export KIBANA_URL="$KB_LOCAL_URL"');
console.error(' export KIBANA_USERNAME="elastic"');
console.error(' export KIBANA_PASSWORD="$ES_LOCAL_PASSWORD"');
process.exit(1);
}
return { url, username, password, apiKey, spaceId, insecure };
}
function getHeaders(config) {
const headers = {
"Content-Type": "application/json",
"kbn-xsrf": "true",
"x-elastic-internal-origin": "kibana",
"User-Agent": "elastic-agentic",
};
if (config.apiKey) {
headers["Authorization"] = `ApiKey ${config.apiKey}`;
} else if (config.username && config.password) {
const auth = Buffer.from(`${config.username}:${config.password}`).toString("base64");
headers["Authorization"] = `Basic ${auth}`;
}
return headers;
}
function getBasePath(config) {
let basePath = config.url.replace(/\/$/, "");
if (config.spaceId && config.spaceId !== "default") {
basePath += `/s/${config.spaceId}`;
}
return basePath;
}
async function kibanaFetch(path, options = {}) {
const config = getKibanaConfig();
const basePath = getBasePath(config);
const url = `${basePath}${path}`;
const fetchOptions = {
...options,
headers: {
...getHeaders(config),
...options.headers,
},
};
// Handle insecure TLS
if (config.insecure && typeof process !== "undefined") {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
}
try {
const response = await fetch(url, fetchOptions);
const contentType = response.headers.get("content-type");
let data;
if (contentType && contentType.includes("application/json")) {
data = await response.json();
} else {
data = await response.text();
}
if (!response.ok) {
return {
success: false,
status: response.status,
error: data.message || data.error || `HTTP ${response.status}`,
details: data,
};
}
return { success: true, data };
} catch (error) {
return {
success: false,
error: error.message,
details: error,
};
}
}
// =============================================================================
// Dashboards API
// =============================================================================
/**
* Get a dashboard by ID
* GET /api/dashboards/:id (with version header)
*/
async function getDashboard(id) {
return kibanaFetch(`/api/dashboards/${encodeURIComponent(id)}`, {
headers: { "Elastic-Api-Version": "2023-10-31" },
});
}
/**
* Create a dashboard
* POST /api/dashboards (with version header)
* Body: { title, panels, time_range?, ... }
*
* Accepts both formats:
* - Flat: { title, panels, ... }
* - Wrapped (legacy): { id?, data: { title, panels, ... }, spaces? }
* The API expects the flat format; this function unwraps if needed.
* POST does not accept an id parameter. Use PUT for upserts.
*/
async function createDashboard(definition) {
const body = definition.data
? definition.data
: (() => {
const { id, spaces, ...rest } = definition;
return rest;
})();
return kibanaFetch("/api/dashboards", {
method: "POST",
headers: { "Elastic-Api-Version": "2023-10-31" },
body: JSON.stringify(body),
});
}
/**
* Update a dashboard
* PUT /api/dashboards/:id (with version header)
* Body: { title, panels, ... } - do NOT include id or spaces
*
* Accepts both formats:
* - Flat: { title, panels, ... }
* - Wrapped (legacy): { data: { title, panels, ... } }
*/
async function updateDashboard(id, definition) {
const body = definition.data
? definition.data
: (() => {
const { id: _id, spaces, ...rest } = definition;
return rest;
})();
return kibanaFetch(`/api/dashboards/${encodeURIComponent(id)}`, {
method: "PUT",
headers: { "Elastic-Api-Version": "2023-10-31" },
body: JSON.stringify(body),
});
}
/**
* Delete a dashboard
* DELETE /api/dashboards/:id (with version header)
*/
async function deleteDashboard(id) {
return kibanaFetch(`/api/dashboards/${encodeURIComponent(id)}`, {
method: "DELETE",
headers: { "Elastic-Api-Version": "2023-10-31" },
});
}
// =============================================================================
// Visualizations API
// =============================================================================
/**
* List Visualizations
* GET /api/visualizations?query=&page=&per_page=
*/
async function listVisualizations(query = "", page = 1, per_page = 100) {
const params = new URLSearchParams({ page: String(page), per_page: String(per_page) });
if (query) {
params.set("query", query);
}
return kibanaFetch(`/api/visualizations?${params.toString()}`, {
headers: { "Elastic-Api-Version": "2023-10-31" },
});
}
/**
* Get a Visualization by ID
* GET /api/visualizations/:id
*/
async function getVisualization(id) {
return kibanaFetch(`/api/visualizations/${id}`, {
headers: { "Elastic-Api-Version": "2023-10-31" },
});
}
/**
* Create a Visualization
* POST /api/visualizations
* Body: visualization definition (without id)
*/
async function createVisualization(definition) {
return kibanaFetch("/api/visualizations", {
method: "POST",
headers: { "Elastic-Api-Version": "2023-10-31" },
body: JSON.stringify(definition),
});
}
/**
* Update a Visualization
* PUT /api/visualizations/:id
* Body: visualization definition
*/
async function updateVisualization(id, definition) {
return kibanaFetch(`/api/visualizations/${id}`, {
method: "PUT",
headers: { "Elastic-Api-Version": "2023-10-31" },
body: JSON.stringify(definition),
});
}
/**
* Delete a Visualization
* DELETE /api/visualizations/:id
*/
async function deleteVisualization(id) {
return kibanaFetch(`/api/visualizations/${id}`, {
method: "DELETE",
headers: { "Elastic-Api-Version": "2023-10-31" },
});
}
// =============================================================================
// Test Connection
// =============================================================================
function parseVersion(versionString) {
if (!versionString) return { major: 0, minor: 0, patch: 0, snapshot: false, raw: "unknown" };
const clean = versionString.replace(/-SNAPSHOT.*$/, "");
const [major, minor, patch] = clean.split(".").map(Number);
return { major, minor, patch: patch || 0, snapshot: versionString.includes("-SNAPSHOT"), raw: versionString };
}
async function testConnection() {
const result = await kibanaFetch("/api/status");
if (result.success) {
const status = result.data;
const versionString = status.version?.number || "unknown";
const buildFlavor = status.version?.build_flavor || "default";
// Serverless Kibana can be identified by build_flavor or a non-semver version string
const isSemver = /^\d+\.\d+\.\d+/.test(versionString);
const isServerless = buildFlavor === "serverless" || (!isSemver && versionString !== "unknown");
const parsed = isSemver ? parseVersion(versionString) : parseVersion("8.11.0");
return {
success: true,
version: isServerless && !isSemver ? `${versionString} (Serverless)` : versionString,
parsed,
buildFlavor: isServerless ? "serverless" : buildFlavor,
isServerless,
status: status.status?.overall?.level || "unknown",
name: status.name || "unknown",
};
}
return result;
}
// =============================================================================
// Output Formatting
// =============================================================================
function formatDashboard(item) {
const lines = [];
lines.push("=== Dashboard ===");
lines.push(`ID: ${item.id}`);
if (item.spaces && item.spaces.length > 0) {
lines.push(`Spaces: ${item.spaces.join(", ")}`);
}
if (item.meta) {
lines.push(`Created: ${item.meta.created_at || "unknown"}`);
lines.push(`Updated: ${item.meta.updated_at || "unknown"}`);
lines.push(`Managed: ${item.meta.managed || false}`);
}
if (item.data) {
lines.push(`Title: ${item.data.title || "Untitled"}`);
lines.push(`Panels: ${item.data.panels?.length || 0}`);
if (item.data.time_range) {
lines.push(`Time Range: ${item.data.time_range.from} to ${item.data.time_range.to}`);
}
}
lines.push("\n--- Definition (data) ---");
lines.push(JSON.stringify(item.data, null, 2));
return lines.join("\n");
}
function formatVisList(response) {
const items = response.data || [];
const meta = response.meta || {};
if (!items || items.length === 0) {
return "No Visualizations found.";
}
const lines = ["ID".padEnd(40) + " | " + "Type".padEnd(15) + " | " + "Title"];
lines.push("-".repeat(40) + "-+-" + "-".repeat(15) + "-+-" + "-".repeat(40));
for (const item of items) {
const id = item.id || "unknown";
const type = item.data?.type || "unknown";
const title = item.data?.title || "Untitled";
lines.push(`${id.substring(0, 40).padEnd(40)} | ${type.padEnd(15)} | ${title}`);
}
lines.push("");
lines.push(
`Page ${meta.page || 1} | Per Page: ${meta.per_page || items.length} | Total: ${meta.total || items.length}`,
);
return lines.join("\n");
}
function formatVisualization(item) {
const lines = [];
lines.push("=== Visualization ===");
lines.push(`ID: ${item.id}`);
if (item.meta) {
lines.push(`Created: ${item.meta.created_at || "unknown"}`);
lines.push(`Updated: ${item.meta.updated_at || "unknown"}`);
lines.push(`Managed: ${item.meta.managed || false}`);
}
lines.push("\n--- Definition (data) ---");
lines.push(JSON.stringify(item.data, null, 2));
return lines.join("\n");
}
// =============================================================================
// Main CLI
// =============================================================================
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === "help" || args[0] === "--help" || args[0] === "-h") {
printUsage();
process.exit(args.length === 0 ? 1 : 0);
}
const [resource, action, ...params] = args;
try {
switch (resource) {
case "test":
await handleTest();
break;
case "dashboard":
case "dashboards":
case "dash":
await handleDashboard(action, params);
break;
case "vis":
await handleVis(action, params);
break;
default:
console.error(`Unknown resource: ${resource}`);
printUsage();
process.exit(1);
}
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
}
}
async function handleTest() {
console.log("=== Testing Kibana Connection ===\n");
const result = await testConnection();
if (result.success) {
const { parsed, isServerless, buildFlavor } = result;
const { major, minor } = parsed;
console.log("✓ Connected successfully!");
console.log(` Name: ${result.name}`);
console.log(` Version: ${result.version}`);
console.log(` Build flavor: ${buildFlavor}`);
if (parsed.snapshot) console.log(` Snapshot: yes (treating as ${major}.${minor})`);
if (isServerless) console.log(" NOTE: Serverless — features available regardless of reported version");
console.log(` Status: ${result.status}`);
// Check if Dashboards API is available
console.log("\n=== Dashboards API Check ===");
// Try to get a non-existent dashboard - 404 means API is available
const dashResult = await getDashboard("__test_nonexistent__");
if (dashResult.success || dashResult.status === 404) {
console.log("✓ Dashboards API is available");
} else if (dashResult.status === 400) {
console.log("⚠ Dashboards API may not be available in this version");
} else {
console.log("✗ Dashboards API check failed:", dashResult.error);
}
// Check if Visualizations API is available
console.log("\n=== Visualizations API Check ===");
const visResult = await listVisualizations("", 1, 1);
if (visResult.success) {
console.log("✓ Visualizations API is available");
} else {
console.log("✗ Visualizations API check failed:", visResult.error);
}
} else {
console.error("✗ Connection failed:", result.error);
if (result.details) {
console.error("Details:", JSON.stringify(result.details, null, 2));
}
process.exit(1);
}
}
async function handleDashboard(action, params) {
switch (action) {
case "get": {
const id = params[0];
if (!id) {
console.error("Error: Dashboard ID required");
console.error("Usage: ./kibana-dashboards.js dashboard get <id>");
process.exit(1);
}
const result = await getDashboard(id);
if (result.success) {
console.log(formatDashboard(result.data));
} else {
console.error("Error:", result.error);
process.exit(1);
}
break;
}
case "create": {
const file = params[0];
if (!file) {
console.error("Error: Definition file/stdin required");
console.error("Usage: ./kibana-dashboards.js dashboard create <file.json>");
console.error(
' echo \'{"title":"My Dashboard","panels":[]}\' | ./kibana-dashboards.js dashboard create -',
);
process.exit(1);
}
const definition = await loadSpec(file);
const result = await createDashboard(definition);
if (result.success) {
console.log("✓ Dashboard created successfully!");
console.log(` ID: ${result.data.id}`);
console.log(` Title: ${result.data.data?.title || "Untitled"}`);
console.log(` Panels: ${result.data.data?.panels?.length || 0}`);
if (result.data.spaces) {
console.log(` Spaces: ${result.data.spaces.join(", ")}`);
}
} else {
console.error("Error:", result.error);
if (result.details) {
console.error("Details:", JSON.stringify(result.details, null, 2));
}
process.exit(1);
}
break;
}
case "update": {
const id = params[0];
const file = params[1];
if (!id || !file) {
console.error("Error: Dashboard ID and definition file/stdin required");
console.error("Usage: ./kibana-dashboards.js dashboard update <id> <file.json>");
console.error(
' echo \'{"title":"Updated","panels":[...]}\' | ./kibana-dashboards.js dashboard update <id> -',
);
process.exit(1);
}
const definition = await loadSpec(file);
const result = await updateDashboard(id, definition);
if (result.success) {
console.log("✓ Dashboard updated successfully!");
console.log(` ID: ${result.data.id}`);
console.log(` Title: ${result.data.data?.title || "Untitled"}`);
} else {
console.error("Error:", result.error);
if (result.details) {
console.error("Details:", JSON.stringify(result.details, null, 2));
}
process.exit(1);
}
break;
}
case "upsert": {
const id = params[0];
const file = params[1];
if (!id || !file) {
console.error("Error: Dashboard ID and definition file/stdin required");
console.error("Usage: ./kibana-dashboards.js dashboard upsert <id> <file.json>");
process.exit(1);
}
const definition = await loadSpec(file);
const result = await updateDashboard(id, definition);
if (result.success) {
console.log("✓ Dashboard upserted successfully!");
console.log(` ID: ${result.data.id}`);
console.log(` Title: ${result.data.data?.title || "Untitled"}`);
} else {
console.error("Error:", result.error);
if (result.details) {
console.error("Details:", JSON.stringify(result.details, null, 2));
}
process.exit(1);
}
break;
}
case "delete":
case "rm": {
const id = params[0];
if (!id) {
console.error("Error: Dashboard ID required");
console.error("Usage: ./kibana-dashboards.js dashboard delete <id>");
process.exit(1);
}
const result = await deleteDashboard(id);
if (result.success) {
console.log("✓ Dashboard deleted successfully!");
} else {
console.error("Error:", result.error);
process.exit(1);
}
break;
}
default:
console.error(`Unknown dashboard action: ${action}`);
console.error("Available actions: get, create, update, upsert, delete");
process.exit(1);
}
}
async function handleVis(action, params) {
switch (action) {
case "list":
case "ls": {
const query = params[0] || "";
const result = await listVisualizations(query);
if (result.success) {
console.log("=== Visualizations ===\n");
console.log(formatVisList(result.data));
} else {
console.error("Error:", result.error);
process.exit(1);
}
break;
}
case "get": {
const id = params[0];
if (!id) {
console.error("Error: Visualization ID required");
console.error("Usage: ./kibana-dashboards.js vis get <id>");
process.exit(1);
}
const result = await getVisualization(id);
if (result.success) {
console.log(formatVisualization(result.data));
} else {
console.error("Error:", result.error);
process.exit(1);
}
break;
}
case "create": {
const file = params[0];
if (!file) {
console.error("Error: Definition file/stdin required");
console.error("Usage: ./kibana-dashboards.js vis create <file.json>");
console.error(' echo \'{"type":"metric",...}\' | ./kibana-dashboards.js vis create -');
process.exit(1);
}
const definition = await loadSpec(file);
const result = await createVisualization(definition);
if (result.success) {
console.log("✓ Visualization created successfully!");
console.log(` ID: ${result.data.id}`);
console.log(` Type: ${result.data.data?.type || "unknown"}`);
} else {
console.error("Error:", result.error);
if (result.details) {
console.error("Details:", JSON.stringify(result.details, null, 2));
}
process.exit(1);
}
break;
}
case "update": {
const id = params[0];
const file = params[1];
if (!id || !file) {
console.error("Error: Visualization ID and definition file/stdin required");
console.error("Usage: ./kibana-dashboards.js vis update <id> <file.json>");
console.error(' echo \'{"type":"metric",...}\' | ./kibana-dashboards.js vis update <id> -');
process.exit(1);
}
const definition = await loadSpec(file);
const result = await updateVisualization(id, definition);
if (result.success) {
console.log("✓ Visualization updated successfully!");
console.log(` ID: ${result.data.id}`);
} else {
console.error("Error:", result.error);
if (result.details) {
console.error("Details:", JSON.stringify(result.details, null, 2));
}
process.exit(1);
}
break;
}
case "upsert": {
const id = params[0];
const file = params[1];
if (!id || !file) {
console.error("Error: Visualization ID and definition file/stdin required");
console.error("Usage: ./kibana-dashboards.js vis upsert <id> <file.json>");
process.exit(1);
}
const definition = await loadSpec(file);
const result = await updateVisualization(id, definition);
if (result.success) {
console.log("✓ Visualization upserted successfully!");
console.log(` ID: ${result.data.id}`);
} else {
console.error("Error:", result.error);
if (result.details) {
console.error("Details:", JSON.stringify(result.details, null, 2));
}
process.exit(1);
}
break;
}
case "delete":
case "rm": {
const id = params[0];
if (!id) {
console.error("Error: Visualization ID required");
console.error("Usage: ./kibana-dashboards.js vis delete <id>");
process.exit(1);
}
const result = await deleteVisualization(id);
if (result.success) {
console.log("✓ Visualization deleted successfully!");
} else {
console.error("Error:", result.error);
process.exit(1);
}
break;
}
default:
console.error(`Unknown vis action: ${action}`);
console.error("Available actions: list, get, create, update, upsert, delete");
process.exit(1);
}
}
function printUsage() {
console.log(`
Kibana as Code - Dashboards & Visualizations API
Usage:
./kibana-dashboards.js <resource> <action> [options]
Resources:
dashboard Manage dashboards via API
vis Manage visualizations via API
test Test Kibana connection and API availability
Dashboard Actions:
get <id> Get dashboard definition
create <file|-> Create from JSON file (use - for stdin)
update <id> <file|-> Update from JSON file (use - for stdin)
upsert <id> <file|-> Create or update (use - for stdin)
delete <id> Delete dashboard
Visualization Actions:
list [search] List Visualizations (optional search)
get <id> Get visualization definition
create <file|-> Create from JSON file (use - for stdin)
update <id> <file|-> Update from JSON file (use - for stdin)
upsert <id> <file|-> Create or update (use - for stdin)
delete <id> Delete visualization
Environment Variables:
KIBANA_CLOUD_ID Elastic Cloud deployment ID (if KIBANA_URL is not set)
KIBANA_URL Kibana URL (required if KIBANA_CLOUD_ID is not set)
KIBANA_USERNAME Username for basic auth
KIBANA_PASSWORD Password for basic auth
KIBANA_API_KEY API key for authentication
KIBANA_SPACE_ID Kibana space ID (optional)
KIBANA_INSECURE Set to "true" to skip TLS verification
Dashboard Panel Types:
vis Visualization panel
markdown Markdown panel
links Links panel
map Maps panel
discover_session Saved search panel
(and more embeddable types)
Chart Types:
metric, xy, gauge, heatmap, tag_cloud,
region_map, data_table, pie, treemap, mosaic, waffle
Examples:
# Test connection and API availability
./kibana-dashboards.js test
# Get a dashboard definition
./kibana-dashboards.js dashboard get my-dashboard-id
# Create a dashboard from file
./kibana-dashboards.js dashboard create ./my-dashboard.json
# Create a dashboard from stdin
echo '{"title":"Test","panels":[]}' | \\
./kibana-dashboards.js dashboard create -
# Update a dashboard (do not include id/spaces in body)
echo '{"title":"Updated Title","panels":[]}' | \\
./kibana-dashboards.js dashboard update my-dashboard-id -
# Delete a dashboard
./kibana-dashboards.js dashboard delete my-dashboard-id
# List all Visualizations
./kibana-dashboards.js vis list
# Create metric visualization from stdin
echo '{"type":"metric","data_source":{"type":"esql","query":"FROM logs | STATS count=COUNT()"},"metrics":[{"type":"primary","column":"count"}]}' | \\
./kibana-dashboards.js vis create -
# Copy dashboard: get from source, create on destination
./kibana-dashboards.js dashboard get source-id > dashboard.json
# Edit dashboard.json as needed, then create
./kibana-dashboards.js dashboard create dashboard.json
`);
}
main().catch((error) => {
console.error("Fatal error:", error.message);
process.exit(1);
});