Skip to content

Tips & Tricks: Template Cookbook for Chores, Rewards, and Approvals

ccpk1 edited this page May 9, 2026 · 5 revisions

🧠 Template Cookbook for Chores, Rewards, and Approvals

Note

This page shows simple patterns you can use when you want to program against ChoreOps data in Home Assistant templates.

The learning path is:

  1. Start with one dashboard helper
  2. Reuse the same formulas across multiple users
  3. Only use the advanced lookup patterns if you need to narrow to a specific ChoreOps config entry

Warning

Older patterns that scan broad groups of sensors with expand(states.sensor) and regex matching are now an expensive way to get this data.

Those patterns still work, but they force Home Assistant to evaluate a much larger group of entities than necessary. ChoreOps dashboard helpers exist so templates can read the data they need directly, or at least get a direct pointer to it.

Tip

If your goal is to bulk modify chore fields with a Home Assistant script instead of reading chore data in templates, see Bulk Updating Chores via Scripts.


📌 Why use a dashboard helper?

Each user has a ChoreOps dashboard helper sensor that already exposes useful data such as:

  • chores
  • rewards
  • pending_approvals
  • user_name

That makes the dashboard helper a good starting point for things like:

  • total chore counts
  • completed chore counts
  • overdue chore counts
  • pending approval counts
  • any pending approvals for one user
  • any pending approvals across many users

1. Start with one helper

This is the foundation for almost everything else in this guide.

How to find the helper entity

Go to Developer ToolsStates and search for:

  • dashboard helper
  • or part of the user's name

You are looking for that user's ChoreOps dashboard helper sensor.

Recommended single-helper setup block

Use this setup block as your default pattern for one user if you want the template to stay dynamic as chore counts grow:

{% set helper = 'sensor.ava_choreops_ui_dashboard_helper' %}
{% set dashboard_helpers = state_attr(helper, 'dashboard_helpers') or {} %}
{% set ns = namespace(chores=(state_attr(helper, 'chores') or [])) %}
{% for chore_helper in dashboard_helpers.get('chore_helper_eids', []) %}
  {% set ns.chores = ns.chores + (state_attr(chore_helper, 'chores') or []) %}
{% endfor %}
{% set chores = ns.chores %}
{% set rewards = state_attr(helper, 'rewards') or [] %}
{% set pending = state_attr(helper, 'pending_approvals') or {} %}
{% set pending_chores = pending.get('chores', []) %}
{% set pending_rewards = pending.get('rewards', []) %}

Once you have that block, the only thing you usually change is the final formula.

Why this is the recommended pattern:

  • Below the shard threshold, dashboard_helpers.chore_helper_eids is just [], so this behaves the same as the older inline-only pattern.
  • Above the shard threshold, ChoreOps may split the chore list across helper shards, and this pattern keeps returning the full merged chore list.

Simpler inline-only setup block for lower per-user chore counts

If you know the user will stay below the higher per-user chore-count range and you want the shortest possible template, the older setup still works:

{% set helper = 'sensor.ava_choreops_ui_dashboard_helper' %}
{% set chores = state_attr(helper, 'chores') or [] %}
{% set rewards = state_attr(helper, 'rewards') or [] %}
{% set pending = state_attr(helper, 'pending_approvals') or {} %}
{% set pending_chores = pending.get('chores', []) %}
{% set pending_rewards = pending.get('rewards', []) %}

That version is simpler, and it does work while the helper stays inline-only. The tradeoff is that once a user gets to roughly 40 assigned chores, the helper will often be large enough to split, and the inline-only pattern can stop seeing the full chore list. Use the recommended dynamic pattern above for anything meant to keep working as per-user chore counts scale.

Common formulas for one helper

Approval formulas

Pending chore approvals:

{{ pending_chores | count }}

Pending reward approvals:

{{ pending_rewards | count }}

Total pending approvals:

{{ (pending_chores | count) + (pending_rewards | count) }}

Any pending approvals:

{{ ((pending_chores | count) + (pending_rewards | count)) > 0 }}
Chore formulas

Total chore count:

{{ chores | count }}

Completed chore count:

{{ chores | selectattr('state', 'eq', 'completed') | list | count }}

Pending chore count:

{{ chores | selectattr('state', 'eq', 'pending') | list | count }}

Claimed chore count:

{{ chores | selectattr('state', 'eq', 'claimed') | list | count }}

Overdue chore count:

{{ chores | selectattr('state', 'eq', 'overdue') | list | count }}
Reward formulas

Total reward count:

{{ rewards | count }}

Example: one full template sensor

If you want a complete copy-and-paste example, here is one for total pending approvals:

