Skip to main content

Sending Toast Notifications in Windows 10 and 11

· 9 min read

This post uses code in part from the SMSAgent Blog.

This post takes a snippet from the SMSAgent Blog and adds some additional magic along with two new custom functions.

If you're a Windows 10 or 11 user you'll be familiar with the toast notifications that appear in the bottom right of your screen. These are a great way to get a quick message to the user without interrupting what they're doing. In this article we'll look at how to send a toast notification from PowerShell.

We could use the excellent BurntToast PowerShell module to send a toast notification, but in the interests of reducing the number of third-party modules installed on client machines we'll be using the underlying .NET APIs directly as our needs are fairly simple.

Sending toast notifications is fairly simple once you get to grips with the underlying XML schema but we want our Toasts to be next-level so we're going to make them beautiful by registering a custom Notification App with Windows so we can customise the App Name and Icon which appear.

Creating a Notification App

This script takes a few parameters to create a Notification App. The App Name and Icon are the most important as these are what will appear in the toast notification.

Parameter documentation follows:

ParameterTypeDescription
IconURIURIThe URI of the app icon to use for the notification app registration. This will be downloaded to the working directory.
IconFileNameStringFile name to use for the app icon. Optional. If not specified, the file name from the URI will be used.
WorkingDirectoryDirectoryInfoThe working directory to use for the app icon. If not specified, 'C:\RMM\NotificationApp\' will be used.
AppIdStringThe app ID to use for the notification app registration. Expected format is something like: 'CompanyName.AppName'.
AppDisplayNameStringThe app display name to use for the notification app registration.
AppIconBackgroundColorStringThe background color to use for the app icon. Optional. If not specified, the background color will be transparent. Expected format is a hex value like 'FF000000' or '0' for transparent.
ShowInSettingsIntShow the app in the Windows Settings app. Optional. If not specified, the app will not be shown in the Settings app.

The Script

