Weekly PLG HubSpot Sync + Slack Digest
What this does
- Pulls domain scores and champion contacts from BigQuery
- Resolves and updates HubSpot companies + contacts with PQA/PQL properties
- Applies dampening logic (bootstrap promotes immediately on first sync)
- Excludes Target Account companies from PLG pipeline
- Associates champion contacts to their companies
- Legacy Clay enrichment and HubSpot workflow routing are disabled in
scoring_sync.py. Dagster now owns Lead creation/update and writes the existing enrichment/routing workflow trigger fields plus Tier 1 Product Qualified Leads sequence intent. Tier 2 is held for now: no Lead creation, no enrichment trigger, and no sequence enrollment.
- Posts the weekly Slack digest:
📊 This week at a glance (action plays), 🆕 Newly Active Accounts, 📈 Top Movers, 🏆 Tier 1 PQAs, ⚠️ Watch List, 💰 Pipeline Wins.
Enrichment policy
Clay enrichment is expensive, so the Dagster sync restricts it to accounts BDRs will actually work:
- Only enrich Tier 1 Product Qualified contacts for now. Tier 1 contacts receive
available_for_enrichment and clay_enrichment_queue plus Product Qualified Leads sequence trigger fields.
- Hold Tier 2 for now. Tier 2 contacts still get PQL property refreshes for reporting, but no Lead is created, no enrichment trigger is written, and no sequence enrollment is requested.
- Never enrich Tier 3. Tier 3 champions still get PQL property refreshes for reporting, but no Lead is created and no outreach trigger fields are written.
Plays engine (📊 This week at a glance)
plg_upsell/scripts/scoring_digest.py generates up to five account-specific bullets from a typed candidate pool. Each candidate is ranked by priority, and at most one play per account surfaces so the top of the message isn't dominated by a single domain.
All numeric thresholds below (cohort size, employee counts, credit growth, credit totals, member counts) are illustrative examples — calibrate them for your own funnel.
- Tier 1 promotion (priority 5) — any active account whose
pqa_tier just flipped to tier_1 this sync (weeks_above == 0 + tier == tier_1). Shown as: 🏆 <Company> crossed Tier 1 this sync (score X, ±Y WoW).
- Enterprise cohort (priority 5) — fires when ≥
ENTERPRISE_COHORT_MIN (default 3) newly-active accounts have company_size >= ENTERPRISE_SIZE_MIN (default 2000). One themed bullet replaces the per-company enterprise bullets for those same domains, so BDRs see it as a segment-level signal rather than N near-identical one-offs. Shown as: 🏢 Enterprise wave: N large orgs (≥2,000 employees) newly active — <Company A>, <Company B>, … (combined ~E,EEE employees, U active product users). Cohort members still remain eligible for other plays (credit surge, land-and-expand, etc.) through their own domain.
- Enterprise-scale account (priority 4) — fires when an active account has
company_size >= ENTERPRISE_SIZE_MIN (default 2000) AND is not already in the enterprise cohort above. Bullet names the company, employee count, active product users in the domain (30d), and current tier + score. Shown as: 🏢 <Company> is enterprise-scale (N,NNN employees, M active product users) — Tier X at S.S. Engineer-specific count isn't in the dataset today; active product users (active_users_last_30d) is the closest proxy.
- Credit surge (priority 4) — WoW credit growth ≥ 30% and 30d credits ≥ 10,000.
- Credit limits hit (priority 4) — ≥ 1 user hit limits in the last 14d; reload $ included when non-zero.
- Land-and-expand (priority 3) — ≥ 2 new domain members joined in the last 14d.
- Self-upgrades (priority 3) — ≥ 1 user self-upgraded in the last 30d.
- Tier 1 drift (priority 3) — Tier 1 account with a negative WoW score delta.
- Admin champion emerged (priority 2) — Tier 1 account whose rank-1 champion is a team admin.
All bullets are pure signal framing (account + what changed + time window) — no prescriptive actions. BDRs already own the outreach motion for Tier 1/2 accounts, so the digest surfaces the what and lets reps decide the how. Time windows are explicit on every stat: (14d), (30d), (4w avg), WoW.
Required secrets
GENERAL_HUBSPOT_APP_TOKEN (or HUBSPOT_PRIVATE_APP_TOKEN) — HubSpot private app token
PLG_SLACK_BOT_TOKEN — Slack bot token for digest posting (uses chat.postMessage)
GCP_SERVICE_ACCOUNT_JSON — GCP service account with BigQuery read access
HUBSPOT_SLACK_WEBHOOK — (optional fallback) Slack incoming webhook URL
PLG_OUTREACH_PROMPT_URL — (optional) Warp Drive Prompt share URL for the draft-champion-outreach skill. When set, each Tier 1 champion card in the digest renders a ✍️ Draft outreach link that opens the prompt with the champion's context pre-filled. Leave unset to suppress the link.
PLG_PRODUCT_QUALIFIED_SEQUENCE_ID — HubSpot sequence ID for the Product Qualified Leads sequence. Dagster stamps this on Tier 1 champion contacts so the HubSpot workflow can perform actual sequence enrollment. Find this in HubSpot CRM > Sequences (the numeric ID appears in the URL when you open a sequence).
PLG_PRODUCT_QUALIFIED_SEQUENCE_NAME — optional display name for sequence trigger fields (e.g. Tier 1 PQL).
One-time HubSpot setup (required before first live run)
- Create
pqa_bdr_routed_at property on the Company object (HubSpot Settings → Properties → Companies → Create property). Type: Date and time. This timestamp prevents double-routing on re-runs.
- Create
pqa_enriched_at property on the Company object. Type: Date and time. This timestamp gates Clay enrichment so a given PQA is only enriched once in its lifetime (Step 7.5 + 10.5). Without it, the preflight will flag every first-time T1/T2 account as eligible on every run.
- Fill in sequence IDs in
plg_upsell/scripts/scoring_sync.py (TIER1_SEQUENCE_ID / TIER2_SEQUENCE_ID). Find the IDs in HubSpot CRM → Sequences — the numeric ID is in the URL when you open a sequence.
- Confirm workflow re-enrollment is disabled on your Account Router and Contact Router workflows (
PLG_ACCOUNT_ROUTING_WORKFLOW_ID and PLG_CONTACT_ROUTING_WORKFLOW_ID) so contacts that drift back out and re-promote don't get enrolled a second time through the workflow itself.
Steps
Ensure we are on the correct branch and dependencies are installed:
git checkout main && git pull
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
Note: Use Python 3.10+ — the fastmcp dependency requires it. On macOS the Homebrew binary is typically /opt/homebrew/bin/python3.
Write HubSpot credentials to .env (fallback for scripts that read from file):
echo "HUBSPOT_PRIVATE_APP_TOKEN=${GENERAL_HUBSPOT_APP_TOKEN:-${HUBSPOT_PRIVATE_APP_TOKEN}}" > .env
PLG_SLACK_BOT_TOKEN is read directly from the environment — no .env entry needed.
Run the scoring sync (BQ → HubSpot):
.venv/bin/python plg_upsell/scripts/scoring_sync.py
Expected output: domains processed, companies updated, contacts updated, promotions/demotions count.
Run the Slack digest (posts to #your-plg-alerts-channel):
.venv/bin/python plg_upsell/scripts/scoring_digest.py
Expected output: "✓ Digest posted to #your-plg-alerts-channel".
The digest's 🏆 Tier 1 PQAs section renders a multi-line card per Tier 1 company (score + WoW, usage stats with explicit windows, urgency signals when present, and the top champion). When PLG_OUTREACH_PROMPT_URL is configured, the champion line carries a ✍️ Draft outreach link that opens the Warp Drive Prompt invoking the draft-champion-outreach skill with the champion's context; the skill drafts 3–4 tailored emails and replies in the digest thread.
Report success with:
- Number of companies updated
- Number of contacts updated
- Number of promotions → Active
- Number of accounts BDR-routed (and how many skipped due to recent sales email)
- Number of contacts flagged for Clay enrichment + number of companies stamped with
pqa_enriched_at (pulled from the Enrichment flagged / Enrichment stamped lines in the sync summary)
- Slack digest channel confirmation
If either script fails, return the exact error and stop. Do not proceed to the digest if the sync fails.
If PLG_OUTREACH_PROMPT_URL is not set, warn that Tier 1 champion cards will render without ✍️ Draft outreach links. The digest still posts correctly — the links are additive.
Flags
- Add
--dry-run to either script to preview without writing to HubSpot or posting to Slack
- Add
--top-n 20 to scoring_sync.py to limit to top 20 domains (for testing)
- Add
--domains "domain1.com,domain2.com" to scoring_sync.py to target specific domains
--route-newly-active is deprecated and disabled; use the Dagster plg_hubspot_sync job for Lead routing
--approve-tier2-domains is deprecated and disabled; Dagster records Tier 1/Tier 2 sequence intent on Leads
1---2name: hubspot-plg-sync3description: Run the weekly PLG scoring sync and Slack digest. Pulls PQA/PQL scores from BigQuery, syncs them to HubSpot (companies + contacts), then posts the Monday morning digest to Slack. Use this skill when an Oz cloud agent needs to run the weekly BDR pipeline update.4---56# Weekly PLG HubSpot Sync + Slack Digest78## What this does91. Pulls domain scores and champion contacts from BigQuery102. Resolves and updates HubSpot companies + contacts with PQA/PQL properties113. Applies dampening logic (bootstrap promotes immediately on first sync)124. Excludes Target Account companies from PLG pipeline135. Associates champion contacts to their companies146. Legacy Clay enrichment and HubSpot workflow routing are disabled in `scoring_sync.py`. Dagster now owns Lead creation/update and writes the existing enrichment/routing workflow trigger fields plus Tier 1 Product Qualified Leads sequence intent. Tier 2 is held for now: no Lead creation, no enrichment trigger, and no sequence enrollment.157. Posts the weekly Slack digest: `📊 This week at a glance` (action plays), `🆕 Newly Active Accounts`, `📈 Top Movers`, `🏆 Tier 1 PQAs`, `⚠️ Watch List`, `💰 Pipeline Wins`.16## Enrichment policy17Clay enrichment is expensive, so the Dagster sync restricts it to accounts BDRs will actually work:18- **Only enrich Tier 1** Product Qualified contacts for now. Tier 1 contacts receive `available_for_enrichment` and `clay_enrichment_queue` plus Product Qualified Leads sequence trigger fields.19- **Hold Tier 2** for now. Tier 2 contacts still get PQL property refreshes for reporting, but no Lead is created, no enrichment trigger is written, and no sequence enrollment is requested.20- **Never enrich Tier 3**. Tier 3 champions still get PQL property refreshes for reporting, but no Lead is created and no outreach trigger fields are written.2122## Plays engine (📊 This week at a glance)23`plg_upsell/scripts/scoring_digest.py` generates up to five account-specific bullets from a typed candidate pool. Each candidate is ranked by priority, and at most one play per account surfaces so the top of the message isn't dominated by a single domain.2425> All numeric thresholds below (cohort size, employee counts, credit growth, credit totals, member counts) are illustrative examples — calibrate them for your own funnel.2627- **Tier 1 promotion** (priority 5) — any active account whose `pqa_tier` just flipped to `tier_1` this sync (`weeks_above == 0` + `tier == tier_1`). Shown as: `🏆 <Company> crossed Tier 1 this sync (score X, ±Y WoW)`.28- **Enterprise cohort** (priority 5) — fires when ≥ `ENTERPRISE_COHORT_MIN` (default 3) newly-active accounts have `company_size >= ENTERPRISE_SIZE_MIN` (default 2000). One themed bullet replaces the per-company enterprise bullets for those same domains, so BDRs see it as a segment-level signal rather than N near-identical one-offs. Shown as: `🏢 Enterprise wave: N large orgs (≥2,000 employees) newly active — <Company A>, <Company B>, … (combined ~E,EEE employees, U active product users)`. Cohort members still remain eligible for other plays (credit surge, land-and-expand, etc.) through their own domain.29- **Enterprise-scale account** (priority 4) — fires when an active account has `company_size >= ENTERPRISE_SIZE_MIN` (default 2000) AND is not already in the enterprise cohort above. Bullet names the company, employee count, active product users in the domain (30d), and current tier + score. Shown as: `🏢 <Company> is enterprise-scale (N,NNN employees, M active product users) — Tier X at S.S`. Engineer-specific count isn't in the dataset today; active product users (`active_users_last_30d`) is the closest proxy.30- **Credit surge** (priority 4) — WoW credit growth ≥ 30% and 30d credits ≥ 10,000.31- **Credit limits hit** (priority 4) — ≥ 1 user hit limits in the last 14d; reload $ included when non-zero.32- **Land-and-expand** (priority 3) — ≥ 2 new domain members joined in the last 14d.33- **Self-upgrades** (priority 3) — ≥ 1 user self-upgraded in the last 30d.34- **Tier 1 drift** (priority 3) — Tier 1 account with a negative WoW score delta.35- **Admin champion emerged** (priority 2) — Tier 1 account whose rank-1 champion is a team admin.3637All bullets are pure signal framing (account + what changed + time window) — no prescriptive actions. BDRs already own the outreach motion for Tier 1/2 accounts, so the digest surfaces the what and lets reps decide the how. Time windows are explicit on every stat: `(14d)`, `(30d)`, `(4w avg)`, `WoW`.3839## Required secrets40- `GENERAL_HUBSPOT_APP_TOKEN` (or `HUBSPOT_PRIVATE_APP_TOKEN`) — HubSpot private app token41- `PLG_SLACK_BOT_TOKEN` — Slack bot token for digest posting (uses `chat.postMessage`)42- `GCP_SERVICE_ACCOUNT_JSON` — GCP service account with BigQuery read access43- `HUBSPOT_SLACK_WEBHOOK` — (optional fallback) Slack incoming webhook URL44- `PLG_OUTREACH_PROMPT_URL` — (optional) Warp Drive Prompt share URL for the `draft-champion-outreach` skill. When set, each Tier 1 champion card in the digest renders a `✍️ Draft outreach` link that opens the prompt with the champion's context pre-filled. Leave unset to suppress the link.45- `PLG_PRODUCT_QUALIFIED_SEQUENCE_ID` — HubSpot sequence ID for the Product Qualified Leads sequence. Dagster stamps this on Tier 1 champion contacts so the HubSpot workflow can perform actual sequence enrollment. Find this in HubSpot CRM > Sequences (the numeric ID appears in the URL when you open a sequence).46- `PLG_PRODUCT_QUALIFIED_SEQUENCE_NAME` — optional display name for sequence trigger fields (e.g. `Tier 1 PQL`).4748## One-time HubSpot setup (required before first live run)491. **Create `pqa_bdr_routed_at` property** on the Company object (HubSpot Settings → Properties → Companies → Create property). Type: `Date and time`. This timestamp prevents double-routing on re-runs.502. **Create `pqa_enriched_at` property** on the Company object. Type: `Date and time`. This timestamp gates Clay enrichment so a given PQA is only enriched once in its lifetime (Step 7.5 + 10.5). Without it, the preflight will flag every first-time T1/T2 account as eligible on every run.513. **Fill in sequence IDs** in `plg_upsell/scripts/scoring_sync.py` (`TIER1_SEQUENCE_ID` / `TIER2_SEQUENCE_ID`). Find the IDs in HubSpot CRM → Sequences — the numeric ID is in the URL when you open a sequence.524. **Confirm workflow re-enrollment is disabled** on your Account Router and Contact Router workflows (`PLG_ACCOUNT_ROUTING_WORKFLOW_ID` and `PLG_CONTACT_ROUTING_WORKFLOW_ID`) so contacts that drift back out and re-promote don't get enrolled a second time through the workflow itself.5354## Steps55561. Ensure we are on the correct branch and dependencies are installed:57 ```58 git checkout main && git pull59 python3 -m venv .venv60 .venv/bin/python -m pip install -r requirements.txt61 ```62 > Note: Use Python 3.10+ — the `fastmcp` dependency requires it. On macOS the Homebrew binary is typically `/opt/homebrew/bin/python3`.63642. Write HubSpot credentials to `.env` (fallback for scripts that read from file):65 ```66 echo "HUBSPOT_PRIVATE_APP_TOKEN=${GENERAL_HUBSPOT_APP_TOKEN:-${HUBSPOT_PRIVATE_APP_TOKEN}}" > .env67 ```68 > `PLG_SLACK_BOT_TOKEN` is read directly from the environment — no `.env` entry needed.69703. Run the **scoring sync** (BQ → HubSpot):71 ```72 .venv/bin/python plg_upsell/scripts/scoring_sync.py73 ```74 Expected output: domains processed, companies updated, contacts updated, promotions/demotions count.75764. Run the **Slack digest** (posts to `#your-plg-alerts-channel`):77 ```78 .venv/bin/python plg_upsell/scripts/scoring_digest.py79 ```80 Expected output: "✓ Digest posted to #your-plg-alerts-channel".81 The digest's `🏆 Tier 1 PQAs` section renders a multi-line card per Tier 1 company (score + WoW, usage stats with explicit windows, urgency signals when present, and the top champion). When `PLG_OUTREACH_PROMPT_URL` is configured, the champion line carries a `✍️ Draft outreach` link that opens the Warp Drive Prompt invoking the `draft-champion-outreach` skill with the champion's context; the skill drafts 3–4 tailored emails and replies in the digest thread.82835. Report success with:84 - Number of companies updated85 - Number of contacts updated86 - Number of promotions → Active87 - Number of accounts BDR-routed (and how many skipped due to recent sales email)88 - Number of contacts flagged for Clay enrichment + number of companies stamped with `pqa_enriched_at` (pulled from the `Enrichment flagged` / `Enrichment stamped` lines in the sync summary)89 - Slack digest channel confirmation90916. If either script fails, return the exact error and stop. Do not proceed to the digest if the sync fails.92937. **If `PLG_OUTREACH_PROMPT_URL` is not set**, warn that Tier 1 champion cards will render without `✍️ Draft outreach` links. The digest still posts correctly — the links are additive.9495## Flags96- Add `--dry-run` to either script to preview without writing to HubSpot or posting to Slack97- Add `--top-n 20` to `scoring_sync.py` to limit to top 20 domains (for testing)98- Add `--domains "domain1.com,domain2.com"` to `scoring_sync.py` to target specific domains99- `--route-newly-active` is deprecated and disabled; use the Dagster `plg_hubspot_sync` job for Lead routing100- `--approve-tier2-domains` is deprecated and disabled; Dagster records Tier 1/Tier 2 sequence intent on Leads