Find decision makers at a HubSpot company using Apollo and a Claude Code skill

low complexityCost: Usage-based

Prerequisites

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
  • One of the agents listed above
  • HubSpot private app token with contacts read/write and companies read scopes
  • Apollo API key with People Search and Enrichment credits
Environment Variables
# HubSpot private app token with crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.companies.read scopes
HUBSPOT_ACCESS_TOKEN=your_value_here
# Apollo API key for People Search and People Match (each lookup uses credits)
APOLLO_API_KEY=your_value_here

Why a Claude Code skill?

The other approaches in this guide are deterministic: they run the same logic every time, the same way. An Claude Code skill is different. You tell Claude a company name and what you're looking for, and the skill gives it enough context to do it reliably.

That means you can say:

  • "Find decision makers at Acme Corp"
  • "Who are the VP+ engineering contacts at TargetCo?"
  • "Add the buying committee at company ID 12345678 to HubSpot"

The skill contains workflow guidelines, API reference materials, and field mappings that the agent reads on demand. When you invoke the skill, Claude reads these files, writes a script on the fly, runs it, and reports results. If you ask for something different next time — different titles, a specific department, fewer seniority levels — the agent adapts without you touching any code.

How it works

The skill directory has three parts:

  1. SKILL.md — workflow guidelines telling the agent what steps to follow, which env vars to use, and what pitfalls to avoid
  2. references/ — Apollo API patterns (People Search, People Match) and HubSpot API patterns (company lookup, contact creation, associations)
  3. templates/ — field mapping reference so contacts are created with consistent HubSpot property names

When invoked, the agent reads SKILL.md, consults the reference files as needed, writes a Python script, executes it, and reports what it created. The reference files act as guardrails — the agent knows exactly which endpoints to hit and what the responses look like, so it doesn't have to guess.

What is a Claude Code skill?

An Claude Code skill is a reusable command you add to your project that Claude Code can run on demand. Skills live in a .claude/skills/ directory and are defined by a SKILL.md file that tells the agent what the skill does, when to run it, and what tools it's allowed to use.

In this skill, the agent doesn't run a pre-written script. Instead, SKILL.md provides workflow guidelines and points to reference files — API documentation, field mappings — that the agent reads to generate and execute code itself. This is the key difference from a traditional script: the agent can adapt its approach based on what you ask for while still using the right APIs and field names.

Once installed, you can invoke a skill as a slash command (e.g., /find-decision-makers), or the agent will use it automatically when you give it a task where the skill is relevant. Skills are portable — anyone who clones your repo gets the same commands.

Step 1: Create the skill directory

mkdir -p .claude/skills/find-decision-makers/{templates,references}

This creates the layout:

.claude/skills/find-decision-makers/
├── SKILL.md                          # workflow guidelines + config
├── templates/
│   └── hubspot-field-mapping.md      # HubSpot property name mapping
└── references/
    ├── apollo-people-api.md          # Apollo People Search + Match endpoints
    └── hubspot-contacts-api.md       # HubSpot company lookup, contact creation, associations

Step 2: Write the SKILL.md

Create .claude/skills/find-decision-makers/SKILL.md:

---
name: find-decision-makers
description: Find VP+ decision makers at a target HubSpot company using Apollo, enrich them, and create associated contacts
disable-model-invocation: true
allowed-tools: Bash, Read
---
 
## Goal
 
Given a target company (by name or HubSpot ID), find VP+/C-suite decision makers via Apollo's People Search API, enrich them for verified emails, and create associated contacts in HubSpot.
 
## Configuration
 
Read these environment variables:
 
- `HUBSPOT_ACCESS_TOKEN` — HubSpot private app token with contacts read/write and companies read scopes (required)
- `APOLLO_API_KEY` — Apollo API key for People Search and People Match (required)
 
Default target titles: VP Sales, CRO, VP Marketing, CMO, VP RevOps, Head of Sales, VP Engineering, CTO
Default seniorities: vp, c_suite, director
The user may request different titles or departments.
 
## Workflow
 
1. Validate that all required env vars are set. If any are missing, print which ones and exit.
2. Look up the target company in HubSpot by ID or name search. Get the company's domain and record ID. See `references/hubspot-contacts-api.md`.
3. If the company has no domain, print an error and exit (Apollo requires a domain to search).
4. Search Apollo for decision makers at that domain, filtered by target titles and seniorities. See `references/apollo-people-api.md`.
5. For each person found, enrich via Apollo People Match to get a verified email. Skip people without verified emails.
6. For each enriched person, check if a HubSpot contact with that email already exists. If yes, skip creation but still associate.
7. Create new HubSpot contacts using the field mapping in `templates/hubspot-field-mapping.md`.
8. Associate each contact (new or existing) with the target company.
9. Print a summary of how many contacts were found, created, and associated.
 
## Important notes
 
