Send personalized LinkedIn direct messages to a list of existing 1st-degree connections via browser automation. Use when the user wants to message LinkedIn connections with AI-personalized outreach — e.g. nurturing leads, following up after events, reconnecting with contacts, or announcing something. Takes a data file (CSV/TSV) or plain list with connection names and companies, asks for outreach context/goal, generates a tailored message per person, and sends each one via browser automation. Handles message compose flow, character limits, and incremental status tracking.
Sends personalized LinkedIn messages to existing 1st-degree connections. Each message has:
Ask the user for their data file or list. Must include (or be added):
If only a plain list is provided, offer to convert to TSV.
Before writing any messages, navigate to /in/me/ and read the sender's profile:
Store these facts. They are used to identify relationship hooks with each connection.
Ask the user:
"What's your pitch / product message? This will be the consistent part of every message. Describe it in 1–2 sentences."
Then draft a polished pitch section (2–4 sentences max, punchy and clear). Show it to the user and get explicit approval. Do not start sending until the pitch is confirmed.
Example prompt: "Here's the pitch I'll use for everyone — confirm or edit:
'I'm building an AI calling agent — you give it a phone number + context, and it handles the call end-to-end. Think customer follow-ups, research calls, vendor coordination — anything phone-based that eats into your day. Happy to show you a demo if this sounds useful.'"
profile="chrome"): extension attached to LinkedIn tab (badge ON) — recommended for flagged accountsprofile="openclaw"): openclaw-managed Chrome, LinkedIn logged inAsk the user for a Google Sheet ID/URL to log outreach results. If they don't have one, offer to set one up (create tab + write headers). Confirm gog is authenticated (gog auth list).
If the user skips this, fall back to local linkedin_dm_progress.json but remind them the follow-up skill needs the sheet.
Only proceed once:
Before writing a message, compare the connection's profile against the sender's profile to find the strongest hook. Use this hierarchy — pick the highest that applies:
| Priority | Hook | Example opener |
|---|---|---|
| 1 | Same company (current or past) | "You and I both spent time at CRED…" |
| 2 | Same college + overlapping years | "Fellow BITS Goa 2018 batch here…" |
| 3 | Same college (different years) | "BITS connect here — saw your journey from…" |
| 4 | Same industry/function | "Both been in fintech/product for a while…" |
| 5 | Mutual connection | "We're both connected to [Name]…" |
| 6 | Their work context (no personal hook) | "Seen what you've built at [Company]…" |
Combine the hook with a line about their current work to show you know what they do.
Send as two separate messages per person, back to back:
Message 1 — Personalized opener (unique per person)
[Relationship hook — 1 sentence]
[Acknowledgement of their work/role — 1 sentence]
Target: 100–180 chars. Feels like a genuine reach-out from someone who knows them.
Message 2 — Pitch (identical for everyone, confirmed upfront)
[Product description — 1–2 sentences]
[Relevant use case for their role — 1 sentence]
[Soft CTA — 1 sentence]
Target: 150–250 chars. Clear, punchy, no filler.
Why two messages?
Fallback: If sending two messages is technically difficult (e.g. bubble re-focusing issues), use Shift+Enter twice between the opener and pitch to create a paragraph break within a single message.
Do not:
Generate messages for the entire list first. Present them in a table:
| Name | Company | Relationship Hook Used | Message Preview |
|---|---|---|---|
| Shorya Saini | Razorpay | Same BITS batch | Hey Shorya, BITS Goa 2018 batch… |
Get user approval on the full batch before opening the browser. Allow edits per row.
/feed/ — mandatory, no exceptions, no skippinglinkedin.com/mynetwork/invite-connect/connections/ — type name in "Search by name"Not a Connection, skipgog sheets append with all fields (see CRM Tracking section)See references/browser-workflow.md for exact browser automation steps.
| Status | Meaning |
|---|---|
Sent | Message delivered this session |
Already Messaged | Recent conversation exists — skip |
Not a Connection | No Message button or not in connections search |
Profile Not Found | Could not identify the right person |
Skipped | User chose to skip |
Failed | Browser error — retry next session |
/feed/ before every single profile — non-negotiableAfter each message is sent, append a row to a Google Sheet. This sheet is the source of truth for all outreach — current session and future follow-up.
Ask the user for a Google Sheet ID or URL at the start of the session (or offer to create a new one). The sheet should have a tab named Outreach with these columns:
| Col | Field | Notes |
|---|---|---|
| A | Date Sent | ISO date, e.g. 2026-02-13 |
| B | Person Name | Full name |
| C | Role / Title | Their current headline from LinkedIn |
| D | Company | Current company |
| E | LinkedIn URL | Profile URL |
| F | Relationship Hook | What hook was used (e.g. "Same batch BITS Goa 2018", "Both at CRED 2022–23") |
| G | Opener Sent | Exact text of Message 1 |
| H | Pitch Sent | Exact text of Message 2 |
| I | Campaign | Short label for this batch (e.g. "AI Calling - Feb 2026") |
| J | Status | Always Sent when first logged — updated by follow-up skill |
| K | Notes | Anything notable (prior conversation, context, mutual connection used) |
| L | Last Updated | Timestamp of last status change |
Column I (Status) lifecycle — only Sent is written by this skill. The follow-up skill will update to:
Replied · Call Scheduled · Demo Done · Follow Up Sent · No Response · Closed Won · Closed Lost
After each message pair is sent, run:
gog sheets append <SHEET_ID> "Outreach!A:L" \
--values-json '[["<date>","<name>","<role>","<company>","<url>","<hook>","<opener>","<pitch>","<campaign>","Sent","<notes>","<timestamp>"]]' \
--insert INSERT_ROWS
If no sheet exists yet, tell the user:
"I'll need a Google Sheet to track outreach. Share an existing sheet ID/URL, or I can create one with the right columns."
To create a new sheet, use Drive (or ask user to create one and share the ID). Then write the header row:
gog sheets update <SHEET_ID> "Outreach!A1:L1" \
--values-json '[["Date Sent","Person Name","Role / Title","Company","LinkedIn URL","Relationship Hook","Opener Sent","Pitch Sent","Campaign","Status","Notes","Last Updated"]]' \
--input USER_ENTERED
If Google Sheets is not set up, fall back to a local linkedin_dm_progress.json:
{
"campaign": "AI Calling - Feb 2026",
"pitch": "confirmed pitch text",
"rows": [
{
"date": "2026-02-13",
"name": "Shorya Saini",
"role": "Senior Analytics Specialist",
"company": "Razorpay",
"url": "https://linkedin.com/in/shorya-saini",
"hook": "Same batch BITS Goa 2018",
"opener": "Hey Shorya...",
"pitch": "I'm building...",
"status": "Sent",
"notes": ""
}
]
}