← 返回 Skills 市场
tomgranot

Delete No Email Contacts

作者 TomGranot · GitHub ↗ · v1.0.0 · MIT-0
cross-platform ⚠ suspicious
106
总下载
0
收藏
0
当前安装
1
版本数
在 OpenClaw 中安装
/install delete-no-email-contacts
功能描述
Delete contacts with no email address from a HubSpot CRM instance. These contacts cannot receive any communication and inflate billing. Fully automated via t...
使用说明 (SKILL.md)

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"\
Deleted: {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

Mechanism Detail
Abort threshold Hard-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 trail Every contact ID, name, and create date is exported to CSV before any deletion occurs.
Confirmation prompt Always present the Before State count to the user and wait for explicit confirmation before running Execute.
90-day recovery Deleted contacts can be restored via HubSpot Settings > Data Management > Deleted Objects for 90 days.
Archived contacts audit After 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
安全使用建议
This skill appears to implement what it claims, but take these precautions before installing or running it: - Verify credentials: The scripts expect a HubSpot private app access token (HUBSPOT_ACCESS_TOKEN) with crm.objects.contacts.read and crm.objects.contacts.write scopes. The registry metadata does not declare this — do not proceed until you confirm where the token will come from and that it's least-privilege. - Review the code: Inspect before.py and the CSV produced to ensure the contacts targeted are the ones you intend to remove. - Run in stages: Run before.py first, review the generated data/no-email-contacts-before.csv, and only run execute.py after manual confirmation. The scripts already ask for a manual 'DELETE' confirmation but verify the CSV yourself first. - Adjust safety thresholds: The scripts include SAFETY_THRESHOLD/ABORT_THRESHOLD values. Set these conservatively (based on your before-state count) to avoid mass accidental deletion. - Secure the token: The scripts load a .env file in the repository root. Storing tokens in plaintext files has risk — prefer setting the token in your environment or a secure secrets manager and avoid committing .env to source control. After use, consider rotating the token. - Dependencies: The package metadata is only in script comments; ensure you have Python 3.10+ and install requests and python-dotenv before running. - Test in a safe environment: If possible, test against a HubSpot sandbox or a copy of your data first. Because the manifest fails to disclose the required credential, treat the registry entry as incomplete and proceed cautiously.
功能分析
Type: OpenClaw Skill Name: delete-no-email-contacts Version: 1.0.0 The skill bundle is a legitimate utility for HubSpot CRM maintenance, designed to delete contacts lacking email addresses to reduce billing costs. It implements several safety best practices, including a hard-coded safety threshold (scripts/execute.py), mandatory user confirmation before deletion, and the generation of CSV audit logs (scripts/before.py) for tracking changes. The code is transparent, well-documented, and strictly interacts with the official HubSpot API (api.hubapi.com) without any signs of data exfiltration or malicious intent.
能力评估
Purpose & Capability
The skill's name, SKILL.md, and included scripts all consistently implement deletion of HubSpot contacts missing an email address via the HubSpot CRM APIs. However, the skill package/registry metadata does NOT declare the required environment variable or primary credential (HUBSPOT_ACCESS_TOKEN) even though the scripts require it, which is an inconsistency.
Instruction Scope
All runtime instructions and scripts are scoped to HubSpot API actions (search, export for audit, batch archive) and local CSV logging. The SKILL.md requires user confirmation and describes a safety threshold prior to deletion. The scripts do not attempt to read unrelated system paths or call external endpoints other than api.hubapi.com.
Install Mechanism
There is no install spec in the registry (instruction-only), which is low risk. The scripts include comment metadata listing Python and two dependencies (requests, python-dotenv) but the registry will not automatically install them. Users must install the Python runtime and the listed packages manually (or via their own environment). No external downloads or unexpected installers are used.
Credentials
The code requires a HubSpot private app access token (HUBSPOT_ACCESS_TOKEN) with read/write scopes — this is proportionate to the task. The concern is that the published registry metadata does not list any required environment variables or a primary credential, which is misleading and could cause users to miss the fact that a high-privilege token is needed and will be read from a .env file.
Persistence & Privilege
The skill is not force-included (always: false) and does not request persistent system privileges or modify other skills or system-wide configurations. It runs as normal scripts and requires explicit interactive confirmation before deleting.
如何使用
  1. 确保已安装 OpenClaw(本地或 Docker 部署)
  2. 在对话框中输入安装命令:/install delete-no-email-contacts
  3. 安装完成后,直接呼叫该 Skill 的名称或使用 /delete-no-email-contacts 触发
  4. 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
版本历史
v1.0.0
Initial release of the skill for cleaning HubSpot contacts with no email addresses. - Fully automates identification and deletion of contacts without email addresses in HubSpot CRM. - Includes 4-stage execution: Plan, Before State (count + sample), Execute (CSV export, batch delete), and After State (verification). - Safety features: explicit user confirmation, audit log CSV export, and abort threshold (default 500 contacts). - Requires HubSpot private app with proper contact access scopes. - Deleted contacts are recoverable for 90 days via HubSpot Settings.
元数据
Slug delete-no-email-contacts
版本 1.0.0
许可证 MIT-0
累计安装 0
当前安装数 0
历史版本数 1
常见问题

Delete No Email Contacts 是什么?

Delete contacts with no email address from a HubSpot CRM instance. These contacts cannot receive any communication and inflate billing. Fully automated via t... 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 106 次。

如何安装 Delete No Email Contacts?

在 OpenClaw 或 Claude Code 对话框中运行命令「/install delete-no-email-contacts」即可一键安装,无需额外配置。

Delete No Email Contacts 是免费的吗?

是的,Delete No Email Contacts 完全免费,采用 MIT-0 许可证,可自由下载、安装和使用。

Delete No Email Contacts 支持哪些平台?

Delete No Email Contacts 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。

谁开发了 Delete No Email Contacts?

由 TomGranot(@tomgranot)开发并维护,当前版本 v1.0.0。

💬 留言讨论