Get a push notification when a long-running script finishes
Scrapers, ETL runs, model training, database migrations — the jobs that take twenty minutes or six hours are the ones you keep alt-tabbing back to check on. Wrap the work in a try/finally and Lert pings your phone the moment it ends, whether it finished clean or fell over halfway.
Notify on success and crash
The naive approach — a POST on the last line of the script — has one fatal flaw: if the job raises, that line never runs, so the run that actually needed you is the one that stays silent. The fix is a finally block, which Python runs whether the work succeeded or blew up:
import time, traceback, requests LERT_URL = "https://lert.dev/p/your-id" def notify(title, body): requests.post(LERT_URL, json={"title": title, "body": body, "tag": "batch-job"}, timeout=10) start = time.time() try: run_the_long_job() # scrape / train / migrate / crunch mins = (time.time() - start) / 60 notify("Job finished ✅", f"Done in {mins:.1f} min") except Exception as e: notify("Job crashed ❌", f"{type(e).__name__}: {e}") raise # keep the real traceback
Now every run ends with a buzz. On success you get the duration; on failure you get the exception type and message so you can decide from your phone whether it needs you right now or can wait until morning. The bare raise re-throws the original error so your logs and exit code are unchanged.
Set it up in three steps
1. Copy your Lert URL
Open the Lert app and copy your unique endpoint — something like https://lert.dev/p/your-id. Treat it like a password: anyone who has it can send you a notification, and you can regenerate it any time if it leaks.
2. Wrap the job in try/finally
Put your existing work inside the try and send from the except/finally. You don't have to restructure the job — the wrapper goes around whatever function or block already does the work. Note the timeout=10 on the request: it guarantees a network problem at the finish line can never hang a job that already succeeded.
3. Run it and walk away
Start the job and close the laptop lid. When it ends you get a real iOS push that wakes a locked phone and buzzes a paired Apple Watch — usually under a second after the POST.
Different runtime? The same idea works anywhere. Use a try/finally in Node, a trap in a bash wrapper, or fire the POST from a cron job. The endpoint is just an HTTP URL, so any language reaches it in one line.
A reusable decorator
If several scripts should all report in, wrap the pattern once and apply it as a decorator so each job just declares that it wants notifying:
import functools, time, requests def notifies(name): def deco(fn): @functools.wraps(fn) def wrap(*a, **k): t = time.time() try: r = fn(*a, **k) _post(f"{name} ✅", f"{(time.time()-t)/60:.1f} min") return r except Exception as e: _post(f"{name} ❌", f"{type(e).__name__}: {e}") raise return wrap return deco @notifies("Nightly scrape") def main(): ...
Where _post is the same one-line requests.post to your Lert URL. Decorate main() and forget about it — every future run reports its own outcome.
Where this earns its keep
- Scrapers & crawlers: know when a multi-hour crawl finished or hit a rate limit and died.
- Data pipelines / ETL: get the row count or the failing step in the body without opening a dashboard.
- ML training: a ping when the epoch loop ends — or when it OOMs at 3am — beats watching a progress bar.
- Migrations: a schema migration that either "done in 4.2 min" or "IntegrityError on users" is exactly what you want on your lock screen.
Why Lert for long jobs
You don't want to stand up a monitoring stack for a script you run occasionally. Lert is the opposite of that: one URL, one line, and the endpoint is the authentication — no key, no SDK, no account. It turns "I'll just check on it in a bit" into a job that tells you the second it's done, so you can actually leave your desk.
FAQ
Why use try/finally instead of just posting at the end?
A POST on the last line never runs if the job raises — the crashed run, the one you most want to hear about, stays silent. A finally (or matched except) runs on both the success and the failure path, so you're always notified.
Can I include the error message when it crashes?
Yes. Catch the exception and put a short summary — type(e).__name__ plus the message — in the body before re-raising. Keep it under the 2000-character body limit; a one-line summary is usually enough to triage from your phone.
Will this slow my script down?
No. It's a single HTTP request at the very end, a few hundred milliseconds. The timeout=10 means even a network hiccup can't hang a job that already finished.
Do I need an API key or account?
No. Your Lert endpoint URL is the authentication — there's no separate key, token, or login to wire up.
How fast does the notification arrive?
Typically within a second or two of the POST. It's a real push, so it wakes a locked phone and buzzes a paired Apple Watch.
Stop babysitting your batch jobs.
Download Lert, copy your URL, wrap the work in try/finally. Your phone tells you how it went.