返回技能列表

delete-no-email-contacts

TomGranot
更新于 2 days ago
7 次查看
33
11
33
在 GitHub 上查看
文档aiapiautomation

关于

This skill automatically identifies and deletes HubSpot contacts that lack an email address, helping reduce billed contact counts and maintain CRM hygiene. It uses HubSpot's Search and Batch Archive APIs for full automation. Developers should implement this for regular database cleanup when non-communicable contacts accumulate.

快速安装

Claude Code

推荐
主要方式
npx skills add TomGranot/hubspot-admin-skills -a claude-code
插件命令备选方式
/plugin add https://github.com/TomGranot/hubspot-admin-skills
Git 克隆备选方式
git clone https://github.com/TomGranot/hubspot-admin-skills.git ~/.claude/skills/delete-no-email-contacts

在 Claude Code 中复制并粘贴此命令以安装该技能

技能文档

Delete Contacts With No Email Address

Purpose

Contacts without an email address serve no functional purpose in a HubSpot Marketing Hub instance. They cannot receive marketing emails, sales sequences, or transactional messages. They inflate the billed contact count. This skill identifies and deletes them via the API.

Prerequisites

  • A HubSpot private app access token with crm.objects.contacts.read and crm.objects.contacts.write scopes
  • Python 3.10+ with uv for package management
  • A .env file containing HUBSPOT_ACCESS_TOKEN

Execution Pattern

This skill follows a 4-stage execution pattern: Plan -> Before State -> Execute -> After State.

Stage 1: Plan

Before writing any code, confirm these items with the user:

  1. Root cause: Ask whether any integrations (CRM sync, form tool, import process) are intentionally creating contacts without email. If so, fix the inflow first.
  2. Threshold: The default safety abort threshold is 500 contacts. If the user expects more, adjust the threshold in the execute script.
  3. Recovery window: Confirm the user understands that deleted contacts are recoverable for 90 days via HubSpot Settings > Data Management > Deleted Objects.

Stage 2: Before State

Run a count query to establish the baseline. Save results for comparison.

"""
Before State: Count contacts with no email address.
"""
import os
import json
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",
}

search_payload = {
    "filterGroups": [
        {
            "filters": [
                {
                    "propertyName": "email",
                    "operator": "NOT_HAS_PROPERTY",
                }
            ]
        }
    ],
    "properties": ["firstname", "lastname", "createdate", "hs_object_id"],
    "limit": 1,  # Only need the total count
}

url = f"{BASE}/crm/v3/objects/contacts/search"
response = requests.post(url, headers=headers, json=search_payload)
response.raise_for_status()

data = response.json()
total = data.get("total", 0)

print(f"BEFORE STATE: {total} contacts exist with no email address.")

if total > 0 and data.get("results"):
    sample = data["results"][0]
    props = sample.get("properties", {})
    print(f"  Sample: ID {sample['id']}, "
          f"{props.get('firstname', '(empty)')} {props.get('lastname', '(empty)')}, "
          f"created {props.get('createdate', '(unknown)')}")

Expected output: A count of contacts with no email and a sample record for sanity checking.

Present findings to the user before proceeding. Ask for explicit confirmation to continue.

Stage 3: Execute

Collect all contact IDs via paginated search, export a CSV audit trail, then batch-delete.

"""
Execute: Delete all contacts with no email address.
Steps:
  1. Paginated search to collect all contact IDs
  2. Export CSV audit log before deletion
  3. Batch archive in groups of 100
"""
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",
}

# --- Step 1: Collect all contact IDs ---
all_contacts = []
after = None

search_payload = {
    "filterGroups": [
        {
            "filters": [
                {
                    "propertyName": "email",
                    "operator": "NOT_HAS_PROPERTY",
                }
            ]
        }
    ],
    "properties": ["firstname", "lastname", "createdate", "hs_object_id"],
    "limit": 100,
}

while True:
    payload = search_payload.copy()
    if after:
        payload["after"] = after

    resp = requests.post(
        f"{BASE}/crm/v3/objects/contacts/search",
        headers=headers, json=payload,
    )
    resp.raise_for_status()
    data = resp.json()

    for contact in data.get("results", []):
        props = contact.get("properties", {})
        all_contacts.append({
            "id": contact["id"],
            "firstname": props.get("firstname", ""),
            "lastname": props.get("lastname", ""),
            "createdate": props.get("createdate", ""),
        })

    paging = data.get("paging", {})
    after = paging.get("next", {}).get("after")
    if not after:
        break
    time.sleep(0.2)  # Rate limiting

print(f"Total contacts to delete: {len(all_contacts)}")

# --- Step 2: SAFETY CHECK ---
ABORT_THRESHOLD = 500
if len(all_contacts) > ABORT_THRESHOLD:
    print(f"SAFETY ABORT: Found {len(all_contacts)} contacts, "
          f"exceeds threshold of {ABORT_THRESHOLD}.")
    print("Review the data and adjust the threshold if this is expected.")
    exit(1)

# --- Step 3: Export CSV audit trail ---
os.makedirs("data/audit-logs", exist_ok=True)
csv_path = "data/audit-logs/deleted-no-email-contacts.csv"

