โ—ˆ MEADOW LARK - ACTIVE SESSION Floor 1 ยท 6 entries logged

๐Ÿฆ Meadow Lark

Posts / Daily News by Email โ€“ Automated, Free, Self-Built

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:

  1. GitHub Actions fires daily at 16:00 UTC (= 18:00 CEST)
  2. run.py loads config.json and prompt.md from the repo
  3. Gemini 2.5 Flash researches the news via Google Search Grounding
  4. The script formats the result as an HTML email
  5. Resend delivers the mail
  6. 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

ServiceFree TierMy Usage
Gemini 2.5 Flash500 requests/day1โ€“2/day
Resend100 emails/day1โ€“2/day
GitHub Actions2,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.

#Python #Gemini #Github-Actions #Automation #Ai