Every Friday morning for the better part of a year, I spent an hour pulling numbers from three dashboards, pasting them into a spreadsheet and emailing a one-paragraph summary to a small group of people. It was the kind of task that nobody on a team would notice if it disappeared, but everyone would notice if it stopped.
That hour, multiplied across fifty-some weeks, is more than a working week of my life that I will never get back. Eventually I admitted that to myself and decided to fix it.
The decision is the hard part
Most of the writing about automation skips the actual hard question. Which boring thing do I automate first? The pile is always larger than the time available. The instinct is to grab the most painful one, but that one usually hides the most edge cases, and you give up halfway through.
The better filter is boring + repetitive + bounded. Boring so you’ll feel the wins. Repetitive so the savings compound. Bounded so the project actually finishes. The Friday digest fit all three.
The script
The whole thing is about 60 lines of Python. It pulls from three internal endpoints, formats the result into Markdown and hands it off to sendmail. The shape:
import os, requests, smtplib
from email.message import EmailMessage
from datetime import date
SOURCES = {
"Sales": "https://internal.example.com/api/sales/yesterday",
"Signups": "https://internal.example.com/api/signups/yesterday",
"Errors": "https://internal.example.com/api/errors/yesterday",
}
def fetch(name, url):
r = requests.get(url, headers={"Authorization": f"Bearer {os.environ['API_TOKEN']}"})
r.raise_for_status()
return r.json()["count"]
def build_digest():
lines = [f"# Daily digest - {date.today():%Y-%m-%d}", ""]
for name, url in SOURCES.items():
lines.append(f"- **{name}**: {fetch(name, url):,}")
return "\n".join(lines)
if __name__ == "__main__":
body = build_digest()
msg = EmailMessage()
msg["Subject"] = f"Digest - {date.today():%b %d}"
msg["From"] = "digest@samson.zone"
msg["To"] = "me@samson.zone"
msg.set_content(body)
with smtplib.SMTP("localhost") as s:
s.send_message(msg)
That is the entire program. There is nothing clever in it. It is the plainest Python I could write, because in six months when something breaks I want to read it, fix it and move on.
The cron line
The cron entry is a single line, which is half the point of using cron in the first place:
0 6 * * 1-5 /usr/bin/python3 /opt/digest/digest.py
Six in the morning, Monday through Friday. The first time it ran on its own and the email landed in my inbox before I’d made coffee, I sat there grinning at it like I’d just gotten away with something.
What I learned
Three small things, in order of importance:
- Boring beats clever. The first version had a templating engine, retries and a pretty HTML email. None of that survived the second week. The plain version has not been touched in months.
- Logs are the secret. When something does break (and it will, whether it is DNS, auth tokens or a renamed endpoint), the only thing between you and twenty minutes of debugging is whether the script wrote down what it was doing.
- Tell people you did it. The whole point of automating boring work is to free up time for less boring work. If nobody knows the digest is automated, nobody knows you have that time back.
The best automation is the kind that, when it breaks, takes you longer to figure out it broke than to fix it.
One Friday morning back, every week, forever. Worth the afternoon it took.