Our GTM agent already researched target accounts and pushed company news into Slack. It knew nothing about the two dozen companies we lose deals to. Competitive intel lived in people’s heads and in sales calls nobody re-reads.

Here’s the recipe I used to fix that, built on Parallel’s Monitor API.

You need a Parallel API key and the parallel-web SDK, a public webhook endpoint, a document store, a Slack bot token, and a task queue.

01

List the entities and give each a stable ID

Start with a seed manifest of name and URL, and derive the document ID from the canonical hostname:

const competitorId = competitorIdFromHostname(canonicalizeCompetitorHostname(entry.url));

Using the hostname means https://www.example.com and https://example.com land on the same record, and a rebrand doesn’t fork one competitor into two.

Seed inside a transaction and leave existing records alone. Mine classifies each manifest entry as created, existing, or conflicting, where conflicting means the stored hostname no longer matches. Re-running the seed script is then a no-op.

02

Write one query that defines “material”

Parallel will find news either way. The query decides what counts and what gets thrown away, so this is where most of the value sits. Mine takes the name and hostname and produces:

Monitor material news about {name} ({hostname}) that could affect Wingspan’s positioning, product strategy, go-to-market motion, or competitive risk.

Include product or feature launches; pricing or packaging changes; APIs, integrations, partnerships, or channels; material customer wins or losses; funding, acquisitions, divestitures, or ownership changes; executive or board changes; geographic or vertical expansion; regulatory action, litigation, worker-classification changes, or compliance incidents; security incidents or major outages; material layoffs or unusual hiring; signs of financial distress; and credible changes in positioning or target market.

Exclude routine content marketing, events and webinars, reposted or syndicated stories with no new information, individual job postings or normal hiring, unsupported social claims, low-authority speculation, and old news presented as new. Prefer primary, regulatory, and high-authority editorial sources. Group multiple sources about the same development into one event.

The exclude list ended up longer than the include list, because without it the feed fills with the competitor’s own marketing. The source-authority line keeps press-release rewrites out. The grouping line makes one development arrive as one event with several citations instead of four near-duplicate alerts.

Version the query and store the version on each record. You’ll change this prompt and you’ll want to know which events came from which version.

03

Create one monitor per entity with an output schema

const monitor = await client.monitor.create({
  type: 'event_stream',
  frequency: '1d',
  processor: 'base',
  settings: {
    query: buildCompetitorMonitorPrompt( competitor.name, competitor.canonicalHostname),
    advanced_settings: { location: 'US' },
    include_backfill: false,
    output_schema: COMPETITOR_OUTPUT_SCHEMA,
  },
  webhook: {
    url: config.parallel.monitorWebhookUrl,
    event_types: [
      'monitor.event.detected',
      'monitor.execution.completed',
      'monitor.execution.failed',
    ],
  },
  metadata: {
    competitor_id: competitor.competitorId,
    company: competitor.name.slice(0, 512),
    domain: competitor.canonicalHostname.slice(0, 512),
    prompt_version: PROMPT_VERSION,
    source: 'gtm-agent-competitor',
  },
});

The output schema is what makes the history queryable months later. Mine requires category, headline, and summary, with category constrained to a 14-value enum covering launches, pricing, partnerships, customer wins and losses, funding, M&A, executive changes, expansion, regulatory action, worker classification, security incidents, financial distress, positioning shifts, and a catch-all. Constraining the enum is what gives you a searchable corpus instead of a pile of prose.

Keep include_backfill false unless you want launch day to dump months of history into Slack. Subscribe to the failure event type as well, so a monitor that breaks tells you rather than going quiet. Whatever you put in metadata comes back on the webhook, which is how an incoming event finds its record without a lookup table.

GTM Agent competitor dashboard showing monitored companies, canonical domains, active monitor health, and the date of latest material news.
One monitor per competitor makes coverage and monitor health visible at a glance.
Expanded graphic
GTM Agent competitor dashboard showing monitored companies, canonical domains, active monitor health, and the date of latest material news.

04

Reconcile monitors instead of creating them once

Compute the configuration you want, compare it against what’s stored, and only call the API when they differ:

if ( competitor.monitorId && configurationsMatch( competitor.monitorConfiguration, configuration)) {
  await this.store.setMonitorActive({ /* refresh health, no API call */ });
  return this.store.getCompetitor( competitorId);
}

Three things go wrong here.

Change the query and you create a new monitor while the old one keeps running and keeps billing. Track previousMonitorIds and cancel them on the next pass, tolerating cancellation failures so a transient error retries instead of losing the ID.

If the API call succeeds and your database write fails, you own a monitor you have no record of. Cancel it in the catch block.

One broken entity shouldn’t fail the whole run. I use pLimit(10) and count checked, active, and errored separately.

05

Normalize the webhook and build a dedupe key

Parallel delivers detected events with structured output plus a basis holding citations. Drop anything missing a headline, summary, category, or at least one non-social citation:

const sources = collectSources(content, event.output.basis);
if (!headline || !summary || !category || sources.length === 0 || sources.every(isSocialSource)) {
  return undefined;
}

The social-source rule came out of a production audit an hour after launch. A social post is a fine supporting citation next to an authoritative source and worthless as the only evidence for a claim.

Build the dedupe key from the entity, the event date, and the primary source URL, so the same development arriving through two monitor executions collapses into one record. Key the Slack notification off that same value and a retried webhook overwrites one row instead of double-posting.

06

Store history and link to it from every message

Keep every event under its entity and put a link to that history page in the footer of each Slack message.

Individual alerts are usually skippable. The sequence is where the value shows up. Over five days the feed reported that a regulator had rejected one competitor’s application for a national bank charter, then that its shares fell around 10% on the news, then that it planned to refile under a different regulatory framework with a pivot toward stablecoin payments. On one page, that’s a competitor’s regulatory strategy changing in public.

Slack message from Wingspan Notifier showing material competitor news summaries, source links, and links to each competitor's history.
Each alert carries its evidence and a path back to the competitor’s full event history.
Expanded graphic
Slack message from Wingspan Notifier showing material competitor news summaries, source links, and links to each competitor's history.

07

Fix this before you copy it

Score each event against your own product surface. The highest-volume competitor in my set over the first two weeks was a large enterprise software vendor whose overlap with us is marginal. It produced more events than anyone else and almost nothing I’d act on: a defense contract, an energy commitment, cloud distribution in another country.

A monitor pointed at a company returns news about the company, and what I want is the intersection of that company and us. Scoring events before they reach Slack is what I’m building next.