Linkedin Outreach
Run a full LinkedIn outreach campaign via the LinkupAPI MCP — define an ICP (Ideal Customer Profile), find target companies, extract real decision makers (CEO/CTO/Founder), filter false positives, ...
Run a full LinkedIn outreach campaign via the LinkupAPI MCP — define an ICP (Ideal Customer Profile), find target companies, extract real decision makers (CEO/CTO/Founder), filter false positives, send connection requests, and set up automated follow-up via webhooks. Use when the user says "outreach campaign", "prospect on LinkedIn", "find decision makers", "send connection requests", "build a target list", "campaign LinkedIn", "lead generation", "ICP", "target CEO/CTO", or describes a B2B ou...
Install
Quick install
npx skills add https://github.com/claude-dev-code/claude-skills-linkedinnpx skills add claude-dev-code/claude-skills-linkedin --agent claude-codenpx skills add claude-dev-code/claude-skills-linkedin --agent cursornpx skills add claude-dev-code/claude-skills-linkedin --agent codexnpx skills add claude-dev-code/claude-skills-linkedin --agent opencodenpx skills add claude-dev-code/claude-skills-linkedin --agent github-copilotnpx skills add claude-dev-code/claude-skills-linkedin --agent windsurfMore install options
Shorthand — useful for multi-skill repos:
npx skills add claude-dev-code/claude-skills-linkedinManual — clone the repo and drop the folder into your agent's skills directory:
git clone https://github.com/claude-dev-code/claude-skills-linkedin.gitcp -r claude-skills-linkedin ~/.claude/skills/Linkedin Outreach
Run a full LinkedIn outreach campaign via the LinkupAPI MCP — define an ICP (Ideal Customer Profile), find target companies, extract real decision makers (CEO/CTO/Founder), filter false positives, send connection requests, and set up automated follow-up via webhooks. Use when the user says "outreach campaign", "prospect on LinkedIn", "find decision makers", "send connection requests", "build a target list", "campaign LinkedIn", "lead generation", "ICP", "target CEO/CTO", or describes a B2B ou...
---
name: linkedin-outreach
description: Run a full LinkedIn outreach campaign via the LinkupAPI MCP — define an ICP (Ideal Customer Profile), find target companies, extract real decision makers (CEO/CTO/Founder), filter false positives, send connection requests, and set up automated follow-up via webhooks. Use when the user says "outreach campaign", "prospect on LinkedIn", "find decision makers", "send connection requests", "build a target list", "campaign LinkedIn", "lead generation", "ICP", "target CEO/CTO", or describes a B2B outreach goal. Requires the linkupapi MCP server to be connected with at least one LinkedIn account (status connected).
---
LinkedIn Outreach Campaign — LinkupAPI V2
Use this skill to run cold-outreach campaigns on LinkedIn via the linkupapi MCP. It codifies the full prospecting → connect → follow-up playbook in 7 stages, with rate-limit awareness and false-positive filtering built in.
Required prerequisites — check before you start
- The user must have the
linkupapiMCP connected. Verify withlinkupapi_list_accounts.
- If the list is empty, tell the user they have two options to connect a LinkedIn account, then stop and wait:
- Hosted UI — open https://app.linkupapi.com/account-connection (fastest, handles checkpoints in-browser).
- MCP login — run
linkupapi_logindirectly (platform=linkedin, with email+password OR alogin_token). Oncheckpoint_required→ runlinkupapi_checkpoint.
- If a webhook for the chosen account(s) doesn't exist with the
accepted_invitationevent, propose to create one (linkupapi_create_webhook) so accepts trigger automated follow-up.
Daily LinkedIn safety caps (MANDATORY — enforced before Stage 1)
Per LinkedIn account, this skill respects:
- 20 connection requests / day (invites)
- 100 profile gets / day (
linkedin_profiles get) - 15 searches / day (
search_people/search_companies)
These are **shared budgets across ALL linkedin-* skills (linkedin-outreach, linkedin-high-intent, linkedin-feed-engage, linkedin-enrich). Hitting the cap from any one skill carries over.
Before Stage 1**, run linkupapi_get_logs filtered to the last 24h on the chosen account. Count today's:
linkedin_network/invitecalls → invites used todaylinkedin_profiles/getcalls → profile gets used todaylinkedin_profiles/search_*calls → searches used today
Compute remaining budget per category. Surface in the Stage 0 echo:
Daily caps remaining today (account {email}):
Invites: X / 20 (Y used today)
Profiles: X / 100 (Y used today)
Searches: X / 15 (Y used today)
Hard rules:
- If
Volume cap > remaining_invites→ trim Volume cap toremaining_invites(never override). - If
searches_needed > remaining_searches(Stage 1 + Stage 3 combined) → reduce Stage 1limitand number ofsearch_peoplecalls until it fits, or stop and ask the user to run tomorrow. - If
profile_gets_needed > remaining_profiles(Stage 4) → enrich the highest-priority candidates and write the rest to a "deferred" list in the campaign log.
If linkupapi_get_logs is unavailable or empty, fall back to scanning today's ./campaigns/.md and ./enrichments/.md for evidence of API calls and counting.
Stage 0 — Discover intent, then derive the ICP, then validate
Don't run any MCP tool yet. Stage 0 has three steps and is non-negotiable — it's what protects the user from a wasted run on the wrong audience.
The skill does NOT start with company filters. It starts with why the user is doing outreach. The flow branches from there: a recruiter targets people directly, a salesperson targets companies first then decision makers, a networker targets people by topic/role.
Step 0.1 — Ask intent (single question)
Use AskUserQuestion with exactly this question (don't bundle other questions yet — the answer drives every subsequent option list):
questions:
- header: "Goal"
question: "What's the goal of this outreach campaign?"
multiSelect: false
options:
- { label: "Sell to companies", description: "B2B sales — find companies that match an ICP, then reach their decision makers (CEO/CTO/Head of …)" }
- { label: "Recruit talent", description: "Hiring — find individual candidates by role / skill / seniority, no company-level search" }
- { label: "Network / partnerships", description: "Connect with peers, investors, partners, or thought leaders by topic / role / community" }
- { label: "Custom (I'll explain)", description: "Free-text — describe the goal in one sentence" }
If AskUserQuestion is unavailable, ask in plain text: "Quick check before I build the campaign — what are you trying to do? (1) sell to companies, (2) recruit talent, (3) network/partnerships, (4) something else."
Step 0.2 — Derive a draft ICP from the intent
Based on the chosen goal, propose a draft ICP in plain text (don't ask yet — show what you'd build). Pick reasonable defaults from the user's product/repo context if available, otherwise leave placeholders.
| Goal | Flow used | What you propose |
|---|---|---|
| Sell to companies | Stage 1 → 2 → 3 → 4 → 6 | Theme/keywords, target sector, target company size, target geo, decision-maker titles |
| Recruit talent | Skip Stage 1–2. Go directly to Stage 3 (search_people) | Role title(s), seniority level, skills/keywords, location, optionally past_company (poach from competitor) or school_url (alumni) |
| Network / partnerships | Skip Stage 1–2. Go directly to Stage 3 (search_people) | Topic/keyword, role title(s), location, optionally follower_of or connection_of for warm paths |
| Custom | Ask the user to describe in one sentence, then map to one of the three flows above | — |
Render the draft as a clean block, e.g. for "Sell to companies":
Here's the ICP I'd build — tell me what to change:
Goal: Sell to companies
Theme: AI recruitment / RecTech
Target sectors: Software Development, Human Resources Services
Company size: 11-50, 51-200
Geo: France
Personas: CEO / Founder, CTO / VP Engineering
Volume: 25 invites (warm-up)
Note: No note (best accept rate on cold C-level)
Message after accept: Auto-generate a short personalized message per connection
Step 0.3 — Let the user edit the filters (batched form)
Now use AskUserQuestion with the filter set that matches the chosen flow. Pre-select sensible defaults from Step 0.2 by listing them first / marking (Recommended). The form tool caps at 4 questions per call, so split into 2 batches if needed.
For "Sell to companies" — ask the company-side filters first, then persona + tactics:
Round A (audience): Theme · Sectors · Company size · Geo
Round B (execution): Persona titles · Volume · Note style · Message-after-accept
For "Recruit talent" — skip company filters entirely:
Round A (candidate): Role titles · Seniority · Skills/keywords · Location
Round B (execution): Volume · Note style · Message-after-accept · Sourcing twist (past_company / school_url / none)
For "Network / partnerships" — also skip company filters:
Round A (audience): Topic/keyword · Role titles · Location · Warm-path twist (follower_of / connection_of / none)
Round B (execution): Volume · Note style · Message-after-accept
Step 0.4 — Pick the sending account(s)
After the ICP is locked, run linkupapi_list_accounts and present the accounts to the user via AskUserQuestion with multiSelect: true. Each option label is the account's display name; description shows status + email/handle. Only show accounts with status = connected.
question: "Which LinkedIn account(s) should send the invites?"
multiSelect: true
options:
- { label: "<account name 1>", description: "<email> — connected" }
- { label: "<account name 2>", description: "<email> — connected" }
...
If the user picks multiple accounts, the campaign is split across them and the same person must NEVER be invited from two different accounts (LinkedIn flags duplicate invites and accept rate collapses). Enforce this:
- Pool all candidate profiles after Stage 4.
- Deduplicate by
profile_url. - Round-robin assign each unique candidate to exactly one sending account.
- Cap each account at the per-session/weekly safe limits independently (80/session, 100/rolling week per account).
- In Stage 6, run the
check_invitationandinvitecalls with the assigned account_id for each candidate.
After this step, echo the final ICP + account selection back in 4–6 lines and ask "Should I proceed?" (yes/no). Do NOT touch any further MCP tool until the user confirms.
How the answers map to the next stages
| Goal | Stage 1 (companies) | Stage 3 (people) | Notes |
|---|---|---|---|
| Sell to companies | runs with keyword+sector+location+company_size | runs with company_url (from Stage 1 results) + title | Full 7-stage flow |
| Recruit talent | skipped | runs with title+location+keyword (skills) + optional past_company / school_url | Stage 4 still runs (verify current role) |
| Network / partnerships | skipped | runs with title+location+keyword + optional follower_of / connection_of | Stage 4 still runs |
| Generic answer | Used in |
|---|---|
| Volume | Hard cap on Stage 6 invite count, also informs limit in Stage 1/3 (Volume × 2.5 ≈ candidates needed since some are filtered) |
| Note style | Stage 6 invite.params.message |
| Message-after-accept | Stage 7 webhook + send-message-on-accept logic (auto-personalized / custom template / none) |
Stage 1 — Find target companies
Tool: linkedin_profiles action search_companies.
The V2 exposes exactly 4 filters on companies (this is the full list — there's no industry filter on companies, use sector). Pass values as JSON arrays for OR-stacking — that's the V2-native syntax. Plain strings are also accepted (single value).
| Param | Type | Purpose |
|---|---|---|
| keyword | array of strings | Free-text match (name, description, tagline). Example: ["AI recruitment", "applicant tracking"] |
| sector | array of strings | LinkedIn industry of the company. Example: ["Software Development", "Internet", "Computer Software"] |
| location | array of strings | Geo of the HQ / branch. Example: ["France", "United Kingdom"] |
| company_size | array of strings | Employee bracket. Allowed values: 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001+ |
Filter combination semantics:
- Multiple values inside one array → OR (
["AI recruitment", "ATS"]= either match) - Different fields → AND (
sector=[...]ANDcompany_size=[...]AND …)
{
"account_id": "<from list_accounts>",
"action": "search_companies",
"params": {
"keyword": ["AI recruitment", "recruiting copilot", "AI sourcing"],
"sector": ["Software Development", "Human Resources Services"],
"location": ["France", "United Kingdom"],
"company_size": ["11-50", "51-200"],
"limit": 25
}
}
- Pagination:
offset+limit(preferred) orstart_page+end_page. - Counts against the 15 searches / day cap.
- The result objects come back with
industryandlocationsplit out (V2 post-processing splits"industry • city"into separate fields), so filtering in Stage 2 is straightforward. - Save the raw list, then in Stage 2 manually filter out non-fits (staffing agencies dressed as tech, pure consultancies, etc.).
Stage 2 — Filter companies (LLM judgment)
Look at each result's industry, name, location, employee count. Drop:
- Companies whose industry is
Staffing and Recruitingif the user wants RecTech vendors (not agencies). - Companies whose name suggests they are a service buyer of the user's product, not a fit.
- Duplicates (same root domain) and obvious test/personal pages.
Show the filtered shortlist to the user before continuing if filtering is aggressive (>50% dropped). Otherwise continue.
Stage 3 — Extract decision makers per company
Tool: linkedin_profiles action search_people.
Use title, NOT keyword, to filter on roles. keyword matches anywhere in the profile (headline, posts, summary) → too noisy. title matches the current job title only → precise.
The V2 exposes 13 filters on people search. Pass values as JSON arrays for OR-stacking; different fields are AND-combined. (Plain strings are accepted for single values too.)
| Param | Type | Purpose | Example |
|---|---|---|---|
| title | array | Current job title — use this for personas | ["CEO", "CTO", "Co-Founder", "Founder", "Chief Executive Officer"] |
| company_url | array | Filter to current employees of one or more companies | ["https://www.linkedin.com/company/talentify-io/"] |
| company_name | array | Same but by company name string | ["Talentify", "Workable"] |
| location | array | Geo of the person | ["France", "United Kingdom", "Paris"] |
| industry | array | LinkedIn industry of the person | ["Software Development", "Human Resources"] |
| network | string | Connection degree — single value | "F" (1st), "S" (2nd), "O" (3rd+). Use "S" for best accept rate |
| keyword | array | Free-text full-profile search | Only when role isn't well-defined by title |
| school_url | array | Alumni filter — people who studied at X | ["https://www.linkedin.com/school/hec-paris/"] |
| past_company | array | Ex-employees of a company (e.g. competitor) | ["https://www.linkedin.com/company/competitor/"] |
| connection_of | array | People connected to a specific profile | ["https://www.linkedin.com/in/mutual-friend/"] |
| follower_of | array | Followers of a thought leader / brand | ["https://www.linkedin.com/in/influencer/"] |
| first_name | array | Look-alike search | ["Othamar", "Sameer"] |
| last_name | array | Look-alike search | ["Magiatis"] |
For a standard outreach campaign, the high-precision combo is:
{
"account_id": "...",
"action": "search_people",
"params": {
"company_url": ["<exact company_url returned by Stage 1>"],
"title": ["CEO", "CTO", "Co-Founder", "Founder"],
"network": "S",
"limit": 5
}
}
CRITICAL — never fabricatecompany_url. Use the exactcompany_urlvalue LinkupAPI returned in Stage 1 (search_companiesresults). Do NOT guess or build one from the company name (e.g. inferringlinkedin.com/company/acme/from "Acme Inc" — slugs often differ:acme-corp,acme-io,acme-tech). A fabricated URL silently returns 0 results or worse, employees of a different company with a colliding slug. Always copy the URL field from the Stage 1 result object verbatim.
network: "S" (2nd degree) is gold — these people share at least one mutual connection with the user, which dramatically boosts accept rate (LinkedIn shows the mutual on the invite). Drop network only if the user wants 3rd-degree volume.
Power-user patterns
- Recruit ex-Acme talent:
{"past_company": ["https://linkedin.com/company/acme"], "title": ["Engineer", "Senior Engineer"]} - Alumni outreach to HEC grads in product roles:
{"school_url": ["https://linkedin.com/school/hec-paris/"], "title": ["Product Manager", "CPO"]} - Warm intro via mutual:
{"connection_of": ["https://linkedin.com/in/your-investor"]} - Audience of a thought leader:
{"follower_of": ["https://linkedin.com/in/famous-vc"]}
Operational notes
- Each
search_peoplecall counts against the 15 searches / day cap. - Run calls sequentially in a bash loop (not parallel) with 1–2 sec sleeps. LinkedIn rate-limits bursts.
- Profiles shown as
LinkedIn Memberare private/anonymous → discard. Their URL contains/search/results/people/headless?...not/in/<handle>.
Stage 4 — Enrich profiles & filter false positives (CRITICAL)
LinkedIn search_people regularly returns ex-employees of a company because they appear on the company page from past employment. You MUST verify the current role.
Tool: linkedin_profiles action get with params.identifier=<linkedin handle>.
{
"account_id": "...",
"action": "get",
"params": {"identifier": "<handle from profile_url>"}
}
- Each
getcall counts against the 100 profile gets / day cap. - For each enriched profile, parse
experience[0](the current job). Keep only ifexperience[0].companymatches (or is similar to) the target company ANDexperience[0].titlematches the persona titles. - Common false positives to drop:
- Person now at a different company (LinkedIn tagged them due to past employment).
- Person with title "Software Engineer" / "Solutions Architect" when targeting CEO/CTO.
- Generic "Founder @ Stealth" entries (not the target boîte).
- Surface to the user a clean two-column table: ✅ confirmed targets vs ❌ false positives with the real current job.
Stage 5 — (Optional) Find professional emails
If the user wants email-based outreach in parallel to LinkedIn, use linkupapi_enrich:
{"action": "find_email", "params": {"linkedin_url": "<profile_url>"}}
or
{"action": "find_email", "params": {
"first_name": "...", "last_name": "...",
"company_domain": "..."
}}
- Skip this step if pure LinkedIn outreach.
Stage 6 — Pre-flight check & send invites
Before sending every connection request, check the current relationship via linkedin_network action check_invitation to avoid:
- Re-inviting someone already connected (1st degree)
- Re-sending while a previous invitation is
PENDING - Sending invites to
OUT_OF_NETWORKprofiles (rejected by LinkedIn anyway)
{
"action": "check_invitation",
"params": {"profile_url": "<full linkedin url>"}
}
The response includes invitation_state which is one of:
NO_INVITATION→ safe to invitePENDING→ already sent, skipCONNECTED(member_distance=1) → already connected, skip- Other values → log and skip
Then for every NO_INVITATION profile, send the invite:
{
"account_id": "...",
"action": "invite",
"params": {
"identifier": "<handle>",
"message": "<optional ≤300 char personal note>"
}
}
- Sleep 1–2 seconds between invites (
sleep 1in bash). LinkedIn flags burst patterns. - Hard cap at 80 invites per session, 100 per rolling week per account.
- After the batch, run
linkedin_networkactionlist_sentwithcount=20to confirm the invites appear as "Sent today".
Message templates (when user wants a note)
Keep notes ≤ 250 chars. Patterns that work:
- Reference shared signal:
Hi {first_name}, I saw {Company}'s focus on {theme} — same space we're operating in. Would love to compare notes. — {sender_first_name} - Genuine compliment + soft ask:
Hey {first_name}, your work at {Company} on {topic} caught my eye. Always glad to connect with operators in {space}. - Mutual connection:
Hi {first_name}, {mutual_name} suggested I reach out. We're tackling {problem} from the {angle} side — open to a quick exchange?
Avoid: pitchy openers, "quick question?", "5 minutes of your time?", and any link in the note (LinkedIn flags these).
Stage 7 — Send message after accepted invitation (webhook-triggered)
If the user doesn't already have a webhook for accepted_invitation on the chosen account(s), create one:
{
"tool": "linkupapi_create_webhook",
"args": {
"account_id": "...",
"events": ["accepted_invitation", "message_received"]
}
}
- Hosted mode (no
url) → events are stored, fetch vialinkupapi_get_webhook_events(poll) or via SSE on thestream_url. - Custom mode (with
url) → events POSTed to your endpoint.
For each accepted invitation event you'll receive:
{
"type": "accepted_invitation",
"new_connections": [{"profile_url": "...", "name": "...", "job_title": "..."}]
}
When the user wants to send a message as soon as the invitation is accepted:
{
"tool": "linkedin_messages",
"args": {
"account_id": "...",
"action": "send",
"params": {
"profile_url": "<from event>",
"message_text": "<personalized message>"
}
}
}
Message-after-accept options (asked in Step 0.3 form)
The "Message-after-accept" question in Step 0.3 has exactly three options — present them in this order:
- Generate a short, punchy, personalized message (Recommended) — for each accepted invite, build a custom 2–4 sentence message using the connection's
name,job_title, currentCompany(from Stage 4 enrichment), and the campaign theme. Do NOT send a generic "thanks for connecting" — it tanks reply rate. Aim for: a specific hook tied to their work + one sharp value-prop sentence + a low-friction question. Keep ≤ 600 chars. Generate at send-time, not in advance, so the message references real profile context. Example shape:
- Custom template provided by the user — the user pastes a template with
{first_name},{Company},{job_title},{theme}placeholders. Substitute and send verbatim.
- No message — webhook still arms (so accepts are tracked) but no DM is sent. The user handles messaging manually.
Send timing: fire the message immediately when the accepted_invitation event arrives (no artificial 24h wait — modern outreach plays react fast, and the value-add hook makes the message feel intentional, not robotic).
Stage 8 — Report & persist (mandatory)
At the end of every campaign run, give the user a concise summary AND always persist a full log file. Don't ask the user — just write it. The log is the source of truth for funnel analysis, accept-rate tracking, and de-duplication on the next campaign.
8a — Concise on-screen summary
Campaign: {ICP description}
Account: {account_email} ({account_id})
Companies searched: {n}
Companies kept: {n} ({n_dropped} filtered out)
Profiles enriched: {n}
Decision makers: {n_confirmed} (✅) / {n_false_pos} (❌)
Skipped (already inv): {n}
Invitations sent: {n}
Daily caps remaining: invites X / 20 · profiles Y / 100 · searches Z / 15
Webhook armed: yes/no
8b — Persist the campaign log
Write the full campaign details to ./campaigns/{YYYY-MM-DD}-{slug}.md where slug is a 2-3 word kebab-case theme (e.g. sales-rectech, recruit-engineers-paris, vc-network-fr). If ./campaigns/ doesn't exist, create it.
The log file MUST include:
- ICP block — every Stage 0 answer (goal, theme, sectors, sizes, geo, personas, volume, note style, message-after-accept).
- Account(s) used — id + email + country. If multiple accounts, list per-account assignment.
- Funnel table — counts per stage.
- Companies kept (Stage 2) — names only, comma-joined.
- Companies dropped (Stage 2) — name + 1-line reason each.
- False positives dropped pre-enrichment (Stage 3 → 4) — name + reason.
- Skipped (already invited / connected) — name + state + invitation_id if available.
- Invites sent table —
# | Name | Company | Network | invitation_urn. Theinvitation_urnis the key for laterwithdrawcalls and accept-tracking. - Notes / monitoring — any clusters (multiple invites in same small company), 3rd-degree count, and what to watch for in the next 48-72h. Include the snippet for arming a webhook later if Message-after-accept was "No message".
This file is what the user reads in 7 days to compute accept rate and decide whether to scale or change the ICP.
8c — De-dup on next campaign
When the user runs /linkedin-outreach again on the same account, before Stage 6, glob ./campaigns/*.md for invitation_urn/profile_url/public_identifier of any candidate in the new batch. If a profile was already invited in any prior log, drop it from the invite list (even if check_invitation returns NO_INVITATION — that means LinkedIn auto-withdrew an old invite or the prior one was rejected; re-inviting too fast tanks accept rate). Surface the dedup count in the on-screen summary.
Common pitfalls
- Fabricating
company_urlin Stage 3 — never construct a LinkedIn company URL from the company name. Always reuse the exactcompany_urlreturned by Stage 1'ssearch_companies. Slugs are unpredictable (acme-corpvsacme-iovsacme-tech); a guessed URL returns 0 results or worse, employees of a different company. - Inviting from
LinkedIn MemberURL — those are anonymized, the URL contains/search/results/people/headless?...not/in/<handle>. The invite call will fail. Filter them in Stage 4. - Trusting search_people's listed company — always verify current role in Stage 4. ~30–50% of
search_peopleresults in larger companies are ex-employees. - Sending invites in parallel — LinkedIn rate-limits. Use a sequential loop with sleep.
- Greek/Cyrillic identifiers — when the URL is URL-encoded (
https://www.linkedin.com/in/%CE%B4...), pass the raw handle toidentifier, not the URL. - Forgetting to confirm ICP — running a campaign on the wrong persona is bad UX. Always echo the ICP in plain text and wait for "yes" before Stage 1.
Tool quick reference
| What you want | Tool | Action | Daily cap |
|---|---|---|---|
| List my connected accounts | linkupapi_list_accounts | — | — |
| Login to a new LinkedIn account | linkupapi_login / linkupapi_checkpoint | — | — |
| Find target companies | linkedin_profiles | search_companies | 15/day shared |
| Find people in a company | linkedin_profiles | search_people | 15/day shared |
| Get full profile info | linkedin_profiles | get | 100/day shared |
| Find professional email | linkupapi_enrich | find_email | — |
| Validate an email | linkupapi_enrich | validate_email | — |
| Reverse email → person | linkupapi_enrich | reverse_email | — |
| Check invitation status | linkedin_network | check_invitation | — |
| Send connection request | linkedin_network | invite | 20/day |
| List sent invitations | linkedin_network | list_sent | — |
| Send a DM | linkedin_messages | send | — |
| Create webhook | linkupapi_create_webhook | — | — |
| Poll webhook events | linkupapi_get_webhook_events | — | — |
| Check daily usage | linkupapi_get_logs | — | run at Stage 0 |
Default campaign template
Stage 0 → confirm ICP with user + daily-cap budget check
Stage 1 → search_companies (counts against searches/day cap)
Stage 2 → LLM filter to ~12-18 real fits
Stage 3 → search_people on each (counts against searches/day cap)
Stage 4 → get on every candidate (counts against profiles/day cap)
Stage 5 → skip (LinkedIn-only campaign)
Stage 6 → check_invitation + invite on confirmed targets (counts against invites/day cap)
Stage 7 → ensure webhook is armed
Stage 8 → report + persist log
---
Source: https://github.com/claude-dev-code/claude-skills-linkedin
Author: claude-dev-code
Discovered via: skillsdirectory.com
Genre: ai-agents
SKILL.md source
---
name: Linkedin Outreach
description: Run a full LinkedIn outreach campaign via the LinkupAPI MCP — define an ICP (Ideal Customer Profile), find target companies, extract real decision makers (CEO/CTO/Founder), filter false positives, ...
---
# Linkedin Outreach
Run a full LinkedIn outreach campaign via the LinkupAPI MCP — define an ICP (Ideal Customer Profile), find target companies, extract real decision makers (CEO/CTO/Founder), filter false positives, send connection requests, and set up automated follow-up via webhooks. Use when the user says "outreach campaign", "prospect on LinkedIn", "find decision makers", "send connection requests", "build a target list", "campaign LinkedIn", "lead generation", "ICP", "target CEO/CTO", or describes a B2B ou...
---
name: linkedin-outreach
description: Run a full LinkedIn outreach campaign via the LinkupAPI MCP — define an ICP (Ideal Customer Profile), find target companies, extract real decision makers (CEO/CTO/Founder), filter false positives, send connection requests, and set up automated follow-up via webhooks. Use when the user says "outreach campaign", "prospect on LinkedIn", "find decision makers", "send connection requests", "build a target list", "campaign LinkedIn", "lead generation", "ICP", "target CEO/CTO", or describes a B2B outreach goal. Requires the `linkupapi` MCP server to be connected with at least one LinkedIn account (status `connected`).
---
# LinkedIn Outreach Campaign — LinkupAPI V2
Use this skill to run cold-outreach campaigns on LinkedIn via the `linkupapi` MCP. It codifies the full prospecting → connect → follow-up playbook in 7 stages, with rate-limit awareness and false-positive filtering built in.
## Required prerequisites — check before you start
1. The user must have the `linkupapi` MCP connected. Verify with `linkupapi_list_accounts`.
- If the list is **empty**, tell the user they have two options to connect a LinkedIn account, then stop and wait:
- **Hosted UI** — open https://app.linkupapi.com/account-connection (fastest, handles checkpoints in-browser).
- **MCP login** — run `linkupapi_login` directly (platform=linkedin, with email+password OR a `login_token`). On `checkpoint_required` → run `linkupapi_checkpoint`.
2. If a webhook for the chosen account(s) doesn't exist with the `accepted_invitation` event, propose to create one (`linkupapi_create_webhook`) so accepts trigger automated follow-up.
## Daily LinkedIn safety caps (MANDATORY — enforced before Stage 1)
Per LinkedIn account, this skill respects:
- **20 connection requests / day** (invites)
- **100 profile gets / day** (`linkedin_profiles get`)
- **15 searches / day** (`search_people` / `search_companies`)
These are **shared budgets across ALL linkedin-* skills** (linkedin-outreach, linkedin-high-intent, linkedin-feed-engage, linkedin-enrich). Hitting the cap from any one skill carries over.
**Before Stage 1**, run `linkupapi_get_logs` filtered to the last 24h on the chosen account. Count today's:
- `linkedin_network/invite` calls → invites used today
- `linkedin_profiles/get` calls → profile gets used today
- `linkedin_profiles/search_*` calls → searches used today
Compute remaining budget per category. Surface in the Stage 0 echo:
```
Daily caps remaining today (account {email}):
Invites: X / 20 (Y used today)
Profiles: X / 100 (Y used today)
Searches: X / 15 (Y used today)
```
**Hard rules**:
- If `Volume cap > remaining_invites` → trim Volume cap to `remaining_invites` (never override).
- If `searches_needed > remaining_searches` (Stage 1 + Stage 3 combined) → reduce Stage 1 `limit` and number of `search_people` calls until it fits, or stop and ask the user to run tomorrow.
- If `profile_gets_needed > remaining_profiles` (Stage 4) → enrich the highest-priority candidates and write the rest to a "deferred" list in the campaign log.
If `linkupapi_get_logs` is unavailable or empty, fall back to scanning today's `./campaigns/*.md` and `./enrichments/*.md` for evidence of API calls and counting.
## Stage 0 — Discover intent, then derive the ICP, then validate
**Don't run any MCP tool yet.** Stage 0 has three steps and is non-negotiable — it's what protects the user from a wasted run on the wrong audience.
The skill does NOT start with company filters. It starts with **why** the user is doing outreach. The flow branches from there: a recruiter targets people directly, a salesperson targets companies first then decision makers, a networker targets people by topic/role.
### Step 0.1 — Ask intent (single question)
Use `AskUserQuestion` with exactly this question (don't bundle other questions yet — the answer drives every subsequent option list):
```yaml
questions:
- header: "Goal"
question: "What's the goal of this outreach campaign?"
multiSelect: false
options:
- { label: "Sell to companies", description: "B2B sales — find companies that match an ICP, then reach their decision makers (CEO/CTO/Head of …)" }
- { label: "Recruit talent", description: "Hiring — find individual candidates by role / skill / seniority, no company-level search" }
- { label: "Network / partnerships", description: "Connect with peers, investors, partners, or thought leaders by topic / role / community" }
- { label: "Custom (I'll explain)", description: "Free-text — describe the goal in one sentence" }
```
If `AskUserQuestion` is unavailable, ask in plain text: *"Quick check before I build the campaign — what are you trying to do? (1) sell to companies, (2) recruit talent, (3) network/partnerships, (4) something else."*
### Step 0.2 — Derive a draft ICP from the intent
Based on the chosen goal, **propose** a draft ICP in plain text (don't ask yet — show what you'd build). Pick reasonable defaults from the user's product/repo context if available, otherwise leave placeholders.
| Goal | Flow used | What you propose |
|---|---|---|
| **Sell to companies** | Stage 1 → 2 → 3 → 4 → 6 | Theme/keywords, target sector, target company size, target geo, decision-maker titles |
| **Recruit talent** | **Skip Stage 1–2.** Go directly to Stage 3 (`search_people`) | Role title(s), seniority level, skills/keywords, location, optionally `past_company` (poach from competitor) or `school_url` (alumni) |
| **Network / partnerships** | **Skip Stage 1–2.** Go directly to Stage 3 (`search_people`) | Topic/keyword, role title(s), location, optionally `follower_of` or `connection_of` for warm paths |
| **Custom** | Ask the user to describe in one sentence, then map to one of the three flows above | — |
Render the draft as a clean block, e.g. for "Sell to companies":
```
Here's the ICP I'd build — tell me what to change:
Goal: Sell to companies
Theme: AI recruitment / RecTech
Target sectors: Software Development, Human Resources Services
Company size: 11-50, 51-200
Geo: France
Personas: CEO / Founder, CTO / VP Engineering
Volume: 25 invites (warm-up)
Note: No note (best accept rate on cold C-level)
Message after accept: Auto-generate a short personalized message per connection
```
### Step 0.3 — Let the user edit the filters (batched form)
Now use `AskUserQuestion` with the **filter set that matches the chosen flow**. Pre-select sensible defaults from Step 0.2 by listing them first / marking `(Recommended)`. The form tool caps at 4 questions per call, so split into 2 batches if needed.
**For "Sell to companies"** — ask the company-side filters first, then persona + tactics:
Round A (audience): Theme · Sectors · Company size · Geo
Round B (execution): Persona titles · Volume · Note style · Message-after-accept
**For "Recruit talent"** — skip company filters entirely:
Round A (candidate): Role titles · Seniority · Skills/keywords · Location
Round B (execution): Volume · Note style · Message-after-accept · Sourcing twist (`past_company` / `school_url` / none)
**For "Network / partnerships"** — also skip company filters:
Round A (audience): Topic/keyword · Role titles · Location · Warm-path twist (`follower_of` / `connection_of` / none)
Round B (execution): Volume · Note style · Message-after-accept
### Step 0.4 — Pick the sending account(s)
After the ICP is locked, run `linkupapi_list_accounts` and present the accounts to the user via `AskUserQuestion` with `multiSelect: true`. Each option label is the account's display name; description shows status + email/handle. Only show accounts with `status = connected`.
```yaml
question: "Which LinkedIn account(s) should send the invites?"
multiSelect: true
options:
- { label: "<account name 1>", description: "<email> — connected" }
- { label: "<account name 2>", description: "<email> — connected" }
...
```
If the user picks **multiple** accounts, the campaign is split across them and **the same person must NEVER be invited from two different accounts** (LinkedIn flags duplicate invites and accept rate collapses). Enforce this:
- Pool all candidate profiles after Stage 4.
- Deduplicate by `profile_url`.
- Round-robin assign each unique candidate to exactly one sending account.
- Cap each account at the per-session/weekly safe limits independently (80/session, 100/rolling week per account).
- In Stage 6, run the `check_invitation` and `invite` calls **with the assigned account_id** for each candidate.
After this step, **echo the final ICP + account selection back in 4–6 lines** and ask "Should I proceed?" (yes/no). Do NOT touch any further MCP tool until the user confirms.
### How the answers map to the next stages
| Goal | Stage 1 (companies) | Stage 3 (people) | Notes |
|---|---|---|---|
| Sell to companies | runs with `keyword`+`sector`+`location`+`company_size` | runs with `company_url` (from Stage 1 results) + `title` | Full 7-stage flow |
| Recruit talent | **skipped** | runs with `title`+`location`+`keyword` (skills) + optional `past_company` / `school_url` | Stage 4 still runs (verify current role) |
| Network / partnerships | **skipped** | runs with `title`+`location`+`keyword` + optional `follower_of` / `connection_of` | Stage 4 still runs |
| Generic answer | Used in |
|---|---|
| Volume | Hard cap on Stage 6 invite count, also informs `limit` in Stage 1/3 (Volume × 2.5 ≈ candidates needed since some are filtered) |
| Note style | Stage 6 `invite.params.message` |
| Message-after-accept | Stage 7 webhook + send-message-on-accept logic (auto-personalized / custom template / none) |
## Stage 1 — Find target companies
Tool: `linkedin_profiles` action `search_companies`.
The V2 exposes exactly **4 filters** on companies (this is the full list — there's no `industry` filter on companies, use `sector`). **Pass values as JSON arrays** for OR-stacking — that's the V2-native syntax. Plain strings are also accepted (single value).
| Param | Type | Purpose |
|---|---|---|
| `keyword` | array of strings | Free-text match (name, description, tagline). Example: `["AI recruitment", "applicant tracking"]` |
| `sector` | array of strings | LinkedIn industry of the company. Example: `["Software Development", "Internet", "Computer Software"]` |
| `location` | array of strings | Geo of the HQ / branch. Example: `["France", "United Kingdom"]` |
| `company_size` | array of strings | Employee bracket. Allowed values: `1-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001+` |
**Filter combination semantics:**
- Multiple values inside one array → **OR** (`["AI recruitment", "ATS"]` = either match)
- Different fields → **AND** (`sector=[...]` AND `company_size=[...]` AND …)
```json
{
"account_id": "<from list_accounts>",
"action": "search_companies",
"params": {
"keyword": ["AI recruitment", "recruiting copilot", "AI sourcing"],
"sector": ["Software Development", "Human Resources Services"],
"location": ["France", "United Kingdom"],
"company_size": ["11-50", "51-200"],
"limit": 25
}
}
```
- Pagination: `offset` + `limit` (preferred) or `start_page` + `end_page`.
- Counts against the **15 searches / day** cap.
- The result objects come back with `industry` and `location` split out (V2 post-processing splits `"industry • city"` into separate fields), so filtering in Stage 2 is straightforward.
- Save the raw list, then in Stage 2 manually filter out **non-fits** (staffing agencies dressed as tech, pure consultancies, etc.).
## Stage 2 — Filter companies (LLM judgment)
Look at each result's `industry`, `name`, `location`, employee count. **Drop**:
- Companies whose industry is `Staffing and Recruiting` if the user wants RecTech vendors (not agencies).
- Companies whose name suggests they are a service buyer of the user's product, not a fit.
- Duplicates (same root domain) and obvious test/personal pages.
Show the filtered shortlist to the user before continuing if filtering is aggressive (>50% dropped). Otherwise continue.
## Stage 3 — Extract decision makers per company
Tool: `linkedin_profiles` action `search_people`.
**Use `title`, NOT `keyword`, to filter on roles.** `keyword` matches anywhere in the profile (headline, posts, summary) → too noisy. `title` matches the current job title only → precise.
The V2 exposes **13 filters** on people search. **Pass values as JSON arrays** for OR-stacking; different fields are AND-combined. (Plain strings are accepted for single values too.)
| Param | Type | Purpose | Example |
|---|---|---|---|
| `title` | array | **Current job title** — use this for personas | `["CEO", "CTO", "Co-Founder", "Founder", "Chief Executive Officer"]` |
| `company_url` | array | Filter to current employees of one or more companies | `["https://www.linkedin.com/company/talentify-io/"]` |
| `company_name` | array | Same but by company name string | `["Talentify", "Workable"]` |
| `location` | array | Geo of the person | `["France", "United Kingdom", "Paris"]` |
| `industry` | array | LinkedIn industry of the person | `["Software Development", "Human Resources"]` |
| `network` | string | Connection degree — single value | `"F"` (1st), `"S"` (2nd), `"O"` (3rd+). Use `"S"` for best accept rate |
| `keyword` | array | Free-text full-profile search | Only when role isn't well-defined by `title` |
| `school_url` | array | Alumni filter — people who studied at X | `["https://www.linkedin.com/school/hec-paris/"]` |
| `past_company` | array | Ex-employees of a company (e.g. competitor) | `["https://www.linkedin.com/company/competitor/"]` |
| `connection_of` | array | People connected to a specific profile | `["https://www.linkedin.com/in/mutual-friend/"]` |
| `follower_of` | array | Followers of a thought leader / brand | `["https://www.linkedin.com/in/influencer/"]` |
| `first_name` | array | Look-alike search | `["Othamar", "Sameer"]` |
| `last_name` | array | Look-alike search | `["Magiatis"]` |
For a standard outreach campaign, the high-precision combo is:
```json
{
"account_id": "...",
"action": "search_people",
"params": {
"company_url": ["<exact company_url returned by Stage 1>"],
"title": ["CEO", "CTO", "Co-Founder", "Founder"],
"network": "S",
"limit": 5
}
}
```
> **CRITICAL — never fabricate `company_url`.** Use the exact `company_url` value LinkupAPI returned in Stage 1 (`search_companies` results). Do NOT guess or build one from the company name (e.g. inferring `linkedin.com/company/acme/` from "Acme Inc" — slugs often differ: `acme-corp`, `acme-io`, `acme-tech`). A fabricated URL silently returns 0 results or worse, employees of a different company with a colliding slug. Always copy the URL field from the Stage 1 result object verbatim.
`network: "S"` (2nd degree) is gold — these people share at least one mutual connection with the user, which dramatically boosts accept rate (LinkedIn shows the mutual on the invite). Drop `network` only if the user wants 3rd-degree volume.
### Power-user patterns
- **Recruit ex-Acme talent**: `{"past_company": ["https://linkedin.com/company/acme"], "title": ["Engineer", "Senior Engineer"]}`
- **Alumni outreach to HEC grads in product roles**: `{"school_url": ["https://linkedin.com/school/hec-paris/"], "title": ["Product Manager", "CPO"]}`
- **Warm intro via mutual**: `{"connection_of": ["https://linkedin.com/in/your-investor"]}`
- **Audience of a thought leader**: `{"follower_of": ["https://linkedin.com/in/famous-vc"]}`
### Operational notes
- Each `search_people` call counts against the **15 searches / day** cap.
- Run calls **sequentially in a bash loop** (not parallel) with 1–2 sec sleeps. LinkedIn rate-limits bursts.
- Profiles shown as `LinkedIn Member` are private/anonymous → discard. Their URL contains `/search/results/people/headless?...` not `/in/<handle>`.
## Stage 4 — Enrich profiles & filter false positives (CRITICAL)
LinkedIn `search_people` regularly returns **ex-employees** of a company because they appear on the company page from past employment. You MUST verify the *current* role.
Tool: `linkedin_profiles` action `get` with `params.identifier=<linkedin handle>`.
```json
{
"account_id": "...",
"action": "get",
"params": {"identifier": "<handle from profile_url>"}
}
```
- Each `get` call counts against the **100 profile gets / day** cap.
- For each enriched profile, parse `experience[0]` (the current job). **Keep only if** `experience[0].company` matches (or is similar to) the target company AND `experience[0].title` matches the persona titles.
- Common false positives to drop:
- Person now at a different company (LinkedIn tagged them due to past employment).
- Person with title "Software Engineer" / "Solutions Architect" when targeting CEO/CTO.
- Generic "Founder @ Stealth" entries (not the target boîte).
- Surface to the user a clean two-column table: ✅ confirmed targets vs ❌ false positives with the real current job.
## Stage 5 — (Optional) Find professional emails
If the user wants email-based outreach in parallel to LinkedIn, use `linkupapi_enrich`:
```json
{"action": "find_email", "params": {"linkedin_url": "<profile_url>"}}
```
or
```json
{"action": "find_email", "params": {
"first_name": "...", "last_name": "...",
"company_domain": "..."
}}
```
- Skip this step if pure LinkedIn outreach.
## Stage 6 — Pre-flight check & send invites
Before sending **every** connection request, check the current relationship via `linkedin_network` action `check_invitation` to avoid:
- Re-inviting someone already connected (1st degree)
- Re-sending while a previous invitation is `PENDING`
- Sending invites to `OUT_OF_NETWORK` profiles (rejected by LinkedIn anyway)
```json
{
"action": "check_invitation",
"params": {"profile_url": "<full linkedin url>"}
}
```
The response includes `invitation_state` which is one of:
- `NO_INVITATION` → safe to invite
- `PENDING` → already sent, skip
- `CONNECTED` (member_distance=1) → already connected, skip
- Other values → log and skip
Then for every `NO_INVITATION` profile, send the invite:
```json
{
"account_id": "...",
"action": "invite",
"params": {
"identifier": "<handle>",
"message": "<optional ≤300 char personal note>"
}
}
```
- **Sleep 1–2 seconds between invites** (`sleep 1` in bash). LinkedIn flags burst patterns.
- Hard cap at **80 invites per session**, **100 per rolling week** per account.
- After the batch, run `linkedin_network` action `list_sent` with `count=20` to confirm the invites appear as "Sent today".
### Message templates (when user wants a note)
Keep notes ≤ 250 chars. Patterns that work:
- **Reference shared signal**: `Hi {first_name}, I saw {Company}'s focus on {theme} — same space we're operating in. Would love to compare notes. — {sender_first_name}`
- **Genuine compliment + soft ask**: `Hey {first_name}, your work at {Company} on {topic} caught my eye. Always glad to connect with operators in {space}.`
- **Mutual connection**: `Hi {first_name}, {mutual_name} suggested I reach out. We're tackling {problem} from the {angle} side — open to a quick exchange?`
Avoid: pitchy openers, "quick question?", "5 minutes of your time?", and any link in the note (LinkedIn flags these).
## Stage 7 — Send message after accepted invitation (webhook-triggered)
If the user doesn't already have a webhook for `accepted_invitation` on the chosen account(s), create one:
```json
{
"tool": "linkupapi_create_webhook",
"args": {
"account_id": "...",
"events": ["accepted_invitation", "message_received"]
}
}
```
- Hosted mode (no `url`) → events are stored, fetch via `linkupapi_get_webhook_events` (poll) or via SSE on the `stream_url`.
- Custom mode (with `url`) → events POSTed to your endpoint.
For each accepted invitation event you'll receive:
```json
{
"type": "accepted_invitation",
"new_connections": [{"profile_url": "...", "name": "...", "job_title": "..."}]
}
```
When the user wants to send a message as soon as the invitation is accepted:
```json
{
"tool": "linkedin_messages",
"args": {
"account_id": "...",
"action": "send",
"params": {
"profile_url": "<from event>",
"message_text": "<personalized message>"
}
}
}
```
### Message-after-accept options (asked in Step 0.3 form)
The "Message-after-accept" question in Step 0.3 has exactly three options — present them in this order:
1. **Generate a short, punchy, personalized message (Recommended)** — for each accepted invite, build a custom 2–4 sentence message using the connection's `name`, `job_title`, current `Company` (from Stage 4 enrichment), and the campaign theme. **Do NOT send a generic "thanks for connecting"** — it tanks reply rate. Aim for: a specific hook tied to their work + one sharp value-prop sentence + a low-friction question. Keep ≤ 600 chars. Generate at send-time, not in advance, so the message references real profile context. Example shape:
> {first_name}, saw you're {role} at {Company} — we just shipped a way for teams like yours to {specific value tied to theme}. Curious if {pain_point_question}?
2. **Custom template provided by the user** — the user pastes a template with `{first_name}`, `{Company}`, `{job_title}`, `{theme}` placeholders. Substitute and send verbatim.
3. **No message** — webhook still arms (so accepts are tracked) but no DM is sent. The user handles messaging manually.
Send timing: fire the message immediately when the `accepted_invitation` event arrives (no artificial 24h wait — modern outreach plays react fast, and the value-add hook makes the message feel intentional, not robotic).
## Stage 8 — Report & persist (mandatory)
At the end of every campaign run, give the user a concise summary AND **always** persist a full log file. Don't ask the user — just write it. The log is the source of truth for funnel analysis, accept-rate tracking, and de-duplication on the next campaign.
### 8a — Concise on-screen summary
```
Campaign: {ICP description}
Account: {account_email} ({account_id})
Companies searched: {n}
Companies kept: {n} ({n_dropped} filtered out)
Profiles enriched: {n}
Decision makers: {n_confirmed} (✅) / {n_false_pos} (❌)
Skipped (already inv): {n}
Invitations sent: {n}
Daily caps remaining: invites X / 20 · profiles Y / 100 · searches Z / 15
Webhook armed: yes/no
```
### 8b — Persist the campaign log
Write the full campaign details to `./campaigns/{YYYY-MM-DD}-{slug}.md` where `slug` is a 2-3 word kebab-case theme (e.g. `sales-rectech`, `recruit-engineers-paris`, `vc-network-fr`). If `./campaigns/` doesn't exist, create it.
The log file MUST include:
1. **ICP block** — every Stage 0 answer (goal, theme, sectors, sizes, geo, personas, volume, note style, message-after-accept).
2. **Account(s) used** — id + email + country. If multiple accounts, list per-account assignment.
3. **Funnel table** — counts per stage.
4. **Companies kept** (Stage 2) — names only, comma-joined.
5. **Companies dropped** (Stage 2) — name + 1-line reason each.
6. **False positives dropped pre-enrichment** (Stage 3 → 4) — name + reason.
7. **Skipped (already invited / connected)** — name + state + invitation_id if available.
8. **Invites sent table** — `# | Name | Company | Network | invitation_urn`. The `invitation_urn` is the key for later `withdraw` calls and accept-tracking.
9. **Notes / monitoring** — any clusters (multiple invites in same small company), 3rd-degree count, and what to watch for in the next 48-72h. Include the snippet for arming a webhook later if Message-after-accept was "No message".
This file is what the user reads in 7 days to compute accept rate and decide whether to scale or change the ICP.
### 8c — De-dup on next campaign
When the user runs `/linkedin-outreach` again on the same account, **before Stage 6**, glob `./campaigns/*.md` for `invitation_urn`/`profile_url`/`public_identifier` of any candidate in the new batch. If a profile was already invited in any prior log, drop it from the invite list (even if `check_invitation` returns NO_INVITATION — that means LinkedIn auto-withdrew an old invite or the prior one was rejected; re-inviting too fast tanks accept rate). Surface the dedup count in the on-screen summary.
## Common pitfalls
- **Fabricating `company_url` in Stage 3** — never construct a LinkedIn company URL from the company name. Always reuse the exact `company_url` returned by Stage 1's `search_companies`. Slugs are unpredictable (`acme-corp` vs `acme-io` vs `acme-tech`); a guessed URL returns 0 results or worse, employees of a different company.
- **Inviting from `LinkedIn Member` URL** — those are anonymized, the URL contains `/search/results/people/headless?...` not `/in/<handle>`. The invite call will fail. Filter them in Stage 4.
- **Trusting search_people's listed company** — always verify current role in Stage 4. ~30–50% of `search_people` results in larger companies are ex-employees.
- **Sending invites in parallel** — LinkedIn rate-limits. Use a sequential loop with sleep.
- **Greek/Cyrillic identifiers** — when the URL is URL-encoded (`https://www.linkedin.com/in/%CE%B4...`), pass the raw handle to `identifier`, not the URL.
- **Forgetting to confirm ICP** — running a campaign on the wrong persona is bad UX. Always echo the ICP in plain text and wait for "yes" before Stage 1.
## Tool quick reference
| What you want | Tool | Action | Daily cap |
|---|---|---|---|
| List my connected accounts | `linkupapi_list_accounts` | — | — |
| Login to a new LinkedIn account | `linkupapi_login` / `linkupapi_checkpoint` | — | — |
| Find target companies | `linkedin_profiles` | `search_companies` | **15/day shared** |
| Find people in a company | `linkedin_profiles` | `search_people` | **15/day shared** |
| Get full profile info | `linkedin_profiles` | `get` | **100/day shared** |
| Find professional email | `linkupapi_enrich` | `find_email` | — |
| Validate an email | `linkupapi_enrich` | `validate_email` | — |
| Reverse email → person | `linkupapi_enrich` | `reverse_email` | — |
| Check invitation status | `linkedin_network` | `check_invitation` | — |
| Send connection request | `linkedin_network` | `invite` | **20/day** |
| List sent invitations | `linkedin_network` | `list_sent` | — |
| Send a DM | `linkedin_messages` | `send` | — |
| Create webhook | `linkupapi_create_webhook` | — | — |
| Poll webhook events | `linkupapi_get_webhook_events` | — | — |
| Check daily usage | `linkupapi_get_logs` | — | run at Stage 0 |
## Default campaign template
```
Stage 0 → confirm ICP with user + daily-cap budget check
Stage 1 → search_companies (counts against searches/day cap)
Stage 2 → LLM filter to ~12-18 real fits
Stage 3 → search_people on each (counts against searches/day cap)
Stage 4 → get on every candidate (counts against profiles/day cap)
Stage 5 → skip (LinkedIn-only campaign)
Stage 6 → check_invitation + invite on confirmed targets (counts against invites/day cap)
Stage 7 → ensure webhook is armed
Stage 8 → report + persist log
```
---
**Source**: https://github.com/claude-dev-code/claude-skills-linkedin
**Author**: claude-dev-code
**Discovered via**: skillsdirectory.com
**Genre**: ai-agents
Related skills 6
running-claude-code-via-litellm-copilot
Use when routing Claude Code through a local LiteLLM proxy to GitHub Copilot, reducing direct Anthropic spend, configuring ANTHROPIC_BASE_URL or ANTHROPIC_MODEL overrides, or troubleshooting Copilot proxy setup failures such as model-not-found, no localhost traffic, or GitHub 401/403 auth errors.
skills-cli
Use when users ask to discover, install, list, check, update, remove, back up, restore, sync, or initialize Agent Skills, mention `bunx skills`, `npx skills`, `skills.sh`, or `skills-lock.json`, ask "find a skill for X", or want help extending agent capabilities with installable skills.
repo-intake-and-plan
Narrow RigorPilot helper for README-first deep learning repo reproduction. Use when the task is specifically to scan a repository, read the README and common project files, extract documented commands, classify inference, evaluation, and training candidates, and return the smallest trustworthy reproduction plan to the main orchestrator. Do not use for environment setup, asset download, command execution, final reporting, paper lookup, or end-to-end orchestration.
image-to-video
Animate any still image on RunComfy — this skill is a smart router that matches the user's intent to the right i2v model in the RunComfy catalog. Picks HappyHorse 1.0 I2V (Arena #1, native audio, identity preservation) for general animations, Wan 2.7 with `audio_url` for custom-voiceover lip-sync, or Seedance 2.0 Pro for multi-modal animation from image + reference video + reference audio. Bundles each model's documented prompting patterns so the caller gets sharper output without burning ite...
video-edit
Edit existing video on RunComfy — this skill is a smart router that matches the user's intent to the right edit model in the RunComfy catalog. Picks Wan 2.7 Edit-Video (general restyle / background swap / packaging swap, identity + motion preservation), Kling 2.6 Pro Motion Control (transfer precise motion from a reference video to a target character), or Lucy Edit Restyle (lightweight identity-stable restyle / outfit swap). Bundles each model's documented prompting patterns so the skill gets...
nano-banana-2
Generate images with Google Nano Banana 2 (Gemini-family flash-tier text-to-image) on RunComfy — bundled with the model's documented prompting patterns so the skill gets sharper output than naive prompting against the same model. Documents Nano Banana 2's strengths (rapid iteration, in-image typography rendering, predictable framing, optional web-grounded context), the resolution-tier pricing, the safety-tolerance dial, and when to route to Nano Banana Pro / GPT Image 2 / Flux 2 / Seedream in...