- Apollo People Search costs 1 credit per result returned. People Match costs 1 additional credit per call. Log credit usage.
- Apollo rate limits are ~5 requests/second. Add a 200ms delay between API calls.
- HubSpot uses internal property names: `firstname` (not first_name), `jobtitle` (not job_title). See the field mapping template.
- Apollo People Match returns a single person under the `person` key (singular), not `people` (plural).
- Some contacts may have left the company — Apollo will return null email for these. Skip them.
- Use `urllib.request` for HTTP calls (no external dependencies required).
- Use `requests` library if available, but don't fail if it's not installed — fall back to urllib.

Understanding the SKILL.md

SectionPurpose
GoalTells the agent what outcome to produce
ConfigurationWhich env vars to read and what defaults to use
WorkflowNumbered steps with pointers to reference files
Important notesNon-obvious context that prevents common mistakes

The allowed-tools: Bash, Read setting lets the agent both read reference files and execute code. The agent writes its own script based on the workflow steps and reference materials.

Step 3: Add reference files

references/apollo-people-api.md

Create .claude/skills/find-decision-makers/references/apollo-people-api.md:

# Apollo People API Reference
 
## People Search
 
Find people at a company by domain, titles, and seniority.
 
**Request:**
 
```
POST https://api.apollo.io/api/v1/mixed_people/search
Content-Type: application/json
X-Api-Key: <APOLLO_API_KEY>
```
 
**Body:**
 
```json
{
  "q_organization_domains_list": ["targetco.com"],
  "person_titles": ["VP Sales", "CRO", "VP Marketing", "CMO"],
  "person_seniorities": ["vp", "c_suite", "director"],
  "page": 1,
  "per_page": 25
}
```
 
**Response shape:**
 
```json
{
  "people": [
    {
      "id": "abc123",
      "first_name": "Sarah",
      "last_name": "Chen",
      "title": "VP of Engineering",
      "organization": {
        "name": "TargetCo",
        "primary_domain": "targetco.com"
      },
      "email": null
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total_entries": 8
  }
}
```
 
**Notes:**
- Costs 1 credit per result returned.
- Email in search results is often null — use People Match for verified emails.
- Rate limit: ~5 requests/second. Add 200ms delay between calls.
 
## People Match (Enrichment)
 
Get verified email for a specific person.
 
**Request:**
 
```
POST https://api.apollo.io/api/v1/people/match
Content-Type: application/json
X-Api-Key: <APOLLO_API_KEY>
```
 
**Body:**
 
```json
{
  "first_name": "Sarah",
  "last_name": "Chen",
  "organization_name": "TargetCo",
  "reveal_personal_emails": false
}
```
 
**Response shape:**
 
```json
{
  "person": {
    "id": "abc123",
    "first_name": "Sarah",
    "last_name": "Chen",
    "title": "VP of Engineering",
    "email": "schen@targetco.com",
    "linkedin_url": "https://linkedin.com/in/sarahchen",
    "phone_numbers": [
      { "sanitized_number": "+14155551234" }
    ],
    "organization": {
      "name": "TargetCo"
    }
  }
}
```
 
**Notes:**
- Returns a single person under `person` key (singular).
- Costs 1 credit per call.
- Email may be null if no verified work email exists.
- `phone_numbers` is an array — access first element's `sanitized_number`.

references/hubspot-contacts-api.md

Create .claude/skills/find-decision-makers/references/hubspot-contacts-api.md:

# HubSpot Contacts API Reference
 
## Look up a company by ID
 
```
GET https://api.hubapi.com/crm/v3/objects/companies/<COMPANY_ID>
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
```
 
Query parameter: `properties=domain,name`
 
## Search companies by name
 
```
POST https://api.hubapi.com/crm/v3/objects/companies/search
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
Content-Type: application/json
```
 
Body:
```json
{
  "filterGroups": [{"filters": [{
    "propertyName": "name",
    "operator": "CONTAINS_TOKEN",
    "value": "TargetCo"
  }]}],
  "properties": ["domain", "name"],
  "limit": 1
}
```
 
## Search contacts by email (deduplication)
 
```
POST https://api.hubapi.com/crm/v3/objects/contacts/search
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
Content-Type: application/json
```
 
Body:
```json
{
  "filterGroups": [{"filters": [{
    "propertyName": "email",
    "operator": "EQ",
    "value": "schen@targetco.com"
  }]}]
}
```
 
Returns `results` array — if length > 0, contact exists.
 
## Create a contact
 
```
POST https://api.hubapi.com/crm/v3/objects/contacts
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
Content-Type: application/json
```
 
Body:
```json
{
  "properties": {
    "email": "schen@targetco.com",
    "firstname": "Sarah",
    "lastname": "Chen",
    "jobtitle": "VP of Engineering",
    "company": "TargetCo",
    "phone": "+14155551234",
    "hs_lead_status": "NEW"
  }
}
```
 
**Important:** HubSpot uses `firstname` (not first_name), `lastname` (not last_name), `jobtitle` (not job_title).
 
## Associate contact with company
 
