Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
Developer Handover Pack
Competitor Intelligence Tracking
[YourCompany.com] · Sales Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to construct, connect, and validate the Competitor Intelligence Tracking automation. It covers the current-state step map with bottleneck flags, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. You and your team are responsible for confirming the competitor source list in Google Sheets, aligning HubSpot field naming, and reviewing the weekly digest output during the parallel QA run. FullSpec handles all build, wiring, testing, and handoff.
01Process snapshot
02Agent specifications
Runs on a daily 6 AM schedule. Reads the current competitor list and associated source URLs from the Google Sheets config tab, queries web monitoring and news aggregation endpoints for each competitor, and passes each raw signal through an AI relevance scoring step. Each signal is scored 1 to 5 for sales relevance and tagged with a category (pricing, product, messaging, market expansion, or hiring). Signals scoring below the configured threshold (default: 3) are discarded without being written anywhere. Signals at or above the threshold are written to the Notion intelligence database with structured fields, and any signal that matches an open HubSpot deal's competitor field triggers an appended timestamped note on that deal record.
// Input
google_sheets.config_tab -> {
competitor_name: string,
website_url: string,
pricing_page_url: string,
linkedin_url: string,
g2_profile_url: string,
keyword_list: string[],
alert_feed_url: string
}
// Raw signal shape (per source, before scoring)
raw_signal -> {
competitor_name: string,
source_type: 'website' | 'news' | 'review_site' | 'linkedin' | 'job_post',
source_url: string,
raw_text: string,
captured_at: ISO8601 timestamp
}
// Output (scored, written to Notion)
notion.intelligence_db -> {
competitor_name: string,
category: 'pricing' | 'product' | 'messaging' | 'market_expansion' | 'hiring',
relevance_score: integer (1-5),
summary: string (max 200 chars),
source_url: string,
source_type: string,
signal_date: ISO8601 date,
logged_at: ISO8601 timestamp,
hubspot_deal_ids: string[] | null
}
// HubSpot deal note (conditional, when competitor match found)
hubspot.deal_note -> {
deal_id: string,
note_body: string (timestamped competitor signal summary),
associated_contact_ids: string[]
}- Google Sheets access requires the config sheet to use exact column headers: competitor_name, website_url, pricing_page_url, linkedin_url, g2_profile_url, keyword_list (comma-separated), alert_feed_url. Any deviation breaks the row parser. Confirm column naming with Tom Aldridge (RevOps Lead) before build begins.
- Web monitoring queries must be rate-limited to avoid triggering bot-detection on competitor sites. Use a minimum 3-second delay between page fetches per competitor domain. Rotate User-Agent strings across requests.
- AI scoring prompt must be versioned and stored in the orchestration layer config, not hardcoded in the workflow step. The relevance threshold (default: 3) must be a configurable environment variable (INTEL_SCORE_THRESHOLD) so it can be adjusted without redeployment.
- Notion write requires a Notion Integration token with Insert Content and Read Content permissions on the target database. The database must be shared with the integration explicitly. Confirm the Notion workspace plan (Plus or above) supports API-created pages.
- HubSpot deal note append uses the Engagements API v1 (POST /engagements/v1/engagements). The competitor_name value in Google Sheets must match exactly how the competitor appears in the HubSpot deal property (hs_competitor or a custom field). A name normalisation mapping step must be built before the HubSpot write. Confirm field name and exact values with Tom Aldridge before wiring.
- Dedupe: before writing to Notion, check for an existing entry with the same competitor_name, source_url, and signal_date within the past 48 hours. If a match exists, skip the write. Use a Notion filter query (filter by competitor_name AND source_url AND signal_date) to perform this check on each signal.
- Fallback: if the Notion write fails (timeout or permission error), log the failed signal payload to the error table in Supabase and send a Slack alert to the ops channel. Do not retry automatically more than twice.
Runs on a weekly Friday 8:00 AM schedule. Reads all Notion intelligence database entries logged in the past seven days, groups them by competitor and then by category, and uses an AI summarisation step to produce a concise plain-English briefing. The digest is structured as: one section per competitor (ordered by total signal count descending), with bullet points per category. The formatted digest is posted to the designated Slack sales channel and optionally emailed to the sales manager via Gmail. Each Slack message includes a deep link back to the filtered Notion database view for that week.
// Input
notion.intelligence_db query -> {
filter: { logged_at: >= (today - 7 days) },
sort: { logged_at: descending },
fields: [ competitor_name, category, relevance_score, summary, source_url, signal_date ]
}
// Grouped intermediate structure (in-memory, pre-summarisation)
digest_payload -> {
week_ending: ISO8601 date,
competitors: [
{
competitor_name: string,
signal_count: integer,
categories: {
pricing: signal[],
product: signal[],
messaging: signal[],
market_expansion: signal[],
hiring: signal[]
}
}
]
}
// Output: Slack message
slack.post_message -> {
channel: ENV:SLACK_SALES_CHANNEL_ID,
text: string (formatted digest, plain text with markdown),
blocks: [ section per competitor ],
unfurl_links: false
}
// Output: Gmail (optional, controlled by ENV:DIGEST_EMAIL_ENABLED)
gmail.send -> {
to: ENV:DIGEST_EMAIL_RECIPIENT,
subject: 'Weekly Competitor Intelligence Digest - [week_ending date]',
body: string (same content as Slack, HTML formatted)
}- Slack posting requires a Slack Bot Token (xoxb-) with chat:write and channels:read OAuth scopes. The bot must be invited to the target sales channel before the first run. Confirm channel ID (not channel name) and store as SLACK_SALES_CHANNEL_ID environment variable.
- Gmail send uses OAuth 2.0 with the gmail.send scope only. Do not request broader Gmail scopes. The sending account must be a Google Workspace account. The sales manager email address is stored as DIGEST_EMAIL_RECIPIENT. The Gmail step is gated by ENV:DIGEST_EMAIL_ENABLED (boolean, default: true) so it can be disabled without touching the workflow.
- If the Notion query returns zero signals for the week, the Digest Agent must still post a brief Slack message stating no new signals were logged this week and prompting the team to check the competitor source list. Do not silently skip the Friday post.
- Digest AI summarisation prompt must cap output at 80 words per competitor section to keep the Slack post readable on mobile. This limit must be enforced in the prompt as a hard constraint, not a guideline.
- The Notion deep-link URL in the Slack message must use a filtered view scoped to the current week. Pre-build the filtered Notion view and store the base URL as ENV:NOTION_DIGEST_VIEW_URL, appending the week-ending date as a query parameter where Notion's sharing URL supports it.
- Confirm with Jordan Mercer (Sales Manager) that the Friday 8 AM posting time is correct before go-live. The schedule must be stored as a configurable cron expression (ENV:DIGEST_CRON) to allow adjustment without redeployment.
03End-to-end data flow
// ── TRIGGER ──────────────────────────────────────────────────────────────
SCHEDULE: daily 06:00 AM
-> automation_platform.trigger()
-> google_sheets.read(sheet='competitor_config', range='A2:H*')
returns: competitor_rows[] {
competitor_name, website_url, pricing_page_url,
linkedin_url, g2_profile_url, keyword_list[], alert_feed_url
}
// ── INTELLIGENCE AGENT: FETCH SIGNALS ────────────────────────────────────
FOR EACH competitor_row IN competitor_rows[]:
web_monitor.fetch(url=competitor_row.website_url)
-> raw_signal { source_type='website', raw_text, source_url, captured_at }
web_monitor.fetch(url=competitor_row.pricing_page_url)
-> raw_signal { source_type='website', raw_text, source_url, captured_at }
news_aggregator.query(keywords=competitor_row.keyword_list[])
-> raw_signal[] { source_type='news', raw_text, source_url, captured_at }
rss_reader.fetch(url=competitor_row.alert_feed_url)
-> raw_signal[] { source_type='news', raw_text, source_url, captured_at }
web_monitor.fetch(url=competitor_row.g2_profile_url)
-> raw_signal[] { source_type='review_site', raw_text, source_url, captured_at }
web_monitor.fetch(url=competitor_row.linkedin_url)
-> raw_signal[] { source_type='linkedin', raw_text, source_url, captured_at }
// ── INTELLIGENCE AGENT: SCORE AND FILTER ─────────────────────────────────
FOR EACH raw_signal IN all_raw_signals[]:
ai_scoring_step.run(
input: { competitor_name, source_type, raw_text },
prompt_version: ENV:INTEL_PROMPT_VERSION
)
-> scored_signal {
competitor_name,
category: 'pricing'|'product'|'messaging'|'market_expansion'|'hiring',
relevance_score: integer (1-5),
summary: string (max 200 chars),
source_url,
source_type,
signal_date: date,
captured_at: timestamp
}
IF scored_signal.relevance_score < ENV:INTEL_SCORE_THRESHOLD:
DISCARD -> no write
ELSE:
CONTINUE to dedupe check
// ── INTELLIGENCE AGENT: DEDUPE CHECK ─────────────────────────────────────
notion.query(
database_id: ENV:NOTION_INTEL_DB_ID,
filter: {
competitor_name == scored_signal.competitor_name AND
source_url == scored_signal.source_url AND
signal_date >= (today - 2 days)
}
)
IF results.count > 0:
SKIP write (duplicate detected)
ELSE:
CONTINUE to Notion write
// ── INTELLIGENCE AGENT -> NOTION HANDOFF ─────────────────────────────────
notion.create_page(
database_id: ENV:NOTION_INTEL_DB_ID,
properties: {
competitor_name: scored_signal.competitor_name,
category: scored_signal.category,
relevance_score: scored_signal.relevance_score,
summary: scored_signal.summary,
source_url: scored_signal.source_url,
source_type: scored_signal.source_type,
signal_date: scored_signal.signal_date,
logged_at: now()
}
)
-> notion_page_id: string
// ── INTELLIGENCE AGENT -> HUBSPOT HANDOFF ────────────────────────────────
hubspot.deals.search(
filter: { competitor_field CONTAINS scored_signal.competitor_name }
)
-> matching_deals[] { deal_id, deal_name, associated_contact_ids[] }
IF matching_deals.count > 0:
FOR EACH deal IN matching_deals[]:
hubspot.engagements.create(
type: 'NOTE',
deal_id: deal.deal_id,
body: '[{scored_signal.signal_date}] {scored_signal.category}: {scored_signal.summary} | Source: {scored_signal.source_url}'
)
// ── ERROR HANDLING (Intelligence Agent) ──────────────────────────────────
ON error (Notion write fail | HubSpot timeout):
supabase.insert(table='automation_errors', row={
agent: 'intelligence_agent',
error_type: error.type,
payload: scored_signal,
occurred_at: now()
})
slack.alert(channel: ENV:SLACK_OPS_CHANNEL_ID, message: 'Intelligence Agent write error - check Supabase')
// ── WEEKLY TRIGGER ────────────────────────────────────────────────────────
SCHEDULE: Friday 08:00 AM (ENV:DIGEST_CRON)
-> automation_platform.trigger()
// ── DIGEST AGENT: READ NOTION ─────────────────────────────────────────────
notion.query(
database_id: ENV:NOTION_INTEL_DB_ID,
filter: { logged_at >= (today - 7 days) },
sort: { logged_at: 'descending' }
)
-> weekly_signals[] {
competitor_name, category, relevance_score,
summary, source_url, signal_date
}
// ── DIGEST AGENT: GROUP AND SUMMARISE ────────────────────────────────────
group_by(weekly_signals[], key=['competitor_name', 'category'])
-> digest_payload {
week_ending: date,
competitors[]: {
competitor_name,
signal_count,
categories: { pricing[], product[], messaging[], market_expansion[], hiring[] }
}
}
ai_summarisation_step.run(
input: digest_payload,
max_words_per_competitor: 80,
output_format: 'slack_markdown'
)
-> formatted_digest: string
// ── DIGEST AGENT -> SLACK HANDOFF ─────────────────────────────────────────
slack.post_message(
channel: ENV:SLACK_SALES_CHANNEL_ID,
text: formatted_digest,
blocks: [ per-competitor sections ],
footer_link: ENV:NOTION_DIGEST_VIEW_URL + '?week=' + week_ending
)
// ── DIGEST AGENT -> GMAIL HANDOFF (conditional) ──────────────��────────────
IF ENV:DIGEST_EMAIL_ENABLED == true:
gmail.send(
to: ENV:DIGEST_EMAIL_RECIPIENT,
subject: 'Weekly Competitor Intelligence Digest - ' + week_ending,
body_html: html_format(formatted_digest)
)
// ── END ───────────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Competitor Intelligence Tracking.