Detect an automation that silently stopped working
An automation can remain enabled and show no obvious error while its trigger never fires, a dependency is unavailable or the action fails to achieve the result. Record confirmed completion and monitor its age against when the job should have run.
The fault this project detects
This project detects a scheduled automation that has not produced and confirmed its intended result within the calibrated interval. It separates a missed trigger, unavailable dependency and unmet outcome from an ordinary period when the job is not expected.
The generic example calls a non-destructive script, waits for an independent result binary sensor, and records success in an Input datetime. Adapt it to a report, synchronisation, data refresh or other reversible job before considering higher-consequence equipment.
The evidence model
A useful alarm does not promote one noisy reading straight to an emergency. It combines measurements that fail in different ways:
- Value: the completion timestamp states when a verified run last finished.
- Direction: completion age rises continuously until new evidence resets it.
- Agreement: an independent result entity proves the action achieved something, not merely that a service call returned.
- Context and time: a Schedule helper says when a run is due, while dependency availability and a grace period explain legitimate delay.
The heartbeat is written after confirmation, not at trigger time. That distinction catches automations that start but silently fail before delivering their outcome.
Freshness is separate evidence
An entity can keep a plausible old value without becoming unavailable. Inspect last_changed on a source that should genuinely change, and note the longest gap between meaningful state changes you actually see across a normal week in your own History - that measured interval is the freshness threshold to record for your setup. If the measured process can legitimately stay flat, add a changing source heartbeat or timestamp; do not treat an unchanged temperature, level or power value as proof that the transport is alive.
Rate maths: never copy a gradient
The Trend integration expresses min_gradient in units per second. The arithmetic example from the project brief is 4 °C/hour ÷ 3600 = 0.00111 °C/s. That is an example conversion, not a threshold for this equipment. Calculate the rate from your own timestamped readings: take two points from your own History, divide the change in the measured value between them by the elapsed seconds, and that result is your site-specific gradient. A Derivative helper can instead display a friendlier per-hour unit when configured that way.
Collect this data before choosing a threshold
Use traces and timestamps from successful runs, delayed runs, Home Assistant restarts and dependency outages. Define the job's service expectation in plain language before setting a timeout.
| Measurement | Why it matters | Required value |
|---|---|---|
input_datetime.monitored_job_last_success | Confirmed completion evidence | Watch this over several normal runs and note the successful timestamps you actually see, so you know what a healthy pattern looks like on your own system. |
binary_sensor.monitored_job_result_confirmed | Independent outcome | Trace how and when this genuinely flips true on your own runs, so the confirmation window you set matches reality. |
schedule.monitored_job_check_window | When a completed run is expected | Note your own actual due window and any legitimate exceptions (maintenance, restarts) before you lock in a schedule. |
binary_sensor.monitored_job_dependency_healthy | Dependency context | Observe how this behaves through a real outage and recovery on your own setup before treating its state as reliable context. |
Automation traces | Failure location | Save your own successful and failed traces from real runs so you have a concrete comparison when something goes wrong. |
The experiment that makes this article defensible
- Run the monitored script manually and verify the result entity changes before the success timestamp is written.
- Use a safe test Toggle to simulate an unconfirmed result and confirm the automation stops before recording success.
- Restart Home Assistant inside a maintenance window and observe the check-window and timestamp behaviour.
- Disable only the test automation briefly, then confirm Advisory, Warning and recovery without involving locks, doors or safety equipment.
Before you rely on any of this, gather your own timestamped readings, note the Home Assistant version you're running, record your exact entity IDs, and save one or two History or dashboard screenshots as your evidence baseline. Do not stage an unsafe electrical, refrigeration, pressure, battery or water fault merely to make a graph.
Create the helpers
Use Settings → Devices & services → Helpers. Create the named Number or Toggle helpers before loading the templates. Where this guide uses a Derivative, Statistics, History Stats, Integral, Schedule or Utility Meter helper, the current Home Assistant interface can create it; the source links below describe the current options.
| Helper or derived entity | Purpose | Setting |
|---|---|---|
input_datetime.monitored_job_last_success | Stores the last confirmed completion | Enable this and note the date and time you turned it on, so you can tell a genuine first success from a blank starting value. |
schedule.monitored_job_check_window | Period when a completed run is due | Build this from the actual due window you recorded for your own job in the data-collection step above. |
input_number.automation_watchdog_max_silence_hours | Maximum age after a run should exist | Set this in hours, based on the longest gap between successful runs you saw was still normal on your own system. |
input_number.automation_watchdog_confirmation_minutes | Time allowed for independent result | Set this in minutes, based on how long the independent result genuinely took to confirm during your own test runs. |
input_number.automation_watchdog_hold_minutes | Persistence before Warning | Set this in minutes, long enough to ride out your own job's normal delays without a false Warning. |
input_boolean.automation_watchdog_monitoring_enabled | Master notification gate | Leave this off while you test the automation against your own readings, then switch it on once you trust the thresholds. |
input_number.automation_watchdog_critical_repeat_minutes | Critical repeat spacing | Choose a repeat spacing greater than zero based on how often you want a critical alert repeated for your own household. |
Current Home Assistant YAML
Merge top-level keys with your existing configuration instead of duplicating them. Replace the example entity IDs consistently. If you keep automations in automations.yaml, paste only the automation list items there, without a top-level automation: wrapper.
Record success only after the result is confirmed
Replace the script and confirmation entity with a safe, real job. The timeout deliberately stops the sequence, so the success timestamp is not updated when confirmation never arrives.
automation:
- id: monitored_job_with_confirmed_heartbeat
alias: "Monitored job - Run and record confirmed success"
mode: single
triggers:
- trigger: state
entity_id: schedule.monitored_job_run_window
to: "on"
conditions:
- condition: state
entity_id: binary_sensor.monitored_job_dependency_healthy
state: "on"
actions:
- action: script.monitored_job
- wait_template: >-
{{ is_state('binary_sensor.monitored_job_result_confirmed', 'on') }}
timeout:
minutes: >-
{{ states('input_number.automation_watchdog_confirmation_minutes') | int(0) }}
continue_on_timeout: false
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.monitored_job_last_success
data:
timestamp: "{{ as_timestamp(now()) }}"Alarm on stale confirmed completion, not enabled state
Initialise the Input datetime with one known successful run before enabling alerts. The age sensor updates each minute because it uses now().
template:
- sensor:
- name: "Monitored job success age"
unique_id: monitored_job_success_age
default_entity_id: sensor.monitored_job_success_age
device_class: duration
state_class: measurement
unit_of_measurement: "h"
availability: >
{{ states('input_datetime.monitored_job_last_success')
not in ['unknown', 'unavailable', 'none'] }}
state: >
{{ ((as_timestamp(now())
- as_timestamp(states('input_datetime.monitored_job_last_success'))) / 3600) | round(2) }}
- binary_sensor:
- name: "Automation watchdog warning"
unique_id: automation_watchdog_warning
default_entity_id: binary_sensor.automation_watchdog_warning
device_class: problem
delay_on:
minutes: >
{{ states('input_number.automation_watchdog_hold_minutes') | int(0) }}
availability: >
{{ has_value('sensor.monitored_job_success_age') }}
state: >
{{ is_state('schedule.monitored_job_check_window', 'on')
and (states('sensor.monitored_job_success_age') | float)
>= (states('input_number.automation_watchdog_max_silence_hours') | float) }}
attributes:
reason: >-
Last confirmed success is
{{ states('sensor.monitored_job_success_age') }} hours old;
result is {{ states('binary_sensor.monitored_job_result_confirmed') }} and
dependency is {{ states('binary_sensor.monitored_job_dependency_healthy') }}.
- name: "Automation watchdog critical"
unique_id: automation_watchdog_critical
default_entity_id: binary_sensor.automation_watchdog_critical
device_class: problem
state: >
{{ is_state('binary_sensor.automation_watchdog_warning', 'on')
and is_state('binary_sensor.monitored_job_dependency_healthy', 'on')
and is_state('binary_sensor.monitored_job_result_confirmed', 'off') }}Warning and Critical notification actions
Replace the mobile notification action and media-player entity with your own. Put critical-monitoring-alert.mp3 in /media, test it manually, and leave the monitoring Toggle off until calibration is complete.
automation:
- id: automation_watchdog_warning_push
alias: "Automation watchdog - Warning push"
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.automation_watchdog_warning
to: "on"
conditions:
- condition: state
entity_id: input_boolean.automation_watchdog_monitoring_enabled
state: "on"
actions:
- action: notify.mobile_app_your_phone
data:
title: "Automation watchdog warning"
message: >-
{{ state_attr('binary_sensor.automation_watchdog_warning', 'reason')
or 'The warning condition is active. Check Home Assistant for evidence.' }}
- id: automation_watchdog_critical_repeat
alias: "Automation watchdog - Critical repeat"
mode: restart
triggers:
- trigger: state
entity_id: binary_sensor.automation_watchdog_critical
to: "on"
conditions:
- condition: state
entity_id: input_boolean.automation_watchdog_monitoring_enabled
state: "on"
actions:
- action: notify.mobile_app_your_phone
data:
title: "Automation watchdog critical"
message: "The critical condition is active. Check the equipment and evidence now."
- action: media_player.play_media
target:
entity_id: media_player.alert_speaker
data:
media_content_id: "media-source://media_source/local/critical-monitoring-alert.mp3"
media_content_type: "audio/mpeg"
- repeat:
while:
- condition: state
entity_id: binary_sensor.automation_watchdog_critical
state: "on"
- condition: template
value_template: >-
{{ states('input_number.automation_watchdog_critical_repeat_minutes') | int(0) > 0 }}
sequence:
- delay:
minutes: >-
{{ states('input_number.automation_watchdog_critical_repeat_minutes') | int(0) }}
- condition: state
entity_id: binary_sensor.automation_watchdog_critical
state: "on"
- action: notify.mobile_app_your_phone
data:
title: "Automation watchdog still critical"
message: "The critical condition remains active."
- action: media_player.play_media
target:
entity_id: media_player.alert_speaker
data:
media_content_id: "media-source://media_source/local/critical-monitoring-alert.mp3"
media_content_type: "audio/mpeg"
Why the logic works
The Schedule establishes obligation, the timestamp measures silence, the result entity confirms real-world outcome and dependency state explains why a run may have been skipped. The monitor therefore catches silent non-performance rather than merely inspecting whether an automation is enabled.
The design is intentionally diagnostic rather than controlling. It reports an impossible or abnormal relationship; it does not bypass equipment protection or decide that a single suspected cause is proven.
Severity tiers without alert fatigue
| Tier | Meaning | Action |
|---|---|---|
| Advisory | Completion is later than its recent normal time or a dependency is temporarily unavailable. | Dashboard only |
| Warning | No confirmed completion exists within the calibrated interval during the expected check window. | One push notification |
| Critical | Completion is overdue, dependencies report healthy and the independent intended result remains unmet. | Push, repeat and audible alert |
A weak clue remains visible without interrupting you. A Warning requires persistence or corroboration. Critical is reserved for direct danger or several independent signals. That separation is what stops a useful monitoring system becoming notification wallpaper.
Troubleshooting
- Success is recorded even when the job fails
- Move the Input datetime action after an independent confirmation step; a returned service call is not sufficient proof.
- The watchdog fires before the scheduled run
- Make the check window begin after the run and normal completion grace period.
- The timestamp is unavailable
- Create the Input datetime with date and time, then initialise it only after a known successful test.
- Restarts interrupt the wait
- Choose an automation mode and design that tolerates restart, then validate with a controlled restart and inspect traces.
- A dependency outage causes Critical
- Require the independent dependency to be healthy for Critical and report dependency failure as its own diagnosis.
Safety and limits
Do not use this generic pattern to retry locks, garage doors, pumps, heaters, mains switching or other consequential actions blindly. Make retries idempotent, confirm physical outcomes independently, and use professional risk review where failure can cause harm.
If a webhook is added later, treat its webhook_id like a password, keep local_only: true unless a reviewed design requires otherwise, and never use an unauthenticated webhook for locks, garage doors, power isolation or anything destructive.
Official sources
These are the primary documentation pages used for the design. Check them against the Home Assistant or ESPHome version you are actually running.
Related Smart Home Fix guides
- Fix unreliable entities before trusting alarms
- Separate smart-home devices without breaking discovery
- Reduce false alarms without hiding real faults
FAQ
Does an enabled automation mean it is working?
No. Its trigger may not fire, a condition may block it, a dependency may be unavailable or the action may not achieve the intended result.
Why record success after confirmation?
Recording at trigger time only proves that the automation started. A later independent check proves the useful outcome occurred.
What should the confirmation entity be?
Use evidence independent of the command where possible: a changed reading, completed file, state from the destination or another measurable result.
Will the watchdog survive a Home Assistant restart?
The Input datetime persists, but running waits and dependencies can behave differently through restart. Test the complete path on your installed version.
Should a failed job be retried automatically?
Only when the action is demonstrably safe and idempotent. This guide alerts by default because a blind retry can duplicate or escalate the original action.