A notification that says "the garage door has been open for 20 minutes" is useful. One with a Close button under it is better, because you can deal with the problem from the lock screen without opening a dashboard. The Home Assistant Companion app for iOS and Android supports exactly that: buttons on a notification that send an event back to Home Assistant when tapped. An automation listens for that event and does the work.
The YAML for the buttons is short. Most of the effort goes into the wiring behind them, so that a tap on an old notification does not trigger the wrong automation, a run that nobody answers does not block the next one, and stale notifications disappear from the phone once they no longer apply.
What you need
- The Home Assistant Companion app installed and logged in on each phone, with notification permission granted during setup.
- The notify action for each phone. The app registers it as
notify.mobile_app_followed by the device ID, which defaults to the phone's name from the system settings, with spaces and other non-alphanumeric characters replaced by underscores. The Companion app menu in Home Assistant's settings shows the exact ID.
Every example below uses notify.mobile_app_your_phone. Replace it with your own.
The basic actionable notification
Buttons go into an actions list inside the inner data block:
action: notify.mobile_app_your_phone
data:
title: "Garage door"
message: "The garage door has been open for 20 minutes."
data:
actions:
- action: "CLOSE_GARAGE"
title: "Close"
- action: "IGNORE_GARAGE"
title: "Leave it open"
action is the identifier Home Assistant receives when the button is tapped, and title is the label on the button. When someone taps Close, Home Assistant fires a mobile_app_notification_action event with action: CLOSE_GARAGE in its data. Nothing else happens until an automation reacts to it.
The number of buttons is limited by the phone. Android shows at most three. iOS accepts more, but the Companion docs say the system UI starts having scrolling problems at around ten. Plan for three if anyone in the household uses Android.
The simple way to handle a tap
The quickest approach is a separate automation that listens for the identifier:
alias: "Garage: close from notification"
triggers:
- trigger: event
event_type: mobile_app_notification_action
event_data:
action: "CLOSE_GARAGE"
actions:
- action: cover.close_cover
target:
entity_id: cover.garage_door
This works, and for a button that always means the same thing, such as a shared SILENCE action for every alarm notification, it is the right pattern. The docs call it a catch-all trigger.
It has one weakness. The identifier is shared by every notification that ever used it. If the phone still shows yesterday's garage notification and someone taps Close on it today, the automation closes the door just the same. For a garage door or a lock that can be a nasty surprise.
Unique identifiers and waiting for an answer
The Companion docs recommend building the identifiers from the run's context ID, so that each notification gets its own buttons, and then waiting for the answer in the same automation. The pattern looks like this:
alias: "Garage: ask to close when left open"
mode: parallel
triggers:
- trigger: state
entity_id: cover.garage_door
to: "open"
for: "00:20:00"
actions:
- variables:
action_close: "{{ 'CLOSE_' ~ context.id }}"
action_ignore: "{{ 'IGNORE_' ~ context.id }}"
notification_tag: "garage-open"
- action: notify.mobile_app_your_phone
data:
title: "Garage door"
message: "The garage door has been open for 20 minutes."
data:
tag: "{{ notification_tag }}"
actions:
- action: "{{ action_close }}"
title: "Close"
- action: "{{ action_ignore }}"
title: "Leave it open"
- wait_for_trigger:
- trigger: event
event_type: mobile_app_notification_action
event_data:
action: "{{ action_close }}"
- trigger: event
event_type: mobile_app_notification_action
event_data:
action: "{{ action_ignore }}"
- trigger: state
entity_id: cover.garage_door
to: "closed"
timeout: "01:00:00"
- if:
- "{{ wait.trigger is not none and wait.trigger.platform == 'event' and wait.trigger.event.data.action == action_close }}"
then:
- action: cover.close_cover
target:
entity_id: cover.garage_door
- action: notify.mobile_app_your_phone
data:
message: "clear_notification"
data:
tag: "{{ notification_tag }}"
Here is what each part does.
The identifiers. context.id is different for every run, so CLOSE_ plus that ID only matches the buttons of this one notification. A tap on an older notification sends an identifier that nothing is waiting for, and nothing happens.
The wait. wait_for_trigger pauses the run until one of its triggers fires. Three things can end it: either button, or the door closing some other way (by remote, from the wall button, from a dashboard). Without the third trigger the run would keep waiting for a tap that no longer makes sense.
The timeout. If nobody answers within an hour, the wait ends anyway. continue_on_timeout defaults to true, so the run continues, wait.trigger is none, and the if does nothing. Checking wait.trigger is not none first keeps the template from failing on that path.
The cleanup. The last step sends the special message clear_notification with the same tag, which removes the notification from the phone whatever ended the wait. The docs warn that clearing may need the app to have been used recently: this applies to all iOS notifications and to Android notifications that are not marked as critical. Treat clearing as best effort.
Pick the automation mode on purpose
An automation that waits for a reply is running for the whole wait. That interacts with the mode:
- single (the default): if the trigger fires again during the wait, the new run does not start and Home Assistant logs a warning. For "the door has been open for 20 minutes" that is usually fine, because the door cannot open again while it is still open.
- restart: a new trigger stops the waiting run. The buttons on the first notification then point at a run that no longer exists, so tapping them does nothing.
- parallel or queued: every trigger gets its own run. The default limit is 10 runs. The Companion docs suggest parallel mode together with timeouts and clearing, which is what the example above uses.
There is one more limit to plan for. A Home Assistant restart stops every automation that is waiting, and they do not resume afterwards. A notification sent before an update or a restart can still be on the phone, but its buttons go nowhere. If a button controls something important, re-send the question after a restart or put the same control on a dashboard.
Buttons that open something instead of running an action
A button can also open a page. Set action to URI and add a uri:
actions:
- action: "URI"
title: "Show cameras"
uri: "/lovelace/cameras"
A relative path opens a view in Home Assistant: /lovelace/view-path for the default dashboard, or /dashboard-url/view-path for another one. On Android, entityId:light.kitchen opens the More Info dialog of an entity, app:// followed by a package name opens another app, and deep-link://tel:... opens the dialer. On iOS, ordinary URLs such as tel: and https:// work. Android requires the action to be exactly URI for any of these to work.
To choose what happens when someone taps the notification itself rather than a button, use url (iOS) and clickAction (Android) in the inner data block.
Safety switches for sensitive buttons
Anyone who can see the lock screen can tap a button on it. For an Unlock or Disarm button that matters.
actions:
- action: "{{ action_unlock }}"
title: "Unlock"
authenticationRequired: true
- action: "{{ action_ignore }}"
title: "Ignore"
destructive: true
authenticationRequired: true means the phone has to be unlocked before the action runs. On Android this needs Android 12 or newer, while iOS supports it on all versions. destructive: true is iOS only and shows the title in red, which is useful as a visual warning on buttons such as Sound alarm.
You can also look at who tapped. The event's context includes a user_id, so in the template wait.trigger.event.context.user_id tells you which Home Assistant user answered. That is useful for logging, or for ignoring a tap from a user who should not open the front door.
Asking for a text reply
Setting the identifier to REPLY, or adding behavior: textInput to any button, makes the phone ask for text when the button is tapped. The text arrives in the event as reply_text:
actions:
- action: "{{ action_note }}"
title: "Add a note"
behavior: textInput
After the wait, wait.trigger.event.data.reply_text contains what was typed. It is handy for things like adding an item to a shopping list without opening the app.
Send to more than one phone
Each phone has its own notify action. To ask the whole household, either call several notify actions one after another or define a notify group in configuration.yaml, as the Companion docs show:
notify:
- name: all_phones
platform: group
services:
- action: mobile_app_phone_one
- action: mobile_app_phone_two
With unique identifiers, the first person who taps a button wins, because the wait ends on the first event. The clear_notification step then has to go to each phone as well, so send it through the same group.
Replacing and grouping notifications
Two more keys keep the notification list tidy:
tagreplaces an earlier notification with the same tag instead of adding another one. A sensor that keeps reporting "still open" then shows one up-to-date notification. iOS does not replace critical notifications.groupstacks related notifications together. The docs advise against reusing the sametagin different groups.
On Android, timeout in the inner data block removes the notification by itself after the given number of seconds. That is a simple way to keep old questions from lingering if the automation's own timeout is shorter than the time you expect someone to look at the phone.
Testing before you rely on it
- Send the notification from Settings > Tools > Actions to confirm the buttons appear on each phone.
- Open Settings > Tools > Events, subscribe to
mobile_app_notification_action, tap a button and look at the event data. That shows exactly what your trigger has to match. - Run the full automation and check the trace for each path: button tapped, door closed manually and timeout. All three should end with the notification cleared.
- Tap a button on an old notification after the run has ended. Nothing should happen.