
Pulls contact segments from HubSpot by filtering on lifecycle stage, engagement, job title, geography, or firmographics, then exports to JSONL for campaigns or downstream tools. The skill handles the filter syntax quirks (token matching with tilde, cross-object company-to-contact traversal for industry filters, batching to avoid per-record API hits) and follows the bulk operations safety patterns with dry-run digests. Saves segments as reusable JSONL files you can re-query or update later. One honest take: the cross-object company filtering recipe is the most useful part since firmographic data lives on a different object and the naive approach kills your rate limit.
npx -y skills add hubspot/agent-cli-skills --skill audience-targeting --agent claude-codeInstalls into .claude/skills of the current project.
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).
Source of truth: hubspot objects search --help.
--filter flag = one AND group: --filter "lifecyclestage=lead AND !hubspot_owner_id".--filter flags are OR'd. Use for enum-OR-enum.=, !=, >, >=, <, <=, ~ (CONTAINS_TOKEN — whole-word, NOT substring), @= (IN), @!= (NOT_IN).name or name?. NOT_HAS_PROPERTY: !name. Dates: YYYY-MM-DD.--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.@!= 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.
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.
# 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.
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.
# 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
A segment is a JSONL file. Re-use for updates, exports, or re-fetches:
# 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.
A JSONL segment is portable, but you can also persist an audience in HubSpot:
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.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.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.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.
~ is token-match, not substring. No regex operator.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.bulk-operations/SKILL.md.