suppress-global-unsubscribes
Identify and suppress globally unsubscribed contacts to ensure legal compliance and reduce billing.
Skill by tomgranot
Chalk Labs
Need help deploying suppress-global-unsubscribes?
Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/tomgranot-suppress-global-unsubscribes
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: design, backend, security, legal
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 25, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
- Runs as
- Python package
Network hosts it talks to 2
api.hubapi.com· found in codecodeload.github.com· found in install command
Files it touches
~/.claude/skills/suppress-global-unsubscribes
What's inside
- Skill name
suppress-global-unsubscribes- Description
- Identify and suppress globally unsubscribed contacts to ensure legal compliance and reduce billing. Hybrid approach: API for discovery and audit, manual UI for suppression (hs_marketable_status is read-only).
- License
- MIT
- Metadata
- author: tomgranot · version: 1.1 · category: database-hygiene
- Instructions
- 364 lines in SKILL.md · view on GitHub
Before installing
- Ships executable scripts
- Instructions call the network
Files (3)
- SKILL.md
- after.py · scripts/
- before.py · scripts/
SKILL.md (first lines)
# Suppress Globally Unsubscribed Contacts
## Purpose
Contacts who have globally unsubscribed cannot legally be sent marketing emails under CAN-SPAM, GDPR, or CASL. Despite this, they may still be classified as "marketing contacts" in HubSpot, meaning the organization is paying for contacts it cannot and must not email. This skill identifies all globally unsubscribed contacts, analyzes their lifecycle distribution, and guides suppression.
## Prerequisites
- A HubSpot service key (`HUBSPOT_ACCESS_TOKEN` in `.env`; an existing legacy private app token also works) with `crm.objects.contacts.read` and `crm.lists.read`/`crm.lists.write` scopes
- Python 3.10+ with `uv` for package management
- A `.env` file containing `HUBSPOT_ACCESS_TOKEN`
- Super Admin or Marketing Hub Admin permissions for the manual UI suppression step
## Scripts
Script paths are relative to this skill's directory. Run them from your project directory so `.env`, `data/`, and `reports/` resolve there: prefix each path with wherever this skill is installed (`skills/suppress-global-unsubscribes/` in a clone of the repo).
| Stage | Script | Run with |
|-------|--------|----------|
| Before | [`scripts/before.py`](./scripts/before.py) | `uv run scripts/before.py` |
| After | [`scripts/after.py`](./scripts/after.py) | `uv run scripts/after.py` |
There is no execute script: the marketing-status change itself must happen via a HubSpot workflow or the UI (see Key Constraint and Stage 3).
## Key Constraint
**`hs_marketable_status` is read-only via the API.** The API handles discovery and audit. Actual suppression must happen in the HubSpot UI.
## Execution Pattern
This skill follows a 4-stage execution pattern: **Plan -> Before -> Execute -> After**.
### Stage 1: Plan
Before writing any code, confirm with the user:
1. **Do not re-subscribe anyone.** Even if the unsubscribe pattern looks like a batch import error, re-subscribing contacts without explicit consent violates CAN-SPAM and GDPR.
2. **Billing impact**: Non-marketing status changes take effect at the start of the next billing cycle.
3. **Investigate uniform patterns**: If all subscription types show nearly identical unsubscribe counts, this suggests a batch event (migration, import error, compliance sweep) rather than organic opt-outs. Understanding the origin prevents recurrence.
### Stage 2: Before
Count globally unsubscribed contacts, break down by lifecycle stage, and generate an audit CSV.
```python
"""
Before State: Count globally unsubscribed contacts.
Note: >10K results means the Search API pagination limit may apply.
Uses count-only queries and segmented exports.
"""
import os
import csv
import time
import requests
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]
BASE = "https://api.hubapi.com"
headers = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
}
url = f"{BASE}/crm/v3/objects/contacts/search"
# --- Step 1: Total count ---
resp = requests.post(url, headers=headers, json={
"filterGroups": [{"filters": [
{"propertyName": "hs_email_optout", "operator": "EQ", "value": "true"}
]}],
"limit": 1,
})
resp.raise_for_status()
total_unsubscribed = resp.json().get("total", 0)
print(f"Total globally unsubscribed: {total_unsubscribed}")
# --- Step 2: How many are still marketing? ---
resp2 = requests.post(url, headers=headers, json={
"filterGroups": [{"filters": [
{"propertyName": "hs_email_optout", "operator": "EQ", "value": "true"},
{"propertyName": "hs_marketable_status", "operator": "EQ", "value": "true"},
]}],
"limit": 1,
})
resp2.raise_for_status()
still_marketing = resp2.json().get("total", 0)
already_non_marketing = total_unsubscribed - still_marketing
print(f"Still marketing (need suppression): {still_marketing}")
print(f"Already non-marketing: {already_non_marketing}")
# --- Step 3: Lifecycle stage breakdown ---
print("\nLifecycle stage breakdown:")
stages = [
"lead", "subscriber", "marketingqualifiedlead",
"salesqualifiedlead", "opportunity", "customer",
"evangelist", "other",
]
for stage in stages:
resp_s = requests.post(url, headers=headers, json={
"filterGroups": [{"filters": [
{"propertyName": "hs_email_optout", "operator": "EQ", "value": "true"},
{"propertyName": "lifecyclestage", "operator": "EQ", "value": stage},
]}],
"limit": 1,
})
if resp_s.status_code == 200:
count = resp_s.json().get("total", 0)
if count > 0:
print(f" {stage}: {count}")
time.sleep(0.1)
# Check contacts with no lifecycle stage
resp_empty = requests.post(url, headers=headers, json={
"filterGroups": [{"filters": [
{"propertyName": "hs_email_optout", "operator": "EQ", "value": "true"},From TomGranot/hubspot-admin-skills, MIT.
More from TomGranot/hubspot-admin-skills
- 0.675score
sandbox-self-test
tomgranot
Verify the entire toolkit against a disposable HubSpot developer test account or sandbox that you bring: seed synthetic fixtures, run every scripted skill's…
SkillAFreshness: A★ 77Claude Code +9 - 0.675score
hubspot-audit
tomgranot
Run a full, read-only HubSpot portal audit covering both the marketing and sales sides: database, deliverability, engagement, owners, pipelines, deal quality…
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
delete-no-email-contacts
tomgranot
Delete contacts with no email address from a HubSpot CRM instance.
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
lifecycle-progression-workflow
tomgranot
Build workflows to automate contact progression through the sales funnel: Lead to MQL to SQL to Opportunity to Customer.
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
create-icp-tiers
tomgranot
Classify companies into Ideal Customer Profile (ICP) tiers based on firmographic data (industry + employee count).
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
new-contact-hygiene-workflow
tomgranot
Build a HubSpot workflow that auto-enriches and stages new contacts upon creation.
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
build-smart-lists
tomgranot
Create foundational segmented lists for marketing and sales operations via the Lists API, plus advanced UI-built segments: a master sendable list, ICP-based…
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
bounce-monitoring-workflow
tomgranot
Build a workflow to protect sender reputation through automated bounce monitoring.
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
cleanup-deals
tomgranot
Find past-due, stale, incomplete, unexplained-loss, and likely test deals, then route each owner's flagged deals to them as one review task.
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
assign-unowned-contacts
tomgranot
Assign an owner to marketing contacts that have no owner.
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
merge-duplicate-companies
tomgranot
Identify duplicate company records by domain and name, export audit CSVs for review, and guide merging.
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
engagement-suppression-workflow
tomgranot
Build a two-tier sunset workflow that re-engages dormant contacts before suppressing them.
SkillAFreshness: A★ 77Claude Code +9
Signals
- Stars
- 77
- Forks
- 18
- Open issues
- 0
- Last commit (this folder)
- Oct 12, 2026 (18 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- Version
- 1.1
- License
- MIT
- Website
- hubspot.granot.io
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from tomgranot
- 0.675score
hubspot-audit
tomgranot
Run a full, read-only HubSpot portal audit covering both the marketing and sales sides: database, deliverability, engagement, owners, pipelines, deal quality…
SkillAFreshness: A★ 77Claude Code +9 - 0.675score
sandbox-self-test
tomgranot
Verify the entire toolkit against a disposable HubSpot developer test account or sandbox that you bring: seed synthetic fixtures, run every scripted skill's…
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
new-contact-hygiene-workflow
tomgranot
Build a HubSpot workflow that auto-enriches and stages new contacts upon creation.
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
assign-unowned-contacts
tomgranot
Assign an owner to marketing contacts that have no owner.
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
merge-duplicate-companies
tomgranot
Identify duplicate company records by domain and name, export audit CSVs for review, and guide merging.
SkillAFreshness: A★ 77Claude Code +9 - 0.672score
lifecycle-progression-workflow
tomgranot
Build workflows to automate contact progression through the sales funnel: Lead to MQL to SQL to Opportunity to Customer.
SkillAFreshness: A★ 77Claude Code +9
Done for you
Rather not set it up yourself?
Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.
See how we build AI agents →Read the case studies
Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.
- launches shipped
- 50+
- launches shipped
- media placements
- 500+
- media placements
- founders on every account
- 2
- founders on every account
Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block
Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.
Get suppress-global-unsubscribes running in your business
Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.
Cite this page
suppress-global-unsubscribes skill. Agent Mart. https://agentmart.dev/skills/tomgranot-suppress-global-unsubscribes (last updated 2026-10-24)