#requires -RunAsAdministrator
[CmdletBinding()]
Param(
# The URI of the app icon to use for the notification app registration.
[Parameter(Mandatory)]
[uri]$IconURI,
# File name to use for the app icon. Optional. If not specified, the file name from the URI will be used.
[string]$IconFileName,
# The working directory to use for the app icon. If not specified, 'C:\RMM\NotificationApp\' will be used.
[System.IO.DirectoryInfo]$WorkingDirectory = 'C:\RMM\NotificationApp\',
# The app ID to use for the notification app registration. Expected format is something like: 'CompanyName.AppName'.
[Parameter(Mandatory)]
[string]$AppId,
# The app display name to use for the notification app registration.
[Parameter(Mandatory)]
[string]$AppDisplayName,
# The background color to use for the app icon. Optional. If not specified, the background color will be transparent. Expected format is a hex value like 'FF000000' or '0' for transparent.
[ValidatePattern('^(0)$|^([A-F0-9]{8})$')]
[string]$AppIconBackgroundColor = 0,
# Whether or not to show the app in the Windows Settings app. Optional. If not specified, the app will not be shown in the Settings app. Expected values are 0 or 1 (0 = false, 1 = true).
[int]$ShowInSettings = 0
)
# Functions
function Get-NotificationApp {
<#
.SYNOPSIS
Gets the notification app registration information.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[string]$AppId
)
$HKCR = Get-PSDrive -Name HKCR -ErrorAction SilentlyContinue
If (!($HKCR)) {
$null = New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT -Scope Script
}
$AppRegPath = 'HKCR:\AppUserModelId'
$RegPath = "$AppRegPath\$AppID"
If (!(Test-Path $RegPath)) {
return $null
} else {
$NotificationApp = Get-Item -Path $RegPath
return $NotificationApp
}
}
Function Register-NotificationApp {
<#
.SYNOPSIS
Registers an application to receive toast notifications.
.NOTES
Original Author: Trevor Jones
Original Author Link: https://smsagent.blog/author/trevandju/
Version: 2.0
Version Date: 2023-01-17
Version Description: Added AppIcon and AppIconBackground parameters.
Version Author: Mikey O'Toole
Version: 1.0
Version Date: 2020-10-20
Version Description: Initial release by Trevor Jones.
.LINK
https://smsagent.blog/2020/10/20/adding-your-own-caller-app-for-custom-windows-10-toast-notifications/
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[string]$AppId,
[Parameter(Mandatory)]
[string]$AppDisplayName,
[System.IO.FileInfo]$AppIcon = $null,
[ValidatePattern('^(0)$|^([A-F0-9]{8})$')]
[string]$AppIconBackgroundColor = $null,
[int]$ShowInSettings = 0
)
$HKCR = Get-PSDrive -Name HKCR -ErrorAction SilentlyContinue
If (!($HKCR)) {
$null = New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT -Scope Script
}
$AppRegPath = 'HKCR:\AppUserModelId'
$RegPath = "$AppRegPath\$AppId"
If (!(Test-Path $RegPath)) {
$null = New-Item -Path $AppRegPath -Name $AppId -Force
}
$DisplayName = Get-ItemProperty -Path $RegPath -Name DisplayName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DisplayName -ErrorAction SilentlyContinue
If ($DisplayName -ne $AppDisplayName) {
$null = New-ItemProperty -Path $RegPath -Name DisplayName -Value $AppDisplayName -PropertyType String -Force
}
$Icon = Get-ItemProperty -Path $RegPath -Name IconUri -ErrorAction SilentlyContinue | Select-Object -ExpandProperty IconUri -ErrorAction SilentlyContinue
if ($Icon -ne $AppIcon) {
$null = New-ItemProperty -Path $RegPath -Name IconUri -Value $AppIcon -PropertyType String -Force
}
$BackgroundColor = Get-ItemProperty -Path $RegPath -Name IconBackgroundColor -ErrorAction SilentlyContinue | Select-Object -ExpandProperty IconBackgroundColor -ErrorAction SilentlyContinue
if ($BackgroundColor -ne $AppIconBackgroundColor) {
$null = New-ItemProperty -Path $RegPath -Name IconBackgroundColor -Value $AppIconBackgroundColor -PropertyType String -Force
}
$ShowInSettingsValue = Get-ItemProperty -Path $RegPath -Name ShowInSettings -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ShowInSettings -ErrorAction SilentlyContinue
If ($ShowInSettingsValue -ne $ShowInSettings) {
$null = New-ItemProperty -Path $RegPath -Name ShowInSettings -Value $ShowInSettings -PropertyType DWORD -Force
}
$null = Remove-PSDrive -Name HKCR -Force
}
function Get-AppIcon {
<#
.SYNOPSIS
Downloads the app icon from a URI and saves it to a file.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[uri]$IconURI,
[string]$IconFileName = $null,
[Parameter(Mandatory)]
[System.IO.DirectoryInfo]$WorkingDirectory
)
if (!($WorkingDirectory.Exists)) {
$WorkingDirectory.Create()
}
if (!($IconFileName)) {
$IconFileName = $IconURI.Segments[-1]
}
$IconFilePath = Join-Path -Path $WorkingDirectory.FullName -ChildPath $IconFileName
$IconFile = New-Object System.IO.FileInfo $IconFilePath
If ($IconFile.Exists) {
$IconFile.Delete()
}
Invoke-WebRequest -Uri $IconURI -OutFile $IconFile.FullName | Out-Null
return $IconFile.FullName
}
# Main Script
$AppId = $AppId.TrimStart('"').TrimEnd('"')
$AppIcon = Get-AppIcon -IconURI $IconURI -WorkingDirectory $WorkingDirectory
$NotificationAppParams = @{
AppID = $AppId
AppDisplayName = $AppDisplayName
AppIcon = $AppIcon
AppIconBackgroundColor = $AppIconBackgroundColor
}
if ($ShowInSettings) {
$NotificationAppParams.Add('ShowInSettings', $ShowInSettings)
}
Register-NotificationApp @NotificationAppParams
$NotificationApp = Get-NotificationApp -AppID $AppId
if (!($NotificationApp)) {
Write-Error 'Failed to register the notification app.'
Exit 1
} else {
Write-Output ('Successfully registered the notification app {0}.' -f $NotificationApp.GetValue('DisplayName'))
Exit 0
}

Generic Example

