Google Alerts is free, but it's an email digest you can't easily pipe into anything. Hand-rolled news scrapers have the opposite problem: they return the same top ten headlines every day, and you end up building deduplication yourself.

This post builds a daily "only new articles" monitor:

  1. the DIY approach and why deduplication is harder than it looks,
  2. a hosted Actor that keeps the memory for you, with real output,
  3. posting new articles to a Slack channel, plus cost and limits.

The DIY version

The naive approach is to fetch the results for each query and keep a set of seen URLs:

seen = load_seen() # set of URLs from last time for article in fetch_news(query): if article["url"] not in seen: alert(article) seen.add(article["url"]) save_seen(seen) 

In practice:

  • The same story has many URLs: tracking parameters, www. vs. bare domain, AMP versions. Exact URL matching sends duplicates.
  • Failed fetches poison the state. If a fetch times out and you still save, you can lose articles.
  • The first run floods you with everything currently ranking.
  • Several schedules or clients need separate memories.

The hosted version

Google News Monitor: Brand & Topic Alerts, Only New Articles keeps that memory in your own Apify account. Disclosure: I build this Actor at SteadyScrape. For each query it returns the title, source, publish time and original link, and on scheduled runs only the articles that are new since the last run with the same Monitor ID. It returns metadata only, not article text.

Input matching the current schema:

{ "queries": ["Acme Robotics", "\"warehouse automation\""], "mode": "monitor", "monitorId": "acme-daily", "location": "United States", "language": "en", "maxResultsPerQuery": 10, "excludeDomains": ["example-press-release-site.com"] } 

How the memory works:

  • First run: records a baseline and returns no rows (set firstRunEmitsAll: true if you want them). Baseline articles are free; the query check is charged.
  • Later runs: an article is new if its normalised link (tracking parameters and www./AMP variants removed), or its title on the same site, hasn't been seen.
  • A failed or timed-out query doesn't update memory, so its articles come back next time.
  • Use one monitorId per schedule or client, and don't run the same one twice at the same time.

There's also financeCompanies for watchlists ("AAPL: Apple"): each company becomes one query and rows carry ticker and company. It returns headlines only: no stock prices, no market data and no advice.

For a quick look without memory, use "mode": "oneOff". Real rows from a verified one-off run for SpaceX (trimmed):

[ {"query": "SpaceX", "title": "SpaceX says Google exec Don Harrison has resigned from its board", "source": "CNBC", "domain": "www.cnbc.com", "url": "https://www.cnbc.com/2026/11/06/tesdonald-harrison.html", "publishedAt": "2026-11-06T22:35:34Z", "isTopStory": true}, {"query": "SpaceX", "title": "Google executive Donald Harrison resigns from SpaceX board", "source": "Reuters", "domain": "www.reuters.com", "publishedAt": "2026-11-06T23:35:34Z", "isTopStory": true} ] 

Posting new articles to Slack

Schedule the Actor daily in Apify (Schedules → cron 0 8 * * *), then run a small script after it, or call it directly from a cron job:

import os, requests from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) run = client.actor("steady-scrape/google-news-monitor").call(run_input={ "queries": ["Acme Robotics", "\"warehouse automation\""], "mode": "monitor", "monitorId": "acme-daily", }) webhook = os.environ["SLACK_WEBHOOK_URL"] # your Slack incoming webhook for a in client.dataset(run["defaultDatasetId"]).iterate_items(): text = f"*{a['query']}* · {a['source']}\n<{a['url']}|{a['title']}>" requests.post(webhook, json={"text": text}, timeout=20) 

Because the Actor only returns new rows, the script needs no state of its own. If you'd rather not write code, connect the dataset to Slack, email, Make, Zapier or n8n with an Apify integration or webhook.

What it costs

From the Store pricing:

  • $0.004 per query checked (also when nothing new turned up)
  • $0.002 per new article returned
  • $0.005 once per run, only if at least one query was answered
  • Apify's standard $0.00005 start event

Ten queries in one daily run, with about three new articles each, cost $0.10505 a day (10 × $0.004 + 30 × $0.002 + $0.005 + $0.00005), or about $3.15 over 30 days. Failed or timed-out queries and first-run baseline articles aren't charged, and a run where no query is answered (for example, invalid input) doesn't pay the $0.005 run fee; Apify's $0.00005 start event still applies to every run. Test mode isn't charged.

Limits

  • Results are Google News search results for your query, location and language, not every article ever published.
  • Advanced operators like site: are rejected; use plain words or "exact phrases".
  • Runs usually take 1–2 minutes because results come from a standard data queue.
  • No article text, and no paywall or login access.
  • The free Apify plan allows up to 5 queries per run, with a daily limit.

SteadyScrape (steady-scrape) builds small, pay-per-result data tools on Apify.