Batch enrich HubSpot contacts missing job title or company size using a Claude Code skill

low complexityCost: Usage-based
Compatible agents

This skill works with any agent that supports the Claude Code skills standard, including Claude Code, Claude Cowork, OpenAI Codex, and Google Antigravity.

Prerequisites

Prerequisites
  • Claude Code or another AI coding agent installed
  • HubSpot private app with crm.objects.contacts.read and crm.objects.contacts.write scopes
  • Apollo.io account with API access (bulk match endpoint)
Environment Variables
# HubSpot private app token with contacts read/write scopes
HUBSPOT_ACCESS_TOKEN=your_value_here
# Apollo.io API key with access to the bulk match endpoint
APOLLO_API_KEY=your_value_here

Why a Claude Code skill?

An Claude Code skill turns batch enrichment into a conversation. Instead of configuring nodes or maintaining scripts, you tell the agent what you need:

  • "Enrich contacts missing job titles"
  • "Find contacts without a company and fill from Apollo"
  • "Run the weekly enrichment — only contacts created this month"
  • "How many contacts are missing phone numbers?"

The agent reads API reference files, builds the right search filters, calls Apollo's bulk endpoint, and updates HubSpot — all without you writing or maintaining code. You can refine the batch (change which fields to check, adjust the limit, exclude specific lists) in natural language.

How it works

The skill has three parts:

  1. SKILL.md — instructions telling the agent the workflow steps and which reference files to consult
  2. references/ — API documentation for HubSpot Search, Apollo bulk_match, and HubSpot PATCH endpoints
  3. templates/ — field mapping rules defining which Apollo fields map to which HubSpot properties, and the "only fill empty fields" logic

When you invoke the skill, the agent reads these files, writes a script that follows the patterns, executes it, and reports the results.

What is a Claude Code skill?

An Claude Code skill is a directory of reference files and instructions that teach an AI coding agent how to complete a specific task. Unlike traditional automation that runs the same code every time, a skill lets the agent adapt — it can modify search filters, change batch sizes, handle errors, and explain what it did, all based on your natural-language request. The agent generates and runs code on the fly using the API patterns in the reference files.

Step 1: Create the skill directory

mkdir -p .claude/skills/batch-enrich/{templates,references}

Step 2: Write the SKILL.md file

Create .claude/skills/batch-enrich/SKILL.md:

---
name: batch-enrich
description: Finds HubSpot contacts missing critical fields (job title, company, phone) and batch-enriches them via Apollo. Only fills empty fields — never overwrites existing data.
disable-model-invocation: true
allowed-tools: Bash, Read
---
 
## Workflow
 
1. Read `references/hubspot-contacts-api.md` for Search API patterns
2. Read `references/apollo-bulk-match-api.md` for bulk enrichment patterns
3. Read `templates/field-mapping.md` for field mapping rules
4. Search HubSpot for contacts missing the requested field (default: jobtitle) using NOT_HAS_PROPERTY
5. Filter to business emails only (exclude gmail.com, yahoo.com, hotmail.com, outlook.com, aol.com)
6. Batch contacts into groups of 10 and call Apollo's bulk_match endpoint for each batch
7. For each match, build an update payload using ONLY fields that are currently empty on the contact
8. PATCH each contact in HubSpot with the enrichment data plus enrichment_date and enrichment_source metadata
9. Print a summary: total searched, matched, enriched, and fields filled
 
## Rules
 
- NEVER overwrite existing field values — only fill fields that are null/empty
- Always set enrichment_date (YYYY-MM-DD) and enrichment_source ("apollo-batch") on every updated contact
- Rate limit: 1 second between Apollo bulk_match calls, 200ms between HubSpot paginated searches
- Filter out personal email domains before calling Apollo to save credits
- Use environment variables: HUBSPOT_ACCESS_TOKEN, APOLLO_API_KEY

Step 3: Add reference and template files

Create references/hubspot-contacts-api.md:

# HubSpot Contacts API Reference
 
## Search for contacts missing fields
 
