| name | linkedin-sales-outreach |
| description | Turn LinkedIn into an AI-powered sales channel. Discover prospects via search, warm them up with authentic engagement, send personalized connection requests, and run post-connection DM sequences. Complete LinkedIn sales funnel from discovery to meeting booked. |
LinkedIn Sales Outreach
The complete LinkedIn sales funnel: prospect discovery, warm-up engagement, personalized connection requests, and post-connection DM sequences. Unlike email-only tools (Reply.io, Outreach), this skill turns LinkedIn into a proper sales channel.
When to Use
- "Find 20 potential customers for [product] on LinkedIn"
- "Send connection requests to these 10 people with personalized notes"
- "Check if any pending connections accepted and DM them"
- "Warm up these 5 prospects by engaging with their posts"
- "Research this list of LinkedIn profiles and draft outreach"
- "Run my LinkedIn outreach for [campaign name]"
Required Context (ask if not provided)
- ICP criteria: Target role, industry, company size, location
- Product/service: What are you selling? Key value prop in one sentence
- Campaign type: New prospecting, follow-up on accepted connections, or warm-up engagement
- Tone: Casual-professional (default), formal, or direct
- Volume: How many prospects this session (default: 10, max: 20)
Prerequisites
- User must be logged into LinkedIn in the browser
- Google Sheets access for tracking (optional but strongly recommended)
Connect Sheets (for tracking)
await requestApproval({
reason: "Need Sheets access to track LinkedIn outreach pipeline",
type: "service_auth",
payload: { provider: "google", service: "drive" }
})
Safety Rules (CRITICAL - LinkedIn is VERY strict about automation)
- Maximum 3 LinkedIn searches per session.
- Maximum 10 profile visits per session (with 8-15 second randomized delays).
- Maximum 10 connection requests per day (LinkedIn's safe limit).
- Maximum 5 DMs per session.
- Maximum 3 post comments per session.
- Minimum 90 seconds between ANY write action (connection request, DM, comment).
- Minimum 5 seconds between page navigations.
- ALL write actions require explicit user approval (connection requests, DMs, comments).
- If ANY CAPTCHA, verification, or unusual page appears: STOP IMMEDIATELY.
- Never mention user's product in warm-up comments (must be authentic).
- Never send identical messages to multiple people.
- Track all actions in Google Sheets for audit trail.
Workflow A: Prospect Discovery
Step 1: Build Search Query
var searchParams = {
keywords: targetRole,
location: targetLocation,
industry: targetIndustry,
companySize: targetSize,
connectionDegree: ["S", "O"]
}
var searchUrl = "https://www.linkedin.com/search/results/people/?keywords=" +
encodeURIComponent(searchParams.keywords)
if (searchParams.location) searchUrl += "&geoUrn=" + encodeURIComponent(searchParams.location)
var page = await browser.newtab(searchUrl)
await page.wait({ waitTime: 3 + Math.floor(Math.random() * 3) })
Step 2: Parse Search Results
var prospects = []
var maxPages = 3
for (var pageNum = 1; pageNum <= maxPages; pageNum++) {
var snap = await page.snapshot()
if (pageNum < maxPages) {
await page.wait({ waitTime: 5 + Math.floor(Math.random() * 5) })
}
}
console.log("Found " + prospects.length + " matching prospects")
Step 3: Profile Enrichment (Top Matches)
Visit top 10 profiles for deeper intel:
for (var i = 0; i < Math.min(prospects.length, 10); i++) {
var prospect = prospects[i]
await page.goto({ url: prospect.profileUrl })
await page.wait({ waitTime: 8 + Math.floor(Math.random() * 7) })
var snap = await page.snapshot()
prospect.enriched = {
fullTitle: "",
tenure: "",
about: "",
previousRoles: [],
education: "",
recentPosts: [],
isActive: false,
mutualConnections: [],
skills: []
}
console.log("Enriched " + (i + 1) + "/10: " + prospect.name +
" | " + prospect.enriched.fullTitle +
" | Active: " + prospect.enriched.isActive)
}
Step 4: Build Prospect Sheet
var sheet = await google.sheets.createSpreadsheet({
title: "LinkedIn Prospects - " + campaignName + " - " + new Date().toISOString().slice(0, 10),
sheets: ["Prospects", "Outreach Tracking", "Warm-Up Log"]
})
var headers = [[
"Name", "Title", "Company", "LinkedIn URL", "Connection Degree",
"Mutual Connections", "Active on LinkedIn", "Recent Post Topic",
"Best Outreach Hook", "Warm-Up Status", "Connection Status", "DM Status", "Notes"
]]
var rows = prospects.map(p => [
p.name,
p.enriched?.fullTitle || p.headline,
p.company,
p.profileUrl,
p.connectionDegree,
(p.enriched?.mutualConnections || []).join(", "),
p.enriched?.isActive ? "Yes" : "No",
p.enriched?.recentPosts?.length > 0 ? p.enriched.recentPosts[0] : "",
"",
"Not Started",
"Not Sent",
"Not Sent",
""
])
await google.sheets.updateValues({
spreadsheetId: sheet.id,
range: "'Prospects'!A1",
values: headers.concat(rows)
})
console.log("Prospect sheet: https://docs.google.com/spreadsheets/d/" + sheet.id)
Workflow B: Warm-Up Engagement (Pre-Outreach)
Why: Commenting on someone's post before sending a connection request increases acceptance rate from ~30% to ~60%+. This is Account-Based Marketing (ABM) applied to LinkedIn.
Timeline: 3-5 days of warm-up before connection request.
Step 1: Monitor Prospect Posts
for (var i = 0; i < warmUpQueue.length; i++) {
var prospect = warmUpQueue[i]
await page.goto({ url: prospect.profileUrl + "/recent-activity/all/" })
await page.wait({ waitTime: 5 + Math.floor(Math.random() * 3) })
var snap = await page.snapshot()
prospect.warmUpOpportunity = {
hasRecentPost: false,
postTopic: "",
postUrl: "",
suggestedComment: ""
}
}
Step 2: Draft Authentic Comments
For each prospect with a recent post, draft a 1-2 sentence comment:
Step 3: Engage (Like + Comment)
for (var prospect of warmUpQueue.filter(p => p.warmUpOpportunity.hasRecentPost)) {
console.log("Prospect: " + prospect.name)
console.log("Post topic: " + prospect.warmUpOpportunity.postTopic)
console.log("Suggested comment: " + prospect.warmUpOpportunity.suggestedComment)
console.log("---")
}
await requestApproval({
reason: "Like and comment on " + approvedCount + " LinkedIn posts for warm-up engagement"
})
for (var prospect of approved) {
await page.goto({ url: prospect.warmUpOpportunity.postUrl })
await page.wait({ waitTime: 3 })
var snap = await page.snapshot()
console.log("Engaged with " + prospect.name + "'s post")
await page.wait({ waitTime: 120 + Math.floor(Math.random() * 60) })
}
Workflow C: Connection Request Campaign
Step 1: Generate Personalized Connection Notes
var templates = {
mutual: function(prospect) {
return "Hi " + prospect.firstName + ", I noticed we both know " +
prospect.enriched.mutualConnections[0] + ". I work in " + userIndustry +
" and your work at " + prospect.company + " caught my eye. Would love to connect!"
},
engaged: function(prospect) {
return "Hi " + prospect.firstName + ", really enjoyed your recent post about " +
prospect.warmUpOpportunity.postTopic + ". I'm in a similar space and would " +
"love to stay connected on this."
},
shared: function(prospect) {
return "Hi " + prospect.firstName + ", fellow " + sharedContext + " here! " +
"Working on " + relevantTopic + " and thought we'd have a lot to discuss."
},
direct: function(prospect) {
return "Hi " + prospect.firstName + ", I've been following " + prospect.company +
"'s growth in " + prospect.industry + ". Working on something that might be " +
"relevant to " + prospect.enriched.fullTitle.split(" at ")[0] + "s. Happy to " +
"share insights!"
}
}
for (var prospect of prospects) {
var note = ""
if (prospect.warmUpOpportunity?.engaged) {
note = templates.engaged(prospect)
} else if (prospect.enriched?.mutualConnections?.length > 0) {
note = templates.mutual(prospect)
} else {
note = templates.direct(prospect)
}
if (note.length > 300) {
note = note.substring(0, 297) + "..."
}
prospect.connectionNote = note
console.log(prospect.name + " (" + note.length + "/300): " + note)
}
Step 2: Send Connection Requests
console.log("Connection requests ready: " + prospects.length)
for (var p of prospects) {
console.log(" " + p.name + " @ " + p.company)
console.log(" Note: " + p.connectionNote)
console.log(" ---")
}
await requestApproval({
reason: "Send " + approvedProspects.length + " LinkedIn connection requests"
})
for (var i = 0; i < approvedProspects.length; i++) {
var prospect = approvedProspects[i]
await page.goto({ url: prospect.profileUrl })
await page.wait({ waitTime: 3 + Math.floor(Math.random() * 3) })
var snap = await page.snapshot()
console.log("Sent " + (i + 1) + "/" + approvedProspects.length + ": " + prospect.name)
if (i < approvedProspects.length - 1) {
var delay = 90 + Math.floor(Math.random() * 90)
console.log("Waiting " + Math.round(delay / 60) + " min before next request...")
await page.wait({ waitTime: delay })
}
}
Step 3: Track Acceptance
await page.goto({
url: "https://www.linkedin.com/mynetwork/invitation-manager/sent/"
})
await page.wait({ waitTime: 3 })
await page.goto({
url: "https://www.linkedin.com/search/results/people/?network=%5B%22F%22%5D&origin=FACETED_SEARCH&sortBy=%22recently_connected%22"
})
Workflow D: Post-Connection DM Sequence
After a prospect accepts your connection request, run a warm DM sequence:
Touch 1: Day 0 (Right After Acceptance)
var dm1Template = function(prospect) {
return "Hey " + prospect.firstName + "! Thanks for connecting. " +
"Really enjoyed your perspective on [" + prospect.warmUpOpportunity?.postTopic ||
prospect.enriched?.about?.substring(0, 50) + "]. " +
"I'm working on [brief one-liner about your product] - would love to hear " +
"how you're currently handling [relevant pain point] at " + prospect.company + ". " +
"No pitch, genuinely curious!"
}
Touch 2: Day 3 (ONLY if They Replied)
var dm2Template = function(prospect, theirReply) {
return "That's really interesting about [reference their reply]. " +
"We've seen similar challenges with teams at [similar company/role]. " +
"Would you be open to a 15-min chat? I have some ideas that might " +
"help with [specific thing they mentioned]. Happy to share regardless!"
}
Touch 3: Day 5 (ONLY if Touch 1 Got No Reply)
var dm3Template = function(prospect) {
return "Hey " + prospect.firstName + ", just wanted to share this " +
"[relevant resource/insight/article] I came across - thought it might " +
"be useful for [their situation at their company]. No worries if " +
"you're swamped!"
}
DM Sequence Rules (NON-NEGOTIABLE)
1. NEVER send Touch 2 if they didn't reply to Touch 1
2. Maximum 3 DMs per prospect total (lifetime, not per session)
3. If they reply positively -> offer to schedule a call
4. If they reply "not interested" or negatively -> thank them, STOP forever
5. ALL DMs require user approval before sending
6. Minimum 24 hours between DMs to the same person
7. Each DM must be personalized (no copy-paste across prospects)
8. Never pitch in Touch 1 (it's a conversation starter, not a sales email)
9. Track all DMs in Google Sheet with dates and responses
Send DMs with Rate Limiting
await requestApproval({
reason: "Send " + dmQueue.length + " LinkedIn DMs to accepted connections"
})
for (var i = 0; i < dmQueue.length; i++) {
var dm = dmQueue[i]
await page.goto({
url: "https://www.linkedin.com/messaging/compose/?recipients=" + dm.profileUrn
})
await page.wait({ waitTime: 3 })
var snap = await page.snapshot()
console.log("DM sent to " + dm.name + " (Touch " + dm.touchNumber + ")")
if (i < dmQueue.length - 1) {
var delay = 90 + Math.floor(Math.random() * 90)
console.log("Waiting " + Math.round(delay / 60) + " min before next DM...")
await page.wait({ waitTime: delay })
}
}
Tracking Sheet Structure
Sheet: Prospects
Name | Title | Company | LinkedIn URL | Connection Degree |
Warm-Up Status | Warm-Up Date | Connection Status | Connection Date |
DM1 Status | DM1 Date | DM2 Status | DM2 Date | DM3 Status | DM3 Date |
Meeting Booked? | Notes
Sheet: Outreach Tracking
Date | Prospect | Action Type | Details | Response | Next Action Due
Sheet: Warm-Up Log
Date | Prospect | Post Topic | Action (Like/Comment) | Comment Text | Their Response
Campaign Performance Metrics
Track and report on:
Connection acceptance rate: Target >50% (with warm-up)
DM response rate: Target >25%
Meeting book rate: Target >10% of connections
Average time to meeting: Target <14 days from first touch
Account safety: Target 100% (zero restrictions/bans)
Error Handling
- CAPTCHA appears: STOP ALL ACTIONS immediately. Report to user. Do not retry.
- "You've reached the weekly invitation limit": Stop connections, continue DMs only
- Profile not found / 404: Skip, note in tracking sheet, move to next
- "Message failed to send": Retry once. If still fails, save message text for manual send
- Connection button not available: Prospect may have restricted invitations. Note and skip.
- LinkedIn logged out: Stop workflow, alert user to re-login