# Company & Keyword News Monitor: New Articles, Deduped (`southern-data/company-news-monitor`) Actor

Watch companies and keywords in world news. Get only new articles since your last run, real publisher links, one row per story across outlets, and event tags. GDELT + your RSS feeds.

- **URL**: https://apify.com/southern-data/company-news-monitor.md
- **Developed by:** [Southern Data](https://apify.com/southern-data) (community)
- **Categories:** News, Lead generation, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 story returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Company & Keyword News Monitor: New Articles, Deduped

Watch companies, competitors and keywords across world news. Each run returns **only the stories that are new since your last run**. The same story from many outlets becomes **one row**, links go **straight to the publisher**, and every run gives you the same 25 spreadsheet-ready columns.

**Typical uses:** competitor monitoring ("Acme" + "outage" / "lawsuit" / "complaints"), PR and brand mentions, funding / layoffs / acquisition triggers for sales, and keyword alerts for a newsletter.

### Read this first: what it is and what it isn't

- **This is not a Google News scraper.** Google allows its News RSS feeds only "within a personal feed reader for personal, non-commercial use", and scraping Google News pages gets blocked (that's the main complaint about Google News scrapers). This actor uses sources that permit this use and keep working:
  1. **The GDELT Project**: an open index of world news (tens of thousands of outlets, 50,000–110,000 English articles a day, plus ~150,000 a day translated from ~65 languages). It is free to use with attribution, so every row carries an `attribution` column. Keep it when you republish.
  2. **Your own RSS/Atom feeds** (publishers, company newsrooms, blogs). It can also find a company's own newsroom feed from its domain.
  3. **Hacker News** (optional).
- **Coverage is GDELT's, not Google's.** Most news sites are included, but some big outlets block GDELT's crawler or appear rarely (in our tests, a 24-hour search for "China" limited to reuters.com, bbc.co.uk, bbc.com and abc.net.au found a single ABC story). For must-have outlets, add their RSS feeds in `feedUrls`.
- **Delay: about 15–60 minutes.** GDELT publishes a file every 15 minutes, and an article usually shows up 15–60 minutes after it appears. For GDELT rows, `publishedAt` is the time GDELT first saw the article (`dateType = first_seen_by_gdelt`), not the publisher's own timestamp. Feed and Hacker News rows carry the real publication time (`dateType = published`).
- **Matching is on headlines and links, plus organisations named in the body.** It isn't a full-text keyword search:
  - **Keywords** are matched in the headline and the link (and in feed summaries).
  - **Companies** are also matched when GDELT lists them among the organisations named in the article text, if they're named at least twice (`minArticleMentions`, default 2). These rows show `matchedIn = "article text"`. They are the most tangential, so filter on `matchedIn` if you only want headline mentions, or turn `searchArticleEntities` off.
- **Keywords match literally.** "raises" also finds "raises alarm", and "down" finds "breaks down". Prefer phrases ("funding round", "data breach", "outage"), use `excludeKeywords`, or filter on the `eventTypes` column.
- **No article text.** You get the headline, link, date, outlet, share image and (for feeds) the publisher's own summary. Paywalled content is not read.
- **One run covers at most 7 days.** Run a longer history week by week (`dateFrom` / `dateTo`). History goes back to 19 Feb 2015.
- **A full 7-day backfill takes longer** (about 10–16 minutes for 672 GDELT files). A daily or hourly monitor run usually finishes in seconds to a few minutes.
- Some feeds refuse automated readers (one company newsroom answered HTTP 500 in our tests). Failed feeds are listed in the `SOURCES` record and are never charged.

### How "new articles only" works

`mode = "new_articles"` (the default) remembers which stories and which GDELT hours each monitor has already handled. It keeps that memory in a named key-value store (`monitorName`, or one per saved task).

- **First run:** returns the last `lookbackHours` (default 24).
- **Every later run:** continues from where the previous one stopped. You only get (and pay for) stories you haven't seen, and an hour of news is never scanned twice.
- **Same story, new outlet:** a story you already received doesn't come back because one more outlet picked it up.
- **Fresh start:** use a new `monitorName`.

Schedule it hourly, daily or weekly. The total cost is about the same, because you pay per hour of news scanned, not per run.

`mode = "all_articles"` returns everything in a window: `dateFrom` / `dateTo`, or the last `lookbackHours`.

- **Dates** can be plain dates (UTC), date-times or relative times ("3 days").
- **A plain `dateTo` includes that whole day.** From `2026-10-20` to `2026-10-21` covers 48 hours.

### Input example

```json
{
  "companies": ["Atlassian (atlassian.com)", "Asana", "Meta | Facebook"],
  "keywords": ["outage", "data breach", "layoffs"],
  "matchLogic": "any",
  "mode": "new_articles",
  "monitorName": "competitor-news",
  "lookbackHours": 24,
  "sources": ["gdelt", "feeds"],
  "feedUrls": ["https://techcrunch.com/feed/"],
  "excludeKeywords": ["stock price", "shares sold"]
}
```

- **Companies:** a name, a domain, both (`"Acme Corp (acme.com)"`), or variants separated by `|`. Legal suffixes (Inc, Ltd, Corp…) are ignored when matching.
- **Competitor complaints:** set `matchLogic: "company_and_keyword"`. Then the company must be in the headline or link (or the article is on its own site or feed), and so must one of your keywords. A passing mention of the company in the body isn't enough there.
- **Other languages:** set `languages: ["de","fr"]` (or `"any"`). Matching uses GDELT's English translation, but headlines stay in the original language.
- **Filters:** `onlyDomains` and `excludeDomains`, plus `maxArticles` (newest first; in a monitor, the rest wait for the next run).

### Output (25 columns, same order every run)

| Column | Meaning |
|---|---|
| storyKey | Stable id of the story (same across runs) |
| title, url, sourceDomain | Headline, publisher link (never a Google redirect), outlet |
| publishedAt, dateType, ageHours | Date (see above), what kind of date it is, and its age at run time |
| language | Two-letter language of the article |
| matchedCompanies, matchedKeywords, matchedIn | What matched and where: title, link, summary, article text, company site, company feed, your feed |
| eventTypes | Tags from the headline: funding, acquisition, ipo, earnings, layoffs, hiring, expansion, product, partnership, leadership, legal, regulatory, security, outage, complaints |
| tone | GDELT tone score (negative = negative coverage) |
| outletCount, otherOutlets, otherUrls | How many outlets ran this story, and the others' domains and links (`; `-separated text, so CSV columns stay fixed) |
| summary | Feed summary (feeds only) |
| imageUrl | Share image |
| source, feedUrl, hnUrl | gdelt / feed / hackernews, the feed it came from, the Hacker News discussion |
| isNew, firstSeenAt | Monitor mode: true for stories new to this monitor, and when the monitor first saw them |
| attribution | Source credit (GDELT requires citation) |
| scrapedAt | Run time |

Each run also writes:

- **RUN_SUMMARY**: counts, filters, what was not returned and why, and charged events.
- **SOURCES**: every source read, files, errors and coverage.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| Story returned | $0.0015 | Per row. Duplicates and filtered-out articles are free. A monitor only returns new stories |
| Hour of world news scanned (English) | $0.0006 | Per hour of GDELT's English stream scanned. At least 1 per run when the stream is read; a monitor never scans the same hour twice |
| Hour of world news scanned (other languages) | $0.0015 | Only when you add languages other than English |
| RSS/Atom feed read | $0.0003 | Per feed read successfully; failed feeds are free |

**Store discount tiers:** free-plan and Bronze users pay the prices above. Silver gets 10% off and Gold (and higher) gets 20% off.

Examples:

- A **daily English monitor** costs $0.0144 per day for news scanning, plus $0.0015 per new story. For example, 30 new stories a day comes to about $0.06 a day.
- An **hourly schedule** costs the same per day as a daily one. Every run that checks GDELT is charged at least 1 news hour, even if GDELT has no new file since the previous run, so don't schedule more often than every 15 minutes (GDELT adds a file every 15 minutes). Hourly or daily is best.
- **Invalid input** and the **self-test** cost $0.

To cap spending, set the run's maximum cost. The actor stops returning rows at the cap and tells you how many it didn't return.

### Reliability

- GDELT files are downloaded with retries and timeouts. A missing or not-yet-published file is skipped and reported, not guessed. If GDELT can't be reached at all, the run fails rather than return an empty "success", and nothing is charged for it.
- Bad input (no companies or keywords, an unknown language, an impossible date window, only broken feed URLs, a Google News feed) ends with a clear message and costs $0.
- A built-in self-test (`{"selfTest": true}`) runs offline checks of matching, de-duplication, dates, feed parsing and memory, plus live checks of each source. The developer runs it daily.

### FAQ

**Why not Google News?** Its feeds are licensed for personal, non-commercial feed readers only, and scraping it gets blocked. A tool that breaks every few weeks isn't a monitor.

**Do I get Google redirect links?** No. Every link points to the publisher's own article.

**Why did I get an article that doesn't mention my company in the headline?** Its `matchedIn` is "article text": GDELT found your company named at least twice in the article. Raise `minArticleMentions` or switch `searchArticleEntities` off.

**Can I republish the results?** Headlines and links, yes, with the attribution column. Article content belongs to the publishers.

# Actor input Schema

## `companies` (type: `array`):

One company per line. Write the name ("Acme Corp"), a domain ("acme.com"), both ("Acme Corp (acme.com)"), or name variants separated by | ("Meta | Facebook"). Legal suffixes (Inc, Ltd, Corp…) are also matched without them. Matched in headlines, links and (if searchArticleEntities is on) organisations named anywhere in the article. Max 200.

## `keywords` (type: `array`):

Words or phrases, one per line, e.g. "data breach", "outage", "layoffs". Whole-word match, case and accents ignored; "recruit\*" also matches recruiter/recruiting. Matched in headlines and links (and feed summaries). Max 200.

## `matchLogic` (type: `string`):

"any": an article matching any company or any keyword. "company_and_keyword": one of your companies must be in the headline or link (or the article is on its own site/feed) AND one of your keywords must be there too — a passing mention of the company in the body is not enough here.

## `excludeKeywords` (type: `array`):

Articles whose headline (or feed summary) contains any of these are dropped, e.g. "stock price", "horoscope".

## `mode` (type: `string`):

"new_articles": each run returns only stories not returned by an earlier run of this monitor (the first run looks back lookbackHours). "all_articles": every matching story in the window (dateFrom/dateTo or the last lookbackHours).

## `monitorName` (type: `string`):

Name of the memory used by "new_articles" (e.g. "competitor-news"). Keep it the same across runs; change it to start a fresh monitor. Optional when you run from a saved task (the task gets its own memory).

## `lookbackHours` (type: `integer`):

"all_articles": the window when dateFrom is empty. "new_articles": how far back the FIRST run looks. Max 168 (7 days).

## `dateFrom` (type: `string`):

Start of the window: a date "2026-10-30" (UTC), a date-time "2026-10-30T08:00:00Z", or relative "3 days" / "12 hours". Any date since 19 Feb 2015. One run covers at most 7 days.

## `dateTo` (type: `string`):

End of the window (same formats). A plain date includes that whole day (UTC): From 2026-10-20 + To 2026-10-21 = 48 hours. Empty = now.

## `sources` (type: `array`):

"gdelt": the GDELT Project's open index of world news (50,000–110,000 English articles a day, plus ~150,000 in other languages if you add languages). "feeds": the feeds in feedUrls (and company newsroom feeds if turned on). "hackernews": stories posted on Hacker News.

## `feedUrls` (type: `array`):

Publisher or newsroom feeds to watch, e.g. "https://techcrunch.com/feed/". A web page link works too if it advertises a feed. Google News feeds are not accepted (Google allows them only in a personal feed reader). Max 300.

## `feedsMatchKeywords` (type: `boolean`):

On: feed items must match your companies/keywords like everything else. Off: every new item of your feeds is returned (exclude keywords still apply).

## `companyNewsroomFeeds` (type: `boolean`):

For companies given with a domain ("acme.com"), find the news/blog feed the company publishes and return its posts. Charged as a feed read when found.

## `languages` (type: `array`):

Two-letter codes, e.g. "en", "de", "fr", or "any". Other languages come from GDELT's translated stream (headlines stay in the original language; company names are matched in the English translation of the text). Each hour of the translated stream is charged separately.

## `searchArticleEntities` (type: `boolean`):

GDELT lists the organisations named in each article's full text. On: an article that mentions your company only in the body is returned too (matchedIn = "article text"). Off: headline and link matches only.

## `minArticleMentions` (type: `integer`):

For matches found only in the article body (matchedIn = "article text"): how many times the company must be named. 2 (default) drops passing mentions such as photo credits or link lists; 1 returns every article that names it once.

## `dedupeStories` (type: `boolean`):

One row per story: the same headline (or 70%+ of its words) on several outlets is one row, with otherOutlets / otherUrls. Off: one row per article link.

## `dropOldArticles` (type: `boolean`):

Some sites re-publish old pages. When an article link carries a date (e.g. /2019/05/02/) older than your window, it is dropped.

## `excludeDomains` (type: `array`):

Domains to ignore, e.g. "example.com" (subdomains included).

## `onlyDomains` (type: `array`):

If set, only articles from these domains (subdomains included).

## `includeImage` (type: `boolean`):

imageUrl column (the article's share image, from GDELT).

## `maxArticles` (type: `integer`):

Newest first. In new_articles mode, stories over the limit are kept for the next run.

## `selfTest` (type: `boolean`):

Run the built-in checks (matching, de-duplication, dates, feed parsing, memory, live sources) instead of searching. Nothing is charged; the run fails if a check fails.

## Actor input object example

```json
{
  "companies": [
    "Atlassian (atlassian.com)",
    "Asana"
  ],
  "keywords": [
    "outage"
  ],
  "matchLogic": "any",
  "mode": "new_articles",
  "lookbackHours": 24,
  "sources": [
    "gdelt",
    "feeds"
  ],
  "feedsMatchKeywords": true,
  "companyNewsroomFeeds": false,
  "languages": [
    "en"
  ],
  "searchArticleEntities": true,
  "minArticleMentions": 2,
  "dedupeStories": true,
  "dropOldArticles": true,
  "includeImage": true,
  "maxArticles": 500,
  "selfTest": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per story. Views: Stories, Story details, All fields (CSV export).

## `sources` (type: `string`):

SOURCES record: every source (GDELT stream, feed, Hacker News) with its status, files/items read, matches and any error.

## `runSummary` (type: `string`):

RUN_SUMMARY record: window, news covered up to, matches, duplicates merged, removed by each filter, charged events, monitor store, any input problem.

## `selfTest` (type: `string`):

SELF_TEST record: only present when the Actor runs with selfTest: true.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companies": [
        "Atlassian (atlassian.com)",
        "Asana"
    ],
    "keywords": [
        "outage"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("southern-data/company-news-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "companies": [
        "Atlassian (atlassian.com)",
        "Asana",
    ],
    "keywords": ["outage"],
}

# Run the Actor and wait for it to finish
run = client.actor("southern-data/company-news-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "Atlassian (atlassian.com)",
    "Asana"
  ],
  "keywords": [
    "outage"
  ]
}' |
apify call southern-data/company-news-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,southern-data/company-news-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/8JelszUzOCN09Tluf/builds/4e5fRyT9hZxThFidy/openapi.json
