Daily email status report for YNAB categories. It pulls your YNAB budget data, computes per-category pacing and weekly allowance, renders a clean HTML/text email, and sends it via Gmail.
This repo uses uv + pyproject.toml for dependency management and includes a GitHub Actions workflow for automated runs.
- Per-category weekly allowance based on remaining days in month
- Pacing vs. target spend (slow down / on track / could spend more)
- Clear status icons and color coding for negative balances and pacing
- HTML and plain text email output (also written to
out/for inspection) - Gmail API integration (OAuth) for sending mail
- Alternatively: SMTP with Gmail App Password (no OAuth token refresh)
- Python
>= 3.11 - A YNAB Personal Access Token (API key)
- Gmail OAuth credentials (
credentials.jsonand a generatedtoken.json)- Or enable SMTP with a Gmail App Password (no OAuth files needed)
- Recommended:
uvfor managing and running the project
-
Install
uv(recommended)- macOS / Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
- macOS / Linux:
-
Sync dependencies
uv sync
-
Configure environment
-
Create a
.envfile at the repo root with at least:YNAB_API_KEY=your_ynab_api_key GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
-
Place your Gmail OAuth client file as
credentials.jsonin the repo root. The first run will perform a browser OAuth flow and writetoken.jsonnext to it. -
Alternate: if you set
GMAIL_APP_PASSWORDin your environment, the app will send via Gmail SMTP and skip OAuth entirely.
-
-
Run locally
uv run -m app.main
On first send, Gmail OAuth will open a browser for consent. After that,
token.jsonwill be reused.
Most knobs live in app/main.py.
app/main.py:1— Set your budget name inBUDGET_NAME.app/main.py:10— Customize theWATCHLISTmapping of category groups → categories. You can:- Use exact names:
"Groceries" - Use wildcards per group:
"*"to include all categories in that group - Provide objects to toggle monitoring per item:
{ "name": "Gifts", "monitor": false }
- Use exact names:
SOFT_WARN_THRESHOLD— Amount below which balance shows an amber warning- Pacing controls:
PACING_ENABLED— Toggle pacing display and status influencePACING_UPPER_OVER_PCT— Threshold over target spend to show slow down (🐢)PACING_LOWER_UNDER_PCT— Threshold under target spend to show could spend more (🐇)
- Email settings:
SENDERandRECIPIENTSDRY_RUN_WRITE_HTML— WhenTrue, also writesout/email.htmlandout/email.txt
Environment variables are loaded via Pydantic Settings from .env (see app/config.py). Required keys:
YNAB_API_KEY— Your YNAB API keyGOOGLE_OAUTH_CLIENT_ID— Your OAuth client ID (used by Google libs)
Optional for SMTP (alternate transport):
GMAIL_APP_PASSWORD— App Password for the sender Gmail account (2FA required)
Gmail credentials are provided via files expected by app/mailer.py:
credentials.json— OAuth client credentialstoken.json— Generated automatically on the first successful auth flow
app/report.py renders both HTML and plain text with:
- Days/weeks remaining and percent of month complete
- Budget last updated timestamp (converted to Pacific Time) and days ago
- Two sections: Monitoring and Not Monitoring, grouped by YNAB category group
- For monitored categories: budgeted, spent, balance, target-by-now, pacing, weekly allowance
A workflow is provided at .github/workflows/ynab-uv-cron.yml.
- Secrets you need to set in your repository settings:
YNAB_API_KEYGOOGLE_OAUTH_CLIENT_ID- If using Gmail API (OAuth):
GMAIL_CREDENTIALS_JSON— Contents of yourcredentials.jsonGMAIL_TOKEN_JSON— Contents of a workingtoken.json
- If using SMTP (App Password):
GMAIL_APP_PASSWORD— App Password for theSENDERaccount (see below)
- The schedule section is currently commented out. You can re-enable and adjust it for your timezone.
- The job uses
uv sync --frozenand runsuv run -m app.main.
Project layout:
app/main.py— Orchestration and configurationapp/ynab_client.py— Thin wrapper around theynabSDKapp/domain.py— Selection, pacing, weekly allowance, and status logicapp/report.py— Jinja templating for HTML/textapp/mailer.py— Gmail API send using OAuthapp/helpers.py— Utility helpers
Dependencies are declared in pyproject.toml; a uv.lock is present for reproducibility.
- Gmail auth keeps prompting: delete
token.jsonand re-run to regenerate it. - Want to avoid token refresh? Enable SMTP by creating a Gmail App Password (Google Account → Security → App passwords), set it as
GMAIL_APP_PASSWORDsecret, and remove the OAuth-secret steps in the workflow. - No categories found: check
BUDGET_NAMEand ensure group/category names inWATCHLISTmatch your YNAB exactly. - Timeouts with YNAB: ensure
YNAB_API_KEYis correct and network access is available. - Emails not received: check the Gmail account’s Sent folder and spam; verify
SENDERandRECIPIENTS.
- Do not commit
.env,credentials.json, ortoken.json. - Use GitHub Secrets for CI. Rotate keys if you suspect exposure.