# Setup parameter hashtable.
$NotificationAppParams = @{
IconURI = 'https://homotechsual.dev/img/Icon.png'
IconFileName = 'homotechsual.png'
WorkingDirectory = 'C:\RMM\NotificationApp'
AppId = 'homotechsual.example'
AppDisplayName = 'Homotechsual Example'
AppIconBackgroundColor = 0
ShowInSettings = 1
}
Register-NotificationApp @NotificationAppParams

RMM Example

Unsurprisingly, I'm going to use NinjaOne as the example RMM here. So I've done the donkeywork that is best documented by NinjaOne themselves on the Dojo and added the script above to the NinjaOne Script Library - but what next?

Option 1: Same App for All Customers

Well the chances are that you want to use the same relatively small set of apps per customer so we'll just store a nice blob of text in the Ninja Script Library.

We could end up with a "Preset Parameter" set like this:

-IconURI https://homotechsual.dev/img/Icon.png -IconFileName homotechsual.png -WorkingDirectory C:\RMM\NotificationApp -AppId homotechsual.example -AppDisplayName "Homotechsual Example" -AppIconBackgroundColor 0 -ShowInSettings 1

You can have multiple preset parameter sets and simply select the one you want to use when you run the script or when you schedule it against a group (or however you want to run this) but what about getting more creative?

Option 2: Pull App Details per Customer

We can use Documentation fields to store the details of the app per client and then use the NinjaOne CLI to pull the details when we run the script. This is a little more work but it's worth it if you want to brand your notifications per customer.

We'll need to add a few fields to the NinjaOne Documentation tab for each client:

FieldTypeDescription
NotificationIconURIURLThe URI of the app icon to use for the notification app registration. This will be downloaded to the working directory.
NotificationAppIdStringThe app ID to use for the notification app registration. Expected format is something like: 'CompanyName.AppName'.
NotificationAppDisplayNameStringThe app display name to use for the notification app registration.
NotificationAppBackgroundColorStringThe background color to use for the app icon. Optional. If not specified, the background color will be transparent. Expected format is a hex value like 'FF000000' or '0' for transparent.

We can then use the NinjaOne CLI to pull the details for the client we're running the script against and use them to create the parameter set for the script.

This means editing our script a little to accept the parameters from the CLI and then using the NinjaOne CLI to pull the details from the Documentation tab. We're going to assume you called your Document Template Example Template and your fields named as above in the table (with spaces between words if you wish).

At the top of the script we're going to replace the entire parameter block:

[CmdletBinding()]
Param(
# The URI of the app icon to use for the notification app registration.
[Parameter(Mandatory)]
[uri]$IconURI,
# File name to use for the app icon. Optional. If not specified, the file name from the URI will be used.
[string]$IconFileName,
# The working directory to use for the app icon. If not specified, 'C:\RMM\NotificationApp\' will be used.
[System.IO.DirectoryInfo]$WorkingDirectory = 'C:\RMM\NotificationApp\',
# The app ID to use for the notification app registration. Expected format is something like: 'CompanyName.AppName'.
[Parameter(Mandatory)]
[string]$AppId,
# The app display name to use for the notification app registration.
[Parameter(Mandatory)]
[string]$AppDisplayName,
# The background color to use for the app icon. Optional. If not specified, the background color will be transparent. Expected format is a hex value like 'FF000000' or '0' for transparent.
[ValidatePattern('^(0)$|^([A-F0-9]{8})$')]
[string]$AppIconBackgroundColor = 0,
# Whether or not to show the app in the Windows Settings app. Optional. If not specified, the app will not be shown in the Settings app. Expected values are 0 or 1 (0 = false, 1 = true).
[int]$ShowInSettings = 0
)

Well that's setting up the notification app registration. Now we need to actually send a notification using the app.

Sending a Notification

So this is an entirely separate script - you can use it with the custom app you just created or you can use it with the default where notifications come from Windows PowerShell. It's up to you.

"Runs as User"

This script runs in the user's context it is unlikely it'll work running as Administrator or as a scheduled task. If you want to run it as a scheduled task, you'll need to use something like RunAsUser to run it as the user.

This script takes a few parameters to create a Notification App. The App Name and Icon are the most important as these are what will appear in the toast notification.

