Home/Guides/Send from cron
Guide · Cron

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:

crontab
# ┌ 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.

crontab · fire-and-forget
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.

bash · capture $?
/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.

notify-run.sh
#!/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:

crontab · with wrapper
# 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-After header.

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.

Make your cron jobs speak up.

Download Lert, copy your URL, wrap the command once. Every scheduled run reports straight to your pocket.

Get the app iOS