with open(csv_path, "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["id", "firstname", "lastname", "createdate"])
    writer.writeheader()
    writer.writerows(all_contacts)

print(f"Audit log saved: {csv_path} ({len(all_contacts)} records)")

# --- Step 4: Batch delete ---
all_ids = [c["id"] for c in all_contacts]
BATCH_SIZE = 100
deleted_count = 0
failed_ids = []

for i in range(0, len(all_ids), BATCH_SIZE):
    batch = all_ids[i : i + BATCH_SIZE]
    delete_payload = {"inputs": [{"id": cid} for cid in batch]}

    resp = requests.post(
        f"{BASE}/crm/v3/objects/contacts/batch/archive",
        headers=headers, json=delete_payload,
    )

    if resp.status_code == 204:
        deleted_count += len(batch)
        print(f"  Batch {i // BATCH_SIZE + 1}: deleted {len(batch)} contacts")
    else:
        failed_ids.extend(batch)
        print(f"  Batch FAILED: {resp.status_code} — {resp.text[:200]}")

    time.sleep(0.5)  # Rate limiting between batches

print(f"\nDeleted: {deleted_count}, Failed: {len(failed_ids)}")

Key API details:

  • POST /crm/v3/objects/contacts/search with NOT_HAS_PROPERTY filter on email
  • Paginate with after cursor, 100 results per page
  • POST /crm/v3/objects/contacts/batch/archive accepts up to 100 IDs per call
  • Successful archive returns HTTP 204 (no content)

Stage 4: After State

Re-run the before-state query to confirm zero contacts remain.

"""
After State: Verify no contacts with missing email remain.
"""
# (Same search payload as Before State)
response = requests.post(url, headers=headers, json=search_payload)
response.raise_for_status()
total = response.json().get("total", 0)

if total == 0:
    print("SUCCESS: 0 contacts with no email remain.")
else:
    print(f"WARNING: {total} contacts with no email still exist.")
    print("New contacts may have been created since deletion. Investigate.")

Present results to the user. If new contacts appeared, investigate the source (form submissions, integrations, imports).

Safety Mechanisms

MechanismDetail
Abort thresholdHard-coded at 500 contacts by default. If the search returns more, the script exits without deleting anything. Adjust only with explicit user confirmation.
CSV audit trailEvery contact ID, name, and create date is exported to CSV before any deletion occurs.
Confirmation promptAlways present the Before State count to the user and wait for explicit confirmation before running Execute.
90-day recoveryDeleted contacts can be restored via HubSpot Settings > Data Management > Deleted Objects for 90 days.
Archived contacts auditAfter deletion, you can retrieve deleted contacts via the standard contacts endpoint with archived=true parameter to verify what was removed.

Technical Gotchas

  1. NOT_HAS_PROPERTY vs EQ "": Use NOT_HAS_PROPERTY operator, not EQ with an empty string. HubSpot treats "property not set" differently from "property set to empty string."

  2. Search API pagination limit: The HubSpot CRM Search API has a hard cap of 10,000 results per query. For this use case (typically a few hundred contacts), this is not an issue. If you encounter it, use segmented queries (e.g., filter by create date ranges).

  3. Rate limiting: The search API allows ~4 requests/second for a private app. The batch archive API is more restrictive. Use time.sleep(0.5) between batch archive calls.

  4. Batch archive returns 204: A successful batch archive returns HTTP 204 with an empty body, not 200. Check for status_code == 204.

  5. Contacts may reappear: If an integration or form is creating contacts without email, new ones will appear after deletion. Always investigate the root cause.

Package Setup

uv init hubspot-cleanup
cd hubspot-cleanup
uv add requests python-dotenv

Create a .env file:

HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxxxxx

GitHub 仓库

TomGranot/hubspot-admin-skills
路径: skills/delete-no-email-contacts
0

相关推荐技能

railway-docs

文档

Railway Docs Skill可实时获取最新的Railway官方文档,确保回答的准确性。当开发者询问Railway功能特性、工作原理或分享docs.railway.com链接时,应优先使用此技能。它通过专门的LLM优化文档源提供最新信息,避免依赖过时记忆来回答技术问题。

查看技能

n8n-code-python

文档

该Skill为在n8n平台的Python代码节点中编写代码提供专家指导,特别适用于需要使用_input/_json/_node语法、Python标准库或了解n8n中Python限制的场景。它强调JavaScript应作为首选方案,仅当需要特定Python功能或对Python语法更熟悉时才使用Python。Skill提供了快速入门模板和关键注意事项,帮助开发者在n8n中高效编写Python代码。

查看技能

archon

文档

Archon Skill为开发者提供了基于RAG的语义搜索和项目任务管理功能,可通过REST API访问知识库。它支持文档搜索、网站爬取、文件上传和版本控制,适用于技术文档查询和项目管理场景。首次使用时需要配置Archon主机地址,建议在处理外部文档时优先使用该Skill。

查看技能

n8n-code-javascript

文档

这个Skill为n8n工作流中的JavaScript代码节点提供专业指导,涵盖数据处理、HTTP请求和日期操作等核心场景。它详细解释了如何正确使用n8n特有的`$input`/`$json`语法、`$helpers`工具以及DateTime对象,并包含关键的错误排查和模式选择建议。开发者通过该Skill能快速掌握Code节点的正确返回格式、数据访问方法和常见陷阱解决方案。

查看技能