AI agent that emails you a personalised news briefing every morning — researched from live web search, written from real articles. Runs free on GitHub Actions (Groq + Tavily), no server.
5
stars
115
commits
Python
primary language
Sep 11, 2026
updated
Wake up to an AI-written news briefing in your inbox every morning, on the
topics you choose, written from real freshly-searched articles.
100% free, no
server, runs entirely on GitHub Actions.
A real briefing. Every headline, summary and link in it was chosen and written by the agent.
It's 8am. Your coffee is still too hot to drink, so you open your inbox, and the only thing worth reading is already there. Three stories that actually matter to you, four sentences each, every source linked and dated. Nothing you were sent yesterday. No feed, no notifications, no twenty tabs you'll never get back to.
While you were asleep, an agent searched the web on your topics, threw out the filler, read what was left and wrote it up. By the time your coffee is drinkable, you're caught up, and you close the tab and get on with your day.
You need two free API keys. No Gmail, no deploy, no account anywhere else.
git clone https://github.com/tballochi/daily-briefing.git
cd daily-briefing
pip install -r requirements.txt
python main.py --setup # asks for your keys and checks each one works
python main.py --dry-run # builds a real briefing, sends nothing
Both keys are free: Groq and
Tavily. Prefer not to use the wizard? Put them in a .env
file yourself as GROQ_API_KEY= and TAVILY_API_KEY=.
--dry-run researches the news, writes the briefing, prints it to your terminal and
saves the rendered email to data/preview.html. It sends nothing and stores nothing,
so you can see exactly what you'd be getting before setting up delivery.
Then edit config.yaml to your own topics and run it again.
Every morning, an autonomous agent:
It's a tool-using agent, not a fixed script: it decides what to search, judges what's worth keeping, and searches again if the results are thin.
In the email: your N top stories, each with a factual summary, source and publication date · a word of the day · a quote of the day. Optionally, one slot is reserved for a focus theme you never want to miss.
config.yamlOne readable file, no code, no secrets:
title: Daily Briefing # shown in the email subject + header
num_articles: 3 # how many stories per morning
topics: # what the agent researches (free text)
- AI & LLMs (GPT, Claude, agents, MCP)
- automation & no-code (n8n, Zapier, Make)
- dev tools & open source
focus: # OPTIONAL: a story that's ALWAYS included
label: shipping / maritime / logistics
priority_query: CMA CGM # searched first; preferred when there's fresh news
keywords: [cma cgm, shipping, maritime, container, freight]
model: openai/gpt-oss-120b # the Groq model the agent thinks with
model_fallbacks: # tried in order if the one above is retired
- openai/gpt-oss-20b
- qwen/qwen3.8-27b
Topics are free text, so point it at finance, climate, football, your industry, whatever
you want to wake up informed about. Don't want a guaranteed story? Delete the focus:
block. No config.yaml at all? Sensible defaults keep it running.
Nothing stays on. No server, no container, no cron on your laptop.
1. Click Use this template to get your own copy (or fork it). Then clone it and run:
python main.py --setup
The wizard asks for each key, verifies it against the real API before saving, so a wrong
Gmail App Password fails here instead of silently at 8am tomorrow. It clears the inherited
de-duplication history, offers to upload your secrets with the gh CLI, and prints your
pinger URL with your repo already filled in.
2. Add 5 Settings → Secrets and variables → Actions secrets (the wizard can do
this for you if you have the gh CLI):
| Secret | Where to get it |
|---|---|
GROQ_API_KEY | console.groq.com/keys, free |
TAVILY_API_KEY | app.tavily.com, free, 1000 searches/month |
GMAIL_ADDRESS | the Gmail that sends the briefing |
GMAIL_APP_PASSWORD | 16-char App Password (needs 2FA). Not your Gmail password |
RECIPIENT_EMAIL | where it gets delivered |
3. Test it now: Actions → Daily Briefing → Run workflow, tick force.
4. Make it punctual by adding a free external pinger (5 minutes, one time):
GitHub's own schedule: cron is unreliable on low-activity repos: it drops most
triggers and can fire hours late, so "before 10am" isn't guaranteed by it alone. The fix,
still 100% free, is a tiny external cron that calls GitHub at a fixed time. GitHub's own
cron stays on as a best-effort backup.
a) Create a GitHub token at new fine-grained token
github_pat_... valueb) Create a free job on cron-job.org, daily at ~08:00 in your timezone:
POSThttps://api.github.com/repos/<you>/daily-briefing/actions/workflows/daily-briefing.yml/dispatches{"ref":"main"}Accept: application/vnd.github+jsonAuthorization: Bearer <your github_pat_...>X-GitHub-Api-Version: 2022-11-28A successful call returns HTTP 204. The pinger fires without forcing, so it still passes the morning-window and once-a-day guards, so it can never double-send.
Renamed the repo later? Update the URL by hand; GitHub redirects GETs but not reliably POSTs.
The things this does differently, and what they cost:
| No paid step, anywhere | It runs inside the Groq, Tavily and GitHub Actions free tiers. A daily run uses ~180 of Tavily's 1000 monthly searches. Worth being clear: those are three companies' free tiers, not a guarantee I can make. If one changes, this changes. |
| No infrastructure | No server, no Docker, no database, no always-on process. The trade-off is that you're running on GitHub's schedule and inside their limits. |
| An agent, not an RSS pipeline | It picks its own search queries, judges result quality, and searches again when results are thin, which also means it's slower and less predictable than a feed reader. |
| Never repeats a story | Sent articles are matched by normalised URL and headline, kept in data/history.json and committed back to the repo. No database to run; the trade-off is a daily commit in your history. |
| Never double-sends | A morning-window guard plus a once-a-day guard, so several triggers still produce one email. |
| Survives model deprecations | The model is configurable with a fallback chain. This exists because a hardcoded model was retired by the provider and took the agent down for a day. |
| Grounded summaries | Written only from real source text, and any URL the model didn't actually find in a search result is dropped before it can reach the email. |
| Keys stay yours | They live in your own repo's secrets or your local .env. The agent talks to Groq, Tavily and Gmail's SMTP server, and nothing else. |
Nothing. A run uses a handful of Groq calls and up to 6 Tavily searches, well inside both free tiers (Tavily gives 1000 searches/month; the briefing uses ~180). GitHub Actions is free for public repos. cron-job.org is free. There is no paid tier to graduate into.
Your keys live in your GitHub repo's Actions secrets (or your local .env, which is
gitignored). The agent talks only to Groq, Tavily and Gmail's SMTP server. Nothing is
sent anywhere else, and the author never sees your keys, topics or briefings.
Edit model: in config.yaml to any model your Groq key can reach, and list backups
under model_fallbacks:. You can also override it without editing the file by setting
the GROQ_MODEL environment variable (or a GROQ_MODEL GitHub Actions variable).
Providers retire models regularly, so check
Groq's deprecations page for what's live.
Yes, with a small edit. email_sender.py uses plain smtplib over SSL, so pointing
SMTP_HOST / SMTP_PORT at another provider (Fastmail, Zoho, a company relay) and using
that account's credentials works. Gmail is the default only because its App Passwords are
free and easy.
Yes. The time is set by your cron-job.org job; the workflow only enforces a
06:00–11:00 Europe/Paris safety window so a stray trigger can't email you at night.
To move outside that window, edit the hour list in the gate step of
.github/workflows/daily-briefing.yml (and TIMEZONE in scheduler.py for local runs).
It retries once after two minutes. If that also fails you get a short "couldn't send today" email explaining why, the full traceback goes to the Actions log, and the run is marked failed, so a broken morning is red in the Actions tab rather than a green check. The next morning's run is unaffected.
Set num_articles: to whatever you like. One briefing per day is enforced by design,
because the de-duplication history keys on the date, so a second daily send would need that
guard changed.
python main.py --setup # guided setup (or copy .env.example by hand)
python main.py --dry-run # preview only, sends nothing
python main.py --now # build & send one briefing now
python main.py # run the scheduler (daily at 09:00 Europe/Paris)
pip install -r requirements-dev.txt
pytest -q
Mocked network, no API keys needed, runs in under a second. Covers the model fallback chain, config defaults, de-duplication and both send guards. CI runs it on every push.
| Tool | Role |
|---|---|
| Python 3.11+ | Core language. 5 dependencies, no framework |
Groq (openai/gpt-oss-120b) | The agent's brain. Configurable, with fallbacks |
| Tavily | Real-time news search |
| GitHub Actions | Runtime for the daily job |
| cron-job.org | External pinger, so it fires on time |
daily-briefing/
├── config.yaml # your preferences: title, topics, focus, model
├── config.py # loads/validates config.yaml, checks required secrets
├── main.py # entry point (--setup / --dry-run / --now)
├── setup_wizard.py # guided setup, verifies every key
├── agent.py # the agent: research loop, writing, HTML rendering
├── email_sender.py # Gmail delivery
├── scheduler.py # the briefing jobs + local daily scheduler
├── history.py # remembers sent articles (no repeats)
├── data/history.json # the de-duplication memory
├── docs/ # screenshot + sample briefing
├── tests/ # pytest suite (no network)
└── .github/workflows/ # daily-briefing.yml (the morning run) + ci.yml
Issues and PRs welcome. See CONTRIBUTING.md.
MIT. See LICENSE.
Built by Timoté Ballochi.
Python
100.0%
AI agent that emails you a personalised news briefing every morning — researched from live web search, written from real articles. Runs free on GitHub Actions (Groq + Tavily), no server.
5
stars
115
commits
Python
primary language
Sep 11, 2026
updated
Wake up to an AI-written news briefing in your inbox every morning, on the
topics you choose, written from real freshly-searched articles.
100% free, no
server, runs entirely on GitHub Actions.
A real briefing. Every headline, summary and link in it was chosen and written by the agent.
It's 8am. Your coffee is still too hot to drink, so you open your inbox, and the only thing worth reading is already there. Three stories that actually matter to you, four sentences each, every source linked and dated. Nothing you were sent yesterday. No feed, no notifications, no twenty tabs you'll never get back to.
While you were asleep, an agent searched the web on your topics, threw out the filler, read what was left and wrote it up. By the time your coffee is drinkable, you're caught up, and you close the tab and get on with your day.
You need two free API keys. No Gmail, no deploy, no account anywhere else.
git clone https://github.com/tballochi/daily-briefing.git
cd daily-briefing
pip install -r requirements.txt
python main.py --setup # asks for your keys and checks each one works
python main.py --dry-run # builds a real briefing, sends nothing
Both keys are free: Groq and
Tavily. Prefer not to use the wizard? Put them in a .env
file yourself as GROQ_API_KEY= and TAVILY_API_KEY=.
--dry-run researches the news, writes the briefing, prints it to your terminal and
saves the rendered email to data/preview.html. It sends nothing and stores nothing,
so you can see exactly what you'd be getting before setting up delivery.
Then edit config.yaml to your own topics and run it again.
Every morning, an autonomous agent:
It's a tool-using agent, not a fixed script: it decides what to search, judges what's worth keeping, and searches again if the results are thin.
In the email: your N top stories, each with a factual summary, source and publication date · a word of the day · a quote of the day. Optionally, one slot is reserved for a focus theme you never want to miss.
config.yamlOne readable file, no code, no secrets:
title: Daily Briefing # shown in the email subject + header
num_articles: 3 # how many stories per morning
topics: # what the agent researches (free text)
- AI & LLMs (GPT, Claude, agents, MCP)
- automation & no-code (n8n, Zapier, Make)
- dev tools & open source
focus: # OPTIONAL: a story that's ALWAYS included
label: shipping / maritime / logistics
priority_query: CMA CGM # searched first; preferred when there's fresh news
keywords: [cma cgm, shipping, maritime, container, freight]
model: openai/gpt-oss-120b # the Groq model the agent thinks with
model_fallbacks: # tried in order if the one above is retired
- openai/gpt-oss-20b
- qwen/qwen3.8-27b
Topics are free text, so point it at finance, climate, football, your industry, whatever
you want to wake up informed about. Don't want a guaranteed story? Delete the focus:
block. No config.yaml at all? Sensible defaults keep it running.
Nothing stays on. No server, no container, no cron on your laptop.
1. Click Use this template to get your own copy (or fork it). Then clone it and run:
python main.py --setup
The wizard asks for each key, verifies it against the real API before saving, so a wrong
Gmail App Password fails here instead of silently at 8am tomorrow. It clears the inherited
de-duplication history, offers to upload your secrets with the gh CLI, and prints your
pinger URL with your repo already filled in.
2. Add 5 Settings → Secrets and variables → Actions secrets (the wizard can do
this for you if you have the gh CLI):
| Secret | Where to get it |
|---|---|
GROQ_API_KEY | console.groq.com/keys, free |
TAVILY_API_KEY | app.tavily.com, free, 1000 searches/month |
GMAIL_ADDRESS | the Gmail that sends the briefing |
GMAIL_APP_PASSWORD | 16-char App Password (needs 2FA). Not your Gmail password |
RECIPIENT_EMAIL | where it gets delivered |
3. Test it now: Actions → Daily Briefing → Run workflow, tick force.
4. Make it punctual by adding a free external pinger (5 minutes, one time):
GitHub's own schedule: cron is unreliable on low-activity repos: it drops most
triggers and can fire hours late, so "before 10am" isn't guaranteed by it alone. The fix,
still 100% free, is a tiny external cron that calls GitHub at a fixed time. GitHub's own
cron stays on as a best-effort backup.
a) Create a GitHub token at new fine-grained token
github_pat_... valueb) Create a free job on cron-job.org, daily at ~08:00 in your timezone:
POSThttps://api.github.com/repos/<you>/daily-briefing/actions/workflows/daily-briefing.yml/dispatches{"ref":"main"}Accept: application/vnd.github+jsonAuthorization: Bearer <your github_pat_...>X-GitHub-Api-Version: 2022-11-28A successful call returns HTTP 204. The pinger fires without forcing, so it still passes the morning-window and once-a-day guards, so it can never double-send.
Renamed the repo later? Update the URL by hand; GitHub redirects GETs but not reliably POSTs.
The things this does differently, and what they cost:
| No paid step, anywhere | It runs inside the Groq, Tavily and GitHub Actions free tiers. A daily run uses ~180 of Tavily's 1000 monthly searches. Worth being clear: those are three companies' free tiers, not a guarantee I can make. If one changes, this changes. |
| No infrastructure | No server, no Docker, no database, no always-on process. The trade-off is that you're running on GitHub's schedule and inside their limits. |
| An agent, not an RSS pipeline | It picks its own search queries, judges result quality, and searches again when results are thin, which also means it's slower and less predictable than a feed reader. |
| Never repeats a story | Sent articles are matched by normalised URL and headline, kept in data/history.json and committed back to the repo. No database to run; the trade-off is a daily commit in your history. |
| Never double-sends | A morning-window guard plus a once-a-day guard, so several triggers still produce one email. |
| Survives model deprecations | The model is configurable with a fallback chain. This exists because a hardcoded model was retired by the provider and took the agent down for a day. |
| Grounded summaries | Written only from real source text, and any URL the model didn't actually find in a search result is dropped before it can reach the email. |
| Keys stay yours | They live in your own repo's secrets or your local .env. The agent talks to Groq, Tavily and Gmail's SMTP server, and nothing else. |
Nothing. A run uses a handful of Groq calls and up to 6 Tavily searches, well inside both free tiers (Tavily gives 1000 searches/month; the briefing uses ~180). GitHub Actions is free for public repos. cron-job.org is free. There is no paid tier to graduate into.
Your keys live in your GitHub repo's Actions secrets (or your local .env, which is
gitignored). The agent talks only to Groq, Tavily and Gmail's SMTP server. Nothing is
sent anywhere else, and the author never sees your keys, topics or briefings.
Edit model: in config.yaml to any model your Groq key can reach, and list backups
under model_fallbacks:. You can also override it without editing the file by setting
the GROQ_MODEL environment variable (or a GROQ_MODEL GitHub Actions variable).
Providers retire models regularly, so check
Groq's deprecations page for what's live.
Yes, with a small edit. email_sender.py uses plain smtplib over SSL, so pointing
SMTP_HOST / SMTP_PORT at another provider (Fastmail, Zoho, a company relay) and using
that account's credentials works. Gmail is the default only because its App Passwords are
free and easy.
Yes. The time is set by your cron-job.org job; the workflow only enforces a
06:00–11:00 Europe/Paris safety window so a stray trigger can't email you at night.
To move outside that window, edit the hour list in the gate step of
.github/workflows/daily-briefing.yml (and TIMEZONE in scheduler.py for local runs).
It retries once after two minutes. If that also fails you get a short "couldn't send today" email explaining why, the full traceback goes to the Actions log, and the run is marked failed, so a broken morning is red in the Actions tab rather than a green check. The next morning's run is unaffected.
Set num_articles: to whatever you like. One briefing per day is enforced by design,
because the de-duplication history keys on the date, so a second daily send would need that
guard changed.
python main.py --setup # guided setup (or copy .env.example by hand)
python main.py --dry-run # preview only, sends nothing
python main.py --now # build & send one briefing now
python main.py # run the scheduler (daily at 09:00 Europe/Paris)
pip install -r requirements-dev.txt
pytest -q
Mocked network, no API keys needed, runs in under a second. Covers the model fallback chain, config defaults, de-duplication and both send guards. CI runs it on every push.
| Tool | Role |
|---|---|
| Python 3.11+ | Core language. 5 dependencies, no framework |
Groq (openai/gpt-oss-120b) | The agent's brain. Configurable, with fallbacks |
| Tavily | Real-time news search |
| GitHub Actions | Runtime for the daily job |
| cron-job.org | External pinger, so it fires on time |
daily-briefing/
├── config.yaml # your preferences: title, topics, focus, model
├── config.py # loads/validates config.yaml, checks required secrets
├── main.py # entry point (--setup / --dry-run / --now)
├── setup_wizard.py # guided setup, verifies every key
├── agent.py # the agent: research loop, writing, HTML rendering
├── email_sender.py # Gmail delivery
├── scheduler.py # the briefing jobs + local daily scheduler
├── history.py # remembers sent articles (no repeats)
├── data/history.json # the de-duplication memory
├── docs/ # screenshot + sample briefing
├── tests/ # pytest suite (no network)
└── .github/workflows/ # daily-briefing.yml (the morning run) + ci.yml
Issues and PRs welcome. See CONTRIBUTING.md.
MIT. See LICENSE.
Built by Timoté Ballochi.
Python
100.0%