template:
  - sensor:
      - name: "Ava pending approvals count"
        state: >
          {% set helper = 'sensor.ava_choreops_ui_dashboard_helper' %}
          {% set dashboard_helpers = state_attr(helper, 'dashboard_helpers') or {} %}
          {% set ns = namespace(chores=(state_attr(helper, 'chores') or [])) %}
          {% for chore_helper in dashboard_helpers.get('chore_helper_eids', []) %}
            {% set ns.chores = ns.chores + (state_attr(chore_helper, 'chores') or []) %}
          {% endfor %}
          {% set chores = ns.chores %}
          {% set rewards = state_attr(helper, 'rewards') or [] %}
          {% set pending = state_attr(helper, 'pending_approvals') or {} %}
          {% set pending_chores = pending.get('chores', []) %}
          {% set pending_rewards = pending.get('rewards', []) %}
          {{ (pending_chores | count) + (pending_rewards | count) }}

Tip

If you want a binary sensor instead of a count, reuse the same setup block and make the last line return true or false.


2. Scale the same formulas across multiple users

Once you understand the single-helper formulas, the next step is simple: gather more than one helper, then apply the same ideas across all of them.

You have two main choices:

  • Dynamic helper discovery for one ChoreOps instance
  • Manual helper lists for maximum readability or explicit control

Note

The dynamic helper examples below are best for a single ChoreOps instance.

If you run multiple ChoreOps instances, either:

  • manually define the helper entities, or
  • move on to Section 3 for config-entry-aware dynamic lookups

2A. Dynamic helper discovery for one instance

Use this setup block to collect all user dashboard helpers for one ChoreOps instance:

{% set helpers = integration_entities('choreops')
  | select('search', '^sensor\\.')
  | list
  | expand
  | selectattr('attributes.purpose', 'defined')
  | selectattr('attributes.purpose', 'eq', 'purpose_dashboard_helper')
  | map(attribute='entity_id')
  | list %}
{% set ns = namespace(
  chores=[],
  rewards=[],
  pending_chores=[],
  pending_rewards=[]
) %}

{% for helper in helpers %}
  {% set pending = state_attr(helper, 'pending_approvals') or {} %}
  {% set dashboard_helpers = state_attr(helper, 'dashboard_helpers') or {} %}
  {% set helper_ns = namespace(chores=(state_attr(helper, 'chores') or [])) %}
  {% for chore_helper in dashboard_helpers.get('chore_helper_eids', []) %}
    {% set helper_ns.chores = helper_ns.chores + (state_attr(chore_helper, 'chores') or []) %}
  {% endfor %}
  {% set ns.chores = ns.chores + helper_ns.chores %}
  {% set ns.rewards = ns.rewards + (state_attr(helper, 'rewards') or []) %}
  {% set ns.pending_chores = ns.pending_chores + (pending.get('chores', [])) %}
  {% set ns.pending_rewards = ns.pending_rewards + (pending.get('rewards', [])) %}
{% endfor %}

This is the safe multi-user pattern because each helper contributes its inline chores plus any shard-backed chores before the household totals are calculated.

Now you can reuse the same formulas across all users.

Any pending approvals across all users:

{{ ((ns.pending_chores | count) + (ns.pending_rewards | count)) > 0 }}

Total pending approvals across all users:

{{ (ns.pending_chores | count) + (ns.pending_rewards | count) }}

Total chores across all users:

{{ ns.chores | count }}

Completed chores across all users:

{{ ns.chores | selectattr('state', 'eq', 'completed') | list | count }}

Overdue chores across all users:

{{ ns.chores | selectattr('state', 'eq', 'overdue') | list | count }}

2B. Manual helper list for readability or multiple instances

If you prefer a more explicit template, or you want to control exactly which helpers are included, define the helper list yourself.

{% set helpers = [
  'sensor.ava_choreops_ui_dashboard_helper',
  'sensor.ben_choreops_ui_dashboard_helper',
  'sensor.liam_choreops_ui_dashboard_helper'
] %}
{% set ns = namespace(
  chores=[],
  rewards=[],
  pending_chores=[],
  pending_rewards=[]
) %}

{% for helper in helpers %}
  {% set pending = state_attr(helper, 'pending_approvals') or {} %}
  {% set dashboard_helpers = state_attr(helper, 'dashboard_helpers') or {} %}
  {% set helper_ns = namespace(chores=(state_attr(helper, 'chores') or [])) %}
  {% for chore_helper in dashboard_helpers.get('chore_helper_eids', []) %}
    {% set helper_ns.chores = helper_ns.chores + (state_attr(chore_helper, 'chores') or []) %}
  {% endfor %}
  {% set ns.chores = ns.chores + helper_ns.chores %}
  {% set ns.rewards = ns.rewards + (state_attr(helper, 'rewards') or []) %}
  {% set ns.pending_chores = ns.pending_chores + (pending.get('chores', [])) %}
  {% set ns.pending_rewards = ns.pending_rewards + (pending.get('rewards', [])) %}
{% endfor %}

The formulas are exactly the same as in the dynamic example above. Only the helper lookup changes.