Parameter documentation follows:

ParameterTypeDescription
AppIdStringThe app ID to use for the notification app registration. Expected format is something like: 'CompanyName.AppName'.
NotificationImageStringThe path to the image to use for the notification.
NotificationTitleStringThe title to use for the notification.
NotificationMessageStringThe message text to use for the notification.
NotificationTypeStringThe type of notification to send. Expected values are alarm, reminder, incomingcall or default. Details on what each type looks like can be found in Microsoft's Toast schema.

The Script

#requires -version 5.1
[CmdletBinding()]
Param(
[string]$AppID = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe',
[string]$NotificationImage,
[Parameter(Mandatory)]
[string]$NotificationTitle,
[Parameter(Mandatory)]
[string]$NotificationMessage,
[ValidateSet('alarm', 'reminder', 'incomingcall', 'default')]
[string]$NotificationType = 'default'
)
$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
$null = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]
$NotificationTemplate = [xml]@"
<toast scenario="$NotificationType">
<visual>
<binding template="ToastGeneric">
<text>$NotificationTitle</text>
<text>$NotificationMessage</text>
<image placement="appLogoOverride" src="$NotificationImage"/>
</binding>
</visual>
</toast>
"@
$NotificationXML = [Windows.Data.XML.DOM.XMLDocument]::New()
$NotificationXML.LoadXml($NotificationTemplate.OuterXml)
$Toast = [Windows.UI.Notifications.ToastNotification]::new($NotificationXML)
$ToastNotifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppID)
$ToastNotifier.Show($Toast)

Generic Example

# Setup parameter hashtable.
$NotificationParams = @{
AppId = 'homotechsual.example'
NotificationImage = 'C:\RMM\NotificationApp\NotificationIcon.png'
NotificationTitle = 'Example Notification'
NotificationMessage = 'This is an example notification.'
NotificationType = 'reminder'
}
Send-Notification @NotificationParams

Script Example

It's probably most useful to use this script as part of a larger script. For example, you could use it to send a notification when a script has finished running. You could also use it to send a notification when a scheduled task has finished running.

# Functions
function Send-Notification {
[CmdletBinding()]
Param(
[string]$AppID = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe',
[string]$NotificationImage,
[Parameter(Mandatory)]
[string]$NotificationTitle,
[Parameter(Mandatory)]
[string]$NotificationMessage,
[ValidateSet('alarm', 'reminder', 'incomingcall', 'default')]
[string]$NotificationType = 'default'
)
$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
$null = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]
$NotificationTemplate = [xml]@"
<toast scenario="$NotificationType">
<visual>
<binding template="ToastGeneric">
<text>$NotificationTitle</text>
<text>$NotificationMessage</text>
<image placement="appLogoOverride" src="$NotificationImage"/>
</binding>
</visual>
</toast>
"@
$NotificationXML = [Windows.Data.XML.DOM.XMLDocument]::New()
$NotificationXML.LoadXml($NotificationTemplate.OuterXml)
$Toast = [Windows.UI.Notifications.ToastNotification]::new($NotificationXML)
$ToastNotifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppID)
$ToastNotifier.Show($Toast)
}
# Main Loop
$SpoolerService = Get-Service -Name 'Spooler'
if ($SpoolerService.Status -eq 'Running') {
try {
Restart-Service -Name 'Spooler'
$NotificationParams = @{
AppId = 'homotechsual.example'
NotificationImage = 'C:\RMM\NotificationApp\NotificationIcon.png'
NotificationTitle = 'Printer Spooler Restarted'
NotificationMessage = 'The printer spooler service was restarted.'
NotificationType = 'reminder'
}
Send-Notification @NotificationParams
} catch {
$NotificationParams = @{
AppId = 'homotechsual.example'
NotificationImage = 'C:\RMM\NotificationApp\NotificationIcon.png'
NotificationTitle = 'Printer Spooler Failed to Restart'
NotificationMessage = 'The printer spooler service failed to restart.'
NotificationType = 'reminder'
}
Send-Notification @NotificationParams
}
}

This would output something like this:

Example Notification

There are many other ways you could use notifications and you can add (and handle) actions within notifications but I'm spoiling some killer upcoming blog posts.