-
-
Notifications
You must be signed in to change notification settings - Fork 19
Tips & Tricks: 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:
- Start with one dashboard helper
- Reuse the same formulas across multiple users
- 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.
Each user has a ChoreOps dashboard helper sensor that already exposes useful data such as:
choresrewardspending_approvalsuser_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
This is the foundation for almost everything else in this guide.
Go to Developer Tools → States and search for:
dashboard helper- or part of the user's name
You are looking for that user's ChoreOps dashboard helper sensor.
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_eidsis 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.
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.
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 }}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 }}Total reward count:
{{ rewards | count }}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.
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
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 }}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.
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.
This section is for users who want the benefits of dynamic lookup but need to avoid mixing data across multiple ChoreOps instances.
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 }}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.
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.
🚀 Getting Started
- Home
- Installation
- Migration from KidsChores
- Quick Start
- Quick Start Scenarios
- Dashboard Generation
- Backup & Restore
- OpsCenter
⚙️ Configuration
- General Options
- Points
- Users
- Chores
- Rewards
- Badges - Overview
- Badges - Cumulative
- Badges - Periodic
- Achievements
- Challenges
- Notifications
🔧 Services
💡 Tips & Tricks
- Template Cookbook for Chores, Rewards, and Approvals
- Bulk Chore Updates via Scripts
- Auto-Approve Chores
- Calendar Event Due Dates
- NFC Claim Workflow
- Overdue Penalty Automation
- Critical Overdue Alerts
- Send ChoreOps Alerts to ntfy
📖 Advanced Topics
- Dashboard Integration
- Access Control
- Chores - Advanced
- Badge Cumulative - Advanced
- Badge Periodic - Advanced
📚 Technical Reference
- Points
- Users
- Entities & States
- Chores
- Badges
- Configuration Detail
- Dashboard Generation
- Notifications
- Weekly Activity Reports
👩🔧 Troubleshooting