linkedin-contact-reacher

star 3

Systematically discover LinkedIn contacts for job referrals or networking using BFS/DFS traversal. Extracts LinkedIn URLs, generates company email candidates, optionally sends connection requests, and saves output locally as JSON + CSV.

yennanliu By yennanliu schedule Updated 5/2/2026

name: linkedin-contact-reacher description: Systematically discover LinkedIn contacts for job referrals or networking using BFS/DFS traversal. Extracts LinkedIn URLs, generates company email candidates, optionally sends connection requests, and saves output locally as JSON + CSV.

LinkedIn Contact Reacher Skill

Discover LinkedIn contacts systematically (BFS or DFS), enrich them with email candidates, optionally send personalized connection requests, and save everything locally.

Specialized Agents

Agent File When to Use
Contact Discovery Agent skills/agents/contact-discovery-agent/SKILL.md BFS/DFS strategy, seed selection, traversal tuning
Outreach Agent skills/agents/outreach-agent/SKILL.md Message templates, connection request best practices, rate limits
Email Generator Agent skills/agents/email-generator-agent/SKILL.md Email pattern logic, domain inference, validation
QA Agent skills/agents/qa-agent/SKILL.md Pre-flight checks, session validation, output quality
Automation Agent skills/agents/automation-agent/SKILL.md Timing, retry logic, rate limiting

Script Files

File Purpose
discoverContacts.js BFS/DFS traversal — finds contacts from seeds
extractContactInfo.js Visits profiles, generates email candidates
reachContacts.js Sends connection requests with personalized notes
saveOutput.js Saves results to JSON + CSV locally

Prerequisites

  • Logged into LinkedIn
  • Playwright MCP tools available
  • Output directory writable (default: ./output/)

Orchestrated Run Order

1. QA Agent          → preFlightCheck(page)
2. Discovery Agent   → discoverContacts()      ← BFS or DFS
3. Extract Agent     → extractContactInfo()    ← enrich + email candidates
4. [Optional] Reach  → reachContacts()         ← send connection requests
5. Save              → saveOutput()            ← JSON + CSV locally
6. QA Agent          → generateReport()

Quick Start

Scenario 1: Find referral contacts at a target company

// ── Step 1: Discover contacts via BFS ─────────────────────────────────
// Paste discoverContacts.js, then:

const contacts = await discoverContacts(page, {
  seeds: [
    { type: 'search', company: 'Google', role: 'Engineering Manager' },
    { type: 'search', company: 'Google', role: 'Software Engineer' }
  ],
  strategy: 'bfs',          // breadth-first: broad coverage of the company
  maxContacts: 30,
  maxDepth: 1,              // depth=1: only direct search results, no expansion
  targetCompanies: ['Google'],
  targetRoles: ['engineer', 'manager', 'tech lead'],
  connectionDegree: ['1st', '2nd', '3rd'],
});

// ── Step 2: Enrich with email candidates ──────────────────────────────
// Paste extractContactInfo.js, then:

const enriched = await extractContactInfo(page, contacts, {
  companyDomains: { 'Google': 'google.com' },
  guessPersonalEmail: false,   // set true to also get gmail/outlook guesses
});

// ── Step 3: Save output ───────────────────────────────────────────────
// Paste saveOutput.js, then:

await saveOutput(enriched, {
  format: 'both',
  outputDir: './output',
  label: 'google-referrals'
});
// Output: ./output/google-referrals_2026-04-27T....{json,csv}

// ── Step 4: (Optional) Send connection requests ───────────────────────
// Paste reachContacts.js, then:

const { results, stats } = await reachContacts(page, enriched, {
  purpose: 'referral',
  userProfile: {
    name: 'Your Name',
    role: 'Software Engineer',
    targetCompany: 'Google'
  },
  maxPerSession: 10,      // stay under LinkedIn's limit
});

// Save final results with reach status
await saveOutput(results, { label: 'google-referrals-reached', format: 'both' });

Scenario 2: Networking DFS — deep dive from one person's profile

const contacts = await discoverContacts(page, {
  seeds: [{ type: 'profile', url: 'https://www.linkedin.com/in/someengineer/' }],
  strategy: 'dfs',          // depth-first: explore one thread deeply
  maxContacts: 20,
  maxDepth: 2,
  targetCompanies: ['Stripe', 'Shopify', 'Airbnb'],
  connectionDegree: ['2nd', '3rd'],
});

const enriched = await extractContactInfo(page, contacts, {
  companyDomains: {
    'Stripe':   'stripe.com',
    'Shopify':  'shopify.com',
    'Airbnb':   'airbnb.com',
  },
  guessPersonalEmail: false,
});

await saveOutput(enriched, { label: 'networking-dfs', format: 'both' });

Scenario 3: Multiple companies, networking only (no outreach)

const allContacts = [];

