Most Home Assistant users start with notifications directly inside automations. It is simple, it works, and it feels very “native”: you pick one notify target and send a message. The catch is that you need to specify a notification target for every single device you want to reach. A typical action looks like this:
action: notify.mobile_app_MyPhone
metadata: {}
data:
message: test message
title: test titleThat is totally fine if you only have one phone … or one user. But once you have a private phone, a work phone, maybe a tablet, and then a partner and kids with their own devices, it turns into repetition fast. You end up copying the same block into lots of automations. And when a device changes (new phone, renamed device, reinstalled companion app, new tablet), you have to hunt down every single automation that contains that notify action. Tagging automations with the tag “notification” helps, but it is still busywork.
The core idea of this post is simple: treat notifications like a shared service in your Home Assistant setup. Your automations should not need to know which devices exist. Automations should only decide “who should get notified and what should it say”. The “how” (which devices, how many, and in what structure) should be outsourced to central scripts that you can update in one place.
Why scripts are the sweet spot here is that Home Assistant scripts can accept parameters (“fields”), they are reusable, and they let you keep your automations clean. Instead of every automation doing its own device targeting, you define notification scripts once and just call them everywhere.
A practical pattern is “one script per person”. Each script sends the same notification to all devices that belong to that person. For example, a “Notify Peter” script might send to a phone and a tablet. You pass in the notification text, optionally a title, and optionally metadata in a data object.
Here is the exact kind of script setup you described, with the important twist that makes it robust:
sequence:
- variables:
_data: "{{ data | default({}, true) }}"
- action: notify.mobile_app_MyPhone
metadata: {}
data:
message: "{{notification}}"
title: "{{title}}"
data: "{{_data}}"
- action: notify.mobile_app_MyIPad
metadata: {}
data:
message: "{{notification}}"
title: "{{title}}"
data: "{{_data}}"
fields:
notification:
selector:
text: null
name: notification
required: true
title:
selector:
text: null
name: title
required: false
data:
selector:
object: null
name: data
required: false
alias: Notify Peter
description: ""
icon: mdi:bell-alert-outlineLet’s unpack what is going on here, because there are two valuable design decisions hidden in this “simple” script.
First, the script takes fields. This is what turns it from a one-off helper into a reusable interface. Your automations can call it like “notify Peter with this message and this title”, without caring about the device list. When your devices change, you edit only this script.
Second, the script makes sure the data object is always a dictionary. This matters because Home Assistant’s notify services support a data field that you can use for things like tags, channels, sticky notifications, click actions, grouping, images, and so on. On mobile app targets, data is often where the fun features live. But there is a gotcha: if you pass data: null (or if a script parameter ends up being None), Home Assistant will complain because it expects a dictionary. The error you saw is typical:
“expected dict for dictionary value @ data['data']. Got None”
That is why this line is gold:
_data: "{{ data | default({}, true) }}"This forces _data to be {} when data is missing, empty, or null. In other words, your notification script always sends a valid dictionary, even if you do not want to attach any metadata at all.
This has two practical benefits:
One, you can call the script with just a message and nothing else. No title, no data, no drama. Your automations stay minimal when they should be minimal.
Two, you can still use advanced metadata when you need it. The same script supports both “quick message” and “rich mobile notification” styles, without creating separate scripts or conditional branches.
There is also a subtle third benefit: it creates a stable contract. Every automation calling script.notify_peter knows it can send message text and optionally provide extra notification options. The script handles the details.
Now let’s see what it looks like in an automation. Instead of writing multiple notify actions for each device, you do one call:
action:
- action: script.notify_peter
data:
notification: "The front door has been open for 10 minutes."
title: "Door alert"If Peter replaces his phone, you do not need to edit this automation. You edit the “Notify Peter” script. That is the whole point.
Once you have “one script per person”, you can go one step further: define group scripts. These are scripts that notify multiple people at once by calling the individual scripts.
A common group is “the entire family”. Here is the pattern you described:
sequence:
- parallel:
- action: script.notify_peter
metadata: {}
data:
notification: "{{notification}}"
title: "{{title}}"
data: "{{data}}"
- action: script.notify_wife
metadata: {}
data:
notification: "{{notification}}"
title: "{{title}}"
data: "{{data}}"
- action: script.notify_daughter
metadata: {}
data:
notification: "{{notification}}"
title: "{{title}}"
data: "{{data}}"
fields:
notification:
selector:
text: null
name: notification
required: true
title:
selector:
text: null
name: title
data:
selector:
object: null
name: data
alias: Notify Family
description: ""
icon: mdi:bell-alert-outlineA few things to appreciate here:
Using parallel is a nice touch. It prevents the “chain effect” where one slow notification blocks the others. If one phone is offline or one notify call takes longer, the rest still go out immediately.
Also, notice how this script does not care which devices exist. It does not contain notify.mobile_app_* at all. It only calls scripts. That makes it much easier to maintain and extend. If you add a new family member or remove a device, you do it in the relevant “person” script. The “Notify Family” script stays stable.
Now, the earlier “data must be a dictionary” lesson applies here too. In your per-person script you normalized data into _data. In the family script you pass data: "{{data}}". That is fine because each person script will normalize it again. This is a good example of defensive design: the group script stays simple, and the person scripts guarantee correct formatting for the actual notify services.
At this point you have something that feels like a tiny notification framework:
- Automations decide “who” and “what”.
- Person scripts decide “which devices”.
- Group scripts compose person scripts.
That structure scales surprisingly far.
Here are a few patterns that usually become easy once you have this foundation:
One: Targeted group notifications. “Notify Parents” (just the adults). “Notify Everyone Except Daughter”. “Notify Whoever Is Home”. You can create these as additional group scripts without touching the underlying device targeting. If you ever refactor device lists, the group scripts do not need to change.
Two: Fewer mistakes. When you manually copy notification YAML between automations, you eventually forget a title somewhere, or you include a data block that is null, or you target the wrong device service. Centralizing reduces that whole class of errors.
Three: Easier migrations. If one day you swap from mobile app notifications to another notify target, your automations can remain exactly the same. You would replace the internals of the scripts. That is the kind of future-proofing that feels boring until you need it, and then it feels like a small miracle.
There is also a human aspect that is worth calling out. Notifications are part of household culture. Some people like frequent updates, some people hate them. Kids might need different phrasing than adults. And your own tolerance changes over time. With scripts, it becomes easy to tune that experience without rewriting logic everywhere. You can change the “Notify Daughter” script to use a calmer title style, or add different channels, or reduce priority, while keeping your automations identical.
The bigger point is that your scripts act like an API. If you treat them that way, you will naturally add guardrails, defaults, and predictable behavior. And that is exactly what makes Home Assistant setups feel “engineered” rather than “grown”.
To wrap it up, the end result is that you go from this:
- Every automation contains notify actions.
- Every device requires its own notify target.
- Device changes mean editing many automations.
To this:
- Automations call scripts.
- Scripts know which devices exist.
- Device changes mean editing one script per person.
If you have ever replaced a phone and spent an evening fixing broken notifications across a dozen automations, you already know why this is worth it.
And if you have not hit that pain yet, this is one of those patterns that you will be very happy to have in place before you do.