from __future__ import annotations
from datetime import datetime
from notify import notify
def build_message() -> tuple[str, str, str]:
"""
Return (title, subtitle, body) for today's notification.
"""
title = "Daily notification"
subtitle = datetime.now().strftime("%a, %b %d")
body = "Hello! Replace build_message() with your own code."
return title, subtitle, body
def main() -> None:
title, subtitle, body = build_message()
notify(title, subtitle, body)
if __name__ == "__main__":
main()I have been anxiously checking the progress of an application that I submitted for an Exciting Thing. To check the progress, I have go to into a web portal and submit a form with my identifying information. It doesn’t take long, but it’s annoying to repeat everyday. So I got to thinking, maybe there’s a way I can set up a daily notification for this using some web scraping (which I’ve dabbled in a bit) and some macOS notification stuff (which I’ve never tried before).
My bff Claude helped me figure it out. Here is the gist of the notification portion:
notification-example/
├── daily_notification.py the script you customise
├── notify.py the macOS notification helper
└── launchd/
└── com.example.daily-notification.plist the daily schedule
In this lil repo you need a script for customizing the notification, a script for generating the notification, and a plist file for scheduling it.
daily_notification.py could contain absolutely anything you need, and may need to go along with other scripts. I leave that part up to you. A minimal sample could look like this:
The key detail is that the message should return three strings: the notification title, subtitle, and body.
notify.py is the function used in daily_notification.py to take the text you prepared and turn it into a macOS notification. A minimal example is:
from __future__ import annotations
import subprocess
def _as_applescript_string(s: str) -> str:
"""
Quote a Python string for use inside an AppleScript "..." literal.
"""
cleaned = "".join(c if c >= " " else " " for c in s)
cleaned = cleaned.replace("\\", "\\\\").replace('"', '\\"')
return f'"{cleaned}"'
def notify(title: str, subtitle: str, body: str) -> None:
"""Pop a single macOS notification banner.
The first time this runs, macOS may show a one-time prompt asking whether
to allow "Script Editor" to post notifications — accept it, or enable
alerts manually under System Settings -> Notifications -> Script Editor.
`display notification` truncates multi-line bodies in the banner UI, so we
flatten the body to one line joined by " · " before sending it.
"""
flat = " · ".join(line for line in body.splitlines() if line.strip())
script = (
f"display notification {_as_applescript_string(flat)} "
f"with title {_as_applescript_string(title)} "
f"subtitle {_as_applescript_string(subtitle)}"
)
# check=False so a notification failure (e.g. permission not yet granted)
# doesn't crash the calling script.
subprocess.run(["osascript", "-e", script], check=False)
if __name__ == "__main__":
notify(
title="Hello",
subtitle="Demo notification",
body="If you can see this, notify.py is wired up correctly.",
)Finally, to schedule the whole thing, you need an XML-style script that tells something called launchd what to do. launchd is a tool built into macOS and is apparently analogous to cron on Linux:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Unique reverse-DNS label. launchctl refers to the job by this name. -->
<key>Label</key>
<string>com.example.daily-notification</string>
<!-- The command to run, argv-style. Use absolute paths.
REPLACE /Users/YOU/path/to/notification-example with your actual path. -->
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/YOU/path/to/notification-example/daily_notification.py</string>
</array>
<!-- Where to run from. Lets daily_notification.py import notify.py. -->
<key>WorkingDirectory</key>
<string>/Users/YOU/path/to/notification-example</string>
<!-- Run every day at 09:00. Change Hour/Minute to whenever you want. -->
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<!-- Capture stdout/stderr so scheduled-run failures aren't invisible. -->
<key>StandardOutPath</key>
<string>/Users/YOU/path/to/notification-example/run.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOU/path/to/notification-example/run.err.log</string>
<!-- Don't fire just because the plist got loaded. The schedule decides. -->
<key>RunAtLoad</key>
<false/>
<!-- Hint: this is background work, not a foreground UI app. -->
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>Make sure to update with your own paths and schedule.
After you’ve got your scripts ready, you install and start the automation job with:
cp launchd/com.example.daily-notification.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.daily-notification.plistIf your mac is asleep at the scheduled time, it will run whenever the computer next wakes up.
To trigger an on-the-fly notification to see how it looks you can run:
launchctl kickstart -k gui/$(id -u)/com.example.daily-notificationCongratulations, you have a daily notification! Now, I didn’t yet figure out how to turn this thing off. For that, for now you are on your own.