jerry@homelab:~/projects$ cat daily-briefing.md
Daily briefing
Every morning at 7:00 my phone gets two Signal messages. One has photos from this day in earlier years. The other has everything I need to know today, in one message.
Why I built it
This used to be a feature of task-bot, a Python Signal bot I wrote and ran myself. It worked, but it was a whole service to maintain: code, tests, dependencies, a container, deploys. Most of what it did was fetching data from one place and sending it somewhere else, which is exactly what n8n is for. So I decommissioned task-bot. The Minecraft features moved into mc-tool, some of the status features into my home-dashboard, and the automations, this briefing included, became n8n workflows. That means much less code to maintain. What's left is a few small JavaScript snippets for formatting, and n8n does the scheduling, the HTTP calls and the credentials.
What's in it
- Weather and what to wear: today's forecast from Open-Meteo, turned into advice. T-shirt or winter coat, plus "bring an umbrella" above a 50% chance of rain, a windbreaker in strong wind, and "layer up" on a cold morning.
- Today's agenda and tasks from my own Radicale server, over CalDAV.
- School and football: today and the next three days from the school's Parro calendar and the football club's calendar, both published as ICS feeds.
- Backups: which backup jobs are healthy, read from Gatus, with "all backups OK" on a good day and the failing ones by name on a bad one.
- Renewals in the next three days, from Wallos.
- Unread news from Miniflux, grouped by feed, with links.
- Minecraft playtime from yesterday, per player and server, from my mc-tool.
- Website visitors to this site, yesterday and in total, from GoatCounter.
- On this day: a separate message with up to five photos from this date in earlier years, from Immich.
How it's built
One schedule trigger fans out to independent branches, one per section. Each branch is an HTTP request plus a small Code node that turns the reply into a short block of text. Then everything is merged into one message.
07:00 โโฌโ Immich: one photo per year, 15 years back โโโโโโโโโโโถ Signal: "On this day" + photos
โ
โโ Open-Meteo โโโโ weather + what to wear โโโ
โโ Radicale โโโโโโ today's events โ
โโ Radicale โโโโโโ tasks due today โ
โโ Parro (ICS) โโโ school, next 3 days โ
โโ club (ICS) โโโโ football, next 3 days โโโโถ merge โโโถ drop empty โโโถ Signal:
โโ Gatus โโโโโโโโโ backup status โ sections one message
โโ Wallos โโโโโโโโ renewals, next 3 days โ
โโ Miniflux โโโโโโ unread news โ
โโ mc-tool โโโโโโโ Minecraft playtime โ
โโ GoatCounter โโโ website visitors โโโโโโโโ
Messages go out through signal-cli-rest-api, which runs on my homelab. Adding a section means adding one branch and one input on the merge node.
Details that matter
One broken source shouldn't kill the briefing
Each section of the main message checks its input before using it. If Wallos is down or the weather API answers with something odd, that section becomes one line, like "โ ๏ธ Couldn't reach Wallos for this morning's renewal digest", and the rest of the briefing still arrives. It also tells me which service needs a look, which makes the briefing double as a morning health check.
Quiet when there's nothing to say
A section with nothing to report (no football this week, no unread news, nobody played Minecraft) returns empty text, and the final step drops it. On a quiet day the message is short, not a list of "nothing here" lines.
Calendar feeds are messier than they look
The school and football calendars are plain ICS files, parsed in a few lines of JavaScript, and getting the details right took more than the happy path:
- long lines are "folded" over several lines and have to be joined first;
- reminders (
VALARM) are nested inside events and must not overwrite the event's own fields; - an all-day event's end date is exclusive: an event that ends on Friday is stored with Saturday as its end date;
- times come in UTC or local time, and both have to end up in Amsterdam time;
- an event spanning several days shows once, as "t/m vr 17 okt" ("up to and including Fri 17 Oct"), not on every day.
Backups have different rhythms
A nightly backup that last reported 30 hours ago is a problem. A weekly Immich backup that last reported 30 hours ago is fine. The backup section reads every job in Gatus's backups group, checks whether the last run succeeded, and compares its age with a limit per job: the default is 30 hours, the weekly backup gets 8 days, and an hourly sync job gets 3 hours.
On this day, including the 29th of February
The photo message runs one Immich search per year, for 15 years back, keeps the first photo from each day, and sends up to five as previews. On 29 February the date library would quietly turn earlier, non-leap years into the 28th, so those years are skipped instead of showing photos from the wrong day.
jerry@homelab:~/projects$ โ
cd ..