A scheduled agent that monitors new releases across books, music, TV, and movies — texting you via Twilio when something worth knowing about comes out. Driven by a personal taste profile stored in a separate taste-profile repo.
Part of: ReleaseWatcherAgent
Build status: Scaffolding in progress. See CLOUD_AGENT_HANDOFF.md for the full implementation spec if picking this up fresh.
- Daily scan — Railway Cron checks each tracked creator for new releases via Spotify, TMDB, and Google Books APIs
- Daily Tier 1 announcement scan — Brave Search for each Tier 1 creator (releases / seasons). Stretch: concerts or tours for Tier 1 music acts.
- Weekly discovery — finds new releases matching your taste profile across all four categories. Music discovery seeds from your top tier 1/2 tracked artists (by profile score), not external playlists. Disabled in production (Nov 2026) — see Deployment.
- Anthropic judge — decides if each candidate is worth a notification based on taste fit, not review scores
- Web search — fires on every hit to find the best article/review link for the SMS
- Twilio SMS — sends a short formatted message with the link
The taste profile (author/artist scores derived from your bookshelf and Spotify history) lives in a separate repo with its own Postgres. This repo reads creator data from that DB via TASTE_PROFILE_DATABASE_URL — a public Railway Postgres URL, not the Railway internal hostname.
The sync_creators.py script syncs creator data into this repo's Postgres tracked_creators table (on Railway in production). All scans run against that DB — no live cross-project DB queries during job execution.
Scheduled jobs and the DB the agent uses are on Railway. Treat laptop runs as dev unless you are intentionally targeting production: use DATABASE_PUBLIC_URL + scripts/sync_creators_remote.py for creator sync from your machine, deploy code after watcher changes, and keep TASTE_PROFILE_DATABASE_URL pointed at the public taste-profile Postgres URL. See .cursor/rules/railway-production-default.mdc.
| Tier | Books / Music | TV | Movies |
|---|---|---|---|
| Tier 1 (top N by score) | Announce + Release | Announce + new season | Genre-based discovery only |
| Tier 2 (mid-range) | Release only (music: albums + singles on Spotify; no announce scan) | New season only | Genre-based discovery only |
| Tier 3 / tail | Books: release notifications. Music: not polled on the daily Spotify watchlist until discovery-style flows exist (tiers still synced from taste-profile). | Similar series via TMDB | TMDB genre filter + judge |
- Music: Tier 1 and 2 — Spotify albums + singles on the daily scan; Tier 1 also gets the Brave announcement scan. Tier 3 music — no proactive album/single SMS from the daily scan (stretch: discovery picks worth notifying later).
- Books: novels and novellas only (not collections or non-fiction from fiction authors by default)
- TV: new seasons only, not individual episodes
pip install -r requirements.txtCopy .env.example to .env. The taste-profile repo must be set up and populated first — see first-run order.
The watcher cannot run meaningfully until the taste profile is populated. Follow this sequence:
- Set up
taste-profilerepo — run migrations, deploy Railway Postgres, run book and Spotify intake - Set tier thresholds in taste-profile's
profile_metadata - Deploy this repo's Railway Postgres and run migrations:
alembic upgrade head - Copy
config.override.example.yamltoconfig.override.yamland fill film taste / playlists /sms_first_name/ TZ — or set the optional env vars documented under Config. Repoconfig.yamlstays generic. - Run
python -m watcher.sync_creatorslocally to populatetracked_creators - Dry-run the daily scan:
python -m watcher.jobs.watchlist --dry-run - Deploy Railway Cron services
- Trigger one live manual run to confirm Twilio SMS delivery
alembic upgrade headAll jobs support --dry-run — logs what would be sent without firing Twilio:
TASTE_PROFILE_DATABASE_URL=... DATABASE_URL=... python -m watcher.jobs.watchlist --dry-run
TASTE_PROFILE_DATABASE_URL=... DATABASE_URL=... python -m watcher.jobs.discovery --dry-runTracked config.yaml is intentionally generic so the repo stays safe to fork. Customize via:
-
config.override.yaml(copy fromconfig.override.example.yaml) — merged on top at runtime and listed in.gitignore. Prefer this on your laptop where you edit YAML normally. -
Environment variables (handy when Railway deploy builds from Git and you prefer not committing override YAML):
Variable Effect SMS_GREETING_NAMEorSMS_FIRST_NAMESets preferences.sms_first_namefor SMS greetings.FILM_TASTEFull film-taste prose for the judge (multiline OK in Railway / .env).FILM_TMDB_GENRE_IDSe.g. 18,53,878→ Drama, Thriller, Sci-Fi-style discovery seeding.SMS_QUIET_TIMEZONEIANA TZ for preferences.quiet_hours.timezone.
Deploying locally with railway up: the CLI respects .gitignore by default, so config.override.yaml is not uploaded. Use railway up --no-gitignore if you bundle an override file locally; or rely on Railway env vars above.
Synced taste data (scores, tiers, tracked creators) still lives in Postgres (taste-profile DB + tracked_creators), not here.
Minimal shape (see repo config.yaml for defaults):
preferences:
film_taste: >
Plain-language description of film taste for the Anthropic judge.
film_tmdb_genre_ids: [18, 53, 878]
quiet_hours:
start: "22:00"
end: "08:00"
timezone: "America/Los_Angeles"
watchlist:
tv:
shows:
- name: "Example show"
tier: 1
tmdb_id: 12345Build / deploy: Railway’s builder (Railpack/Nixpacks) needs a conventional Python start phase. Repo root main.py runs python main.py during image build/deploy only (smoke-imports watcher). Scheduled runs still use each service’s Cron start command — keep those as python -m watcher.... below (and in the Dashboard if railway.toml doesn’t populate them).
| Service | Schedule (UTC) | Command |
|---|---|---|
| daily-scan | 0 14 * * * (6am PT) |
python -m watcher.jobs.watchlist |
| announcement-scan | 0 14 * * * |
python -m watcher.jobs.announcement |
0 15 * * 0 (7am PT Sun) |
python -m watcher.jobs.discovery |
|
| weekly-sync | 0 13 * * 0 (5am PT Sun) |
python -m watcher.sync_creators |
weekly-discovery-cron is turned off (Nov 2026). Its "Music similar to …" / "Books similar to …" recommendations were low-value spam, so the Railway service was disabled and its block in railway.toml is commented out. On Railway the service still exists, but with no cron schedule, start command echo "weekly-discovery disabled (Nov 2026)", and restart policy NEVER. To restore it, set the start command back to python -m watcher.jobs.discovery and re-add a schedule (it was 0 19 * * 5, Fri 19:00 UTC). Change both together: a service with no schedule runs its start command immediately. The code in watcher/jobs/discovery.py is kept for reference. Don't re-enable or redeploy it unless the user asks.
Stretch: a Railway Web service for the inbound SMS webhook (watcher/webhook.py) — only needed if building the SMS reply loop.
See .env.example for the full list.
| Var | Description |
|---|---|
DATABASE_URL |
This repo's Railway Postgres |
TASTE_PROFILE_DATABASE_URL |
Public URL of taste-profile Railway Postgres |
SPOTIFY_CLIENT_ID |
Same Spotify app as taste-profile |
SPOTIFY_CLIENT_SECRET |
Same Spotify app as taste-profile |
SPOTIFY_REFRESH_TOKEN |
Not required by any watcher service. User OAuth token only needed by the taste-profile repo's playlist intake script. |
TMDB_API_KEY |
Free at themoviedb.org |
GOOGLE_BOOKS_API_KEY |
Free at console.cloud.google.com |
BRAVE_SEARCH_API_KEY |
Free tier: 2000 queries/month |
ANTHROPIC_API_KEY |
Already have |
TWILIO_ACCOUNT_SID |
Twilio console |
TWILIO_AUTH_TOKEN |
Twilio console |
TWILIO_MESSAGING_SERVICE_SID |
Preferred for A2P 10DLC; when set, SMS sends via Messaging Service instead of raw from_ |
TWILIO_FROM_NUMBER |
Your Twilio phone number |
YOUR_PHONE_NUMBER |
Your personal number to receive SMS |
release-watcher/
├── watcher/
│ ├── jobs/
│ │ ├── watchlist.py # Daily scan
│ │ ├── announcement.py # Daily Tier 1 Brave Search scan
│ │ └── discovery.py # Weekly discovery
│ ├── sources/
│ │ ├── spotify.py # Spotify client (Client Credentials + user OAuth)
│ │ ├── tmdb.py # TMDB client
│ │ ├── books.py # Google Books client
│ │ └── brave.py # Brave Search client
│ ├── judge.py # All Anthropic calls
│ ├── notify.py # Twilio SMS + quiet hours + error SMS
│ ├── sync_creators.py # Syncs creators from taste profile DB
│ └── webhook.py # Stretch: inbound SMS handler
├── alembic/ # Migrations
├── config.yaml # Operational settings, TV watchlist, film taste
├── .env.example
├── requirements.txt
├── railway.toml
└── CLOUD_AGENT_HANDOFF.md # Full implementation spec for cloud agent build