Score RB2B Visitors by Intent
Turn a raw RB2B CSV export into a ranked table of visitors and companies scored 0.0-10.0 on buying intent, so you can see who's worth a follow-up at a glance.
example prompts
-
Score this RB2B export for intent -
Which of these visitors are worth following up with? -
Any companies in here with multiple people visiting? -
I've got a week of RB2B exports, combine them and tell me who's returning
about this skill
RB2B Intent Scoring
Turns a raw RB2B CSV export into a ranked table of visitors and companies, scored 0.0-10.0 on buying intent, then — if the user wants to go further — chains into MoltSets to turn that ranked list into real contact data: emails, phone numbers, and additional contacts at the companies involved. Scoring is always step one; enrichment is optional and only happens if the user asks for it.
Why a script, not eyeballing
Scoring 60+ rows by reading them is slow and inconsistent between runs. scripts/score_visitors.py computes the score deterministically from the CSV columns, so the same export always produces the same ranking, and it scales to exports with hundreds of rows.
When to use this
Trigger this skill whenever the user:
- Uploads an RB2B CSV export (recognizable by columns like
LinkedInUrl,CompanyName,AllTimePageViews,RecentPageCount,FirstSeenAt,LastSeenAt,NewProfile,ProfileType) - Asks to find "hot leads", "strong signals", "who to follow up with", or similar from that data
- Asks for an "intent score" on website visitors or companies
Before running: ask for a threshold
Always ask the user for a minimum intent score before running the script and presenting results, e.g.: "What's the minimum intent score you want results for? (e.g. 6.7)". Wait for their answer, then pass it via --min-score. Only skip asking if the user already gave a threshold in the same message (e.g. "show me everything 7+").
If the user says "show me everything" or otherwise declines a threshold, run without --min-score and return the full ranked list.
How to run it
python3 scripts/score_visitors.py /path/to/export.csv /path/to/output.csv --min-score 6.7
This writes a scored CSV sorted highest-intent first, containing only rows at or above the threshold. Read that CSV back and present it to the user as a table (in chat, or as a spreadsheet/artifact if they want a file — check the xlsx skill if they want a formatted Excel deliverable). The script prints how many rows scored above the threshold out of the total — mention that ("7 of 63 rows cleared 6.7") so the user has context even when the table itself is short.
Optional flags:
--config path/to/config.json— points at a config file for high-intent-page patterns and ICP criteria. If omitted, it looks forconfig/icp_and_pages.jsonnext to the script. See "ICP setup" below.--min-score 6.7— filters output to that score and above. Omit to get every scored row.
If the user hasn't configured icp.industries or icp.employee_ranges and their export has no FilterMatches data, the ICP signal contributes nothing (not a penalty, just neutral) — flag this to the user rather than silently under-scoring.
If the user has multiple daily exports and wants a rolled-up view (e.g. "who showed up more than once this week"), concatenate the CSVs first (keep only one header row), then run the script on the combined file. Because the company-clustering signal counts distinct visitor rows per company within the file you pass in, combining multiple days into one file is exactly how you'd surface a company that sent different people to the site on different days.
What the score is built from
Five signals, each scored 0-10 internally, then combined:
- Returning visitor (multi-day) —
AllTimePageViewsminusRecentPageCountgives page views accumulated before today's session. IfNewProfileistrue, this is forced to zero. More prior activity = stronger signal, log-scaled so it doesn't run away on outliers. Weight: 25%. - Session depth (multi-page) — how many pages
RecentPageCountshows for the current session. A single page view is the baseline (no extra credit); each additional page adds diminishing-return credit. Weight: 20%. - Account clustering (multi-visitor) — how many distinct rows in the file share the same
CompanyName(falling back toWebsiteif the company name is blank). More people from the same company in the same export window is a stronger buying-committee signal. Weight: 15%. - High-intent pages (what/how often) — parses
RecentPageUrlsand counts how many hits match a configurable list of high-intent URL patterns (pricing, demo, comparison pages, etc. — seeconfig/icp_and_pages.json), and adds extra weight if RB2B has already tagged the row"Hot page"in itsTagscolumn. Repeat visits to the same high-intent page count more than one hit. Weight: 20%. - ICP match — uses RB2B's own
FilterMatchescolumn when populated (that's RB2B's native saved-filter matching, the most authoritative signal available). WhenFilterMatchesis empty, falls back to a configurable check againstIndustryandEstimatedEmployeeCount— see "ICP setup" below. Weight: 20%.
These combine as a weighted average, then a combo bonus (up to +2.0) applies when two or more signals independently clear a "strong" threshold — a returning visitor who also hit the pricing page twice and matches your ICP is meaningfully hotter than any one of those signals alone suggests. That's the "combinations are stronger" behavior.
Final scores are capped at 10.0 and rounded to one decimal place.
ICP setup
The ICP signal only does something once it has data to check against:
- Best case: the user has RB2B "Saved Filters" (or equivalent) configured, which populate
FilterMatchesdirectly in the export. Nothing to configure — the script uses it automatically and gives it real credit (not just presence, but how many filters matched). - Fallback: if
FilterMatchesis empty, editconfig/icp_and_pages.jsonand fill inicp.industriesand/oricp.employee_rangeswith the exact values that appear in the user's own export (check a real file first — RB2B's bucket strings, like employee ranges, aren't perfectly standardized). Leave a list empty to skip that criterion. - If neither is populated, ICP contributes 0 to every row (neutral, not a penalty) — tell the user this explicitly rather than presenting scores as if ICP were factored in.
Output columns
IntentScore, Name, Title, CompanyName, Website, ProfileType, RecentPageCount, AllTimePageViews, HighIntentPageHits, HotPageTag, CompanyVisitorsToday, ICPMatch, Signals, Rationale, WorkEmail, LinkedInUrl, LastSeenAt, FirstSeenAt
Website is carried through from the original export specifically so the enrichment workflows below have a company domain to search against — don't drop it if customizing the output columns.
Signals is a short tag list (e.g. "Returning visitor, High-intent pages, Matches ICP config (2/2 criteria)"). Rationale is a full plain-English sentence or two built from the raw data — prior page view count and first-seen date, today's page count, named co-visitors from the same company if any, which high-intent pages were hit and how many times, and the ICP match detail. Use Rationale when the user wants to know why a specific row scored the way it did without re-deriving it themselves.
Presenting results
- Lead with the top 10-15 rows in a markdown table in chat — don't dump all 60+ rows unless asked.
- Call out anything with a company cluster (
CompanyVisitorsToday> 1) explicitly, since that's the rarest and often most actionable signal. - If the user wants the full scored file, offer the CSV as a download rather than pasting everything inline.
- Don't over-narrate the scoring formula unless the user asks — a one-line summary of why the top few rows scored highly is enough.
After scoring: offer enrichment
Once the scored table is presented, ask the user if they want to do anything further with it — don't just stop at the table and wait. Something like: "Want me to do anything with this — find contact info, or pull more contacts at these companies?" Use ask_user_input_v0 with up to four options so it's a tap rather than typing:
- "Enrich visitors (email + phone)"
- "Find alternate emails only"
- "Companies → ranked contacts"
- "Non-ICP visitor → ICP contact"
If the user declines, types something else, or just moves on, don't push further — the table stands on its own.
Each option is a full workflow documented in references/, written to consume the scored CSV you just produced (not the raw RB2B export):
| Option | Reference file | When it fits |
|---|---|---|
| Enrich visitors (email + phone) | references/enrich-visitors.md |
User wants richer contact info on the people who visited, beyond RB2B's own WorkEmail. |
| Alternate emails only | references/alternate-emails.md |
Same idea, narrower — just a second email address, no phone lookup. |
| Companies → ranked contacts | references/companies-to-contacts.md |
User wants contacts at the visiting companies generally — including Company rows with no person identified, or wants more than just the one person who happened to visit. |
| Non-ICP visitor → ICP contact | references/non-icp-to-icp.md |
The company fits (ICPMatch = Y) but the visitor's own title doesn't look like the real buyer — find the right person at that company instead. |
Read the relevant reference file in full before acting on it — each has its own steps, exact MoltSets tool names/params, and output format. These tools (MoltSets:linkedin_to_business_email, MoltSets:linkedin_to_personal_email_usa, MoltSets:linkedin_to_mobile_phone, MoltSets:search_people, etc.) are deferred — call tool_search first if they're not already loaded in the conversation.
Tuning
The weights, log-curve constants, and combo-bonus threshold are all named variables near the top of scripts/score_visitors.py (returning_score, session_score, company_score, high_intent_score, icp_match_detail, combo_bonus). If the user wants to weight one signal more heavily, or found the scores running too hot/cold after seeing real results, adjust those constants directly rather than re-deriving the approach from scratch. The high-intent URL patterns and ICP criteria live in config/icp_and_pages.json instead, since those are data the user should own and update, not code.
Tips
- Lead with the top 10-15 rows; call out any company cluster (
CompanyVisitorsToday> 1) explicitly — it's the rarest, most actionable signal. - Weights, log-curve constants, and the combo-bonus threshold are named variables near the top of
score_visitors.py; the high-intent URL patterns and ICP criteria live inconfig/icp_and_pages.json. - Offer the full scored file as a download rather than pasting 60+ rows inline.