`expand()` on nested groups gives wildly wrong counts in a script's `variables:` block, but correct when computed inline in a service call's `data`

Hi HA community,

My HA version is 2026.9.3

Setup:
I have a binary_sensor Group helper (binary_sensor.security_openings_away) whose members are themselves other Group helpers, each wrapping a single physical door/window contact sensor — so it’s a group-of-groups, 8 direct members, ~20 leaf sensors once fully expanded. This entity is used by a script that sends an intrusion report by e-mail when the alarm triggers.

Symptom:
When I compute expand('binary_sensor.security_openings_away') | list inside a top-level variables: step of the script (see Script A below), the resulting count is wildly inflated and inconsistent between runs — I saw 2032 and, a bit later with no change to the group itself, 3503. The true member count (confirmed repeatedly via Developer Tools > Template, checked every ~30-60s with nothing changing) is stable at 8 direct / ~20 expanded.

When I take the exact same expand() expression and compute it inline inside the data.message (or data.html) field of the notify.send_message action itself, instead of in a separate variables: step (Script B below), it reliably returns the correct, stable count every single time.

What I ruled out:

  • Group instability / something else modifying the group over time: checked the direct member count repeatedly over several minutes with nothing triggering — stayed at 8 the whole time.
  • Timing/delay: tested with both a 12s and a ~1s delay before the scan, same discrepancy each time (variables: block wrong, inline correct).
  • Nested-group support in expand(): the official docs state it resolves nested groups recursively, and it clearly does — just not consistently when the call sits in variables: on my install.

Minimal reproduction, two versions below — only difference is where the expand() calls live.

Script A (variables: block — broken on my install):

# Version A — "conventional" placement
# expand() is computed once in a top-level "variables:" step, then reused
# in the notify service call.
# => On my instance, this returned wildly inflated / inconsistent counts
#    (e.g. 2032, then 3503 on a later run) for a group that only has
#    8 direct members and ~20 entities once nested groups are expanded.

alias: Alarm report - variables block version
sequence:
  - delay: "00:00:05"

  - variables:
      openings_total: "{{ expand('binary_sensor.security_openings_away') | list }}"
      openings_on: >
        {{ openings_total | selectattr('state','eq','on') | list }}

  - action: notify.send_message
    target:
      entity_id: notify.email_home
    data:
      title: "Alarm triggered"
      message: >
        {{ openings_on | count }} opening(s) currently open,
        out of {{ openings_total | count }} total members.

mode: single

Script B (inline in the service call’s data — works correctly):

# Version B — "inline" placement
# The exact same expand() expression, computed inside the notify service
# call's own data field instead of a separate "variables:" step.
# => On my instance, this reliably returns the correct, stable count
#    every time (matches the true membership of the group).

alias: Alarm report - inline in service data version
sequence:
  - delay: "00:00:05"

  - action: notify.send_message
    target:
      entity_id: notify.email_home
    data:
      title: "Alarm triggered"
      message: >
        {% set openings_total = expand('binary_sensor.security_openings_away') | list %}
        {% set openings_on = openings_total | selectattr('state','eq','on') | list %}
        {{ openings_on | count }} opening(s) currently open,
        out of {{ openings_total | count }} total members.

mode: single

Question for the community: has anyone run into this before? Is this a known quirk/bug with how variables: renders templates that return lists of State objects from expand(), particularly with nested groups? I couldn’t find an existing issue describing this specific combination. Happy to file one if this isn’t already known — pointers to relevant source/discussion welcome in the meantime.


If you’re getting results like that, then in my opinion, it’s an obvious bug. You could check if there’s a circular membership somewhere, but even then, the result from the variable and the inline version should be the same - i.e., an error or the loop being skipped.

Yes, it’s a known quirk. Jinja only return strings. Anything that can’t be determined to be a type that python can easily serialize is made into a string. The full state object contains multiple non-serializable objects, such as the last_changed and last_updated datetime objects. So you are ending up with a string then counting all its characters, hence the values like 2032 and 3503.

Since lists of strings are serializable, having your variable render a list of entity IDs is often the best way to go. A lot of the “newer” filters and tests accept entity ID strings, for example:

list_of_entity_ids | select('is_state', 'on') ...

… instead of:

expanded_state_objects | selectattr('state','eq','on') ...

Thanks @Didgeridrew , that explains it perfectly.
Fixed by storing only entity_id strings in variables: and resolve details via states[entity_id] later.
Works correctly now.

Not a bug, | count gives the number of characters in a string or the number of items in a list. If you put information that cannot be serialized into objects as the output in a variable, it will return a string instead of a list of objects. Then you get the total character count instead of the item count in the list. It’s up to the user to make sure they do not output raw objects to variables to avoid this behavior.