Connecting a source

Home Assistant chores

Housework is work, and nobody starts a timer to empty the dishwasher. If the house keeps its chores in Home Assistant, a small automation hands each finished one over, and the habitat grows from the bins going out. Nothing to install, no add-on to trust: a few lines of YAML and a URL.

New here? Toiling Away turns recorded work into animals that live in a habitat of your own. Desk time from the same Home Assistant has its own guide; this page is chores.

Make a ping URL for chores

  1. In Toiling Away, open Connections, add Ping URL, and label it chores. Make a fresh one even if a desk sensor already has a URL: a count sent to the desk URL while the desk has a run open closes that run instead of counting the chore.
  2. Copy the URL the moment it is shown. It is displayed once and stored hashed, so it cannot be recovered or shown again. Lose it and you make a new one.

A finished chore is a count rather than a stretch of time, so the call is the plain URL with ?count=1 on the end: one chore done. The URL goes in secrets.yaml, which exists next to configuration.yaml for exactly this sort of thing:

# secrets.yaml
toil_chore_url: https://app.toilingaway.com/p/YOUR-CHORES-TOKEN?count=1

And the call, in configuration.yaml. rest_command sends a GET when no method is named, which is what this wants. If the desk guide already gave you a rest_command: block, add toil_chore under that one rather than starting a second:

rest_command:
  toil_chore:
    url: !secret toil_chore_url

The automations below were checked against the Home Assistant documentation at 2026.10.1, on 11 October 2026. The to-do trigger arrived with 2026.7, when the purpose-specific triggers left Labs; the ChoreOps one works on anything recent.

A to-do list

Any list Home Assistant shows: the built-in Local To-do, or one it syncs from somewhere else. Every item ticked off is one chore. In automations.yaml, with todo.chores replaced by your list:

- alias: Chores into Toiling Away
  triggers:
    - trigger: todo.item_completed
      target:
        entity_id: todo.chores
  actions:
    - repeat:
        count: "{{ trigger.item_ids | length }}"
        sequence:
          - action: rest_command.toil_chore

Three items ticked at once fire the trigger once, with all three in item_ids. The repeat sends one ping for each, so the batch is three chores, not one.

Unticking an item and ticking it again counts it again. The list cannot tell a chore done twice from a slip of the thumb, and neither can the server. A list the whole house can reach is a list the whole house can tick, which is the thing to keep in mind before pointing this at the shopping list.

A restart counts nothing. The trigger ignores a list coming back from unavailable or unknown, so Home Assistant starting up does not replay the morning.

ChoreOps

ChoreOps, formerly KidsChores, keeps a list of chores for each person in the house, and a parent approves each one. Count the approval, not the claim: a claim is somebody saying they did it, and the approval is somebody else agreeing. That is the closest thing a chore has to a witness, and it is already built in.

ChoreOps sends no event when a chore is approved, so the automation watches a status sensor instead. There is one for each person and chore, shown as Chore Status - and the chore's name on that person's ChoreOps device, with an entity ID along the lines of sensor.alex_choreops_chore_status_dishwasher. The IDs are generated from the names you gave, so copy yours rather than guessing: Settings, Devices & services, ChoreOps, then the person. The two below are placeholders:

- alias: Approved chores into Toiling Away
  triggers:
    - trigger: state
      entity_id:
        - sensor.REPLACE_choreops_chore_status_dishwasher
        - sensor.REPLACE_choreops_chore_status_bins
      to: approved
      not_from:
        - unavailable
        - unknown
  conditions:
    - condition: template
      value_template: "{{ trigger.from_state is not none }}"
  actions:
    - action: rest_command.toil_chore

A restart counts nothing. The sensors come back from unavailable, or from no state at all, when Home Assistant starts. not_from skips the first and the condition skips the second, so a chore approved this morning is not paid again at lunch.

A chore shared between people can also show approved_in_part, which this automation does not count. Add it under to: as a second entry if a part-approved chore should.

ChoreOps keeps its own points, badges and streaks for whoever did the chore, and those stay where they are. The chore reaches Toiling Away as one count on the URL's habitat, with nothing about who did it. A child needs no account here, and there is nothing on this side to look at but the habitat.

Habitica, without Home Assistant

Habitica calls a URL every time a task is scored, through a webhook, and the ping URL takes the POST it sends. Creating the webhook is one call to Habitica's API, with the user ID and API token from its API settings page:

curl -X POST https://habitica.com/api/v3/user/webhook \
  -H "x-api-user: YOUR-USER-ID" \
  -H "x-api-key: YOUR-API-TOKEN" \
  -H "x-client: YOUR-USER-ID-toilingaway" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://app.toilingaway.com/p/YOUR-CHORES-TOKEN?count=1", "label": "Toiling Away", "type": "taskActivity", "options": {"scored": true}}'

Every score in the account is a chore. Habitica sends the same call for a Habit's plus, a Daily, a To Do, and again for a minus or an unticked box. Toiling Away does not read what Habitica sends, only that it arrived, so each of those counts one. It works best with an account whose tasks are the chores; a Habit for drinking water is a lot of chores by Friday.

Refusals switch it off. Habitica disables a webhook after ten failed calls, and a ping refused for the daily ceiling is a failed call to Habitica. If chores stop arriving, check the webhook is still enabled.

What a chore is worth

A chore is one count, priced like any other count, whatever it was: the bins and the oven earn the same, and the price is the same one a script hitting the ping page gets. A household can log twenty chores a day per link. The twenty-first answers 422, Daily ping limit exceeded., and credits nothing; Home Assistant logs that as an error on the rest_command, which is the usual way to find out. The day starts over at midnight UTC, and a second link is a second twenty.

Keep the URL to yourself

Anybody holding it can record chores as you; it is the whole of the authentication, which is what makes it usable from an automation with no login anywhere. In Home Assistant terms that means secrets.yaml and nowhere else: not in configuration.yaml, not in an exported automation, not in a shared dashboard or a screenshot. In a house, it also means the URL is not on the fridge. If it does get out, delete the connection and make another; the old URL stops working immediately.

What it looks like when it misfires

Every call is written down before the server decides anything about it, so a refused ping leaves a row the same as a credited one. Open Connections, choose the chores connection, and the recent pings list shows the last twenty: when each arrived and from where. It will not say whether toil was credited, only what arrived, which is usually the question.

  • More pings than chores. A ticked item that was unticked and ticked again, or a Habitica account scoring Habits as well as chores. The list in Home Assistant, or the task list in Habitica, says which.
  • Fewer pings than chores. The automation is pointed at the wrong list or the wrong sensor. Open the automation's trace in Home Assistant: a trigger that never fires has nothing to show, and a sensor ID copied from a renamed chore is the usual cause.
  • Pings that credit nothing. The day was already past its twenty. Home Assistant shows the same call as an error, because rest_command records anything but a success as one, so a refused count is visible from both ends.
  • Pings while nobody did anything. The URL is out in the world, or a restart is replaying something: a trigger without the not_from above counts every sensor coming back to life. If it is the URL, delete the connection, make a new one, and put the new one in secrets.yaml.