---
name: moltsets-hubspot-campaign-segment-builder
description: Use this skill when the user wants to build a HubSpot campaign list from a plain-English targeting brief - matching existing HubSpot contacts first and topping up net-new people from MoltSets to hit a target list size.
---

# HubSpot Campaign Segment Builder

Build a campaign-ready HubSpot static list against a plain-English brief. Translate the brief into title, seniority, industry, and company-size filters, match existing HubSpot contacts first, and only pull net-new people from MoltSets to make up the shortfall. Existing contacts are never duplicated; the output is a single static list sized to the brief.

## Triggers

"Build a campaign list of VP+ Fintech contacts at 200-1000 employee companies", "I need 500 Director+ Marketing contacts at mid-market SaaS companies for a campaign", "build me a segment from this brief and top up from MoltSets". Use when the user has a targeting brief and a target list size and wants a HubSpot static list.

## Requirements

- HubSpot remote MCP (mcp.hubspot.com) and the MoltSets MCP connected in the same client
- Custom objects are not reachable through HubSpot's remote MCP - create standard contact records and standard lists

## Chain

```
Targeting brief (title / seniority / industry / size / target count)
  -> hubspot/search-crm-objects        (existing contacts matching the brief)
  -> compare match count against target list size
  -> MoltSets:
       search_people                   (title/seniority/department + industry + employee_range, if shortfall)
  -> dedupe candidates against existing HubSpot contacts
  -> linkedin_to_best_email            (fill email gaps on net-new candidates)
  -> hubspot/create-object             (create net-new contacts)
  -> hubspot/create-list / add-to-list (build the static list from existing + net-new)
```

## Inputs

- **Targeting brief** - title/seniority, department, industry, employee range, and any other firmographic filter
- **Target list size** - how many total contacts the campaign needs
- **Existing-contact priority** - use existing HubSpot contacts first, always, or only above a certain match quality
- **List name** - the HubSpot static list the result should be saved to
- **Acceptable risk scores** - which A-F deliverability grades to keep (recommended: A, B, C and F; D excluded)

## Risk scores - confirm before enriching

Every email MoltSets returns carries a deliverability risk score - a grade from A to F. Ask which grades to keep before running anything:

> "MoltSets grades email deliverability risk from A (best) to F (no data). Which grades should I keep? I'd recommend **A, B, C and F** - and dropping **D**, since D is a confirmed bounce, complaint or spam trap."

| Grade | Meaning | Best practice |
| --- | --- | --- |
| A | Strongest engagement signal available | Safe to send immediately |
| B | Solid | Fine in regular sends; on a new or warming domain, send a smaller batch before scaling |
| C | Deliverability can't be confirmed | Segment separately from A/B, warm at low volume, watch engagement, suppress non-responders after 1-2 attempts |
| D | Confirmed bounce, complaint or spam trap | Never send - fastest way to trip spam traps and damage sender reputation for the whole list |
| F | No data - unknown risk, not "safe" | Re-verify before sending, or treat like C |

Use whatever set the user confirms. Treat the grade as a filter, not a guarantee - it lowers risk, it doesn't remove it. Pair it with the user's own sending domain reputation and list hygiene.

## `search_people` filter notes

- **Filters are free precision.** Execution cost comes from the free-text `query`, not the filters attached to it. A filters-only call (no `query`) runs **~40x cheaper**; a bare `query` is the least precise shape available and no cheaper than a filtered one. Never send `query` alone - at minimum attach `country` (~99% filled). Billing is unchanged either way: tokens are charged per record returned.
- **Full filter set:** `query`, `company`, `company_domain`, `country`, `state`, `city`, `seniority`, `department` *or* `functional_area` (same underlying data - never both), `industry`, `linkedin_industry`, `naics_code`, `employee_range`, `revenue_range`, plus `limit`/`offset`.
- **`employee_range` and `revenue_range` apply to the person's current employer**, so you can target by company size or revenue without a `search_companies` pass first. Prefer the numeric bands over legacy `"Small"`/`"Mid-Market"`/`"Enterprise"`/`"Unknown"`.
- **Three industry vocabularies:** `industry` (21 broad buckets), `linkedin_industry` (LinkedIn's ~150 niche labels), `naics_code` (any hierarchy level - "54" broad, "541120" surgical). Pick the one matching how specific the targeting actually is.
- **Fill rates:** `country` ~99%, `title` ~85%, `headline` ~65%, `seniority`/`industry`/`department` ~60%, `naics_code`/`linkedin_industry` ~50%. Every filter is exact-match and silently drops records with an empty field, so stacking three sparse ones can zero out a viable audience. Widen by dropping the sparsest filter first, not `country`.
- **Exact strings:** `"C Suite"` (space, not hyphen), `"Marketing & Advertising"`, `"Medical & Health"`, `"Texas"` not `"TX"`, `"United Kingdom"` not `"UK"`.
- **Size before you walk.** `search_linkedin_profile` with `count_only: true` returns a match count free of tokens; every search response carries `results.total`. Results are `_score`-ranked - the first page is the best page, 25 max per call.
- **Emails are already in the results** - records carry `business_email` and `business_email_risk_score`, so those rows need no enrichment call.

## Steps

1. Parse the brief into structured filters: seniority, department, industry (or `linkedin_industry`/`naics_code` for a niche), country/state, employee range, and revenue range. Aim to express the whole brief in filters - a filters-only `search_people` call needs no `query` at all, and runs ~40x cheaper than the same brief phrased as free text.
2. Search existing HubSpot contacts against those filters first with `hubspot/search-crm-objects`.
3. If the existing match count falls short of the target list size, run `search_people` in MoltSets with the same filters to source net-new candidates. `employee_range` and `revenue_range` apply to the person's current employer, so firmographic targeting needs no `search_companies` pass first. Read `results.total` before paginating to see whether the shortfall is even coverable.
4. Dedupe candidates against existing HubSpot contacts by email and domain plus name.
5. Run `linkedin_to_best_email` on any net-new candidate missing an email.
6. Create the net-new contacts in HubSpot and add both existing and net-new contacts to the named static list.

Before any write-back, every found address is checked against the accepted grade set. Out-of-range grades are reported in the table with their true score and skipped - never written to the CRM.

## Output

| Name | Company | Title | Seniority | Industry | Email | Risk | Source | Existing/Net-New | Added to List |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |

Show the risk score on every row that has an email, including grades outside the accepted set - flag those in `Added to List` (e.g. "no - grade D") rather than dropping them silently. Only emails inside the accepted set are written back to the CRM.

```
Target list size:            X
Existing matches used:       X
Net-new candidates sourced:  X
Net-new added to HubSpot:    X
Final list size:             X
MoltSets tokens used:        X

Email risk scores:
  A: X   B: X   C: X   D: X   F: X
  kept (accepted grades):  X
  filtered (excluded):     X
```

## Tips

- Prioritising existing HubSpot contacts first keeps token spend down and avoids working an account you already have a relationship with.
- If the brief is vague on seniority, run a small sample first and confirm the mapping before sourcing the full shortfall.
- A per-company cap keeps a single large account from dominating the list at the expense of coverage.
