Daily News by Email โ Automated, Free, Self-Built
Every evening at 6 PM I want to know what moved the day. Not the front page of some news portal with its clickbait headlines. Not a newsletter someone else curated for a different audience. My news โ my topics, my language, my format.
So I built it myself.
The Idea
The concept is simple: a Python script runs once a day, asks Gemini for the most important news of the day, and sends the result as a formatted HTML email to my inbox. Done.
The interesting question was: where does the script run? A dedicated server would be overkill. fly.io, Sprites, Cloud Functions โ all possible, but more effort than necessary. The answer was sitting right in the repository: GitHub Actions.
The repository exists anyway, because I want to version-control the prompt and configuration. If I already have a repo, GitHub Actions can handle the cron job โ free, precise, no additional infrastructure.
The Architecture
GitHub Repository
โโโ run.py โ the actual script
โโโ config.json โ topics, recipients, format
โโโ prompt.md โ instructions for the AI model
โโโ .github/
โโโ workflows/
โโโ news.yml โ cron job, daily at 6 PM
The flow on each run:
- GitHub Actions fires daily at 16:00 UTC (= 18:00 CEST)
run.pyloadsconfig.jsonandprompt.mdfrom the repo- Gemini 2.5 Flash researches the news via Google Search Grounding
- The script formats the result as an HTML email
- Resend delivers the mail
- The runner exits โ no ongoing costs
The Core: Google Search Grounding
The key difference from a regular LLM call is Google Search Grounding. Gemini gets direct access to current web content โ no hallucinating yesterday’s news, no stale training data.
The API call looks like this:
body = {
"system_instruction": {"parts": [{"text": prompt}]},
"contents": [{"role": "user", "parts": [{"text": user_message}]}],
"tools": [{"google_search": {}}] # โ this is the key
}
response = requests.post(
f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}",
json=body,
timeout=60
)
The google_search parameter activates the grounding. Gemini decides when and what to search for โ guided by the prompt.
The Prompt as Control Center
The prompt lives as prompt.md in the repository. That’s the real lever: anyone who wants to change the structure of the email edits a Markdown file and pushes. The next run at 6 PM picks up the new version โ no deployment, no rebuild.
My prompt defines a fixed output structure with seven sections: Top News, Story of the Day, Germany, International, Technology, Markets, and Tomorrow’s Outlook. Plus style rules: always German, no opinions, no articles older than 24 hours, sources in parentheses.
## Output Structure
### Top News
5โ7 headlines. **Bold headline**, 2โ3 sentences of context. *(Source)*
### Story of the Day
The most important event (~150 words): What happened? What's the background? What comes next?
### Markets
- DAX (closing price, change in %)
- EUR/USD
- Brent crude (USD/barrel)
...
The Configuration
Topics and recipients live in config.json:
{
"recipient_email": "[email protected]",
"sender_name": "Evening Edition",
"topics": [
"Automotive & Electric Mobility",
"Artificial Intelligence & Technology",
"German Fiscal and Economic Policy",
"Energy & Climate Policy"
],
"subject_template": "๐ฐ Evening Edition โ {date}"
}
The topics array ends up in the prompt under “My Topics” โ Gemini then actively searches for news from those areas.
The GitHub Actions Workflow
# .github/workflows/news.yml
name: Daily News Mailer
on:
schedule:
- cron: '0 16 * * *' # 16:00 UTC = 18:00 CEST
jobs:
send-news:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run news mailer
run: pip install requests && python run.py
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
SENDER_EMAIL: ${{ secrets.SENDER_EMAIL }}
Secrets are stored under Repository โ Settings โ Secrets and variables โ Actions โ never in code, never in files.
In winter the cron needs to be adjusted to 0 17 * * * (CET instead of CEST).
What It Costs
| Service | Free Tier | My Usage |
|---|---|---|
| Gemini 2.5 Flash | 500 requests/day | 1โ2/day |
| Resend | 100 emails/day | 1โ2/day |
| GitHub Actions | 2,000 min/month | ~2 min/day |
Total: โฌ0 per month. Permanently, no credit card required.
What I Learned
Prompt engineering is the actual product. The Python script took an hour to write. Getting the prompt right โ so that Gemini reliably produces the correct structure, doesn’t invent links, and actually only uses current articles โ took more iterations than the code.
GitHub Actions as a cron is underrated. For anything that runs once a day and doesn’t need a persistently running deployment, it’s the most straightforward solution.