---
name: moltsets-build-target-list
description: Use this skill when the user wants to build a complete, enriched prospect list from scratch - defining a company ICP and ending up with names, companies, LinkedIn profiles, and contact data ready for outreach.
---

# Build Target List

End-to-end: define company ICP to search to enrich to export ready-to-use contact list.

## Triggers

"build me a prospect list", "create a target list", "I need contacts at [type of company] with emails", "generate outbound list", "find and enrich prospects at [type of company]". Full end-to-end ICP-to-contact-list workflow.

## Chain

```
company + people ICP criteria
  to search_people  (find people at matching companies)
  to linkedin_to_business_email     (per-URL parallel calls - business emails)
  to linkedin_to_mobile_phone              (batch array - mobile, optional)
```

## Set Expectations

Before firing any calls, give the user a brief, dry heads-up. Deadpan over enthusiastic - no filler, no corporate speak.

Facts to work with:
- 2 phases: search (paginated at 25/call) then batch enrich
- 50 prospects: ~30–60 seconds total; 100+ prospects: ~60–120+ seconds
- If paginating: search phase = X pages × ~3–5 seconds, then enrichment on top
- Include the target count and which data types will be fetched

## 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.

## Step 0 - Input Quality Gate

Before proceeding, assess how much the user has provided. If no company attribute is specified, **pause and ask before searching**.

### Minimum viable input
`search_people` accepts both company and people-level filters. Gather at least one of:
| Field | Param | Example |
|---|---|---|
| Free-text (name / role keywords) | `query` | "Jane Smith", "VP Sales" |
| Company name | `company` | "Stripe", "HubSpot" |
| Exact domain | `company_domain` | "stripe.com" |
| Industry (broad) | `industry` | "Information Technology", "Finance and Banking" |
| Industry (niche) | `linkedin_industry` | "Staffing and Recruiting" |
| Industry (code) | `naics_code` | "54", "541120" |
| Seniority | `seniority` | "Director", "VP", "C Suite" |
| Department / functional area | `department` or `functional_area` | "Sales", "Marketing", "Engineering" |
| Country | `country` | "United States" |
| State / city | `state`, `city` | "Texas", "Portland" (pair city with state) |
| Employer size | `employee_range` | "21-50", "201-500", "5001+" |
| Employer revenue | `revenue_range` | "$10M - $20M", "Above $50M" |
| Target count | - | "50 contacts", "100 records" |

All of these are request params on `search_people` (use exact enum values), and `query` searches across name + company + title. **`employee_range` and `revenue_range` filter on the person's current employer**, so size and revenue targeting happen in the same call - no `search_companies` pass first.

### Vague input triggers
Trigger this gate if the user gives no company anchor, e.g.:
- "find me some prospects"
- "I need leads" (no company, industry, or size)

### Response when input is vague

Say something like:

> "To build a list I need something to scope on. Which of these can you fill in?
>
> - **Company** - name or domain, e.g. "Stripe", stripe.com
> - **Industry** - e.g. Information Technology, Finance and Banking
> - **Seniority** - e.g. Director, VP, C Suite
> - **Department** - e.g. Sales, Marketing, Engineering
> - **Country** - e.g. United States
> - **How many contacts** - e.g. 50, 100, 250"

Wait for user response before proceeding to Step 1.

### Tips to share if user asks what makes a good search
- **Industry + seniority + department** is a strong combo for role-based ICP targeting across many companies
- **Domain** (`company_domain`) is the most precise scope for a single company
- For enterprise vs. SMB targeting, put `employee_range`/`revenue_range` straight on `search_people` - they filter the person's current employer, so no separate `search_companies` step is needed
- **Filters cost nothing.** Execution cost comes from the free-text `query`; a filters-only search (no `query`) runs ~40x cheaper and is more precise. Never send a bare `query` - at minimum attach `country` (~99% fill rate)
- Filters are exact-match and drop records with an empty field. `country` ~99%, `title` ~85%, `headline` ~65%, `seniority`/`industry`/`department` ~60%, `naics_code`/`linkedin_industry` ~50% - stacking three sparse filters can zero out a viable list

