audience-targeting — Claude Code skill: what it does, how to install
Filters HubSpot contacts by lifecycle stage, engagement, or job title and exports segments to JSONL. Useful when preparing audience batches for sales campaigns.
npx skills add HubSpot/agent-cli-skills --skill audience-targeting -a claude-codeWhat audience-targeting does
The skill instructs the agent on building contact segments using HubSpot CLI commands. It structures queries around hubspot objects search, chaining conditions with --filter flags and streaming raw records directly into JSONL files for downstream tooling.
The CRM search API enforces hard constraints: up to 5 --filter groups, at most 6 conditions per group, and an 18-condition ceiling overall. In addition, the ~ operator matches full tokens rather than arbitrary substrings. Complex string manipulation relies on subsequent jq filtering.
Firmographic fields like industry or numberofemployees belong to companies rather than contact records. Because of that architecture, querying people by employer attributes requires a cross-object workflow: find companies first, page through their associations, and then fetch contact objects in batch.
What it helps with
- extract unassigned lead contacts into a JSONL file
- target decision-makers using title token matching
- fetch contacts tied to specific company industries
- exclude opted-out addresses before scheduling email outreach
Example prompts for the agent
- Find all opted-in leads in the United States who opened an email recently, then save them to
segments/active_leads.jsonl. - Extract decision-maker contacts with director or VP in their job title into JSONL.
- Target contacts working at software companies that have at least 100 employees.
How to install audience-targeting
1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill audience-targeting -a claude-code2Manually: put the files in place
~/.claude/skills/audience-targeting/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/audience-targeting/
1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill audience-targeting -a codex2Manually: put the files in place
.agents/skills/audience-targeting/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill audience-targeting -a cursor2Manually: put the files in place
.cursor/skills/audience-targeting/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install HubSpot/agent-cli-skills audience-targeting2Manually: put the files in place
.github/skills/audience-targeting/SKILL.md1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill audience-targeting -a gemini-cli2Platform CLI
gemini skills install https://github.com/HubSpot/agent-cli-skills --path audience-targetingManually: put the files in place
.devin/skills/audience-targeting/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill audience-targeting -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about audience-targeting
How to install audience-targeting in Claude Code?
Run npx skills add HubSpot/agent-cli-skills --skill audience-targeting -a claude-code in your terminal.
What does the audience-targeting skill do?
It builds filtered contact lists in HubSpot using CRM properties and engagement data, exporting them to JSONL.
How does the skill handle company-level filters like industry?
Because firmographics live on company objects, the skill first searches target companies, iterates their contact associations, and pulls contact records via batched calls.
Source text
SKILL.md · Apache-2.0
---
name: audience-targeting
description: Build a targeted contact segment by filtering on lifecycle, engagement, jobtitle, geography, or firmographics — then export it as JSONL for a campaign or downstream tool.
triggers:
- "segment contacts"
- "target audience"
- "find prospects"
- "build audience"
- "contact segmentation"
- "contacts in industry"
- "decision makers"
- "engaged contacts"
---
## Foundation
Read `bulk-operations/SKILL.md` first — pagination, JSONL piping, destructive-op safety. Reshape recipes in `bulk-operations/resources/json-patterns.md`. Resource: `resources/contact-segmentation-filters.md` is the filter-expression cookbook (lifecycle, lead status, email engagement, activity, deals, owner).
## Filter syntax cheat sheet
Source of truth: `hubspot objects search --help`.
- One `--filter` flag = one AND group: `--filter "lifecyclestage=lead AND !hubspot_owner_id"`.
- Multiple `--filter` flags are OR'd. Use for enum-OR-enum.
- Operators: `=`, `!=`, `>`, `>=`, `<`, `<=`, `~` (CONTAINS_TOKEN — whole-word, NOT substring), `@=` (IN), `@!=` (NOT_IN).
- HAS_PROPERTY: bare `name` or `name?`. NOT_HAS_PROPERTY: `!name`. Dates: `YYYY-MM-DD`.
- **Limits (CRM search API):** max 5 `--filter` flags, max 6 conditions per flag, max 18 conditions total. Prefer `@=` / `@!=` for enum lists — `lifecyclestage@=lead,customer` is **one** condition, `--filter "lifecyclestage=lead" --filter "lifecyclestage=customer"` is two groups.
- **"None of these, or empty" needs two groups** — `@!=` skips records where the property is unset, so OR in a `!name` group and repeat the shared conditions in both:
`--filter "hubspot_owner_id=123 AND lifecyclestage@!=customer,evangelist" --filter "hubspot_owner_id=123 AND !lifecyclestage"`
`~` gotcha: `jobtitle~director` matches the token "director", not arbitrary substrings. No regex operator — search broadly, post-filter with `jq`.
## Properties this skill turns on
Full live list: `hubspot properties list --type contacts`. Enum options: `hubspot properties options-list --type contacts <name> | jq -r '.value'`. As a fallback (e.g. to see which values are actually in use), read live records: `hubspot objects list --type contacts --properties <name> --limit 100 --format json | jq -r '.data[].properties.<name> // empty' | sort -u`.
Core fields used here: `lifecyclestage`, `hubspot_owner_id` (bare/`!` for owned/unowned; `hubspot owners list` for IDs), `hs_email_optout` (`!=true` excludes opted-out), `hs_email_last_open_date` / `notes_last_contacted` (recency), `jobtitle` / `country` / `city` (string `=` or `~`), `num_associated_deals` (0 net-new, `>=1` has-pipeline).
Firmographics (`industry`, `numberofemployees`, `annualrevenue`) live on **companies** — see cross-object section.
## Common segments
```bash
# Recent leads (this quarter, not yet owned)
hubspot objects search --type contacts \
--filter "lifecyclestage=lead AND createdate>2026-01-01 AND !hubspot_owner_id" \
--properties email,firstname,lastname,createdate
# Decision-makers by jobtitle (OR across tokens)
hubspot objects search --type contacts \
--filter "jobtitle~director" --filter "jobtitle~vp" --filter "jobtitle~chief" \
--properties email,jobtitle,company
# Engaged but not yet MQL (opened recently, still lead, opted in)
hubspot objects search --type contacts \
--filter "lifecyclestage=lead AND hs_email_last_open_date>2026-04-01 AND hs_email_optout!=true" \
--properties email,firstname,hs_email_last_open_date
# Geographic — US contacts opted in
hubspot objects search --type contacts \
--filter "country=United States AND hs_email_optout!=true" \
--properties email,state,city
```
More patterns (lead status, deals, owners, combined AND/OR) in `resources/contact-segmentation-filters.md`.
## Cross-object: companies-in-industry → their contacts
`industry`/`numberofemployees`/`annualrevenue` live on the company. Build the company set, then traverse — never `xargs -I{} hubspot objects get` per company. `associations list` emits `{"id":"...","labels":[...],"associationTypes":[...]}` — `{id}` feeds directly into a single batched `objects get`.
```bash
# Step 1: target companies. Industry options are portal-specific — discover with:
# hubspot objects list --type companies --properties industry --limit 100 --format json \
# | jq -r '.data[].properties.industry // empty' | sort -u
hubspot objects search --type companies \
--filter "industry=SOFTWARE AND numberofemployees>=100" \
--properties name,industry,numberofemployees \
> target_companies.jsonl
# Step 2: gather association IDs (associations list has no batch --from), then ONE batched
# objects get for all contacts. Page each company with --limit/--after — a company with >100
# associated contacts truncates silently on a single call.
while read -r cid; do
after=""
while :; do
page=$(hubspot associations list --from "companies:$cid" --to contacts --limit 100 ${after:+--after "$after"} --format json)
echo "$page" | jq -c '.data[]?'
after=$(echo "$page" | jq -r '.meta.next // empty')
[ -z "$after" ] && break
done
done \
< <(jq -r '.id' target_companies.jsonl) \
| jq -c '{id}' | sort -u \
| hubspot objects get --type contacts --properties email,firstname,jobtitle,hs_email_optout \
> target_contacts.jsonl
# Optional: drop opted-out
jq -c 'select(.properties.hs_email_optout != "true")' target_contacts.jsonl > campaign_audience.jsonl
```
## Saving and reusing a segment
A segment is a JSONL file. Re-use for updates, exports, or re-fetches:
```bash
# Save
hubspot objects search --type contacts \
--filter "lifecyclestage=lead AND hs_email_optout!=true" \
--properties email,firstname,lastname,jobtitle \
> segments/opted_in_leads.jsonl
# Assign owner (dry-run first per bulk-operations/SKILL.md)
jq -c '{id, properties:{hubspot_owner_id:"12345"}}' segments/opted_in_leads.jsonl \
| hubspot objects update --type contacts --dry-run
# Re-fetch with different properties later
jq -c '{id}' segments/opted_in_leads.jsonl \
| hubspot objects get --type contacts --properties email,lifecyclestage,hs_lead_status
```
Destructive ops on a saved segment follow the dry-run → digest → confirm flow in `bulk-operations/SKILL.md`.
## Save as a HubSpot list or view
A JSONL segment is portable, but you can also persist an audience in HubSpot:
- **List** — `hubspot segments create` saves a list; `segments get` / `list` inspect it; `segments members-list` reads members; `segments members-add` / `members-remove` manage membership. Use `segments update-filters` to set a dynamic list's filter criteria.
- **View** — `hubspot views create --type <t> --name "..." --columns a,b [--filters-file filters.json] [--sort prop:asc]` saves a filter set as a reusable object view; `views list` / `get` / `update` / `replace-field` / `delete` manage it.
- **Size it first** — `hubspot objects count --type contacts --filter "..."` returns `{"object_type":"contacts","total":N}` without paging, so you can size an audience before saving or exporting it.
## Related read-only signals (context for targeting)
- `hubspot buyer-intent` — visiting-company intent signals (read-only). **Requires OAuth login (`hubspot auth login`); service keys are NOT supported.**
- `hubspot analytics traffic sources` — where sessions/contacts came from, for source-based segmentation.
- `hubspot marketing forms list` — form definitions, to segment by which form a contact converted on.
Run each command's `--help` for the full surface.
## Known limits
- `~` is token-match, not substring. No regex operator.
- Enum options: `hubspot properties options-list --type contacts <name>` (fallback: read live records via `objects list` + `jq`).
- `associations list` has no batch `--from`, and returns one page (default 100) per call. Loop to gather IDs and page each call with `--limit`/`--after` until `.meta.next` is null, then batch the downstream `objects get` — otherwise a company with >100 associated contacts truncates silently.
- For >100 results, use the pagination loop in `bulk-operations/SKILL.md`.
© HubSpot · Apache-2.0 · HubSpot/agent-cli-skills