Imported from ezjonline/ezj-automations (
skills/linkedin-response/SKILL.md). Install upstream withnpx skills add ezjonline/ezj-automations --skill linkedin-response. Copyright stays with the author.
Demo Library Skill — This skill is from a demo library. Some configuration values use placeholders (e.g.
{{USER_NAME}},{{COMMUNITY_ID}}). If something doesn't work, check for placeholder values and replace them with your own information first.
LinkedIn DM Response
Goal
Triage and respond to LinkedIn DMs with short, lowercase, human-like messages matching {{USER_NAME}}'s casual texting voice.
Model Recommendation
Use Sonnet for this skill. The task is procedural (classify → execute DOM action) and doesn't need Opus-level reasoning. Sonnet at ~$0.27/DM vs Opus at ~$1.30/DM.
CRITICAL SAFETY CONSTRAINTS
NEVER send a message without explicit user approval. Always:
- Show the proposed response (or batch table in dry-run)
- Wait for user confirmation
- Only then type and send
CRITICAL: No Double Messages
NEVER reply if {{USER_NAME}}'s message is the most recent one in the conversation. The cadence must always be them→me→them→me. Never me→me. If {{USER_NAME}} already replied last, skip the conversation entirely. This is the single most important rule — double-messaging looks automated and not human.
Voice & Style Rules
{{USER_NAME}}'s LinkedIn DM voice:
- Always lowercase (never capitalize, even sentence starts)
- Short — typically 2-8 words, occasionally a short sentence
- Casual/friendly — like texting a friend
- No punctuation overkill — one exclamation max, no periods on short replies
- No emojis unless the other person used them first (then mirror sparingly)
- Never formal — no "Thank you for your kind words" or "I appreciate your message"
- Never salesy — don't pitch {{COMMUNITY_NAME}} or link to anything
- Context-aware — generate responses intelligently based on what they actually said. Don't just pick from templates. If someone mentions something specific ("your course saved me 10 hours"), weave that in naturally ("10 hours is wild, glad it helped!")
Three Categories
Category 1: Gratitude / Happiness / Compliments
Trigger: Someone expresses thanks, satisfaction, gratitude, compliments {{USER_NAME}}'s work/content, says something nice, or shares how their content helped them.
Action: Reply with a short, lowercase, warm acknowledgment. Generate contextually — read their actual message and respond to what they said, not a generic template.
Tone calibration examples (for style reference only, don't copy verbatim):
- "np man"
- "my pleasure!"
- "glad to hear it"
- "means a lot, thanks"
- "really appreciate that man"
For longer/heartfelt messages, lean slightly longer but still casual:
- "really appreciate that man, means a lot"
- "that's so cool to hear, glad I could help"
Category 2: Confirmations ("it worked", "got it", "Yes", "Thanks" with no substance)
Trigger: Someone confirms a lead magnet link worked, says "got it", bare "Thanks", "Yes", or sends a simple acknowledgment like 👍. Purely transactional, no emotional content.
Action: React with 👍 (thumbs up emoji reaction on their message). Do NOT send a text reply.
Important distinction: If their confirmation ALSO includes genuine gratitude or a compliment (e.g., "It works! Thanks, huge fan of your work"), use Category 1 instead. Category 2 is only for bare confirmations with zero emotional content.
Category 3: Complex / Needs Manual Review
Trigger: Any of:
- Complicated questions that need real thought
- Long pitches or business proposals
- Requests for collaboration/partnerships
- Technical questions {{USER_NAME}} should answer personally
- Anything ambiguous or that doesn't clearly fit Cat 1 or Cat 2
- "It didn't work" / troubleshooting requests
- Messages in foreign languages where intent is unclear
Action: Mark the conversation as unread. {{USER_NAME}} will review these themselves.
Selection Logic
- Check if {{USER_NAME}}'s message is the most recent → if yes, SKIP (no double messaging)
- Read the other person's last message
- Classify into Category 1, 2, or 3
- If Cat 1: generate a contextual reply based on their actual message
- If Cat 2: react with 👍
- If Cat 3: mark as unread
- Present proposed action to user, then execute
Technical Notes (LinkedIn DOM)
Sending a message (Cat 1)
LinkedIn's contenteditable does NOT work with type_text or fill. You MUST use evaluate_script:
Step 1 — Type the message:
() => {
const editor = document.querySelector('div[contenteditable="true"].msg-form__contenteditable');
if (!editor) return 'no editor';
editor.focus();
editor.innerHTML = '<p>YOUR MESSAGE HERE</p>';
editor.dispatchEvent(new Event('input', { bubbles: true }));
return 'typed';
}
Step 2 — Click send:
() => {
const btn = document.querySelector('.msg-form__send-button');
if (!btn) return 'no send button';
btn.click();
return 'sent';
}
Common mistake: Do NOT use selector .msg-form__contenteditable [contenteditable="true"] (descendant). The class is ON the contenteditable element itself: div[contenteditable="true"].msg-form__contenteditable.
Reacting with 👍 (Cat 2)
JS mouseenter dispatch does NOT reveal the reaction toolbar. You MUST:
- Use MCP
hovertool on the message text UID (withincludeSnapshot: trueto save a round-trip) - In the returned snapshot, find
menuitem "React with thumbs up"UID - Click that UID
Make sure you're hovering on the OTHER person's message, not {{USER_NAME}}'s.
Mark as unread (Cat 3)
- Click the "Open the options list..." button on the conversation list item (not the thread header)
- In the dropdown, click "Mark as unread"
Finding the last sender
In the thread, each message group has an a[href*="/in/"] profile link with text like "View [Name]'s profile". Check whether the last message group belongs to {{USER_NAME}} or the other person.
Token Optimization
These are critical for keeping costs low:
-
Use
includeSnapshot: trueonclickandhovercalls whenever you need the page state afterward. This returns the snapshot in the same response — saves a separatetake_snapshotcall (1 round-trip + ~5K tokens per snapshot saved). -
Minimize snapshots for mechanical actions. Cat 2 (👍) needs exactly 2 calls: hover with includeSnapshot → click menuitem. Cat 3 (mark unread) needs 2 calls: click options with includeSnapshot → click "Mark as unread". Don't take extra snapshots.
-
Don't re-read the conversation list between actions unless you need to find the next conversation. After finishing one conversation's action, just click the next conversation in the list directly.
-
Skip already-processed conversations. If the list preview shows "You: ..." that means {{USER_NAME}} replied last — skip without clicking in.
Execution Flow
Process conversations ONE AT A TIME:
- Click into the conversation (use
includeSnapshot: true) - Verify {{USER_NAME}} is NOT the last sender
- Identify the last message from the other person
- Classify into category 1/2/3
- Execute the appropriate action (reply / react / mark unread)
- Move to next conversation
Dry Run Mode
When asked to "dry run", do NOT type, send, react, or mark anything. Just present a table:
| # | From | Their Message | Category | Proposed Action |
|---|---|---|---|---|
| 1 | Name | "quote..." | 1 | Reply: "np man" |
| 2 | Name | "got it" | 2 | React 👍 |
| 3 | Name | "long pitch..." | 3 | Mark unread |
| 4 | Name | ({{USER_NAME}} replied last) | SKIP | No action (would double-msg) |
Batch Mode
Process conversations ONE AT A TIME. For each:
- Click into the conversation
- Check last sender (skip if {{USER_NAME}})
- Read last message
- Classify, execute action immediately
- Move to next
Do NOT do two passes (scan all → compile table → execute all). That wastes tokens and degrades quality. Process each conversation fully before moving to the next. Present dry-run table only if explicitly asked for a dry run.
Setup
- Navigate to LinkedIn messaging:
https://www.linkedin.com/messaging/?filter=unread - Use the
chrome-stealthMCP server (notchrome-devtools) - Process all visible unread conversations, then click "Load more conversations" if present