How to Automate Newsletter Based on SecondBrain and Bluesky Changes
I automate the gathering of content for my Newsletter, but not the writing of it. Everything mechanical that I used to do by hand — pulling links to recently-updated Second Brain notes, finding books I’ve read, copying top Bluesky posts — runs from a small Python script. The personal writing stays mine.
Inspired by Simon Willison’s Substack automation see also Simon Willison, but where Simon does it fully programmatically with SQL and Datasette into Substack, I go markdown-first into Listmonk via my listmonk-rss repo. No notebook, no AI in the loop.
GitHub Repo
Find at sspaeti/listmonk-rss. This repo also contains an automatic newsletter sent via
listmonk-rss.pybased on new blog posts (documented at Listmonk), as well as the automationnewsletter.pyand templating.
# Why This Process?
This is not to replace my own writing with my subscribers, but for me as I do so much work on my second brain, to surface latest changes, to give the readers a possibility to keep up with what has changes since the last email in a very simple and easy to scan way, and for me to reduce the workload to go through all my changes and posts manually.
# How it Looks
A generated email with gathered information from my second brain, my recent book notes I read or currently reading, from Bluesky post and engagment (can be pulled via DuckDB and simple query - see Querying Bluesky with DuckDB and SQL):

With make newsletter-send it will be scheduled on Listmonk automatically:
|
|
And this is how it looks:

All in beautiful Markdown - and the preview looks like this - beautiful, no? 🙂

# How it Works
The script (newsletter.py) has two commands: gather and send.
gather collects, since the last sent newsletter:
- Blog posts: capped at 2 — RSS auto-announces them via the older
listmonk_rss.pyworkflow already. - Brain notes:
git log --numstatinside thesecond-brain-public/content/submodule. A line-count threshold (default 20 added lines) filters out commits that are just grammar fixes. Notes are split into Major (≥100 lines added) and Smaller updates, each with a placeholder paragraph for me to write framing prose. - Books: scans my
Books/folder for any book whoseCreated,Started reading, orFinished readingdate falls in the window, and pulls the>callout and## Notes During Reading. The folder is copied to a local.copy/books/snapshot first so the script never touches my live vault. - Bluesky: DuckDB query against
app.bsky.feed.getAuthorFeed, top 15 by engagement. See DuckDB + Bluesky AT protocol. see ATProto queries
Output is a markdown draft with editorial slots (intro, per-section framing, closing). I edit in Neovim, then make newsletter-send pushes to Listmonk as a scheduled draft so I get a Pushover notification and a final review window.
# Design Choices
- No AI in the loop. AI would flatten my voice. The script gathers; I write.
- Date advances only on send. Re-running
gatherbetween sends always shows the same backlog — nothing is consumed until I actually send. If I skip a few weeks, the next draft has all of it. - Filename-as-slug for URLs. Hugo derives URLs from the filename, not the frontmatter title, so the script slugifies the file stem (e.g.
zen mode for writing.md→/brain/zen-mode-for-writing/, even when the title is “Zen Mode for Writing (Obsidian, Neovim)”). - Wikilink handling depends on source. Brain-note intros keep wikilinks converted to
ssp.sh/brain/URLs (they’re all published). Book notes get wikilinks stripped to plain text — they may reference notes in my private vault that don’t exist on the web.
# Commands
|
|
# Further Reads
Origin: ssp.sh Newsletter Drafts, Simon Willison
References: Newsletter