If you intentionally want the simpler legacy version for users who will stay below the higher per-user chore-count range, you can replace the shard merge block with state_attr(helper, 'chores') or []. That shortcut is easier to read, but it is not the version to publish when you want the template to survive helper splitting.

Example: full multi-user template sensor

template:
  - sensor:
      - name: "Household pending approvals count"
        state: >
          {% set helpers = [
            'sensor.ava_choreops_ui_dashboard_helper',
            'sensor.ben_choreops_ui_dashboard_helper',
            'sensor.liam_choreops_ui_dashboard_helper'
          ] %}
          {% set ns = namespace(pending_chores=[], pending_rewards=[]) %}

          {% for helper in helpers %}
            {% set pending = state_attr(helper, 'pending_approvals') or {} %}
            {% set ns.pending_chores = ns.pending_chores + (pending.get('chores', [])) %}
            {% set ns.pending_rewards = ns.pending_rewards + (pending.get('rewards', [])) %}
          {% endfor %}

          {{ (ns.pending_chores | count) + (ns.pending_rewards | count) }}

Tip

The easiest way to keep this simple is to learn the formulas once in Section 1, then only change how you build the helpers list.


3. Advanced: narrow dynamic lookups to one ChoreOps config entry

This section is for users who want the benefits of dynamic lookup but need to avoid mixing data across multiple ChoreOps instances.

3A. One user's helper for one config entry

template:
  - binary_sensor:
      - name: "Selected user has pending approvals"
        state: >
          {% set entry_id = 'YOUR_CONFIG_ENTRY_ID' %}
          {% set user_id = 'YOUR_USER_ID' %}
          {% set lookup_key = entry_id ~ ':' ~ user_id %}

          {% set helper = integration_entities('choreops')
            | select('search', '^sensor\\.')
            | list
            | expand
            | selectattr('attributes.purpose', 'defined')
            | selectattr('attributes.purpose', 'eq', 'purpose_dashboard_helper')
            | selectattr('attributes.dashboard_lookup_key', 'eq', lookup_key)
            | map(attribute='entity_id')
            | first
            | default('', true) %}

          {% set pending = state_attr(helper, 'pending_approvals') or {} %}
          {{ ((pending.get('chores', []) | count) + (pending.get('rewards', []) | count)) > 0 }}

3B. All users for one config entry using the shared admin helper

In ChoreOps, shared_admin is the lookup value used for the system-level shared admin dashboard helper for one config entry.

That helper is not tied to one user. Instead, it exposes a user_dashboard_helpers map that points to each user's dashboard helper for that same config entry.

template:
  - binary_sensor:
      - name: "Any user in this config has pending approvals"
        state: >
          {% set entry_id = 'YOUR_CONFIG_ENTRY_ID' %}
          {% set shared_lookup_key = entry_id ~ ':shared_admin' %}

          {% set shared_helper = integration_entities('choreops')
            | select('search', '^sensor\\.')
            | list
            | expand
            | selectattr('attributes.purpose', 'defined')
            | selectattr('attributes.purpose', 'eq', 'purpose_system_dashboard_helper')
            | selectattr('attributes.integration_entry_id', 'eq', entry_id)
            | selectattr('attributes.dashboard_lookup_key', 'eq', shared_lookup_key)
            | map(attribute='entity_id')
            | first
            | default('', true) %}

          {% set helper_map = state_attr(shared_helper, 'user_dashboard_helpers') or {} %}
          {% set ns = namespace(pending_chores=[], pending_rewards=[]) %}

          {% for pair in helper_map | dictsort %}
            {% set helper = pair[1] %}
            {% set pending = state_attr(helper, 'pending_approvals') or {} %}
            {% set ns.pending_chores = ns.pending_chores + (pending.get('chores', [])) %}
            {% set ns.pending_rewards = ns.pending_rewards + (pending.get('rewards', [])) %}
          {% endfor %}

          {{ ((ns.pending_chores | count) + (ns.pending_rewards | count)) > 0 }}

Note

Dynamic lookups are powerful, but they are harder to read. If you do not need automatic discovery or config-entry filtering, the simpler helper-list patterns above are usually the better choice.


🚫 Older pattern to avoid when possible

This older style is still valid, but it is no longer the recommended approach:

{% set sensors = expand(states.sensor)
                | selectattr('entity_id', 'match', '^sensor\.[a-zA-Z0-9_]+_choreops_chore_status_.*')
                | selectattr('state', 'equalto', 'claimed')
                | map(attribute='entity_id')
                | list %}

{{ sensors | count > 0 }}

Why it is less desirable now:

  • It scans a broad set of entities
  • It infers state indirectly from chore status sensors
  • It ignores the helper data ChoreOps already provides directly

The dashboard helper approach is more targeted, easier to extend, and easier to reason about once you learn the base setup block.

Clone this wiki locally