for (const target of [
  { company: 'Stripe', role: 'backend engineer' },
  { company: 'Shopify', role: 'software engineer' },
]) {
  const batch = await discoverContacts(page, {
    seeds: [{ type: 'search', ...target }],
    strategy: 'bfs',
    maxContacts: 15,
    maxDepth: 1,
    targetCompanies: [target.company],
  });
  allContacts.push(...batch);
  await page.waitForTimeout(5000); // pause between companies
}

const enriched = await extractContactInfo(page, allContacts, {
  companyDomains: { 'Stripe': 'stripe.com', 'Shopify': 'shopify.com' }
});

await saveOutput(enriched, { label: 'multi-company', format: 'both' });

Configuration Reference

discoverContacts(page, options)

Option Default Description
seeds [] Array of seed objects: { type: 'search', company, role, keywords } or { type: 'profile', url }
strategy 'bfs' 'bfs' (queue, broad) or 'dfs' (stack, deep)
maxContacts 30 Maximum unique contacts to collect
maxDepth 2 Max traversal depth from seeds. 1 = search results only
targetCompanies [] Filter: only keep contacts at these companies (empty = all)
targetRoles [] Filter: only keep contacts whose title contains one of these strings
connectionDegree ['1st','2nd','3rd'] Which LinkedIn connection degrees to include
delayMin 3000 Min delay between navigations (ms)
delayMax 6000 Max delay (ms)

extractContactInfo(page, contacts, options)

Option Default Description
companyDomains {} Map of { 'Company Name': 'domain.com' } for email generation
guessPersonalEmail false Also generate gmail/outlook/hotmail variants
delayMin 2500 Min delay between profile visits (ms)
delayMax 5000 Max delay (ms)

reachContacts(page, contacts, options)

Option Default Description
purpose 'networking' 'referral' | 'networking' | 'custom'
customMessage '' Used when purpose='custom' (max 300 chars)
userProfile {} { name, role, targetCompany } — used in message templates
maxPerSession 15 Max connection requests per session (LinkedIn limit: ~100/week)
onlyUnreached true Skip contacts already reached
delayMin 4000 Min delay between requests (ms)
delayMax 8000 Max delay (ms)

saveOutput(contacts, options)

Option Default Description
format 'both' 'json' | 'csv' | 'both'
outputDir './output' Output directory (Node.js) or download (browser)
label 'contacts' Filename prefix. Result: label_YYYY-MM-DDTHH-MM-SS.{json,csv}

Output Schema

Each contact in the output has:

{
  "name": "Jane Smith",
  "firstName": "Jane",
  "lastName": "Smith",
  "title": "Engineering Manager",
  "company": "Google",
  "location": "San Francisco, CA",
  "linkedinUrl": "https://www.linkedin.com/in/janesmith/",
  "connectionDegree": "2nd",
  "depth": 0,
  "companyDomain": "google.com",
  "emailCandidates": [
    "jane.smith@google.com",
    "jsmith@google.com",
    "janesmith@google.com",
    "janes@google.com"
  ],
  "personalEmailCandidates": [],
  "publicEmail": null,
  "websites": [],
  "messageSent": false,
  "reachStatus": "",
  "messageText": "",
  "reachedAt": null,
  "via": "seed:search"
}

BFS vs DFS — When to Use Which

BFS DFS
Goal Cover many people at a company broadly Explore one person's extended network deeply
Best for Referral hunting at a specific company Networking via a trusted contact's connections
Depth setting maxDepth: 1 or 2 maxDepth: 2 or 3
Result shape Wide and shallow — many contacts from search Narrow and deep — contacts 2–3 hops away
Risk May include less-relevant matches May drift far from target company

Email Candidate Patterns

Generated email patterns (in priority order):

Pattern Example
firstname.lastname@domain jane.smith@google.com
flastname@domain jsmith@google.com
firstnamelastname@domain janesmith@google.com
firstnamei@domain janes@google.com
firstname@domain jane@google.com
lastname.firstname@domain smith.jane@google.com
firstname_lastname@domain jane_smith@google.com
f.lastname@domain j.smith@google.com
lastname@domain smith@google.com
firstname-lastname@domain jane-smith@google.com

Verify candidates with tools like Hunter.io or email verification APIs before sending.


Safety & Rate Limits

Action Recommended Limit
Connection requests per day ≤ 20
Connection requests per week ≤ 80 (free) / 150 (Premium)
Profile visits per session ≤ 50
Session duration < 45 minutes
Break between sessions 2+ hours

LinkedIn may restrict accounts that send too many requests. Start conservatively.


Legal & Ethical Usage

  • Only contact people you have a genuine professional reason to reach
  • Personalize messages — do not spam
  • Respect LinkedIn's Terms of Service
  • Do not use harvested emails for bulk cold outreach without consent
  • Email candidates are guesses only — verify before use
Install via CLI
npx skills add https://github.com/yennanliu/linkedin-skill --skill linkedin-contact-reacher
Repository Details
star Stars 3
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator