Scott Finkelstein

← Projects

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 writesThe repo owns
songs.json — ten picks, theme, notetemplate.html — every pixel of the browser design
the commit messageemail-template.html — the same design, inlined for Gmail
the email sendrender.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.