Most automation platforms give you triggers that fire on a transition: when a value crosses a threshold, when a state changes, when something has been true for a duration. These are edge triggers, and they are the natural way to express intent. They are also, strictly speaking, opinions held in RAM, and RAM’s loyalty ends at the next restart.
An edge trigger is a promise the system makes to itself at a particular moment, and like most promises made at a particular moment it does not survive a restart, a reload, or a Tuesday. A level trigger is not a promise. It is a fact that keeps being true until it is not. Almost every bug on this site can be summarised as somebody trusting a promise.
They also share a property the documentation mentions approximately never: the pending timer lives in memory. Reload your automations, restart the process, or let the entity blink to unavailable for one second, and the countdown silently starts again, or never arms at all.
Why this bites precisely when it matters
Consider a safety cut-off: "if this heater has been on for four hours, switch it off". Written as a state trigger with a four-hour duration, it works perfectly in testing.
Then reality:
- The heater's plug flickers to unavailable for a moment, a mesh hiccup, not a real change. The timer restarts from zero.
- You reload automations to fix something unrelated. The timer is gone entirely; it will never fire for the currently-running heater.
- You restart during an incident, because of course you do, that is what one does during an incident. Every pending timer resets, including all the ones that were seconds away from protecting you.
The fix: store a timestamp, compare against the clock
Instead of asking the platform to remember a countdown, record when the condition began, and re-evaluate periodically against the current time.
on genuine off→on edge: store start_time = now
on off: clear start_time
every 5 minutes: if start_time is set
and (now − start_time) > limit
→ act
This is level-triggered thinking. It survives restarts, reloads and unavailability blips, because the state lives in persistent storage rather than in a scheduler.
Guard the edge itself
When you latch the start time, ignore transitions that came from unknown or unavailable, otherwise a device reconnecting looks like a fresh start and resets your clock. The same guard prevents a restart from producing a spurious "it just turned on".
Keep both triggers
The edge trigger is still worth having: it reacts immediately. Add the periodic level check beside it, sharing the same action, and let the condition decide. You get fast response in the normal case and correctness in the abnormal one.
triggers:
- state change → id: edge # fast path
- every 10 min → id: level # restart-proof path
- on startup → id: level
condition: the situation is genuinely true right now,
measured from a stored timestamp, not from a timer
Then stop the periodic path from nagging
A level trigger will happily re-fire every cycle while the condition persists. Add a cooldown that applies only to the periodic path, so the edge still fires immediately but the repeat is bounded.
The same defect in threshold alerts
Numeric threshold triggers ("above 90%") are edges too. If your disk is already at 94% when the system starts, no crossing occurs and the alert never fires, the exact scenario where you restarted because the disk was filling. Derive the tier from the current value on a schedule instead of waiting for a crossing that already happened.
If you take one sentence from this page, take this one: the state of the world is not an event, and a house that only listens for events will sleep through most of the world.