Querying Elasticsearch#
The elasticsearch.query action runs an ES|QL query against your cluster, and returns the resulting rows to the automation or script that called it. Use it to bring data back into Home Assistant: the average of a sensor over the last hour, the last time something happened, or data that never passed through Home Assistant at all.
ES|QL is the only query language the action supports. It doesn't need anything else to be set up, and works with any loaded Elasticsearch entry.
Requirements#
-
Elasticsearch 8.19 or later, or 9.1 or later on 9.x. Elastic Cloud Serverless is supported too. Publishing still works from 8.14, but older versions can't be queried:
- Before 8.18, ES|QL can't filter on or compare the
date_nanostimestamps the integration publishes, so most queries of Home Assistant data fail. - Before 8.19, and in 9.0, Elasticsearch can't be told to refuse partial results. The integration never accepts a partial result as complete.
On an older cluster, the action fails with
Querying requires Elasticsearch 8.19 or later, or 9.1 or later on 9.x, and nothing is sent to the cluster. - Before 8.18, ES|QL can't filter on or compare the
-
The
readprivilege on every index the query reads. Thehass_writerrole can only publish, so on its own it can't run queries, not even of the data the integration published. Add a second entry to the role'sindices:{ "names": ["metrics-homeassistant.*"], "privileges": ["read"] }Add more names to query other indices or data streams. For a user, add this to its role. For an API key, update the key's role descriptors, or create a new key with both entries and enter it when you reauthenticate. No cluster privileges are needed beyond the ones publishing already uses.
Action fields#
| Field | Required | Description |
|---|---|---|
config_entry_id |
Yes | The Elasticsearch entry to query. In the UI, choose it from the list. In YAML, find the ID in the entry's URL on the integration page. |
query |
Yes | The ES |
params |
No | A list of values to bind to the query's ? placeholders, in order. Each must be a string, number, boolean, or null. |
Always pass values with params, rather than templating them into the query. Bound values can't change the query's meaning, whatever they contain.
The action only returns data, so it must be called with response_variable.
Response#
The response has the query's columns, and one object per row, keyed by column name:
{
"columns": [
{"name": "@timestamp", "type": "date_nanos"},
{"name": "hass.entity.valueas.float", "type": "double"}
],
"rows": [
{"@timestamp": "2026-11-07T22:06:42.875933123Z", "hass.entity.valueas.float": 21.5}
],
"warnings": [],
"took_ms": 12
}
| Key | Description |
|---|---|
columns |
Each column's name and ES |
rows |
Each row, as an object with a value for every column. |
warnings |
The warnings Elasticsearch sent with the result, or an empty list. |
took_ms |
How long Elasticsearch says the query took, in milliseconds. null if it didn't say. |
- Values are kept as Elasticsearch returns them. Numbers and booleans stay numbers and booleans, and dates are ISO 8601 strings. A field with no value is
null, and a field with several values is a list. - A query that matches nothing isn't an error.
rowsis an empty list, andcolumnsstill describes the result. - Check
warningswhen a value is unexpectedlynull. When ES|QL can't evaluate an expression for a row, for exampleTO_DOUBLE("abc"), it returnsnullfor that row and adds a warning, rather than failing the query. - Queries without a
LIMITalways warnNo limit defined, adding default limit of [1000], even when they summarize withSTATS. It's harmless when the query returns fewer rows than that. - Fields ES|QL can't read have the type
unsupported, and their values are alwaysnull. - Each column name must be unique. ES|QL can return two columns with the same name, which can't both be keys of a row, so the action fails instead. Rename one with
RENAMEorEVAL.
Examples#
Latest reading of an entity#
Runs the query, then uses the latest published value of sensor.outdoor_temperature. Replace YOUR_ENTRY_ID with your entry's ID.
- action: elasticsearch.query
data:
config_entry_id: YOUR_ENTRY_ID
query: |
FROM metrics-homeassistant.*
| WHERE hass.entity.id == ?
| SORT @timestamp DESC
| LIMIT 1
| KEEP @timestamp, hass.entity.valueas.float
params:
- sensor.outdoor_temperature
response_variable: latest
- if: "{{ latest.rows | count > 0 }}"
then:
- action: persistent_notification.create
data:
message: >-
The outdoor temperature was
{{ latest.rows[0]['hass.entity.valueas.float'] }}
at {{ latest.rows[0]['@timestamp'] }}.
Column names with dots or @ must be read with ['...'], as above. To use simpler names, rename the columns in the query, for example | RENAME hass.entity.valueas.float AS temperature.
Average over a time window#
A query that summarizes with STATS returns one row, even when nothing matches.
- action: elasticsearch.query
data:
config_entry_id: YOUR_ENTRY_ID
query: |
FROM metrics-homeassistant.sensor-default
| WHERE hass.entity.id == ? AND @timestamp >= NOW() - 1 hour
| STATS average_temperature = AVG(hass.entity.valueas.float)
params:
- sensor.outdoor_temperature
response_variable: weather
- action: persistent_notification.create
data:
message: >-
{% set average = weather.rows[0].average_temperature %}
{% if average is none %}
No readings in the last hour.
{% else %}
The average outdoor temperature over the last hour was {{ average | round(1) }}.
{% endif %}
Values from templates#
params can be templated. Here, a script queries whichever entity it was called with:
alias: Count state changes today
fields:
entity:
selector:
entity:
sequence:
- action: elasticsearch.query
data:
config_entry_id: YOUR_ENTRY_ID
query: |
FROM metrics-homeassistant.*
| WHERE hass.entity.id == ?
AND event.action == "State change"
AND @timestamp >= NOW() - 1 day
| STATS changes = COUNT(*)
params:
- "{{ entity }}"
response_variable: result
- stop: Done
response_variable: result
Limits#
Each query is bounded, and a result that goes past a limit is an error rather than an incomplete result.
| Limit | Value | What happens |
|---|---|---|
| Rows | 1,000 | More rows fail with The query returned … rows, more than the limit of 1000. Add a LIMIT, or summarize with STATS. |
| Elasticsearch's default limit | Usually 1,000 | Without a LIMIT, Elasticsearch stops at its default and says so in a warning. If the result reaches that default, it may be incomplete, so the action fails. Add an explicit LIMIT. |
| Result size | 1 MiB of JSON | A larger result fails, even within the row limit. Return fewer rows or columns, for example with KEEP. |
| Time | The entry's request timeout, 30 seconds by default | A slower query fails with Elasticsearch didn't answer the query within 30 seconds. |
| Queries at once | 4 per entry | Further queries wait for one to finish, so queries can't starve publishing. Unloading or reloading the entry cancels running and waiting queries. |
The size limit is checked once the response has arrived, so it bounds what reaches your automations, not what Elasticsearch sends. Elasticsearch has its own limits too: by default, it never returns more than 10,000 rows, however high the LIMIT.
There's no pagination. To work with more data, aggregate it in the query.
Errors#
The action fails with an error that says what went wrong. Errors caused by the query or its result are shown as validation errors.
| Error | What to do |
|---|---|
Elasticsearch rejected the query (…): … |
The query is invalid, or uses something the cluster doesn't support. The message includes Elasticsearch's reason, such as Unknown index [weather-*] or a parsing error, cut to 500 characters. |
Querying requires Elasticsearch 8.19 or later, … |
Upgrade the cluster. |
The entry's credentials aren't allowed to run this query. |
Grant the read privilege on the indices the query reads. |
Elasticsearch rejected the credentials, or they lack the required privileges. |
The credentials no longer work. Home Assistant asks you to reauthenticate. |
The query returns more than one column named … |
Give each column a unique name. |
The query returned … rows, … may be incomplete, or The query's result is … |
The result went past a limit. |
Elasticsearch could only return partial results for the query |
Part of the data couldn't be read, for example because a shard is unavailable. Check the cluster's health. |
Elasticsearch didn't answer the query within … seconds. |
Make the query cheaper, for example by narrowing its time range. |
The query was cancelled because its Elasticsearch entry was unloaded or reloaded. |
The entry was reloaded, for example after changing its options, while the query was running or waiting its turn. Run it again once the entry has loaded. |
The query couldn't be run: … |
The cluster couldn't be reached, or failed. The message describes the problem. |
| The config entry wasn't found, or isn't loaded | Check the entry ID, and that the entry is loaded on the integration page. |
To stop an automation failing when a query does, set continue_on_error: true on the action.
Privacy#
- The integration never logs query text, parameter values, or results.
- An error from Elasticsearch can quote part of the query. It's shown to whoever ran the action, and Home Assistant logs errors raised by automations and scripts.
- Like any response variable, the result is stored in the automation's or script's traces, along with the query and its parameters.
Known limitations#
- ES|QL depends on the cluster's version. Functions and commands added in later versions of ES|QL fail on older clusters with an
Elasticsearch rejected the queryerror. Check the ES|QL documentation for the version each one needs. - Only positional parameters are supported. Named parameters such as
?namearen't. - The query runs as the entry's credentials. Anyone who can run actions in Home Assistant can read whatever those credentials can.