appscript.dev
Automation Advanced Sheets

Build a competitor-mention monitor

Summarise what Northwind's rivals are doing each week — feeds in, summary out.

Published Sep 12, 2025

Keeping half an eye on the competition is one of those tasks that everyone agrees matters and nobody actually does. Northwind’s rivals all publish blogs and changelogs, but reading every one each week is the kind of job that quietly falls off the list — until a client mentions a competitor’s new feature and catches the team flat-footed.

This script turns that habit into an automation. Once a week it pulls the latest posts from a list of competitor RSS feeds, hands the headlines to Claude, and asks for a five-bullet recap of what the rivals shipped. The summary lands in your inbox — no dashboards to open, no feeds to scroll, just a short briefing you can read with your coffee.

What you’ll need

  • The RSS or Atom feed URL for each competitor blog or changelog you want to watch. Most blogs expose one at /rss, /feed or /atom.xml.
  • An Anthropic API key saved as ANTHROPIC_API_KEY in Script Properties — see Store API keys and secrets securely.
  • The email address the weekly recap should be sent to.

The script

// The competitor feeds to watch. Add or remove URLs as your market shifts.
const FEEDS = [
  'https://competitor1.example/blog/rss',
  'https://competitor2.example/blog/rss',
];

// Where the weekly recap is emailed.
const RECIPIENT = '[email protected]';

// How many recent headlines to pull from each feed.
const HEADLINES_PER_FEED = 5;

/**
 * Pulls recent headlines from each competitor feed, asks Claude to recap
 * what they shipped, and emails the summary. Designed to run weekly.
 */
function competitorRecap() {
  const items = [];

  // 1. Fetch each feed and pull out its most recent headlines.
  for (const feed of FEEDS) {
    try {
      const xml = UrlFetchApp.fetch(feed).getContentText();

      // Grab every <title> in the feed. The first title is the feed's own
      // name, so skip it and keep the next few — those are the posts.
      const titles = [...xml.matchAll(/<title[^>]*>([^<]+)<\/title>/g)]
        .map((m) => m[1])
        .slice(1, 1 + HEADLINES_PER_FEED);

      if (titles.length) {
        items.push('From ' + feed + ': ' + titles.join('; '));
      }
    } catch (err) {
      // A dead or slow feed should not sink the whole run.
      Logger.log('Could not read ' + feed + ': ' + err);
    }
  }

  // 2. If no feed returned anything, stop before calling the API.
  if (!items.length) {
    Logger.log('No headlines collected — nothing to summarise.');
    return;
  }

  // 3. Ask Claude for a tight, five-bullet recap of the headlines.
  const summary = callClaude(
    'Summarise what these competitors of Northwind shipped this week, ' +
    'in 5 bullets:\n' + items.join('\n'),
    'claude-sonnet-4-6',
    500
  );

  // 4. Email the recap.
  GmailApp.sendEmail(RECIPIENT, 'Competitor recap', summary);
  Logger.log('Sent competitor recap to ' + RECIPIENT + '.');
}

/**
 * Minimal Anthropic API call. The key lives in Script Properties — it
 * is never pasted into the code.
 */
function callClaude(prompt, model = 'claude-haiku-4-5-20251001', maxTokens = 400) {
  const key = PropertiesService.getScriptProperties()
    .getProperty('ANTHROPIC_API_KEY');
  const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
    payload: JSON.stringify({
      model,
      max_tokens: maxTokens,
      messages: [{ role: 'user', content: prompt }],
    }),
    muteHttpExceptions: true,
  });
  return JSON.parse(res.getContentText()).content[0].text.trim();
}

How it works

  1. competitorRecap loops over the FEEDS list and fetches each feed’s raw XML with UrlFetchApp.
  2. For each feed it pulls every <title> element with a regular expression. The first title in an RSS feed is the feed’s own name, so it is skipped; the next five headlines (HEADLINES_PER_FEED) are the recent posts.
  3. Each fetch is wrapped in a try/catch so one dead or slow feed logs a warning and the run carries on with whatever else it collected.
  4. If no feed returned anything, it logs a message and stops before spending an API call.
  5. The collected headlines are joined into a single block and sent to Claude Sonnet, which is asked for a five-bullet recap of what the rivals shipped.
  6. The summary is emailed to RECIPIENT with GmailApp.sendEmail.

Example run

Suppose the two feeds return these headlines this week:

  • competitor1: “Now supporting SSO”, “New pricing tiers”, “API v3 is live”
  • competitor2: “Faster exports”, “Dark mode arrives”, “Mobile app beta”

Claude turns that into an inbox-ready recap:

Competitor recap

  • Competitor 1 shipped single sign-on, closing a gap enterprise buyers ask about.
  • Competitor 1 reworked its pricing into new tiers — worth checking how it compares to ours.
  • Competitor 1 released API v3, signalling a push on integrations.
  • Competitor 2 focused on polish: faster exports and a long-requested dark mode.
  • Competitor 2 opened a mobile app beta — a space Northwind does not yet play in.

That five-line briefing is the whole point: enough to know what changed, short enough to actually read.

Trigger it

Run this once a week so the recap arrives on a predictable day:

  1. In the Apps Script editor, open Triggers (the clock icon).
  2. Click Add Trigger.
  3. Choose the competitorRecap function, an event source of Time-driven, a Week timer, the day you want (Monday works well), and an early-morning time slot.
  4. Save and approve the authorisation prompt the first time.

Watch out for

  • Feed formats vary. The regular expression assumes standard RSS where the feed name is the first <title>. Atom feeds and some hand-rolled feeds order things differently — check the first run and adjust the .slice if a feed’s name leaks into the headlines.
  • It reads headlines, not full posts, so the recap is only as informative as the titles. A vendor with vague headlines (“Product update — March”) will produce a vague bullet.
  • “This week” is a prompt instruction, not a real filter. The script always sends the latest five headlines per feed regardless of date; if a feed is quiet, old posts will reappear in the recap.
  • Each run makes one paid API call. The cost is small at this scale, but every feed you add lengthens the prompt — keep an eye on it if you watch many rivals.

Related