Why this exists
I wanted the first thing read every morning to be a single calm signal — not the news, not group chats, not Slack. Something that answers two questions cleanly: what’s the workout today, and what is it going to feel like outside.
Off-the-shelf “habit” apps drown that in dashboards and streaks. The brief is the opposite: one short email. No app to open.
What it does
Every day at 06:30, an inbox gets a short, warm note that includes:
- The day’s workout (rotating push / pull / legs / pull / legs over the week, with a “light” variant available)
- The Miami forecast — high, low, rain probability, a one-line take (“breezy”, “muggy by noon”)
- A small hand-written touch — a greeting, a sentence of encouragement, a thought to carry into the day
It’s intentionally one screen tall on a phone. If you have to scroll, the brief failed.
How it works
The flow is simple — that’s the point.
- Trigger — A scheduled GitHub Action wakes a Python script at 06:30 local time
- Fetch — Two parallel API calls: OpenWeatherMap for the day’s forecast, and a lookup against a local table of workouts keyed by day-of-week
- Compose — Claude is given the raw signals and a tight prompt that fixes tone, length, and structure. It returns a short, friendly note
- Send — A plain SMTP send to the recipient’s inbox. No service in the middle, no third-party form
End-to-end runtime is under five seconds, which means cost is effectively the Claude token spend (low single-digit cents per day) plus the free tier of OpenWeatherMap.
Choices that mattered
Email, not an app. Apps demand attention; email respects that you already have an inbox open.
One LLM call, templated. Early versions used the LLM for routing and decisions. That added latency and made failures hard to read. Now the script makes the decisions deterministically and the LLM only does the writing.
Hard length cap in the prompt. The model would otherwise drift toward over-helpful — “here are seven tips for your push day”. The prompt caps it at a known number of lines, with examples of the right length right above the call.
Light vs. normal mode. Some mornings call for a 30-minute version of the day’s split. The day’s metadata supports both, and the script picks based on a simple toggle.
What I’d do differently next time
- Switch to a single, idempotent endpoint instead of GitHub Actions cron. Cron is reliable but the cold-start path is opaque when something fails — a logged endpoint would tell me why the 06:30 brief didn’t show up
- Add a feedback loop. A one-tap reply (“less spicy”, “rest day”) would let the brief learn without me opening the code
- Move the workout table out of the script. It’s a small spreadsheet’s worth of data and it doesn’t belong in source code
Status
Shipped. Running daily. The next iteration is mostly about tone and resilience, not new features.