How to send a push notification from a cron job
Cron jobs run in the dark. A nightly backup, a weekly report, an hourly sync — you only find out they broke when something downstream does. Lert flips that around: wrap the command, and your phone tells you the moment it finishes, with the exit status baked in.
What you'll need
A machine with cron and curl, and the Lert app on your iPhone. Copy your endpoint from the app — it looks like https://lert.dev/p/your-id. That URL is the authentication, so there's no key to configure. Keep it private; anyone who has it can notify you.
The approach here works for any scheduler that runs a shell command, not just classic cron — systemd timers, Kubernetes CronJobs, and macOS launchd all call the same wrapper the same way. If it can execute a script, it can buzz your phone.
crontab in 60 seconds
Edit your schedule with crontab -e. Each line is five time fields plus a command: minute, hour, day-of-month, month, day-of-week. A job that runs every day at 2am looks like this:
# ┌ min ┌ hour ┌ day ┌ month ┌ weekday # │ │ │ │ │ 0 2 * * * /home/me/backup.sh
The catch that trips everyone up: cron runs with a minimal environment. Your PATH is short, your shell profile isn't loaded, and variables you rely on interactively won't be there. That's exactly why we wrap the work in a script and use absolute paths.
The simplest possible notify
If you just want a ping when a one-line job runs, chain a curl after it. All Lert fields — title, body, tag — are optional.
30 3 * * * /usr/local/bin/sync.sh && /usr/bin/curl -fsS -X POST \ "https://lert.dev/p/your-id" -d '{"title":"Sync done"}'
This only fires on success (the &&). To be told about failures too — which is usually the whole point — capture the exit status in a wrapper.
Capturing the exit status
The shell stores the last command's exit code in $?. Zero means success; anything else is a failure. Grab it immediately, because the very next command overwrites it.
/home/me/backup.sh status=$? # capture before anything else runs if [ "$status" -eq 0 ]; then echo "ok" else echo "failed with $status" fi
A reusable wrapper script
This is the piece to actually deploy. Save it as notify-run.sh, make it executable with chmod +x, and point cron at it. It runs whatever command you pass, times it, captures the exit status, and sends a success or failure notification with the tail of the output as the body.
#!/usr/bin/env bash # usage: notify-run.sh "Backup" /home/me/backup.sh LERT_URL="https://lert.dev/p/your-id" label="$1"; shift start=$(date +%s) output=$("$@" 2>&1) # run the command, capture stdout+stderr status=$? secs=$(( $(date +%s) - start )) if [ "$status" -eq 0 ]; then title="$label finished ✅" else title="$label failed ❌ (exit $status)" fi # last 3 lines of output as the body, JSON-escaped by jq body=$(echo "$output" | tail -n 3) /usr/bin/curl -fsS -X POST "$LERT_URL" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg t "$title" --arg b "$body ($secs s)" \ '{title:$t, body:$b, tag:"cron"}')" > /dev/null exit "$status" # preserve the real exit code for cron
Using jq -n to build the JSON means titles and output with quotes, =, or & are escaped correctly — no manual string wrangling, no broken payloads. The final exit "$status" keeps cron's own view of success accurate.
Wire it into crontab
Now the crontab line is clean, and every run tells you how it went:
# nightly backup at 2:00, notify either way 0 2 * * * /home/me/notify-run.sh "Backup" /home/me/backup.sh # hourly sync, only notify on failure — pass --quiet-ok in your own variant 0 * * * * /home/me/notify-run.sh "Sync" /usr/local/bin/sync.sh
Handling the response and limits
A success returns 200 {"delivered":1}. Two responses are worth knowing about in an unattended job:
- 200
{"delivered":0}with a warning — no device registered yet; open the Lert app once on your phone. - 429 — you exceeded a limit (60 requests/minute per IP, 50/day on Free). A chatty per-minute cron can hit this, so batch or throttle, and respect the
Retry-Afterheader.
Same idea, different trigger. If your "job" is really a slow manual script, see long-running script notifications. For box-level health, see server monitoring, or read the deep-dive on curl that powers the wrapper. Cron on a schedule you'd rather express in code? Python and Node.js work too.
Why it's this simple
No agent to install on the box, no account for the cron user, no key to rotate. The endpoint URL is the credential, so a cron notification is one curl inside a five-line wrapper — and your dark, silent jobs finally speak up.
Deploy the wrapper once and every future job inherits it: prefix any crontab line with notify-run.sh "Label" and you get success and failure notifications for free, exit codes preserved, output attached. It's the difference between finding out a backup broke a week later and finding out the same night.
FAQ
Why does my curl work in the shell but not in cron?
cron runs with a minimal environment and a short PATH. Use absolute paths (like /usr/bin/curl), define variables inside the script, and don't rely on your shell profile being loaded.
How do I capture the command's exit status?
Run the command, then read $? immediately afterward and store it in a variable — the next command overwrites $?.
Can I notify only when the job fails?
Yes. Check the captured exit code and only send the POST when it's non-zero. Quiet on the happy path, loud when something breaks.
How do I keep my Lert URL out of crontab?
Put the URL in the wrapper script or an env file the script sources, readable only by you. crontab entries are visible to anyone who can read your crontab.
Related guides
Make your cron jobs speak up.
Download Lert, copy your URL, wrap the command once. Every scheduled run reports straight to your pocket.