LLM Cool DJ
A scheduled Claude Code agent that curates a daily Top 10 from my Discogs taste profile — and a repository designed so the agent supplies data while the code stays deterministic.
- Role
- Builder
- Year
- 2026
- Stack
- Claude Code · Python · GitHub · Claude Gmail Connector
Outcomes
- Ten new songs every morning, never a repeat, with zero daily intervention.
- Design drift eliminated by moving the layout out of the model's reach entirely.
- A validator that fails loudly rather than shipping a bad playlist.
What it is
LLM Cool DJ is a resident DJ that works for me one morning at a time. A scheduled Claude Code cloud agent wakes up once a day, reads a taste profile derived from my Discogs collection, picks ten songs I haven't heard from it before — roughly seven discoveries and three deep cuts from artists I already own — and emails me the result as a formatted dashboard.
The picks are ranked one through ten, given a theme ("Jangle and Chime," "Pub Rock at Closing Time") and a two-sentence note explaining the through-line. Every track gets verified by web search before it makes the cut, because a plausible-sounding song attributed to the wrong band is worse than no song at all.
The interesting part isn't the curation
It's the scheduling model. Using Claude Code Routines, the agent runs in a fresh cloud sandbox every day with no memory of the previous run. It has never met itself. Whatever it knew yesterday is gone.
So the repository is the memory. history.json holds every song ever played and
is permanently off-limits for future picks. TASTE.md holds the taste profile, the
list of artists I own, and an adjacency map for discovery. The run ends by
committing and pushing — and if that push fails, tomorrow's run starts blind and
repeats itself. That single failure mode is the one the agent is instructed to
report loudly.
Keeping the deterministic parts deterministic
The first version of this asked the model to produce the HTML. It looked great on day one. By day four the card spacing had shifted, the footer had grown a tagline, and one day's page had quietly invented a new color. Nothing was broken — it just wasn't the same product anymore.
The fix was to stop asking. The repository now draws a hard line between what the agent generates and what it is forbidden to touch:
| The agent writes | The repo owns |
|---|---|
songs.json — ten picks, theme, note | template.html — every pixel of the browser design |
| the commit message | email-template.html — the same design, inlined for Gmail |
| the email send | render.py — validation and all markup generation |
template.html contains only substitution tokens. render.py generates all row
markup from songs.json. The agent produces data, never HTML. A daily
instruction to "please keep the design consistent" would be a bet on the model
behaving 365 times a year; moving the design into a frozen file is not a bet at
all.
The email template is the clearest case. Gmail's sanitizer strips <style> blocks
and CSS variables, so the browser page arrives as unstyled text. render.py emits
a second Gmail-safe twin with every style inlined — a fiddly, exacting piece of
work that only has to be gotten right once. Asking a model to re-derive inline
email CSS every morning would be asking it to re-solve a solved problem, daily,
with a fresh chance to get it wrong.
The validator is the contract
render.py exits non-zero rather than silently producing a bad page. It rejects:
- a song already in
history.json— matching ignores punctuation, leading articles, and(2009 Remaster)-style suffixes, so a remaster can't sneak a repeat past it - an artist played within the last 14 days, or twice in one day
- a missing field, a bad rank, a non-numeric year
- a playlist already published for that date
- an unrecognized token in
template.html, which means someone edited it
That last check is the interesting one: the renderer refuses to run if the frozen design file has been modified. The rule isn't just written down in the run contract, it's enforced by the code the agent has to call to finish its job.
When the validator fails, the agent's instruction is narrow — fix songs.json,
usually by replacing one duplicate pick. Not edit history.json to make the check
pass. Not write the HTML by hand. The failure mode has one correct escape hatch and
it's the one that keeps the system honest.
What I took from it
The useful boundary in an agent system isn't between "hard" and "easy" work. It's between the parts that should be different every time and the parts that should be identical every time. Song selection genuinely needs judgment, taste, and search. Rendering a table row does not.
Every deterministic thing I moved out of the prompt and into the repository was one fewer thing that could drift — and the run contract got shorter, not longer, as a result.