📦 Delete No Email Contacts — 技能工具

v1.0.0

Delete contacts with no email address from a HubSpot CRM instance. These contacts cannot receive any communication and inflate billing. Fully automated via t...

0· 75·0 当前·0 累计
tomgranot 头像by @tomgranot (TomGranot)·MIT-0
下载技能包
License
MIT-0
最后更新
2026/3/27
0
安全扫描
VirusTotal
无害
查看报告
OpenClaw
可疑
medium confidence
The code and instructions appear to do exactly what the skill says (delete HubSpot contacts with no email), but the registry metadata omits required credential information and there are minor packaging/installation omissions you should verify before running.
评估建议
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 ...
详细分析 ▾
用途与能力
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.
指令范围
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.
安装机制
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.
凭证需求
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.
持久化与权限
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.
安全有层次,运行前请审查代码。

License

MIT-0

可自由使用、修改和再分发,无需署名。

运行时依赖

无特殊依赖

版本

latestv1.0.02026/3/27

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.

无害

安装命令

点击复制
官方npx clawhub@latest install delete-no-email-contacts
镜像加速npx clawhub@latest install delete-no-email-contacts --registry https://cn.longxiaskill.com

技能文档

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:

  • Root cause: Ask whether any integrations (CRM sync, form tool, import process) are intentionally creating contacts without email. If so, fix the inflow first.
  • Threshold: The default safety abort threshold is 500 contacts. If the user expects more, adjust the threshold in the execute script.
  • 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

  • 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."
  • 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).
  • 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.
  • Batch archive returns 204: A successful batch archive returns HTTP 204 with an empty body, not 200. Check for status_code == 204.
  • 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

数据来源ClawHub ↗ · 中文优化:龙虾技能库