Install
$ agentstack add skill-felipesalinasr-fsr-stack-linkedin-comment-to-outreach-gsheets ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
LinkedIn Comment to Outreach Pipeline (Google Sheets)
Overview
End-to-end orchestrator: LinkedIn post URL in, paused Instantly cold email campaign out. Leads are stored in Google Sheets (not Airtable), enriched via Apollo + Hunter.io fallback, sequenced, and loaded into Instantly. Every external tool call goes through Composio first when available. Every phase maximizes parallelism to finish faster without cutting corners.
One key difference from the Airtable version: Google Sheets can write hundreds of rows in a single API call, so Phase 2 collapses from "4 parallel batch-loading agents" to one bulk write. That time savings compounds.
Another difference: when total enrichment (Apollo + Hunter.io) returns fewer than 20 leads with verified emails, the sequence-writing phase runs on autopilot -- no interactive review, just write all 3 emails and move straight to Instantly. For 20+ leads, the user co-writes each email as usual.
Hunter.io fallback: After Apollo enrichment, contacts that matched a person but have no email are sent to Hunter.io's Email Finder in parallel (8 concurrent threads). This typically recovers 10-20% of the missing emails, boosting total email coverage by 5-15 percentage points. Only contacts with company info are searched -- Hunter requires a company or domain to work.
When to use
- User provides a LinkedIn post URL and wants to reach out to its commenters via Google Sheets
- User asks to "run the linkedin pipeline with sheets", "scrape and email", or "linkedin comment to outreach gsheets"
- User wants the complete path from a LinkedIn post to a live Instantly campaign, stored in Google Sheets
- User needs commenters scraped, stored in Sheets, enriched with Apollo, sequenced, and loaded into Instantly in one flow
When NOT to use
- Targeting people who reacted to a post (use
linkedin-reaction-to-outreachinstead) - User specifically wants Airtable (use
linkedin-comment-to-outreachinstead) - Just scraping commenters with no outreach (use
linkedin-comment-scraperdirectly) - Just enriching an existing list (use
apollo-enrichmentdirectly) - Just writing a cold email sequence without a lead source (use
cold-email-sequencedirectly) - Just creating an Instantly campaign from leads you already have (use
instantly-campaigndirectly)
Prerequisites
- Composio toolkits connected:
googlesheets,apollo,hunter,instantly - Apify MCP server: Connected with
mcp__apify__*tools available (Composio fallback:APIFY_RUN_ACTOR_SYNC) - User input needed for Phase 4 (if 20+ leads): Social proof, product description, CTA preferences, sender name
Before starting, verify each Composio toolkit is connected. If any is missing, use composio link --no-wait and present the redirect URL to the user. Do all missing connections in parallel -- don't chain them sequentially.
Core Principles
1. Composio First
For every external tool call, try the Composio tool slug first. Fall back to MCP or direct API only if Composio fails or the toolkit isn't connected. This applies universally:
| Service | Composio slug (primary) | Fallback | |---------|------------------------|----------| | Apify | APIFY_RUN_ACTOR_SYNC | mcp__apify__call-actor | | Google Sheets | GOOGLESHEETS_* (see slug table below) | None -- Sheets IS the chosen platform | | Apollo | APOLLO_PEOPLE_ENRICHMENT (parallel single calls) | APOLLO_BULK_PEOPLE_ENRICHMENT (less reliable) | | Hunter.io | HUNTER_EMAIL_FINDER (parallel single calls) | None -- Hunter IS the fallback | | Instantly | INSTANTLY_* (see slug table below) | Direct REST API via curl |
2. Maximum Parallelism
Within every phase, run independent operations concurrently. Between phases, pre-fetch data for the next phase whenever possible. Specific opportunities are called out in each phase below.
3. Auto-Mode for Small Lists
If Apollo enrichment returns fewer than 20 leads with verified emails, skip the interactive email-writing review. Write all 3 emails using the James Shields framework and the context gathered in Phase 1, then proceed directly to Instantly. The user reviews the final campaign in Instantly's dashboard instead of approving each email in chat.
The Pipeline
LinkedIn Post URL
|
v
[Phase 1: SCRAPE] -- Apify via MCP or Composio, deduplicate
|
v
[Phase 2: STORE] -- Create Google Sheet, bulk write all rows
|
v
[Phase 3: ENRICH (Apollo)] -- Apollo parallel single enrichment, batch-update Sheet
| (pre-fetch Instantly accounts here)
v
[Phase 3.5: ENRICH (Hunter)]-- Hunter.io fallback for contacts Apollo missed emails on
| (parallel single calls, 8 threads, update Sheet)
v
[Phase 4: WRITE] -- Co-write 3 emails (or auto-write if = 50` to avoid low-confidence guesses.
Name splitting: same rule as Apollo -- split `fullName` on the FIRST space only.
**Why parallel single calls:** Same rationale as Apollo. Hunter has no reliable bulk endpoint via Composio. Individual calls at 8 concurrent threads give the best throughput and visibility into per-person results.
Expected recovery rates:
- 10-20% of Apollo no-email contacts will get an email from Hunter
- Overall pipeline email coverage typically jumps 5-15 percentage points
- LATAM/emerging market recovery rates may be lower (5-10%)
Collect results into `./output/hunter_results.json`:
```json
{
"idx": 15,
"row": 17,
"fullName": "Jane Smith",
"email": "jane@company.com",
"score": 85,
"source": "hunter",
"profileUrl": "https://www.linkedin.com/in/janesmith"
}
Step 3: Merge results and update Sheet + Apollo
3a -- Merge into apollo_contacts.json: For each Hunter result with an email, update the corresponding record in ./output/apollo_contacts.json to add the email (mark email_source: "hunter" to distinguish from Apollo-found emails).
3b -- Batch-update Google Sheet: Same pattern as Phase 3 Step 3 C1. For each newly found email, update:
J(Lead Status) = "Enriched (Hunter)"K(Email) = Hunter email
Use GOOGLESHEETS_UPDATE_VALUES_BATCH with camelCase valueInputOption.
3c -- Create Apollo contacts for new emails: Add newly found contacts to Apollo using APOLLO_CREATE_BULK_CONTACTS (max 100 per batch, no label_names at root level -- put inside each contact object).
Run 3b and 3c in parallel.
Step 4: Final count
Re-count total emails (Apollo + Hunter). This combined count determines whether Phase 4 runs in auto-mode or interactive mode.
Checkpoint: "Hunter.io recovered X additional emails. Total: Y out of Z people now have emails (W% combined match rate). Y contacts ready for outreach."
Phase 4: Write Email Sequence
Auto-mode (fewer than 20 leads with emails)
Small lists don't justify a multi-round interactive review. Write all 3 emails yourself using the James Shields framework:
- Gather context from earlier phases: the trigger (the LinkedIn post), the author, the audience profile (from headlines/companies), and any product/offer info from the sales context.
- If product, social proof, and offer are not in the sales context, ask the user ONE consolidated question with all missing items.
- Write all 3 emails (Day 0 opener, Day 3 follow-up, Day 7 breakup) following the rules in
cold-email-sequence. - Save to
{campaign-name}-sequence.md. - Show the user the complete sequence and say: "Wrote 3 emails for your X leads. Loading into Instantly now. You can tweak the copy in the Instantly dashboard before activating."
Interactive mode (20+ leads with emails)
Follow the cold-email-sequence skill procedure exactly: gather inputs, draft Email 1, get approval, draft Email 2, get approval, draft Email 3, get approval, save.
James Shields rules (both modes):
- Personalize subject (not body), lowercase, 2-4 words
- 3 sentences + PS per email
- Low-friction CTA ("Reply 'I'm in'"), never a booking link
- No em dashes, no exclamation points, no formatting
- NEW social proof for each email (never repeat)
- Follow-up subjects: empty string (same thread)
Checkpoint (interactive only): "Sequence locked. Ready to load into Instantly."
Phase 5: Launch on Instantly
Step 1: Resolve credentials
Try Composio first:
- Is the
instantlyComposio toolkit connected? (If you already ranINSTANTLY_LIST_ACCOUNTSin Phase 3, you know the answer.) - If not connected, use
composio link instantly --no-waitand present the redirect URL. - If Composio unavailable, check
INSTANTLY_API_KEYenv var. - Last resort: ask for the API key in chat (ephemeral, session-only, never written to disk).
Step 2: Create campaign
Sanitize ALL email bodies first -- replace every & character with "and" (Instantly silently drops bodies containing &).
CRITICAL Composio/Instantly field notes:
- Do NOT include
delay_unitin sequence steps -- the Composio schema doesn't support it. Thedelayfield is already in days. - Timezone: pick one appropriate for the audience. Not all IANA timezone strings work with Composio's Instantly integration. Known working:
America/New_York,America/Chicago,America/Denver,America/Los_Angeles,America/Bogota,America/Sao_Paulo,Europe/London,Europe/Berlin. Known rejected:America/Vancouver. When in doubt, use the major city timezone closest to the audience. - Follow-up email subjects: use empty string
""to thread under the original subject.
composio execute INSTANTLY_CREATE_CAMPAIGN -d '{
"name": "LinkedIn - {Author} {Topic}",
"campaign_schedule": {
"schedules": [{
"name": "Weekday Schedule",
"timing": { "from": "08:00", "to": "17:00" },
"days": { "0": true, "1": true, "2": true, "3": true, "4": true, "5": false, "6": false },
"timezone": "America/New_York"
}]
},
"sequences": [{
"steps": [
{ "type": "email", "delay": 0,
"variants": [{ "subject": "subject line", "body": "Email 1 body" }] },
{ "type": "email", "delay": 3,
"variants": [{ "subject": "", "body": "Email 2 body" }] },
{ "type": "email", "delay": 4,
"variants": [{ "subject": "", "body": "Email 3 body" }] }
]
}],
"email_list": ["sender@domain.com"],
"stop_on_reply": true,
"text_only": true,
"link_tracking": false,
"open_tracking": true,
"daily_limit": 50,
"daily_max_leads": 50,
"email_gap": 5,
"random_wait_max": 10
}'
Delay math: Delays are RELATIVE to the previous step, not absolute from Day 0. So for Day 0 / Day 3 / Day 7: step 1 delay=0, step 2 delay=3, step 3 delay=4 (because 3+4=7).
Save the returned campaign id. Verify all step bodies are non-empty (ampersand check).
Step 2.5: Verify the campaign was created correctly
composio execute INSTANTLY_GET_CAMPAIGN -d '{
"id": "{CAMPAIGN_ID}"
}'
CRITICAL: This endpoint uses id as the parameter name, NOT campaign_id. Check that all 3 email step bodies are populated and the schedule/timezone look correct.
Parallel block D (run together):
D1 -- Bulk load leads:
composio execute INSTANTLY_ADD_LEADS_BULK -d '{
"campaign_id": "{CAMPAIGN_ID}",
"leads": [
{ "email": "j@co.com", "first_name": "Jane", "last_name": "Smith", "company_name": "Co" },
...
]
}'
Max 1000 leads per call. Check leads_uploaded and invalid_email_count.
D2 -- Attach all sending accounts: If the campaign was created with only one account, patch to include all:
composio execute INSTANTLY_UPDATE_CAMPAIGN -d '{
"campaign_id": "{CAMPAIGN_ID}",
"email_list": ["email1@...", "email2@...", "email3@..."]
}'
Step 3: Leave paused
NEVER auto-activate. Campaign starts paused.
Final report to user:
- Campaign name and status (paused)
- Number of leads loaded
- Sending accounts attached
- Schedule: Mon-Fri, 8:00-17:00 in the audience's timezone
- Google Sheet URL for reference
- "Review in Instantly dashboard. Say 'go' when ready to launch."
To activate when user approves:
composio execute INSTANTLY_ACTIVATE_CAMPAIGN -d '{ "campaign_id": "{CAMPAIGN_ID}" }'
Composio Tool Slug Reference
| Service | Action | Composio slug | Key gotchas | |---------|--------|---------------|-------------| | Apify | Get actor info | APIFY_GET_ACTOR | | | Apify | Run actor (sync) | APIFY_RUN_ACTOR_SYNC | Input field is posts not urls | | Sheets | Create spreadsheet | GOOGLESHEETS_CREATE_GOOGLE_SHEET1 | | | Sheets | Get tab names | GOOGLESHEETS_GET_SHEET_NAMES | Always call after create -- tab name is locale-dependent | | Sheets | Write/update values | GOOGLESHEETS_VALUES_UPDATE | Uses snake_case: value_input_option | | Sheets | Batch update | GOOGLESHEETS_UPDATE_VALUES_BATCH | Uses camelCase: valueInputOption | | Sheets | Append rows | GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND | | | Sheets | Read values | GOOGLESHEETS_VALUES_GET | | | Apollo | Single enrichment | APOLLO_PEOPLE_ENRICHMENT | Preferred. Uses organization_name. No reveal_phone_number. | | Apollo | Bulk enrichment | APOLLO_BULK_PEOPLE_ENRICHMENT | Unreliable in parallel -- use single instead. Uses company_name. | | Apollo | Bulk create contacts | APOLLO_CREATE_BULK_CONTACTS | | | Apollo | Check credits | APOLLO_VIEW_API_USAGE_STATS | | | Hunter | Email finder | HUNTER_EMAIL_FINDER | Uses company (not domain) when only name known. max_duration 3-20s. Score >= 50 to accept. | | Hunter | Check credits | HUNTER_ACCOUNT_INFORMATION | searches.available/used for quota. | | Instantly | List accounts | INSTANTLY_LIST_ACCOUNTS | | | Instantly | Get campaign | INSTANTLY_GET_CAMPAIGN | Uses id (NOT campaign_id) | | Instantly | Create campaign | INSTANTLY_CREATE_CAMPAIGN | No delay_unit in steps. Timezone must be a major city. | | Instantly | Update campaign | INSTANTLY_UPDATE_CAMPAIGN | | | Instantly | Bulk add leads | INSTANTLY_ADD_LEADS_BULK | | | Instantly | Activate campaign | INSTANTLY_ACTIVATE_CAMPAIGN | |
Timing Expectations
| Phase | Duration | Notes | |-------|----------|-------| | 1. Scrape | 2-5 min | MCP Apify handles async polling automatically | | 2. Store | ~30 sec | One bulk write -- include tab name discovery | | 3. Enrich (Apollo) | 5-15 min | 8 concurrent single Apollo calls. Sheet batch-update adds ~30s. | | 3.5. Enrich (Hunter) | 3-8 min | 8 concurrent Hunter calls on Apollo no-email contacts. Typically 10-20% recovery. | | 4. Write | 1-2 min (auto) / 10-30 min (interactive) | Auto-mode skips interactive review for small lists | | 5. Launch | 1-3 min | Instantly accounts pre-fetched during Phase 3 | | Total | ~13-30 min (auto) / ~23-60 min (interactive) | |
Common Mistakes
| Phase | Failure mode | Fix | |-------|--------------|-----| | 1 | Zero results: used urls instead of posts | Apify actor field is posts. Always check actor input schema. | | 1 | Only 20 results returned | Actor defaults maxItems to 20. Always override to 1000. | | 1 | Field names don't match expected | Actor returns name/position/linkedinUrl, not fullName/headline/profileUrl. Map them. | | 1 | Invalid LinkedIn URL | Validate format, resolve redirects via WebFetch. | | 2 | "Range not found" error | Tab name is locale-dependent ("Hoja 1", "Feuille 1", etc.). Discover with GOOGLESHEETS_GET_SHEET_NAMES. | | 2 | value_input_option error | GOOGLESHEETS_VALUES_UPDATE uses snake_case. GOOGLESHEETS_UPDATE_VALUES_BATCH uses camelCase. They're inconsistent. | | 2 | values rejected | Must be a native JSON array, not a stringified array. | | 2 | 403 on sheet creation | Google Sheets Composio scopes may be insufficient. Re-link with composio link googlesheets --no-wait. | | 2 | Values array not rectangular | Pad empty cells with "". Every row must have 15 columns. | | 3 | Bulk Apollo returns 0 matches
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: felipesalinasr
- Source: felipesalinasr/fsr-stack
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.