```
POST https://api.hubapi.com/crm/v3/objects/contacts/search
Authorization: Bearer {HUBSPOT_ACCESS_TOKEN}
Content-Type: application/json
```
 
Request body — find contacts where jobtitle is not set:
```json
{
  "filterGroups": [{
    "filters": [{
      "propertyName": "jobtitle",
      "operator": "NOT_HAS_PROPERTY"
    }]
  }],
  "properties": ["email", "firstname", "lastname", "jobtitle", "company", "phone", "linkedin_url", "industry"],
  "limit": 100,
  "after": 0
}
```
 
Response:
```json
{
  "results": [
    {
      "id": "123",
      "properties": {
        "email": "jane@acme.com",
        "firstname": "Jane",
        "lastname": "Smith",
        "jobtitle": null,
        "company": "Acme Inc"
      }
    }
  ],
  "paging": { "next": { "after": "100" } }
}
```
 
Pagination: continue calling with `after` value until `paging.next` is absent. Rate limit: 5 req/sec, add 200ms delay between pages.
 
Multiple missing fields: use separate `filterGroups` (OR logic) to find contacts missing ANY of several fields.
 
## Update a contact
 
```
PATCH https://api.hubapi.com/crm/v3/objects/contacts/{contactId}
Authorization: Bearer {HUBSPOT_ACCESS_TOKEN}
Content-Type: application/json
```
 
Request body:
```json
{
  "properties": {
    "jobtitle": "Director of Marketing",
    "enrichment_date": "2026-03-05",
    "enrichment_source": "apollo-batch"
  }
}
```
 
Rate limit: 150 req/10 sec. No delay needed for batches under 150 contacts.
 
Note: NOT_HAS_PROPERTY matches contacts where the property was never set. Empty string values are considered "set" and won't match. To catch both, combine NOT_HAS_PROPERTY with EQ '' using separate filterGroups.

Create references/apollo-bulk-match-api.md:

# Apollo Bulk Match API Reference
 
## Bulk match people by email
 
```
POST https://api.apollo.io/api/v1/people/bulk_match
x-api-key: {APOLLO_API_KEY}
Content-Type: application/json
```
 
Request body (up to 10 records per request):
```json
{
  "details": [
    { "email": "jane@acme.com" },
    { "email": "bob@widgets.co" }
  ]
}
```
 
Response:
```json
{
  "matches": [
    {
      "title": "Director of Marketing",
      "organization": {
        "name": "Acme Inc",
        "industry": "Computer Software",
        "estimated_num_employees": 250
      },
      "phone_numbers": [
        { "sanitized_number": "+12125550198", "type": "work_direct" }
      ],
      "linkedin_url": "https://www.linkedin.com/in/janesmith"
    },
    null
  ]
}
```
 
Key behaviors:
- matches array is in the same order as the input details array
- Unmatched emails return null at their index position
- Maximum 10 records per request
- 1 credit per person in the request (even for null results)
- Rate limit: 5 req/sec on Basic plan — add 1 second delay between bulk calls
 
The individual match endpoint (POST /api/v1/people/match) accepts a single person with email, first_name, last_name, or organization_name. Use bulk_match for batch processing.

Create templates/field-mapping.md:

# Field Mapping: Apollo → HubSpot
 
## Mapping rules
 
| Apollo field | HubSpot property | Notes |
|---|---|---|
| title | jobtitle | Job title |
| organization.name | company | Company name |
| phone_numbers[0].sanitized_number | phone | Primary phone |
| linkedin_url | linkedin_url | LinkedIn profile URL |
| organization.industry | industry | Industry classification |
| organization.estimated_num_employees | numemployees | Employee count range |
 
## Update rules
 
1. ONLY update a HubSpot field if its current value is null/empty
2. If the contact already has a value for a field, skip it — even if Apollo has a different value
3. Always add these metadata fields to every updated contact:
   - enrichment_date: current date in YYYY-MM-DD format
   - enrichment_source: "apollo-batch"
4. Set enrichment_date even on contacts where Apollo returned null (to exclude from future runs)
 
