Documentation
Phone notifications for shell scripts, cron and backups
Send phone notifications from shell scripts, PowerShell, backups and scheduled jobs. Preserve task exit codes and check notification receipt separately.
Updated
Choose public, HTTPS or encrypted script notifications
#An existing public Topic accepts non-sensitive messages without pairing. A private HTTPS link suits service webhooks; the service reads input before encrypted delivery. The wrappers below use a protected Topic: first pair the CLI as the same user that runs the job, so the sender encrypts content locally.
Keep private logs out of public Topics.
Before you start
#For the protected CLI scripts below, install Ntfyx CLI 0.6.0+ on Windows, macOS or Linux and the app on iPhone (iOS 18.4+) or Android (Android 10+). Android background push requires Google Play services. Pair the CLI as the same user who runs the task; installing it alone does not authorize the sender.
Free includes 100 messages per UTC day. Use ntfyx topics to find your local alias and replace Work in the examples. The shell installer below is for macOS/Linux; Windows installation and current phone availability are on the download page.
- Run ntfyx connect on the trusted computer and keep the fresh QR private.
- Use the phone’s private Topic connection flow to authorize this Source. Update older apps from the store and follow the private scanning entry; current versions are listed in the quickstart.
- Confirm one actual phone message before adding the wrapper to your own task.
curl -fsSL https://ntfyx.me/install.sh | bash
export PATH="$HOME/.local/bin:$PATH"
ntfyx version --json
ntfyx connect
ntfyx topicsSend your first alert
#Once paired, send a plain notification to the Work connection. The CLI encrypts it for your authorized receivers. A successful send means the service accepted the message; the phone still needs notification permissions and a working delivery path.
ntfyx send "Build finished. Ready to review." --title "Build complete" --topic WorkNotify when a command exits
#Download the short wrapper and inspect it before running. It runs the command exactly as passed, then sends a generic exit-status notification. It does not forward the command arguments or terminal output.
curl -fsSLo notify-after.sh https://ntfyx.me/examples/scripts/notify-after.sh
cat notify-after.sh
bash notify-after.sh Work bash -c 'printf "Example job finished\n"'Keep the result in your terminal
#A successful command exits 0. A failed command keeps its own exit status, even when the notification cannot be sent. The wrapper writes a warning if notification acceptance is not confirmed. Avoid putting credentials or sensitive logs into notification text.
bash notify-after.sh Work bash -c 'exit 7'
printf "Original command exit: %s\n" "$?"
Builds and backups
#Pass your existing command as separate arguments. The wrapper runs it once and reports its exit status. The backup example below creates an archive containing only the downloaded wrapper, in a temporary directory. Inspect the command before running it; a successful archive is not proof that a backup can be restored.
bash notify-after.sh Work npm run build
# Harmless backup sample: archive only this downloaded script.
ntfyx_demo_dir="$(mktemp -d)"
bash notify-after.sh Work tar -czf "$ntfyx_demo_dir/sample.tar.gz" notify-after.sh
ntfyx_backup_status=$?
printf "Backup exit status: %s\n" "$ntfyx_backup_status"
# If the backup succeeded, inspect the archive separately.
if [ "$ntfyx_backup_status" -eq 0 ]; then
tar -tzf "$ntfyx_demo_dir/sample.tar.gz"
fiSchedule with cron on Linux
#Install and pair as the same user that runs the job. Cron has a smaller environment than an interactive terminal: set PATH, use absolute paths, and keep job output in a local log. Run the exact command manually first. This sample sends a harmless completion alert at 09:00 in the scheduler’s timezone; replace YOUR_USER and the script path before adding it with crontab -e.
On macOS, use your existing launchd scheduler instead.
# Example user crontab; replace YOUR_USER before use.
PATH=/home/YOUR_USER/.local/bin:/usr/bin:/bin
0 9 * * * /bin/bash /home/YOUR_USER/notify-after.sh Work /bin/true >> /home/YOUR_USER/ntfyx-job.log 2>&1Diagnose a missing or failed notification
#Test a failed job with exit 7 before relying on the wrapper. If the job fails and sending also fails, the wrapper still exits 7 and writes a notification warning. Check the local log, ntfyx doctor, the configured Topic, and phone notification permission, Focus and quiet hours. Acceptance by the relay does not prove the phone displayed an alert.
| Job result | Notification result | Wrapper exit |
|---|---|---|
| Success (0) | Accepted | 0 |
| Failure (7) | Accepted | 7 |
| Failure (7) | Not confirmed | 7, with a warning |
| Success (0) | Not confirmed | 0, with a warning |
What was verified
#These scripts were tested with the compiled CLI, a real local encrypted relay and signed replies from the Web Inbox. Phone authorization in that automated test used a programmatic identity. The screenshots show synthetic messages from that local Web test; they are not iPhone or APNs delivery evidence.
Windows PowerShell: a complete exit-notification wrapper
#Save this as notify-after.ps1 and run it as the paired Windows user. It wraps a native executable, runs the task once and saves $LASTEXITCODE before sending separately. Run PowerShell task scripts through powershell.exe or pwsh.exe -File and give them explicit exit codes. Notification failure does not replace the task result; a task that cannot start returns 127.
Arguments and output stay local.
# Save as notify-after.ps1. Runs a native executable once.
param(
[Parameter(Mandatory=$true)][string]$Topic,
[Parameter(Mandatory=$true)][string]$TaskExecutable,
[string[]]$TaskArguments = @(),
[string]$Ntfyx = 'ntfyx'
)
# Nonzero native exits are job results, not PowerShell exceptions.
$PSNativeCommandUseErrorActionPreference = $false
$ntfyxJobStatus = 127
try {
$ntfyxTask = Get-Command $TaskExecutable -CommandType Application -ErrorAction Stop
& $ntfyxTask.Source @TaskArguments
$ntfyxJobStatus = $LASTEXITCODE
} catch {
[Console]::Error.WriteLine('Task could not start; status 127.')
}
$ntfyxTitle = if ($ntfyxJobStatus -eq 0) { 'Task finished' } else { 'Task failed' }
try {
$ntfyxCommand = Get-Command $Ntfyx -CommandType Application -ErrorAction Stop
& $ntfyxCommand.Source send "Task exited with status $ntfyxJobStatus. Check the local log." --title $ntfyxTitle --topic $Topic | Out-Null
if ($LASTEXITCODE -ne 0) {
[Console]::Error.WriteLine('Notification not confirmed; job exit status is preserved.')
}
} catch {
[Console]::Error.WriteLine('Notification could not start; job exit status is preserved.')
}
exit $ntfyxJobStatusTest success, failure and notification failure
#Run these harmless child processes in PowerShell. The first two should return 0 and 7. The third deliberately uses a nonexistent notifier and must still return 7 with a warning. Each task runs once. Check the matching phone message before replacing the task with your own command. Tools with special success codes, such as robocopy, need their own result mapping before using a generic nonzero-is-failure wrapper.
.\notify-after.ps1 -Topic Work -TaskExecutable "$env:ComSpec" -TaskArguments @('/d', '/c', 'exit 0')
"Job exit: $LASTEXITCODE"
.\notify-after.ps1 -Topic Work -TaskExecutable "$env:ComSpec" -TaskArguments @('/d', '/c', 'exit 7')
"Job exit: $LASTEXITCODE"
.\notify-after.ps1 -Topic Work -TaskExecutable "$env:ComSpec" -TaskArguments @('/d', '/c', 'exit 7') -Ntfyx 'missing-ntfyx-test.exe'
"Job exit: $LASTEXITCODE"Windows Task Scheduler: fix the user and paths
#Schedulers do not inherit the full interactive environment. Run as the same paired account, use absolute CLI/wrapper/task paths and set the task working directory. This runner.ps1 example requires your actual username and task paths. In Task Scheduler, set Program to the full powershell.exe path, Arguments to -NoProfile -File "C:\Jobs\runner.ps1", and Start in to C:\Jobs.
Keep the existing script-execution policy. Run this exact action manually before checking task history and Last Run Result.
# C:\Jobs\runner.ps1; task.ps1 must exit with its own result.
$ntfyxRun = @{
Topic = 'Work'
Ntfyx = 'C:\Users\YOUR_USER\AppData\Local\Ntfyx\bin\ntfyx.exe'
TaskExecutable = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
TaskArguments = @('-NoProfile', '-File', 'C:\Jobs\task.ps1')
}
& 'C:\Jobs\notify-after.ps1' @ntfyxRun
exit $LASTEXITCODEBackups: report the exit code, then verify restoration separately
#Place the inspected notify-after.sh in the job directory. This runner takes a source directory and a new archive path, runs tar once and returns its exit code. The destination is created exclusively, so concurrent runs cannot overwrite an earlier backup. A failed run can leave a partial archive; do not treat it as usable.
Start with a temporary sample directory. A successful notification means tar returned 0; it does not prove completeness or restorability. Keep your backup tool’s own exit semantics and perform restore checks in a separate temporary directory.
#!/usr/bin/env bash
# Save beside notify-after.sh as backup-run.sh.
set -u
if [ "$#" -ne 3 ]; then
printf 'Usage: bash backup-run.sh TOPIC SOURCE_DIR NEW_ARCHIVE\n' >&2
exit 2
fi
ntfyx_topic="$1"
ntfyx_source="$2"
ntfyx_archive="$3"
if [ ! -d "$ntfyx_source" ] || [ -e "$ntfyx_archive" ] || [ -L "$ntfyx_archive" ]; then
printf 'Use an existing source directory and a new archive path.\n' >&2
exit 2
fi
ntfyx_script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" || exit 2
set -C # Refuse an existing output during redirection, including a racing writer.
bash "$ntfyx_script_dir/notify-after.sh" "$ntfyx_topic" \
tar -czf - -C "$ntfyx_source" . > "$ntfyx_archive"
ntfyx_backup_status=$?
exit "$ntfyx_backup_status"Complete your own automation task
#Start with a complete, copyable example and check the job result, send receipt and phone message separately.