```
PUT https://api.hubapi.com/crm/v3/objects/contacts/<CONTACT_ID>/associations/companies/<COMPANY_ID>/contact_to_company
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
```
 
No body required.

templates/hubspot-field-mapping.md

Create .claude/skills/find-decision-makers/templates/hubspot-field-mapping.md:

# HubSpot Field Mapping
 
Map Apollo enrichment fields to HubSpot contact properties:
 
| Apollo field | HubSpot property | Notes |
|---|---|---|
| `person.email` | `email` | Required — skip contact if null |
| `person.first_name` | `firstname` | Not `first_name` |
| `person.last_name` | `lastname` | Not `last_name` |
| `person.title` | `jobtitle` | Not `job_title` |
| `person.organization.name` | `company` | Company name as text |
| `person.phone_numbers[0].sanitized_number` | `phone` | May be null |
| `person.linkedin_url` | `linkedin_url` | Custom property — must exist in HubSpot |
 
Set `hs_lead_status` to `NEW` for all created contacts.
 
**Important:** The `linkedin_url` property is custom — it must be created in HubSpot (Settings > Properties > Create property) before the script can write to it. If it doesn't exist, omit it from the contact creation payload.

Step 4: Test the skill

Invoke the skill conversationally:

/find-decision-makers Acme Corp

Claude will read the SKILL.md, check the reference files, write a script, run it, and report the results. A typical run looks like:

Company: Acme Corp (acme.com) — HubSpot ID: 12345678
Searching Apollo for decision makers...
Found 8 people
 
  CREATED: schen@acme.com — VP of Engineering
  CREATED: jmiller@acme.com — CRO
  EXISTS: tkim@acme.com (ID: 87654321), associating
  CREATED: agarcia@acme.com — VP Marketing
  SKIPPED: Bob Wilson — no verified email
  CREATED: lnguyen@acme.com — VP RevOps
  CREATED: mrobinson@acme.com — Head of Sales
  CREATED: dthompson@acme.com — CTO
 
Done. 6 contacts created, 1 existing associated, 1 skipped. ~16 Apollo credits used.

Because the agent generates code on the fly, you can also make ad hoc requests:

  • "Find engineering leaders at TargetCo" — the agent adjusts the title filter
  • "Add the buying committee at company ID 98765" — the agent uses the ID directly
  • "Who are the C-suite at Acme Corp? Don't create contacts, just list them" — the agent runs a read-only search
Test with a real company

Run the skill against a company with known contacts in Apollo (check app.apollo.io first). If Apollo returns no results, verify the company's domain is set correctly in HubSpot.

Step 5: Schedule it (optional)

Option A: Cron + Claude CLI

# Run weekly on Monday mornings for new target accounts
0 9 * * 1 cd /path/to/your/project && claude -p "Run /find-decision-makers for all Tier 1 companies without contacts" --allowedTools 'Bash(*)' 'Read(*)'

Option B: GitHub Actions + Claude

name: Find Decision Makers
on:
  schedule:
    - cron: '0 14 * * 1'  # Weekly on Monday at 9 AM ET
  workflow_dispatch: {}
jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: anthropics/claude-code-action@v1
        with:
          prompt: "Run /find-decision-makers for all Tier 1 ABM companies that have 0 associated contacts"
          allowed_tools: "Bash(*),Read(*)"
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          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're doing ad hoc prospecting on specific accounts during deal prep
  • You want to quickly multi-thread a new target account without setting up a recurring workflow
  • You want the flexibility to change target titles per company — different titles for enterprise vs. SMB accounts
  • You're already using Claude Code and want one-command account research
  • You want to run tasks in the background via Claude Cowork while focusing on other work

When to switch approaches

  • You need to process dozens of target accounts automatically on a schedule → use Code + Cron
  • Multiple team members need to trigger the workflow without CLI access → use n8n or Make
  • You want execution history and error monitoring in a visual dashboard → use n8n or Make

Common questions

Why not just use a script?

A script runs the same way every time. The Claude Code skill adapts to what you ask — different titles, different departments, read-only mode, single company or batch. The reference files ensure it calls the right APIs even when improvising, so you get flexibility without sacrificing reliability.

Does this use Claude API credits?

Yes. The agent reads skill files and generates code each time. Typical cost is $0.02-0.10 per invocation depending on the number of contacts processed. Apollo credits are separate — budget ~2 credits per decision maker found (1 search + 1 enrichment).

How many Apollo credits does a typical run use?

Each person found costs 2 credits (1 for search, 1 for enrichment). A company with 8 decision makers = 16 credits. Apollo's free plan includes 10,000 credits/year.

Cost

  • Claude API — $0.02-0.10 per invocation (the agent reads files and generates code)
  • Apollo — ~2 credits per decision maker. Free plan: 10,000 credits/year. Paid plans start at $49/mo.
  • HubSpot API — free with any plan that supports private apps
  • GitHub Actions (if scheduled) — free tier includes 2,000 minutes/month

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.