A GitHub Actions automation that emails you about upcoming Dodgers home games — a daily game-day alert and a weekly schedule summary.
rjayasin.github.io/dodgers-notifier — workflow run dashboard
Everything lives in a single script, notifier.py, with four subcommands:
- GitHub Actions cron triggers the workflow at 7:00 AM PDT / 6:00 AM PST every day.
- The script queries the free, unauthenticated MLB Stats API for the Dodgers' schedule on today's date (in Pacific Time).
- It checks whether any game is a home game at Dodger Stadium — it verifies both the home team ID (119) and venue ID (22) to correctly exclude neutral-site games like the London or Tokyo Series.
- Postponed games are skipped automatically. Double-headers trigger one email per game.
- If a home game is found, the script sends an email with the opponent and first pitch time.
- The same workflow then runs
notifier.py scheduleandnotifier.py runsand commitsdocs/schedule.jsonanddocs/runs.jsonback tomain, so the dashboard picks up any change to the schedule and its charts gain a day of history. Both run whether or not an email went out.
- GitHub Actions cron triggers the workflow at 4:00 PM PDT / 3:00 PM PST every Sunday.
- The script fetches the Dodgers schedule for the upcoming Monday–Sunday week.
- All home games are collected and formatted into a single email — the game count and week range in the subject line, one line per game in the body.
- If there are no home games that week, you get a "No Dodgers home games this week" email instead.
- An offseason gate skips that email outside the season, so you aren't emailed "no games" all winter. Through the regular season the gate is the calendar; after it, the gate is whether the Dodgers themselves are still on the schedule — see Postseason.
- The next eight weeks are written to
docs/schedule.jsonand committed back tomain— that's what the dashboard shows at the top of the page. The file is written before the email branches, so the dashboard refreshes even on weeks that send no email.
- The Daily Check workflow runs this every morning, right after the game check.
- It does the
weeklycommand's schedule half and nothing else: fetch the eight-week window, writedocs/schedule.json, send no email. - If the file it would write matches the one already committed, it leaves the file alone, so an unchanged schedule costs no commit and no site redeploy.
- This is what keeps the dashboard current through October, when the schedule changes daily — see Postseason.
- The Daily Check workflow runs this after the game check, on the same daily cron.
- It reads the repo's workflow runs from the GitHub REST API and folds the Daily Check and Weekly Schedule ones into
docs/runs.json, keyed by run id, so the file only ever grows. - It pages back only until it reaches a page it has already stored — a day's catch-up is two API calls, while an empty file backfills as far as GitHub still lists.
- The run doing the recording leaves itself out: it hasn't finished, so it has no completion time yet. The next day's run picks it up complete.
- If nothing is new, the file is left untouched so the workflow has nothing to commit.
The script sends an email via Gmail SMTP from your Gmail account to the address in the NOTIFY_EMAIL secret (or back to the sending Gmail account itself if NOTIFY_EMAIL isn't set).
No paid services, no third-party accounts — just a Gmail account and GitHub.
Daily game-day alert — subject and body are the same line:
⚾ Dodgers home game at 7:10 PM PT vs Kansas City Royals
Dodgers home game at 7:10 PM PT vs Kansas City Royals
If MLB hasn't announced first pitch yet — which happens in the postseason, never in the regular season — the time drops out rather than being guessed at:
⚾ Dodgers home game vs Philadelphia Phillies (start time TBD)
Weekly schedule — the game count and week range in the subject, one line per game in the body, closing with a link to the dashboard:
⚾ 7 Dodgers home games this week (Aug 10–16)
Mon 8/10 @ 7:10 PM 🆚 Kansas City Royals Tue 8/11 @ 7:10 PM 🆚 Kansas City Royals Wed 8/12 @ 7:10 PM 🆚 Kansas City Royals Thu 8/13 @ 7:10 PM 🆚 Milwaukee Brewers Fri 8/14 @ 7:10 PM 🆚 Milwaukee Brewers Sat 8/15 @ 4:15 PM 🆚 Milwaukee Brewers Sun 8/16 @ 1:10 PM 🆚 Milwaukee BrewersSee the full schedule and recent runs on the dashboard
The weekly email is sent as both HTML and plain text. The HTML part renders the schedule as a table so the columns line up in a proportional font; the plain-text part pads them to line up in a monospace client. Start times are Pacific.
Weekly schedule, no home games — sent only while the Dodgers' season is live (see Postseason):
⚾ No Dodgers home games this week (Aug 10–16)
No Dodgers home games this week (Aug 10–16).
See the full schedule and recent runs on the dashboard
October is the one stretch where the schedule stops behaving like a schedule. The regular season is published months ahead, every game has a start time, and every game gets played. The postseason is assigned a round at a time, start times land days late, and a series that ends early takes its remaining games off the calendar. The notifier's job is to absorb all of that so the emails and the dashboard read the same in October as they do in June.
Playoff games are just home games. Nothing filters on game type, and postseason games at Dodger Stadium come back from the same schedule endpoint, so the daily alert fires for a World Series game exactly as it does for a Tuesday in June.
The dashboard refreshes daily, not weekly. notifier.py schedule runs every morning. A weekly-only refresh was fine when the schedule was set months out, but it would leave the page up to six days behind a bracket that resolves round by round.
Start times that MLB hasn't set read as TBD rather than as the placeholder just after midnight that the API carries until the time is announced.
Games that may not be played are marked, not counted. MLB posts every game of a series up front and drops the ones the series ends without needing. Those carry an if necessary tag on the dashboard and in the weekly email, and the subject line counts them separately — ⚾ 2 Dodgers home games this week, 2 if necessary (Oct 5–11) — so a week that promises four games and plays two isn't something you find out afterwards.
An empty week says which kind of empty it is. In June "No Dodgers home games this week" means they're on the road. In October it usually means the bracket hasn't reached them yet, and the dashboard says so instead: Dodgers postseason games this week aren't set yet.
The weekly "no home games" email ends with the Dodgers' season, not the league's. MLB holds a team's bracket slots only while they're alive, so once they're eliminated the empty weeks stop being weeks worth emailing about, and the email stops with them rather than running through a World Series they aren't in. The trade is the few hours between a series ending and the next round being assigned: a Sunday run landing in that gap reads the season as over and stays quiet for the week, which the daily check still covers game by game.
The unassigned bracket never leaks in. Before the field is set, MLB publishes the postseason as placeholder games — "NL Wild Card #1" at "NL Stadium" — and both the team and venue checks throw them out.
Why not SMS? Earlier versions texted via carrier email-to-SMS gateways (e.g.
@vtext.com). Carriers are shutting those gateways down — Verizon retiresvtext.com/vzwpix.comby March 31, 2027, and delivery is already unreliable, with messages arriving late, out of order, or not at all. Email is dependable and has no length limits. To get phone notifications, enable push notifications in the Gmail app for the recipient address (a Gmail filter can label these emails so you can create a distinct alert for them).
rjayasin.github.io/dodgers-notifier is a static page served from docs/ by the Deploy GitHub Pages workflow.
Home game schedule (top of the page) comes from docs/schedule.json, which python notifier.py schedule writes every morning and python notifier.py weekly writes again on Sundays, committed back to main by whichever workflow wrote it:
{
"generated_at": "2026-08-09T23:00:12Z",
"weeks": [
{
"start": "2026-08-10",
"end": "2026-08-16",
"range": "Aug 10–16",
"games": [
{ "date": "2026-08-10", "day": "Mon 8/10", "time": "7:10 PM", "opponent": "Kansas City Royals" }
]
},
{ "start": "2026-08-17", "end": "2026-08-23", "range": "Aug 17–23", "games": [] }
]
}Two keys only ever appear in the postseason. A game gets "if_necessary": true when the series may end before reaching it, and a week gets "pending": true when it holds no Dodgers games at all because the bracket hasn't been assigned that far yet — which is what lets the card say aren't set yet rather than no home games. A start time MLB hasn't announced is written as the string "TBD".
The file is only rewritten when the weeks it would hold have actually changed. Rewriting it daily would bump generated_at alone, and since the workflow commits on any diff, that would redeploy the site every morning for nothing.
Eight consecutive weeks are published, starting with the current one (PUBLISHED_WEEKS in notifier.py) — about two months of home games in roughly 3 KB. The card opens on the week containing today, so it stays on the current week all week rather than jumping ahead the moment Sunday's run lands, and a missed Sunday run still leaves it a week to fall back on.
The ‹ › arrows beside the heading step through the published weeks. They stop at both ends — the range starts at the current week, so there is nothing behind it — and the heading names the week it lands on: this week, next week, then the date range alone.
Within a week, today's game is highlighted and games already played are greyed out, both judged against the current Pacific date. Start times are pre-formatted in Pacific rather than rendered from a timestamp, so first pitch reads the same wherever the page is opened.
Workflow run stats and charts, under a Workflow Runs heading below the schedule, come from docs/runs.json, which python notifier.py runs writes and the daily workflow commits back to main:
{
"generated_at": "2026-08-31T14:02:41Z",
"repo": "rjayasin/dodgers-notifier",
"runs": [
{
"id": 33326397502,
"name": "Dodgers Daily Check",
"run_number": 146,
"event": "schedule",
"status": "completed",
"conclusion": "success",
"run_started_at": "2026-08-30T17:50:14Z",
"updated_at": "2026-08-30T17:51:02Z"
}
]
}Runs are stored newest first, one record per run and roughly 280 bytes each — a season of runs is about 47 KB. html_url is left out and rebuilt in the page from the id, since one identical prefix per run would outweigh the run itself.
The page used to call the GitHub REST API from the browser on every visit. That spent each viewer's own unauthenticated rate limit (60 an hour, shared with everything else on their IP), and it could only ever show what GitHub still listed, so the charts quietly lost their left edge as runs aged out. Accumulating the history in the repo keeps it for good and costs the page one static request.
The file covers the two notifier workflows named in DASHBOARD_WORKFLOWS in notifier.py — Daily Check and Weekly Schedule — so site deploys, Pages builds and Keep Alive are never recorded. The page filters the same two names again through INCLUDED_WORKFLOWS, so a new workflow stays off the charts until it's deliberately added in both places. Manually triggered (workflow_dispatch) runs are stored but filtered out by the page, so they don't skew the completion-time charts.
The charts draw the whole history; the table below them lists the most recent TABLE_LIMIT runs (100) and says how many it's showing out of the total, since the file grows without bound.
Every dot on the charts is a run: hovering shows its date, completion time and result, and clicking opens that run on GitHub in a new tab. The dots are focusable, so the same works from the keyboard with Tab and Enter. Hovering a box in the day-of-week plot shows that day's median, quartiles and range instead — it summarises many runs, so it isn't a link.
Pages deploys on any push touching docs/**, and also when either cron workflow completes — a push made with a workflow's GITHUB_TOKEN deliberately does not trigger push workflows, so the daily commit and the Sunday one both need that second trigger to reach the site.
Make sure the repo lives under your own GitHub account so you can add secrets and the Actions workflow will run under your quota.
Gmail App Passwords require 2-Step Verification to be active.
- Go to myaccount.google.com/security
- Under "How you sign in to Google", click 2-Step Verification and follow the prompts to enable it.
An App Password is a 16-character one-time token that lets the script authenticate with Gmail without exposing your real password.
- Go to myaccount.google.com/apppasswords
- Under "App name", type something like
dodgers-notifierand click Create. - Google displays the 16-character password once — copy it immediately.
In your GitHub repo:
- Go to Settings → Secrets and variables → Actions
- Click New repository secret for each of the following:
| Secret name | Value |
|---|---|
GMAIL_ADDRESS |
Your Gmail address (e.g. yourname@gmail.com) |
GMAIL_APP_PASSWORD |
The 16-character App Password from step 3 |
NOTIFY_EMAIL |
(optional) Where to send notifications — any email address. Defaults to GMAIL_ADDRESS (the account emails itself) |
- Go to the Actions tab in your repo.
- If prompted with "Workflows aren't running", click I understand my workflows, go ahead and enable them.
- Both workflows — Dodgers Daily Check (daily) and Dodgers Weekly Schedule (Sunday) — will start running on their cron schedules automatically.
Test the daily check:
- In the Actions tab, select Dodgers Daily Check from the left sidebar.
- Click Run workflow → Run workflow.
- Watch the run complete — if today is a Dodgers home game you'll get an email within a minute or two. If not, the run will exit cleanly with "No Dodgers home game today."
Test the weekly schedule:
- In the Actions tab, select Dodgers Weekly Schedule from the left sidebar.
- Click Run workflow → Run workflow.
- You'll receive one email with the upcoming week's home game schedule.
Test locally against a known game date:
GMAIL_ADDRESS=you@gmail.com \
GMAIL_APP_PASSWORD=your_app_password \
GAME_DATE=2025-07-04 \
python notifier.py dailyBackfill the run history locally: notifier.py runs needs no Gmail credentials, only a token with read access to the repo's Actions. On a fresh fork this walks back through every run GitHub still lists; afterwards it stops as soon as it recognises a page.
GITHUB_TOKEN=$(gh auth token) python notifier.py runsPoint it somewhere harmless with RUNS_JSON_PATH=/tmp/runs.json to see what it would write without touching docs/.
Preview the dashboard's schedule locally: notifier.py schedule needs no credentials at all, and SCHEDULE_JSON_PATH keeps it out of docs/.
SCHEDULE_JSON_PATH=/tmp/schedule.json python notifier.py scheduleChange the notification time: Edit the cron value in .github/workflows/daily_check.yml or .github/workflows/weekly_schedule.yml. The schedule is in UTC — crontab.guru is helpful for conversions.
Notify multiple people: Add additional secrets (e.g. NOTIFY_EMAIL_2) and call send_email() once per recipient in notifier.py.
Change the team: Update DODGERS_TEAM_ID and DODGER_STADIUM_VENUE_ID in notifier.py. Team IDs and venue IDs can be looked up via the MLB Stats API: https://statsapi.mlb.com/api/v1/teams?sportId=1.