## Personal email domains to skip
 
Do not send these domains to Apollo (wastes credits, near-zero B2B match rate):
gmail.com, yahoo.com, hotmail.com, outlook.com, aol.com, protonmail.com, icloud.com, me.com, live.com

Step 4: Test the skill

# In Claude Code
/batch-enrich

Ask the agent to start with a small batch:

"Enrich 10 contacts missing job titles — show me results before processing the rest"

The agent will search HubSpot, call Apollo, and show you what it plans to write before updating. You can then approve the full batch or adjust the criteria.

Step 5: Schedule it (optional)

Option A: Cron + CLI

# Run weekly on Sunday at 10 PM
0 22 * * 0 cd /path/to/project && claude -p "Run /batch-enrich for all contacts missing job titles. Limit to 200." --allowedTools 'Bash,Read' 2>&1 >> /var/log/batch-enrich.log

Option B: GitHub Actions

name: Weekly Batch Enrichment
on:
  schedule:
    - cron: '0 3 * * 0'  # Sunday 3 AM UTC
  workflow_dispatch: {}
jobs:
  enrich:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: anthropics/claude-code-action@v1
        with:
          prompt: "Run /batch-enrich for all contacts missing job titles. Limit to 200."
          allowed_tools: "Bash,Read"
        env:
          HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_ACCESS_TOKEN }}
          APOLLO_API_KEY: ${{ secrets.APOLLO_API_KEY }}

Option C: Cowork Scheduled Tasks

Claude Desktop's Cowork supports built-in scheduled tasks. Open a Cowork session, type /schedule, and configure the cadence — hourly, daily, weekly, or weekdays only. Each scheduled run has full access to your connected tools, plugins, and MCP servers.

Scheduled tasks only run while your computer is awake and Claude Desktop is open. If a run is missed, Cowork executes it automatically when the app reopens. For always-on scheduling, use GitHub Actions (Option B) instead. Available on all paid plans (Pro, Max, Team, Enterprise).

Troubleshooting

When to use this approach

  • You want to clean up data gaps right now without building automation infrastructure
  • You're testing Apollo's fill rates on your contact list before committing to a platform
  • You want to run tasks in the background via Claude Cowork while focusing on other work
  • You want to enrich a specific segment — tell the agent to filter by lifecycle stage, list, or date range
  • You prefer seeing enrichment results in real-time and approving before the update

When to switch to a dedicated tool

  • You want enrichment to run reliably every week without any human involvement
  • You need visual monitoring dashboards showing fill rates, credit usage, and error trends
  • Multiple team members need to manage the enrichment configuration
  • You want to chain enrichment with scoring, routing, or sequences in one platform

Common questions

Does the agent write code every time I run the skill?

Yes. The agent reads the reference files, generates a script tailored to your request, runs it, and reports results. This means you can ask for variations ("only contacts from this month", "enrich company size instead of job title") without modifying any configuration files.

How many Apollo credits does a typical run use?

1 credit per contact in the batch, regardless of whether Apollo finds a match. A weekly run of 100 contacts uses 100 credits. On the Basic plan ($49/mo, 900 credits), that's 4 months of weekly runs before hitting the limit.

Can I preview what the agent will update before it writes to HubSpot?

Yes. Ask the agent to show you the enrichment results first: "Enrich 20 contacts and show me what you'd write before updating." The agent will display the proposed changes and wait for your approval.

What's the difference between this and the code approach?

The code approach gives you a static script that runs the same way every time. The Claude Code skill generates code on the fly based on your request, so you can change search criteria, batch sizes, or field mappings in natural language without editing files.

Cost

  • Apollo: 1 credit per person in the bulk request. Basic plan ($49/mo) = 900 credits. A weekly batch of 100 contacts = 400 credits/month.
  • HubSpot: Free within API rate limits.
  • Claude Code: Usage-based pricing per conversation. A typical enrichment run uses one conversation turn.
  • GitHub Actions: Free tier covers weekly scheduled runs.

Looking to scale your AI operations?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.