---
name: moltsets-rb2b-intent-triage
description: Use this skill when the user wants to process an entire RB2B export end-to-end - triaging each row, finding ICP contacts, enriching, and producing a single prioritised outreach list.
---

# RB2B Intent Triage

Full-export triage: classify every RB2B row, apply the right enrichment action, and produce one ranked outreach list.

## Triggers

"process my entire RB2B export", "triage my RB2B list", "turn my full RB2B export into an outreach list", "prioritise my website visitors and find contacts", or when a user uploads a full RB2B CSV and wants a complete end-to-end workflow.

For a single focused action (enrich visitors only, companies only, etc.) suggest the appropriate focused skill instead.

## CSV parsing

Read the uploaded or pasted CSV. For every row, extract:
- `ProfileType` - "Person" or "Company"
- `LinkedInUrl` - person or company LinkedIn
- `FirstName`, `LastName`, `Title`, `CompanyName`, `Website`
- `AllTimePageViews`, `RecentPageCount`, `NewProfile`, `Tags`
- `WorkEmail` - RB2B-supplied email for Person rows
- `Industry`, `EstimatedEmployeeCount` - for company fit filtering

Report:
```
Total rows: X  (X Person, X Company)
New profiles (first visit): X
Tagged rows: X  (e.g. "Hot page": X)
```

## 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 - Define ICP + company fit

Need before triaging:

**1. ICP visitor definition** (which RB2B visitor rows count as a direct target):
> "Which visitor titles/roles are your ICP? For example: 'VP of Marketing', 'Head of RevOps, Director+'.
> I'll use this to classify which visitor rows in your export to enrich directly vs. which companies to prospect into."

**2. Company fit** (optional filter for which companies to pursue at all):
> "Any company filters? For example: industry (SaaS, eCommerce), size (50–500 employees), revenue ($5M+).
> Rows where the company doesn't fit will be skipped entirely."

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

## Step 1 - Triage rows

Classify each row into one of three buckets:

**Bucket A - ICP Person (enrich directly)**
- `ProfileType = Person` AND `Title` matches ICP definition AND company fits (if filter set)

**Bucket B - Non-ICP Person, company fits (find ICP contacts)**
- `ProfileType = Person` AND `Title` does NOT match ICP AND company fits
- Extract company, deduplicate

**Bucket C - Company row (find ICP contacts)**
- `ProfileType = Company` AND company fits (if filter set)

**Skip - company doesn't fit:**
- Any row where `Industry`, `EstimatedEmployeeCount`, or `EstimateRevenue` fails the company fit filter

Report triage results:
```
Bucket A (direct enrich):        X Person rows
Bucket B (company pivot):        X non-ICP visitors to X unique companies
Bucket C (company rows):         X company rows
Merged B+C companies:            X unique companies
Skipped (company fit miss):      X rows
```

## Step 2 - Prioritise by intent

Before processing, rank everything by intent signals:

For Bucket A (persons): sort by `AllTimePageViews` + weight `NewProfile = true` and any `Tags` entries.

For Buckets B+C (companies): aggregate page views per company across all visitors from that company. Sort by aggregated `AllTimePageViews` desc. Flag `NewProfile = true` companies.

## Step 3 - Credit check

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

Estimate:
```
Bucket A: X people × enrich cost per tool       = ~XXX cr
Buckets B+C: X companies × ~3 contacts × enrich = ~XXX cr
Total estimated:                                 = ~XXX cr
Current balance:                                 = XXX cr
```

If estimated cost > balance: stop. Report shortfall, ask whether to process top-N by intent or wait for top-up.

## Step 4 - Ask output preference

Ask how they want the results delivered (table, CSV, Google Sheets, etc.) before processing. If file/integration: get filename/destination now.

## Step 5 - Process Bucket A (ICP visitors - direct enrich)

Collect LinkedIn URLs from Bucket A rows.

Run in parallel:
- `MoltSets:linkedin_to_business_email` - SINGLE `linkedin_url`; fire one call per URL in parallel
- `MoltSets:linkedin_to_personal_email` - SINGLE `linkedin_url`; one call per URL in parallel (if user wants personal too)
- `MoltSets:linkedin_to_mobile_phone` with `linkedin_urls: [array]` (the only batch-array endpoint; if requested)

Preserve `WorkEmail` from original CSV as `RB2B Email` column.

## Step 6 - Process Buckets B+C (company-level - find contacts)

Merge unique companies from Bucket B and Bucket C. Process in intent order.

For each company:
1. `search_people` scoped by `company_domain` (the company's `Website`, preferred) or `company`/`query` (company name), optionally narrowed to your ICP role with `seniority`, `department`/`functional_area`, `country` (use enum values), limit target per company (default 3). Filtering to the ICP role here means fewer non-target results to enrich. Title also returns as a response field for ranking.
2. Tag each result with source (B or C) and total company `AllTimePageViews`

Deduplicate LinkedIn URLs across all results.

Business email is often already on the search records - check first and only enrich the gaps. Credit confirm before enrichment: "Found X ICP contacts. Enriching = ~XXX credits. Proceed?"

Run enrichment in parallel:
- `MoltSets:linkedin_to_business_email` - SINGLE `linkedin_url`; one call per URL in parallel (the email gaps)
- `MoltSets:linkedin_to_mobile_phone` with `linkedin_urls: [array]` (the only batch-array endpoint; if requested)

Fire the per-URL email calls in parallel; chunk the phone array at 100 if needed.

## Step 7 - Merge + rank output

Combine all results into one list. Sort by:
1. Intent score (higher `AllTimePageViews` = higher rank)
2. `NewProfile = true` rows boosted
3. Tagged rows (e.g. "Hot page") boosted

**Output format:**

| Priority | Name | Company | Signal | Source | Title | Business Email | Risk | Mobile |
|---|---|---|---|---|---|---|---|---|

- `Priority` = rank (1 = highest intent)
- `Signal` = `AllTimePageViews` value or aggregated views for company
- `Source` = "ICP visitor" / "Non-ICP visitor's company" / "Company row"
- `Risk` = the A-F deliverability grade on `Business Email`. High intent does not override a bad grade - a grade D row stays out of the send regardless of how many page views it has.

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:**
```
Rows processed:         X  (X Person, X Company)
ICP visitors enriched:  X
Non-ICP companies:      X  to X contacts found
Company rows:           X  to X contacts found
Skipped:                X  (company fit miss)
Total outreach list:    X contacts
Business emails:        X/N (XX%)
Mobile numbers:         X/N (XX%)  - if enriched
Credits used:           ~XXX

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

## Edge cases

- Visitor `Title` is empty to treat as non-ICP (Bucket B); note in output
- Same company appears in Bucket B and Bucket C to merge, don't double-process
- ICP visitor already in company search results to deduplicate; keep the directly-enriched record
- Credits run out mid-processing to stop, output all completed rows, report exact resume point
- Large exports (100+ rows) to set expectation it will take a while, report progress after each bucket