## Step 1 - Define ICP

Gather from user (confirm or fill any still missing after Step 0):
- Company name / domain
- Industry
- Seniority / department (role-level ICP)
- Country
- Target count (how many records needed)

## Step 2 - Credit check (required)

Call `MoltSets:get_billing` and `MoltSets:get_usage` with `period: billing_cycle` in parallel (free).

Estimate:
```
N prospects × linkedin_to_business_email cost = email cost
N prospects × linkedin_to_mobile_phone cost    = phone cost (if requested)
Total:                             = ~XXX credits
Current balance:                   = XXX credits
```

If estimated cost > balance: stop. Report shortfall, how many records the balance covers, ask whether to proceed partially or top up.

## Step 3 - Search

`search_people` accepts company and people filters: `query` (free-text across name / company / title), `company` (name), `company_domain` (exact), `country`, `state`, `city`, `seniority`, `department` or `functional_area`, `industry`, `linkedin_industry`, `naics_code`, `employee_range`, `revenue_range`, plus `limit`/`offset`. Use the exact enum values (seniority: Intern, Entry, Senior, Manager, Director, VP, Head, C Suite, Owner, Partner; department/functional_area: Operations, Sales, Information Technology, Education, Engineering, Finance, Medical & Health, Marketing, Human Resources, Design, Consulting, Legal - use one, not both). Title also returns as a response field for ranking.

Build the search as **filters first**. If the ICP is fully expressible in filters (e.g. "VPs of sales at US software companies with 51-200 employees" → `seniority` + `department` + `industry` + `country` + `employee_range`), send no `query` at all - it runs ~40x cheaper and is more precise. Only add `query` for a person's name or a role phrase the enums can't express, and never send it bare.

Size it before you walk it: `search_linkedin_profile` with `count_only: true` returns the match count free of tokens, and every search response carries `results.total`. Read that before paginating to know whether the target count is even reachable.

Probe first: run `search_people` with `limit: 1` (using whatever filters apply). Check top result quality.
- Good match to scale to desired limit (max 25 per call)
- Poor match to adjust filters before scaling
- Thin results to drop the sparsest filter first (`naics_code`, `linkedin_industry`), shorten a NAICS code, or try the neighbouring `industry` value - don't drop `country`

For N > 25: paginate with `offset` (25, 50, 75...) until target count reached.

## Step 4 - Deduplicate

Remove duplicate LinkedIn URLs before enrichment.

## Step 5 - Bulk enrich

Business email is often already on the search records - check first and only enrich the gaps. For the LinkedIn URLs that still need data:
- `linkedin_to_business_email` takes a SINGLE `linkedin_url` - fire one call per URL, in parallel (there is no email batch array)
- `linkedin_to_mobile_phone` takes a `linkedin_urls` array (max 100) - one batch call (if phone requested)

Fire the per-URL email calls in parallel; chunk the phone array at 100 if needed. Timing is N parallel single email calls, not one batch.

## Step 6 - Output

| Name | Title | Company | Country | LinkedIn | Business Email | Risk | Mobile |
|---|---|---|---|---|---|---|---|

Show the risk score on every row that has an email, including grades outside the accepted set - flag those rather than dropping them silently, so the user can see what was filtered and why.

**Summary:**
```
Prospects found:   X
Business emails:   X/N (XX%)
Mobile numbers:    X/N (XX%)
Credits used:      ~XXX

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

Offer to format as a copyable CSV-style table for export.

## Edge cases

- Low email/phone hit rates are normal - credits only charged on hits
- `search_people` max 25/call - loop with `offset` for larger lists
- If filters produce < target count, suggest broadening one constraint at a time
- Stop immediately if credits run out mid-enrichment - output completed rows, report resume point
