Research LinkedIn profiles and write personalized messages for any LinkedIn message type — connection requests, InMails, DMs, message requests, post comments, and comment replies. Takes LinkedIn URLs as input, researches each person (profile data + recent posts via Apify), and generates messages tailored to each lead's background, interests, and recent activity. Exports tool-ready CSVs for Dripify, Expandi, Botdog, PhantomBuster, or generic format. No LinkedIn cookies or login required.
npx gooseworks install --claude # Then in your agent: /gooseworks <prompt> --skill linkedin-message-writer
Research LinkedIn leads and write personalized messages for any LinkedIn message type. Takes LinkedIn URLs, researches each person using Apify (profile + recent posts), and writes messages based on what it finds.
No LinkedIn cookies. No database setup. Just LinkedIn URLs in, personalized messages out.
Load this skill when:
Required for researching LinkedIn profiles and posts. Set in .env:
APIFY_API_TOKEN=your_token_hereNo LinkedIn cookies, login, or session tokens needed. Apify handles scraping without any LinkedIn credentials.
That's it. One env var. Nothing else.
This skill writes any text-based LinkedIn message type. Each type has different constraints.
| Message Type | Who Can Receive | Character Limit | When to Use |
|---|---|---|---|
| Connection request | 2nd/3rd degree connections | 200 (free) / 300 (premium) | First touch. Must earn the accept. No selling. |
| InMail | Anyone (requires premium credits) | Subject: 200, Body: 1,900 | Standalone pitch to people who won't accept cold connections. Senior execs, busy people. |
| DM | 1st-degree connections only | 8,000 | Follow-ups after connection accepted. Conversational, not broadcast. |
| Message request | Group members, event attendees, #OpenToWork | 8,000 | Warm context — you share a group or event. Reference the shared context. |
| Post comment | Anyone (public posts) | 1,250 | Warm-up before connecting. Show you engaged with their content. Not a pitch. |
| Comment reply | Anyone (in a thread) | 1,250 | Engage in a conversation they started. Add value, don't pitch. |
Connection request (200/300 chars):
InMail (subject 200 + body 1,900 chars):
DM (8,000 chars):
Message request (8,000 chars):
Post comment (1,250 chars):
Comment reply (1,250 chars):
Ask the user these questions. Skip any already answered.
Leads:
Message type: 3. What kind of LinkedIn message do you want to write? (connection request, InMail, DM, message request, post comment, comment reply, or a sequence of multiple types) 4. If connection request: do you have a free or premium LinkedIn account? (affects character limit: 200 vs 300)
Goal: 5. What's the objective? (book meetings, drive demo requests, get replies, build relationships, promote content, warm up before outreach) 6. What's the angle or hook? (pain-based, hiring signal, competitor displacement, event-based, content engagement, mutual connection, cold)
Tone: 7. Which tone? Present options:
Context: 9. What does your company/product do? (one-liner for the AI to work with) 10. Any proof points? (customer names, metrics, case studies to reference)
Output: 11. Which LinkedIn outreach tool do you use? (Dripify / Expandi / Botdog / PhantomBuster / Just give me a CSV)
Accept leads from whatever source the user provides:
linkedin_url, LinkedIn URL, LinkedIn, profile_url, url). If ambiguous, ask the user which column.Minimum required: At least one LinkedIn URL per lead.
Present the lead count to the user and confirm before proceeding to research.
Research each lead using two Apify actors. Both require only APIFY_API_TOKEN — no LinkedIn cookies.
Use harvestapi/linkedin-profile-scraper to get profile data for all leads.
API call:
curl -X POST "https://api.apify.com/v2/acts/harvestapi~linkedin-profile-scraper/runs?token=$APIFY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"urls": [
{"url": "https://www.linkedin.com/in/PROFILE_1/"},
{"url": "https://www.linkedin.com/in/PROFILE_2/"}
]
}'Cost: $0.003 per profile. 100 leads = $0.30.
Returns per lead:
Polling for results:
# Check run status
curl "https://api.apify.com/v2/acts/harvestapi~linkedin-profile-scraper/runs/{RUN_ID}?token=$APIFY_API_TOKEN"
# When status is SUCCEEDED, fetch results
curl "https://api.apify.com/v2/datasets/{DATASET_ID}/items?token=$APIFY_API_TOKEN"Use harvestapi/linkedin-profile-posts to get recent posts. Run this when:
Skip this when:
API call:
curl -X POST "https://api.apify.com/v2/acts/harvestapi~linkedin-profile-posts/runs?token=$APIFY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"profileUrls": [
"https://www.linkedin.com/in/PROFILE_1/",
"https://www.linkedin.com/in/PROFILE_2/"
]
}'Cost: $0.002 per post. ~20 posts per profile = ~$0.04 per lead. 50 leads = $2.00.
Returns per post:
Polling: Same pattern as Step 1.
After research completes, present a summary table:
Leads researched: {count}
Profile data: {count} profiles retrieved
Posts scraped: {count} posts from {count} leads (or "skipped")
Research cost: ~${total}
Sample leads:
| Name | Title | Company | Recent Post Topic | Personalization Angle |
|------|-------|---------|-------------------|----------------------|
| Jane Smith | VP Sales | Acme Corp | Posted about AI in sales | Reference her AI post |
| ... | ... | ... | ... | ... |If the user asked to filter/qualify leads, do that now based on profile data (title, company, industry, etc.) and present which leads made the cut.
Generate personalized messages for each lead based on the research.
Use the best available signal for each lead. In order of strength:
If the user provided reference messages that have worked, analyze those for tone, length, structure, and vocabulary. Use them as the template — don't override with defaults.
After generating any message, count the characters. If over the limit:
Generate a CSV with these columns:
linkedin_url, first_name, last_name, company, title, message_type, message_subject, message_bodyFor sequence-based campaigns (connection + follow-ups), use:
linkedin_url, first_name, last_name, company, title, connection_request, followup_1, followup_2, followup_3, inmail_subject, inmail_bodyDripify:
Profile URL, Note, Message 1, Message 2, Message 3Expandi:
LinkedIn URL, Connection message, Follow-up #1, Follow-up #2, Follow-up #3, InMail subject, InMail messageBotdog:
linkedin_profile_url, connection_note, message_1, message_2, message_3PhantomBuster:
profileUrl, messageGeneric CSV / Other:
Save to the current working directory:
{campaign-name}-{YYYY-MM-DD}.csvPresent final summary:
Campaign: {name}
Message type: {type}
Leads: {count}
Tool: {dripify/expandi/etc.}
Personalization: {profile-only / profile+posts}
Research cost: ~${amount}
Export file: {file_path}Show 3-5 sample messages from the export for final review.
Do NOT mark as done without explicit user confirmation. Ask: "Messages look good? Anything to adjust before you import?"
After confirmation:
| Leads | Profile Only | Profile + Posts |
|---|---|---|
| 10 | ~$0.03 | ~$0.43 |
| 50 | ~$0.15 | ~$2.15 |
| 100 | ~$0.30 | ~$4.30 |
| 500 | ~$1.50 | ~$21.50 |
Profile scraper: $0.003/profile. Post scraper: ~$0.04/lead (20 posts × $0.002).
| Error | Fix |
|---|---|
APIFY_API_TOKEN not set | Ask user to add it to .env |
| Apify run fails or times out | Retry once. If still fails, skip that lead and note it. |
| LinkedIn URL is invalid or profile not found | Skip the lead, report it to user |
| 0 profiles returned | Check URL format — must be full LinkedIn URL with https:// |
| Post scraper returns 0 posts | Person doesn't post publicly. Use profile data only for personalization. |
Diagnose Meta Ads campaign performance using Meta's actual system mechanics — Breakdown Effect, Learning Phase, Auction Overlap, Pacing, and Creative Fatigue — and produce structured, testable recommendations that avoid judging segments by average CPA instead of marginal efficiency.
Pre-flight policy check for Meta ads. Takes ad copy plus advertiser context, resolves and fetches the relevant Meta transparency-center policy pages at runtime, and returns a Pass / Fix Required / Block verdict with cited findings and rewrites.
For paid lead-gen and participant-recruitment ads, replaces vanity CPA with true CAC per qualified lead by joining ad-platform data with downstream funnel events, surfaces tracking gaps, and classifies every creative into Scale / Keep / Investigate / Cut.