Allright here it is…
Friday’s Zen Indexer 2.0
WHAT DID you DO Nathan? Script AND Automation? WTH?

BOTH. Yes you will NEED both.
I just moved the index into its own script - it’s callable as an independent tool from anywhere in ha - YES that means that I am ultimately walking away from the ‘grand library’ front door but that was a stopgap anyway to work around the inability to pass multiple vars to a script.
With the last few CRUD tools (yes you see a new name here in a second, no lie, while CRUD is technically accurate in most cases - my wife hated me running around the house yelling “crud” at Friday… Her words, not mine.)
So what does this enable - well FIRST and MOST IMPORTANT. Re-entry… with this:
The DojoTools Zen Index Event Handler (2.0.0)
DojoTools - Friday’s tools in her dojo (this will make sense later)
ZenIndex You guys saw the theme c’mon…
Event Han…
WHAT DID YOU DO?
OK first the event handler… Here it is - then we’ll talk about it.
In short, its ENTIRE reason for living is to handle a call to the index and interpret it and handoff to the script -that’s it. Pretty easy actually - BUT it’s where we put most of the defense code. The script has the normal make sure not null where it shouldn’t etc. and we’ll talk about it in a sec.
DojoTools Zen Index Event Handler (2.0.0) (Yes I’ve been working on this for a while)
alias: DojoTools Zen Index Event Handler (2.0.0)
description: >
Event Listener to recurse index calls. REQUIRES DojoTOOLS Zen Index (2.0.0 or
greater)
triggers:
- event_type: zen_indexer_request
trigger: event
actions:
- variables:
index_command: "{{ trigger.event.data.index_command | default('{}', true) }}"
correlation_id: "{{ trigger.event.data.correlation_id }}"
depth: "{{ trigger.event.data.depth if 'depth' in trigger.event.data else 1 }}"
is_mapping: |-
{% set cmd = index_command | trim %} {% if cmd.startswith('{') %}
{{ (cmd | from_json) is mapping }}
{% else %}
false
{% endif %}
parsed_cmd: >
{% set reserved = ['AND', 'OR', 'XOR', 'NOT'] %} {% set true_tokens =
['true', 't', 'y', 'yes', 'on', '1'] %} {% set false_tokens = ['false',
'f', 'n', 'no', 'off', '0'] %} {% set raw = index_command | string |
trim %} {% set is_json = raw.lstrip().startswith('{') %} {% if is_json
%}
{{ raw }}
{% elif raw == "" or raw == "*" %}
{ "label_1": "*", "operator": "AND", "expand_entities": false }
{% else %}
{% set tokens = raw.split() %}
{% set exp = false %}
{% if tokens[-1] | lower in true_tokens %}
{% set exp = true %}
{% set tokens = tokens[:-1] %}
{% elif tokens[-1] | lower in false_tokens %}
{% set tokens = tokens[:-1] %}
{% endif %}
{% set op_index = namespace(idx = none) %}
{% for i in range(tokens | length) %}
{% if tokens[i] | upper in reserved %}
{% set op_index.idx = i %}
{% break %}
{% endif %}
{% endfor %}
{% if op_index.idx is not none %}
{% set label_1 = tokens[:op_index.idx] | join(" ") | trim("'\"") %}
{% set op = tokens[op_index.idx] | upper %}
{% set label_2 = tokens[op_index.idx+1:] | join(" ") | trim("'\"") %}
{% else %}
{% set label_1 = tokens | join(" ") | trim("'\"") %}
{% set op = "AND" %}
{% set label_2 = "" %}
{% endif %}
{
"label_1": "{{ label_1 }}",
"label_2": "{{ label_2 }}",
"operator": "{{ op }}",
"expand_entities": {{ exp }}
}
{% endif %}
entities_1: >-
{% set ents = parsed_cmd.entities_1 if parsed_cmd.entities_1 is defined
and parsed_cmd.entities_1 else [] %} {% if ents | length == 0 and
parsed_cmd.label_1 %}
{{ label_entities(parsed_cmd.label_1) }}
{% else %}
{{ ents }}
{% endif %}
label_1: "{{ parsed_cmd.label_1 if parsed_cmd.label_1 is defined else '' }}"
entities_2: >-
{% set ents = parsed_cmd.entities_2 if parsed_cmd.entities_2 is defined
and parsed_cmd.entities_2 else [] %} {% if ents | length == 0 and
parsed_cmd.label_2 %}
{{ label_entities(parsed_cmd.label_2) }}
{% else %}
{{ ents }}
{% endif %}
label_2: "{{ parsed_cmd.label_2 if parsed_cmd.label_2 is defined else '' }}"
operator: |-
{% if parsed_cmd.operator is defined and parsed_cmd.operator %}
{{ parsed_cmd.operator }}
{% else %}
AND
{% endif %}
expand_entities: >-
{{ parsed_cmd.expand_entities if parsed_cmd.expand_entities is defined
else false }}
- alias: Dispatch Zen Index Command
data:
entities_1: "{{ entities_1 }}"
label_1: "{{ label_1 }}"
entities_2: "{{ entities_2 }}"
label_2: "{{ label_2 }}"
operator: "{{ operator }}"
expand_entities: "{{ expand_entities }}"
response_variable: zen_index_response
action: script.dojotools_zen_index
- alias: Return Zen Indexer Result
event: zen_index_response
event_data:
correlation_id: "{{ correlation_id }}"
response: "{{ zen_index_response }}"
mode: queued
So the event handler just accepts either an old style index command and formats it as json and hands it back to the script or is given the fields and hands it to the script. Pretty cool when you make it reentrant *10
BECAUSE
If you refactor the index into a script that IF it happens to receive an old style index command - it fires the event to parse it and - it throws the parsed command back at itself… and voila, it can handle parsing a command and breaking it into its component parts.
Cool trick 1:
To prep for potential recursion loop waits, I built it to wait for trigger until timeout… (default with override) (example of how is in the script)
BUT WAIT - it teaches the index a new superpower… I think you can see the breadcrumbs of full recursive indexing calls here .
(THIS PART IS IN PROGRESS, probably lands somewhere around 2.5, technically I THINK it might actually work now with a doc tweak. The structures are there - I’m just not testing the branch yet. As far as Friday is concerned if I haven’t documented a feature it might not as well exist. We use that to our advantage here. So experiment here if you want. Very beta parts use at your own risk. But shouldn’t be destructive… Just explodey)
IF you send a specially structured json into the index command (index command v2) you can call complex recursive structures like
You can send in a list of ents. and compare to another list (thanks frenk - yes this uses all the new bool set ops under the hood)
(Labels are always single, entlists are always lists - even list of one)
NOW:
- ‘*’ the index, of course…
- label AND label
- [entlist] NOT (label)
- label OR [entlist]
FUTURE:
- label AND ( label or [ entlist ] )
(in json, of course…) Prob will limit to 3 levels, but the plan is to unwind any labels into entlists then recurse through the comparison until you walk back to the top of th etree.
Well - here she is…
Friday’s ZenIndexer 2.0.0
(note: intentionally omitted the tool version from the alias here - if you paste this into the script editor the first save sets the name of the script alias. You do jot want this versioned if you want drop in replacement. Or if you prefer version mgt then version the alias and edit script above appropriately after you set the script name.
After it’s set the first time you can alias to your hearts content.
alias: DojoTOOLS Zen Index
description: >-
Friday's Zen - Zen Indexer 2.0 This tool is a DIRECT replacement and the next
version of the Library ~INDEX~ Use it as preferred in place of THE LIBRARY.
Set logic on two lists of entity_ids (each resolved from a label or list of
entities). - If only one side provided, returns that set. - If both, applies
operator (AND, OR, NOT, XOR). - If neither, returns an error or (special case)
full label index (operator = '*'). - Optional expand to resolve groups/areas.
Returns matching entities, adjacent labels, expansion can include state and
attribute list. result.simple ALWAYS returns a list (for chaining). If
index_command is set, fires zen_indexer_request event and exits for recursive
resolution.
fields:
index_command:
name: Zen Index Command
description: >-
Structured query; triggers recursive index resolution (if set, all other
fields ignored) Fallback to field entry on fail
required: false
selector:
text: {}
entities_1:
name: Entities 1
description: List of entity_ids for operand 1 (can be empty if using label_1)
required: false
selector:
entity:
multiple: true
label_1:
name: Label 1
description: Label for operand 1 (used if entities_1 is empty)
required: false
selector:
text: {}
entities_2:
name: Entities 2
description: List of entity_ids for operand 2 (can be empty if using label_2)
required: false
selector:
entity:
multiple: true
label_2:
name: Label 2
description: Label for operand 2 (used if entities_2 is empty)
required: false
selector:
text: {}
operator:
name: Set Operator
required: true
default: AND
selector:
select:
options:
- AND
- OR
- NOT
- XOR
expand_entities:
name: Expand results
required: false
default: false
selector:
boolean: {}
description: >-
Optionally expand the result for state and attribute data. WARNING this
MAY overflow. Best Practice: use index to narrow result set THEN EXPAND.
Default: OFF
timeout:
selector:
number:
min: 0
max: 5
step: 0.25
name: Timeout
description: >-
Timeout, in seconds to wait for the index return (Default - 2, Min - 0,
Max - 5, Step - .25)
default: 2
required: false
sequence:
- variables:
is_index_command: "{{ index_command is defined and index_command | length > 0 }}"
correlation_id: |
{% if is_index_command %}
{{ now().isoformat() ~ '-' ~ (range(1000)|random) }}
{% else %}
''
{% endif %}
timeout_seconds: "{{ timeout | default(2) }}"
- choose:
- conditions:
- alias: Is Index Command - Send to Parser
condition: template
value_template: "{{ index_command is defined and index_command | length > 0 }}"
sequence:
- event: zen_indexer_request
event_data:
index_command: "{{ index_command }}"
correlation_id: "{{ correlation_id }}"
- wait_for_trigger:
- trigger: event
event_type: zen_index_response
event_data:
correlation_id: "{{ id }}"
timeout:
seconds: "{{ timeout_seconds }}"
alias: Wait for zen_index_response (timeout seconds)
- choose:
- conditions:
- alias: Received Data from Trigger
condition: template
value_template: " {{ ( wait.trigger is defined ) and ( wait.trigger is not none ) }}"
sequence:
- variables:
simple: "{{ wait.trigger.event.data.response.result.simple }}"
correlation_id: "{{ id }}"
index_timeout: false
- conditions: []
sequence:
- variables:
result: {}
correlation_id: "{{ id }}"
index_timeout: true
error: |
Indexer call timeout {{ timeout_seconds }}
alias: Timeout
- variables:
index_command: ""
op1: >-
{% set ents = simple if simple is defined and simple else [] %} {% if
simple is defined and simple %}
{% set ents = simple %}
{% else %}
{% set ents = entities_1 if entities_1 is defined and entities_1 else [] %}
{% endif %} {% if ents | length == 0 and label_1 %}
{{ label_entities(label_1) }}
{% else %}
{{ ents }}
{% endif %}
op2: >-
{% set ents = entities_2 if entities_2 is defined and entities_2 else []
%} {% if ents | length == 0 and label_2 %}
{{ label_entities(label_2) }}
{% else %}
{{ ents }}
{% endif %}
op1_empty: "{{ op1 | length == 0 }}"
op2_empty: "{{ op2 | length == 0 }}"
setop: "{{ operator | upper }}"
entities_base: |-
{% if not op1_empty and op2_empty %}
{{ op1 }}
{% elif not op2_empty and op1_empty %}
{{ op2 }}
{% elif not op1_empty and not op2_empty %}
{% if setop == 'AND' %}
{{ op1 | intersect(op2) }}
{% elif setop == 'OR' %}
{{ op1 + op2 | unique | list }}
{% elif setop == 'NOT' %}
{{ op1 | difference(op2) }}
{% elif setop == 'XOR' %}
{{ op1 | symmetric_difference(op2) }}
{% else %}
{{ op1 + op2 | unique | list }}
{% endif %}
{% else %}
[]
{% endif %}
expanded_entities: |-
{% if expand_entities %}
[
{% for s in expand(entities_base) %}
{
"entity_id": "{{ s.entity_id }}",
"state": "{{ s.state }}",
"friendly_name": "{{ s.attributes.friendly_name if 'friendly_name' in s.attributes else '' }}",
"last_changed": "{{ s.last_changed if s.last_changed is defined else '' }}",
"attributes": {{ s.attributes.keys() | list | tojson }},
"labels": {{ labels(s.entity_id) | tojson }}
}{{ "," if not loop.last else "" }}
{% endfor %}
]
{% else %}
{{ entities_base | tojson }}
{% endif %}
result_simple: "{{ entities_base | tojson }}"
adjacent_labels: |-
{%- set ns = namespace(labels=[]) -%}
{%- for e in entities_base -%}
{%- set ns.labels = ns.labels + labels(e) -%}
{%- endfor -%}
{{ ns.labels | unique | list | tojson }}
result_index: |-
{%- set ns = namespace(items=[]) -%}
{%- for e in entities_base -%}
{%- set ns.items = ns.items + [[e, labels(e)]] -%}
{%- endfor -%}
{{ ns.items | tojson }}
error_msg: |-
{% if op1_empty and op2_empty %}
Both operands are empty. Provide at least one label or entity list.
{% else %}
None
{% endif %}
label_entity_logic_result_raw_2: |-
{% if op1_empty and op2_empty %} {
"result": {
"simple": [],
"expanded": [],
"adjacent_labels": [],
"index": {{ labels() | tojson }}
},
"operator": "*",
"inputs": {
"entities_1": {{ entities_1 | default([]) | tojson }},
"label_1": "{{ label_1 | default('') }}",
"entities_2": {{ entities_2 | default([]) | tojson }},
"label_2": "{{ label_2 | default('') }}",
"expand_entities": {{ expand_entities | default(false) }}
},
"error": "Both operands are empty. Returned full label index."
} {% else %} {
"result": {
"simple": {{ result_simple }},
"expanded": {{ expanded_entities }},
"adjacent_labels": {{ adjacent_labels }},
"index": {{ result_index }}
},
"operator": "{{ setop }}",
"inputs": {
"entities_1": {{ entities_1 | default([]) | tojson }},
"label_1": "{{ label_1 | default('') }}",
"entities_2": {{ entities_2 | default([]) | tojson }},
"label_2": "{{ label_2 | default('') }}",
"expand_entities": {{ expand_entities | default(false) }}
},
"error": {{ error_msg | default('') }}
} {% endif %}
label_entity_logic_result: |
{% if label_entity_logic_result_raw is mapping %}
{{ label_entity_logic_result_raw }}
{% else %}
{{ label_entity_logic_result_raw | from_json | default({}) }}
{% endif %}
- stop: Zen Index Call Complete
response_variable: label_entity_logic_result
icon: mdi:cylinder
mode: parallel
max: 10
As with most of this stuff -as is no warranty. It’s going under MIT if you’re interested.
Warning - in well planned label rich environments THIS tool will cause moments of glory in a well grounded LLM - sorry ![]()
So for you - you USE the script. If you enter the fields and get result it will push right through and answer.
Deploy hint: drop both in thier respective editor and save l, restart script and automation in the dev tool then reload the browser you are in. Swap to action and find the script and enter a label you KNOW exists in your label collection. in the Zen Index Command and forces the script to trigger the automation. If that works you’re good… Make the script visible to Assist and restart.
IF however, you call the parser (engages in old style call or recursion JSON) then it passes the hat back and forth until it unwinds the entire call… Right now, the old-style call to new style works so if you are using my old indexer this is a drop in and I tell the LLM so in my description - you can run them in parallel. It will prefer it naturally after first use, it returns better data faster (I did for 4 mo and still am the library index 1.0 is still online. It’s pretty battle tested.)
So…
Welcome to ZenAI
Now we have to talk about libraries, volumes and drawers (may still change that one)
