LLM tool Entity Index:
Here’s my script to help the LLM assistant to find entities.
Sound like something that should work from the beginning, but with more exposed entities and more complex questions they start to struggle.
See my post here and Nathans answer below:
So, I built a system to find entities by tags, like Nathan described in his Grand Index post in the Fridays party thread.
(I made my own, as my setup isn’t as complex as Nathan’s and I think it won’t be for a long time. So I sticked with a simple script for grabbing entities for the beginning of my learning path.)
I added different labels to my exposed entities, which isn’t that hard work as it sounds, as you can filter the entity list by room, device type, … in Home Assistant these days.
You can also multi-select entities there and assign labels to the selected entities right from this view.
Pretty amazing work HA devs. ![]()
My labels are currently:
- Inside / outside the house or everywhere
- rooms
- floors
- device type (like temp-sensor, window-sensor, lights, …)
The LLM assist gets this list of possible tags described in the tool description (so you have to adjust this for your setup).
As the LLM often didn’t respect inside / outside the house, I made this a seperate parameter to the script, so it really has to “think” about that, instead of simply forgetting to include the correct tag (and grabbing outside entities for question about inside the house).
The script then adds the inside or outside tag based on the parameter value to the tag list used for the entity search.
I also used Nathan’s tip to provide good error texts or hints directly in the response.
Sometimes the LLM assistant makes up own tags that don’t exist (even that I told it to NOT do it in the description
)
It receives an error if a tag isn’t correct and a list which tags are possible.
You can often see in the debug view, that it begins with a wrong tag and then starts a second tool call with the correct values. ![]()
(So, the lesson learned is: Read the Friday’s party thread, there are countless gems in to find.
)
With this tool, the LLM assistant never failed for me anymore, to find things like the windows in the attic floor or to sum up their state.
A script like this is REALLY an important piece to a good user experience.
This is a simple script (no intent_script) which has to be exposed to assist.
alias: Entity Index
description: >-
LLM-accessible index of Home-Assistant entities.
Supported operations:
- “get entities by tag”: Collects entities matching ALL provided tags, optionally filtered by state.
- Required:
- location
- Optional:
- tags
- details
- state
Output on success (with 'details' parameter off):
- entities = array of entity_id strings
- count = number of returned entities
Output on success (with 'details' parameter on):
- entities = {
entity_id_1 : {
friendly_name: string,
state: string,
unit: string,
device_descriptions: array of strings,
tags: array of strings,
# MediaPlayer extras (only present for media_player.* entities):
is_master: boolean,
master: { entity_id: string | null, name: string | null },
members_of_master_player: [ { entity_id: string, name: string } ]
}, ...
}
- count = number of returned entities
Output on error:
error = string
Call the tool ONLY with the tags below! DO NOT MAKE UP YOUR OWN! Check the
available tags in this description first. Then think about which tags match
your current task and need to be used. Use them EXTACTLY as provided here!
Tags list:
-------------------
- "Basement"
- "GroundFloor"
- "UpperFloor"
- "AtticFloor"
- "Stairway"
- "Bath"
- "GuestBath"
- "HallWay"
- "LivingRoom"
- "Kitchen"
- "WC"
- "Bedroom"
- "Child1"
- "Child2"
- "HobbyRoom"
- "Study"
- "Garden"
- "Driveway"
- "TemperatureSensor"
- "Light"
- "WindowSensor"
- "MediaPlayer"
Possible State Values:
-------------------
- Light can be "on" / "off"
- WindowSensor can be "on" (means opened) / "off" (means closed)
- TemperatureSensor doesn't have a state that can be filtered
- MediaPlayer doesn't have a state that can be filtered
Hints:
--------------
- Really stick with the tags provided to you and write them exactly that way.
Other strings won't work as tags and the tool will return an error.
- "Garden" and "Driveway" are Outside areas. You need to set the location
parameter to "Outside" for finding entities there.
Examples:
-----------------
- Tags: "LivingRoom, Light", State: "on" → Switched-on lamps in the
living room
- Tags: "WindowSensor", State: "on" → All open windows
- Tags: "GroundFloor, WindowSensor", State: "on" → Open windows in the
ground floor
- Tags: "TemperatureSensor" → All temperatures in the house
- Tags: "TemperatureSensor, Kitchen" → Temperature of the kitchen
- Tags: "Inside, WindowSensor" → All windows of the house
fields:
operation:
description: The chosen operation
example: get entities by tag
required: true
selector:
select:
mode: dropdown
options:
- label: Get entities by tag
value: get entities by tag
location:
description: >-
Required for operation "get entities by tag". Do you want to search for
entities inside the house, outside the house or everywhere? Accepts
"Inside", "Outside" or "Everywhere".
example: Inside
required: false
selector:
select:
mode: dropdown
options:
- label: Inside
value: Inside
- label: Outside
value: Outside
- label: Everywhere
value: Everywhere
tags:
description: >-
List of tags. Provide a comma seperated list as a string. ONLY use the
tags provided in the tool description. NOT OTHER VALUES ALLOWED!
example: Büro, Lampe
required: false
selector:
text: null
details:
description: >-
false = Entities are returned as a list of entity_ids only. true =
Entities are returned as a list of objects with entity_id, friendly_name,
additional entity info, unit and state. This is useful if you want to
know the current state of the entities (e.g. on/off for lights, current
value for temperature sensors, ...).
example: false
required: false
selector:
boolean: {}
state:
description: >-
Optional state-filter as string (e.g. `on`) for operation "get entities by
tag". Leave empty to get all devices for the tags regardless of state.
example: "on"
required: false
selector:
text: null
sequence:
- action: logbook.log
data:
name: "LLM ENTITY INDEX: "
message: "{{ operation, location, tags, details, state }}"
entity_id: "{{ this.entity_id }}"
- choose:
- conditions:
- condition: template
value_template: "{{ operation == 'get entities by tag' }}"
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ location is not defined or not location }}"
sequence:
- variables:
error_result: >-
{{ {'error': 'Missing required parameter: location. Please specify Inside, Outside or Everywhere.'} }}
- stop: Missing location
response_variable: error_result
- variables:
tag_list: |-
{%- set ns = namespace(list = []) %}
{% if tags is string %}
{%- for t in (tags | regex_findall('[^,]+')) %}
{%- set ns.list = ns.list + [ t | trim ] %}
{%- endfor %}
{% elif tags %}
{%- set ns.list = tags %}
{% endif %}
{% if location == 'Inside' %}
{% set ns.list = ns.list + ['Inside'] %}
{% elif location == 'Outside' %}
{% set ns.list = ns.list + ['Outside'] %}
{% elif location == 'Everywhere' %}
{% set ns.list = ns.list + ['Everywhere'] %}
{% endif %}
{{ ns.list | unique | list }}
- variables:
unknown_tags: |-
{%- set ns = namespace(missing=[]) -%}
{%- for t in tag_list %}
{%- if (label_entities(t) | length) == 0 %}
{%- set ns.missing = ns.missing + [t] %}
{%- endif %}
{%- endfor -%}
{{ ns.missing }}
- choose:
- conditions:
- condition: template
value_template: "{{ unknown_tags | length > 0 }}"
sequence:
- variables:
error_result: >-
{{ {'error': 'You provided unknown tag(s), look up the tool description for the correct tag names and think about which is the correct one for your task.',
'unknown_tags': unknown_tags | join(', ')} }}
- stop: Unknown tag(s)
response_variable: error_result
- variables:
matched_entities: |-
{% if tag_list | length == 0 %}
[]
{% else %}
{% set ns = namespace(matches = label_entities(tag_list[0])) %}
{% for t in tag_list[1:] %}
{% set ns.matches = ns.matches | select('in', label_entities(t)) | list %}
{% endfor %}
{% set matches = ns.matches %}
{% if state is defined and state %}
{{ matches | select('is_state', state) | list }}
{% else %}
{{ matches }}
{% endif %}
{% endif %}
- variables:
additional_details: |-
{% if states('sensor.additional_entity_details') != 'unknown' %}
{{ state_attr('sensor.additional_entity_details', 'details') | default({}) }}
{% else %}
{}
{% endif %}
- variables:
result: |-
{% if details | default(false) %}
{%- set ns = namespace(obj = {}) -%}
{%- for e in matched_entities -%}
{%- set u = state_attr(e, 'unit_of_measurement') -%}
{# Base object #}
{%- set data = {
'friendly_name': state_attr(e, 'friendly_name'),
'state': states(e)
} -%}
{%- if u is not none %}
{%- set data = data | combine({'unit': u}) %}
{%- endif %}
{%- set alias_list = additional_details.get(e, {}).get('aliases') %}
{%- if alias_list is iterable %}
{%- set data = data | combine({'device_descriptions': alias_list}) %}
{%- endif %}
{%- set lab_names = labels(e) | map('label_name') | list %}
{%- if lab_names | length > 0 %}
{%- set data = data | combine({'tags': lab_names}) %}
{%- endif %}
{# Media Player grouping extras #}
{%- if e | regex_search('^media_player\..+') %}
{%- set gm_attr = state_attr(e, 'group_members') -%}
{%- set gm = [] -%}
{%- if gm_attr is iterable and not (gm_attr is string) -%}
{# ensure indexable list #}
{%- set gm = (gm_attr | list) -%}
{%- elif gm_attr is string -%}
{%- set gm = (gm_attr | regex_findall('media_player\.[A-Za-z0-9_]+')) -%}
{%- endif -%}
{%- if (gm | length) > 0 -%}
{%- set master_entity = gm[0] -%}
{%- set is_master = (master_entity == e) -%}
{%- set ns_m = namespace(members=[]) -%}
{%- for m in gm -%}
{%- if not loop.first -%}
{%- set ns_m.members = ns_m.members + [ { 'entity_id': m, 'name': state_attr(m, 'friendly_name') } ] -%}
{%- endif -%}
{%- endfor -%}
{%- set members = ns_m.members -%}
{%- set data = data | combine({
'is_master': is_master,
'master': { 'entity_id': master_entity, 'name': state_attr(master_entity, 'friendly_name') },
'members_of_master_player': members
}) -%}
{%- else -%}
{%- set data = data | combine({
'is_master': false,
'master': none,
'members_of_master_player': []
}) -%}
{%- endif -%}
{%- endif -%}
{%- set ns.obj = ns.obj | combine({ e: data }) -%}
{%- endfor -%}
{{ {'entities': ns.obj, 'count': matched_entities | length} | to_json | from_json }}
{% else %}
{{ {'entities': matched_entities, 'count': matched_entities | length} | to_json | from_json }}
{% endif %}
- stop: ""
response_variable: result
default:
- variables:
error_result: "{{ {'error': 'unsupported operation', 'operation': operation} }}"
- stop: Unsupported or missing operation
response_variable: error_result
And this is what I added to the LLMs prompt to make the script the only truth about entities:
When asked about entities in the house, ALWAYS PREFER THE TOOL ‘Entity Index’ over ‘GetLiveContext’ to find them. Use the latter one in case ‘Entity Index’ shows not results.
It’s your source to find entities that belong to the request.
You can use it for filtering the complete entity list of the house.
It also reports to you how many entities where found.
You will also need this tool to find entities that you want to use as parameters for other tools.Hints:
- If you have to make multiple calls in a row (not at the same time please, wait for the response before the next call) to get the count e.g. for multiple rooms or floors, you can use the Calculator tool to get the complete sum.
- The Entity Index has a fixed list of tags to search for and ALL of them are described in the tool. Don’t fake your own ones. Entity types, rooms, floor, and everything else to be placed in the tags property apply to that rule. VERY IMPORTANT!
- ALWAYS think first if you have to search for entities Inside / Outside the house (location parameter of the tool). Try to derive this information form the users question.
Most of the time it will be inside. Use ‘Everywhere’ if you really don’t know if the user asked about an Inside or Outside location.- When I ask about entities HERE, I talk about the current room (which is also the room where you are located while I talk to you. If you don’t know where you are, ask me).
- To check the available media players, players for a room or the current grouping state of a player use the ‘Entity Index’ tool with ‘Inside’ (we have no players in the outside areas) and the tag ‘MediaPlayer’.
Bonus step: Give the LLM access to your entity aliases
(Not needed, but helps the LLM with additional context.)
Many LLM-based assist users in this forum use entity aliases as description/more-info field to provide details and important notes about the entity to assist.
Instead of simply using it for alternative names only.
But aliases normally aren’t accessible in scripts/templates.
This is a problem, as assist doesn’t seem to reliable grab them on it’s own from the data provided by HA, as we gave it much easier access to the entities and their details with this script.
To work around it, this solution requires an additional Node-Red flow to read this details from a HA config file and save it inside the HAs www folder.
Then, an additionally configured rest-sensor reads this info and saves it in it’s attributes.
After that the Entity Index tool can use it and add it to the returned data structure.
Here’s the code for the restful sensor in my configuration.yaml:
rest:
- resource: http://your-home-assistant-ip-or-hostname:8123/local/assist/entity_details.json
scan_interval: 3600
sensor:
- name: Additional Entity Details
device_class: timestamp
value_template: "{{ now() }}"
json_attributes:
- details
And here’s the Node-RED flow:
[{"id":"46e51ac59ff32873","type":"group","z":"64f4dc3ef013c268","name":"Read Assist Aliases from HA config file and send them to an HA attribute for use in Entity Index script","style":{"label":false,"stroke":"none","fill":"#d1d1d1","fill-opacity":"0.5"},"nodes":["5968fec73b0f2999","8383305230fc3bab","ad54a1f216b4c4f3","aa21490c080676f0","d78a421b193540b6","c13e7da5c6891e63","c73517cdcb42d08c","b65361725f7607ab","ed3a056f82aad7e4","b1d840f19bde876f","890885d498e5f065","cccfe88f705f4870"],"x":14,"y":19,"w":752,"h":262},{"id":"5968fec73b0f2999","type":"debug","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"debug 21","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":540,"y":120,"wires":[]},{"id":"8383305230fc3bab","type":"function","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"Map Values","func":"const result = {\n details: {}\n};\n\nfor (const entity of msg.payload.data.entities) {\n if (entity.aliases.length > 0) {\n result.details[entity.entity_id] = {\n aliases: entity.aliases\n };\n }\n}\n\nmsg.payload = result;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":130,"y":180,"wires":[["d78a421b193540b6","c73517cdcb42d08c"]]},{"id":"ad54a1f216b4c4f3","type":"inject","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"","props":[],"repeat":"3600","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":110,"y":120,"wires":[["aa21490c080676f0"]]},{"id":"aa21490c080676f0","type":"file in","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"Read File","filename":"/homeassistant/.storage/core.entity_registry","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":240,"y":120,"wires":[["c13e7da5c6891e63"]]},{"id":"d78a421b193540b6","type":"debug","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"debug 23","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":560,"y":180,"wires":[]},{"id":"c13e7da5c6891e63","type":"json","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"","property":"payload","action":"","pretty":false,"x":370,"y":120,"wires":[["8383305230fc3bab","5968fec73b0f2999"]]},{"id":"c73517cdcb42d08c","type":"json","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"","property":"payload","action":"","pretty":false,"x":270,"y":180,"wires":[["b65361725f7607ab"]]},{"id":"b65361725f7607ab","type":"file","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"Write File","filename":"/homeassistant/www/assist/entity_details.json","filenameType":"str","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"utf8","x":400,"y":180,"wires":[["890885d498e5f065"]]},{"id":"ed3a056f82aad7e4","type":"api-call-service","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"","server":"ef6aa0b.3fe4a6","version":7,"debugenabled":false,"action":"homeassistant.update_entity","floorId":[],"areaId":[],"deviceId":[],"entityId":[],"labelId":[],"data":"{\"entity_id\":\"sensor.additional_entity_details\"}","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"homeassistant","service":"update_entity","x":360,"y":240,"wires":[["b1d840f19bde876f"]]},{"id":"b1d840f19bde876f","type":"debug","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"debug 24","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":580,"y":240,"wires":[]},{"id":"890885d498e5f065","type":"delay","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"2s","pauseType":"delay","timeout":"2","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":170,"y":240,"wires":[["ed3a056f82aad7e4"]]},{"id":"cccfe88f705f4870","type":"comment","z":"64f4dc3ef013c268","g":"46e51ac59ff32873","name":"Read Assist Aliases from HA config file and send them to an HA attribute for use in Entity Index script","info":"","x":390,"y":60,"wires":[]},{"id":"ef6aa0b.3fe4a6","type":"server","name":"Home Assistant","addon":true}]
It gets triggered every hour. If you’ve just added some new aliases and want them to get recognized immediately, simply press the inject button manually.
The rest-sensor in HA is automatically updated at the end of the flow.

