# Apify Link Prospecting Outreach

> Find sites ranking for target keywords, score every prospect with Ahrefs domain authority and page-level traffic, identify the strongest pitch angle per row ("links to competitor", "mentions brand without linking", "top-3 SERP", "resource page", "outdated content"), generate brand-voice-matched outreach emails using an outreach-type-aware template (unlinked-mention claim, competitor-link replacem…

- **Type:** Skill
- **Install:** `agentstack add skill-apify-awesome-skills-apify-link-prospecting-outreach`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [apify](https://agentstack.voostack.com/s/apify)
- **Installs:** 0
- **Category:** [Communication](https://agentstack.voostack.com/c/communication)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [apify](https://github.com/apify)
- **Source:** https://github.com/apify/awesome-skills/tree/main/skills/apify-link-prospecting-outreach

## Install

```sh
agentstack add skill-apify-awesome-skills-apify-link-prospecting-outreach
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Link Prospecting Outreach

Turn a goal + a target keyword + a URL the user wants to promote into a tiered, ready-to-send outreach list: SERP-ranking prospects with Ahrefs-scored authority, the strongest pitch angle per prospect, an outreach-type-matched email draft, and a copy-paste-ready link placement.

## Prerequisites
(No need to check it upfront)

- `.env` file with `APIFY_TOKEN`
- Ahrefs MCP available (the skill calls `mcp__claude_ai_Ahrefs__*` tools for prospect scoring)
- Node.js 20.6+ (for native `--env-file` support)
- One-time setup inside the skill's `scripts/` folder: `npm install`

## Helper scripts (one config, four steps)

After Step 1–2 inputs are collected, write them to a single **`campaign.json`** (schema in [`campaign.json.example`](campaign.json.example)). Every downstream script reads `--config campaign.json`, so the agent doesn't fork per-campaign copies. Sequence:

```bash
# 1. Run the Actor (writes {base}.json + sub-Actor sidecars when --fetch-sub-datasets)
node --env-file=.env scripts/run_actor.js --actor "apify/link-prospecting-tool" --input '' --timeout 1800 --fetch-sub-datasets --output {base}.json --format json

# 2. Build unified prospect table from the sidecars
python3 scripts/build_prospects.py --config campaign.json

# 3. (After Step 5 Ahrefs MCP calls → save to {base}_ahrefs_domain.json + {base}_ahrefs_page.json)
python3 scripts/enrich_prospects.py --config campaign.json

# 4. (After Step 8 sub-agents write outputs to /tmp/placement_outputs/row_*.json)
python3 scripts/merge_subagent_outputs.py --config campaign.json --outputs-dir /tmp/placement_outputs

# 5. Write the final xlsx + metadata sidecar
python3 scripts/write_xlsx.py --config campaign.json
```

If the runner's client-side wait elapses with the Actor still running on Apify, use `scripts/fetch_run_artifacts.js --run-id  --output {base}.json` instead of restarting. If the parent run is missing `SUB_ACTOR_RESULTS` (post-2026-05-20 Actor schema), `scripts/fetch_subactors_from_log.js` resolves sub-Actor runIds from the parent log.

## Workflow

Copy this checklist and track progress:

```
Task Progress:
- [ ] Step 1: Collect required anchor inputs incl. goal (block on these)
- [ ] Step 2: Collect brand voice, partnership type, output format
- [ ] Step 3: Run apify/link-prospecting-tool
- [ ] Step 4: Pull leads, mentions, authors, and sub-Actor datasets
- [ ] Step 5: Enrich every domain with Ahrefs metrics, assign Prospect Tier
- [ ] Step 6: Run skip pass — flag rows to drop before drafting
- [ ] Step 7: Compute "Why This Prospect" tag per surviving row
- [ ] Step 8: Compose per-row 3-artifact placement + outreach-type-aware email
- [ ] Step 9: Render output in chosen format
```

### Step 1: Required Anchor Inputs (ask FIRST, before anything else)

Do NOT proceed to Step 2 until every required input is answered. Surface them as the very first interaction. The dedup input (#7) is optional but must still be explicitly asked.

1. **Concrete goal for this campaign** — pick one preset or supply custom text. The goal drives skip-pass filtering, outreach-type template selection, and Prospect Tier thresholds. Required.

   | Preset | Effect downstream |
   |---|---|
   | `Recover unlinked brand mentions` | Skip pass drops every row where `brand_mentioned_in_source` is `false`. Default outreach type = `unlinked-mention-claim`. |
   | `Replace competitor links` | Skip pass drops every row not tagged `Links to competitor`. Default outreach type = `competitor-link-replacement`. |
   | `Topical authority links to specific URL` | No filter. Tier thresholds tighten (DR ≥ 50 for tier A). Default outreach type chosen per-row from `Why This Prospect`. |
   | `Maximum link volume from any relevant site` | No filter. Tier thresholds relax (DR ≥ 30 for tier A). Default outreach type chosen per-row. |
   | `Custom` | User-supplied paragraph; biases email tone and tier weights. No automatic skip filter. |

2. **Target keyword(s)** — one or more keywords the user wants their link to appear next to. The skill prospects the SERP for each. At least one required.
3. **Brand name** — the user's brand or product name. The Actor will not run without this (it is the `brand` input field).
4. **Product/category description** — one or two sentences describing what the user sells, who they sell to, and what category their product fits in. Example: *"Apify — web scraping platform that runs serverless scrapers as APIs. We sell to developers and data teams who need scraped data without managing infrastructure."* Required. Used in Step 6 (topical-fit gate) and Step 7 (adversarial-mention detection) to recognise prospects who are in the same product category — those won't link no matter the pitch. Without this, the skill cannot distinguish a genuine editorial opportunity from a competitor's blog.
5. **URL of content to link to** — the destination URL that will be inserted into partner articles. Required.
6. **Competitors** — anyone in the user's product category who would publish a "ours vs theirs" comparison page on their own site. Frame the ask this way explicitly: *"List every company that would write an X-vs-YourBrand comparison page. These won't link to you no matter what — small competitors count too."* Encourage 10+ entries; most users default to listing 3–5 obvious ones and miss the long tail. Mapped to `competitorDomains` on the Actor and reused in Steps 6 (adversarial-mention skip) and 7 (`Links to competitor` Why-tag).

   After the user answers, **offer** (do not push) an Ahrefs auto-pull of organic competitors: *"Want me to pull your top organic competitors from Ahrefs and add them to this list? Adds ~50 API units and surfaces smaller competitors you may have missed."* If the user says yes and Ahrefs MCP is available, call `mcp__claude_ai_Ahrefs__site-explorer-organic-competitors` on the user's domain (extracted from input #5) and merge results into `competitorDomains`. If Ahrefs is unavailable or the user declines, proceed with the user-supplied list only.
7. **Already-pitched domains (optional)** — domains the user has already contacted in past campaigns. Accept a comma-separated list, a CSV/Sheet path, or "none". The skill drops these in the skip pass so the user doesn't double-pitch. Not required to proceed.
8. **Number of organic results per keyword** — how many Google organic SERP results to prospect per keyword. Default 10 if the user is unsure, but ask the question so the user knows the lever exists. Mapped to `organicResult`.
9. **LLM sources to track** — multi-select. Each enabled engine queries an additional AI search/chat surface and adds Google Search Scraper sub-Actor cost per result fetched. Default: all enabled. Mapping to Actor input flags:

   | Option | Actor flag | Cost impact |
   |---|---|---|
   | ChatGPT Search | `enableChatGpt` | Per-result Google Search Scraper cost |
   | Gemini | `enableGemini` | Per-result Google Search Scraper cost |
   | Copilot (Microsoft / Bing) | `enableCopilot` | Per-result Google Search Scraper cost |
   | Perplexity | `enablePerplexity` | Per-result Google Search Scraper cost |
   | Google AI Mode | `enableAiMode` | Per-result Google Search Scraper cost |
   | Google AI Overviews | `enableAiOverviews` | **Free** — parsed from the SERP already fetched. Keep on regardless of budget. |

   Surface the multi-select to the user with all six pre-checked. Disabling individual engines is the main cost-cutting lever short of dropping `organicResult` — recommend keeping ChatGPT + Gemini on at minimum (they capture the largest share of LLM-driven discovery traffic in 2026).
10. **Run email verification?** — boolean. Default: `yes`. Mapped to `enableEmailVerification` on the Actor. When enabled, the Actor verifies every email returned by the Contact Details Scraper sub-Actor and tags each lead with a verification status (`verified` / `catch-all` / `risky` / `invalid` / `unknown`). The skill uses the status in Step 6 (invalid emails get auto-skipped) and surfaces it as the `Email Verification` column in the output. Disable only if the user is rate-limited on verification quota or running cost-tight smoke tests.

Once 1–6 and 8–10 are captured (7 is optional), move on.

### Step 2: Secondary Inputs

Ask these next:

1. **Brand info and voice** — a short paragraph describing the product/brand and the tone for outreach (e.g., "casual and helpful", "formal B2B", "founder-led"). Used verbatim to shape every generated email.
2. **Partnership type** — the *offer* the user is willing to make. Determines the offer paragraph substituted into the per-row email. Outreach-type template selection happens separately, per-row, in Step 8.

   | Option | What it offers |
   |---|---|
   | ABC link exchange | Three-way link swap: partner links to user, user links to a third party, third party links to partner. |
   | Direct A B link exchange | Two-way link swap: partner links to user, user links to partner. |
   | Resource page / list inclusion | Ask to be added to an existing curated list or roundup. No reciprocal link offered. |
   | Unilateral ask (no reciprocal) | User asks for the link without offering anything in return — appropriate for unlinked-mention claims and broken-link replacements. |
   | Other | User types their own offer (paid placement, free product, co-authored content, etc.). |

3. **Output format**:

   | Format | Behavior |
   |---|---|
   | `xlsx` | `run_actor.js` writes a styled spreadsheet to disk. |
   | `markdown` | Agent renders the table inline in chat with email drafts beneath each row. |

### Step 3: Run the Actor

The Actor ID is `apify/link-prospecting-tool`. Full input schema lives in `reference/apify-actor-usage.md`.

Recommended call payload for this skill (defaults chosen for outreach-first workflow):

```json
{
  "queries": "\n",
  "brand": "",
  "ownDomains": [""],
  "competitorDomains": [],
  "ignoreDomains": [
    "wikipedia.org", "github.com", "stackoverflow.com", "stackexchange.com",
    "reddit.com", "quora.com", "youtube.com", "twitter.com", "x.com",
    "linkedin.com", "facebook.com", "medium.com", "archive.org",
    "chromewebstore.google.com", "addons.mozilla.org", "apps.apple.com",
    "play.google.com", "microsoftedge.microsoft.com", "marketplace.visualstudio.com"
  ],
  "organicResult": 10,
  "maxContactsPerDomain": 3,
  "department": ["marketing"],
  "searchAuthorName": true,
  "includeMention": true,
  "enableChatGpt": true,
  "enableGemini": true,
  "enableCopilot": true,
  "enablePerplexity": true,
  "enableAiMode": true,
  "enableAiOverviews": true,
  "enableEmailVerification": true
}
```

The six `enable*` LLM-source flags map 1:1 to the user's Step 1 input #9 multi-select. Pass `false` for any engine the user deselected. `enableEmailVerification` maps to Step 1 input #10.

The `ignoreDomains` default includes two groups:
- **Giants and UGC** (wikipedia, github, stackoverflow, reddit, etc.) — too broad to pitch as editorial partners.
- **App / extension marketplaces** (Chrome Web Store, Firefox Add-ons, Apple/Google Play, VS Code Marketplace, etc.) — product directory listings, no editorial decision-makers.

**Do NOT** auto-add to `ignoreDomains` (let the user decide):
- UGC/community sites like `kaggle.com`, `dev.to`, `substack.com`, `producthunt.com`, `g2.com`, `capterra.com`, `trustpilot.com` — some users get real value pitching these.
- API directories like `rapidapi.com`, `programmableweb.com`, `publicapis.dev` — relevant for some products (especially developer-tool brands), irrelevant for others. Surface these as candidates only if the user wants to add them.

The URL-pattern skip rules in Step 6 catch the per-row noise (subdomain prefixes, path patterns) that `ignoreDomains` can't express.

`department` defaults to `["marketing"]` only. The skill prioritises editorial-leaning contacts within the returned `marketing` department during row composition (see Step 8). Only add `sales` if the user explicitly wants BD-style partnership pitches. Only add `c_suite` if the prospect domains are very small (1–5 person shops) where the founder may also be the editor.

Call the runner script:

```bash
node --env-file=.env ${CLAUDE_PLUGIN_ROOT}/scripts/run_actor.js \
  --actor "apify/link-prospecting-tool" \
  --input 'JSON_INPUT' \
  --timeout 1800 \
  --fetch-sub-datasets \
  --output YYYY-MM-DD_outreach.json \
  --format json
```

Notes:
- `--timeout 1800` is the recommended client-side wait. The Actor itself runs 15-50+ min depending on keyword count, LLM-engine fan-out, and `enableEmailVerification`. Past calibration runs land in the 20–55 min range. Bumping the default avoids the partial-result situation where the runner gives up but the Actor keeps going.
- If the client-side wait still elapses with the Actor still running on Apify (status `RUNNING` or `READY` when the runner exits), do **not** restart the Actor. Use `scripts/fetch_run_artifacts.js --run-id  --output ` to poll the existing run and download all artifacts — same output shape as `run_actor.js --fetch-sub-datasets`.
- `--fetch-sub-datasets` downloads sibling files alongside the main output: `*_mentions.json`, `*_authors.json`, `*_serp.json`, `*_wcc.json`. You need all of them to populate every output column.

### Step 4: Access All Datasets

The Actor's output schema **changed on or before 2026-05-20**. The build_prospects script must handle the new shape; older skill versions that joined a separate MENTIONS dataset are broken.

**Current schema (verified 2026-05-20):**

| File written by runner / fetcher | Source | Populates |
|---|---|---|
| `*_output.json` (main) | "All leads" dataset | `Contact Full Name`, `Contact Job Title`, `Department`, `Seniority`, `Contact Email`, `Email Verification` (when `enableEmailVerification: true`), `Contact LinkedIn`, `Company`, `Domain`. **Each lead's `source_url[]` array contains the article URLs that produced this contact, each with a `brand_mentioned_in_source` boolean** — this is the new home of the per-(URL, contact) mention data. |
| `*_serp.json` | Google Search Results Scraper sub-Actor (one item per `(query × engine)` combination) | `SERP Position`, `Article Title`, `Publish Date` (via `organicResults[]`), and **engine attribution per URL** (Google Organic, ChatGPT, Gemini, Copilot, Perplexity, Google AI Mode) by joining `aiModeResult.sources[]`, `perplexitySearchResult.sources[]`, `chatGptSearchResult.sources[]`, `geminiSearchResult.sources[]`, `copilotSearchResult.sources[]`. URLs from ChatGPT carry a `?utm_source=chatgpt.com` query suffix — normalise URLs (strip tracking params) before joining. |
| `*_wcc.json` | Website Content Crawler sub-Actor | `Placement Source Sentence`, `Placement With Link`, `Placement New Insertion`, `Article Author` cross-check, outbound-link inspection for `Links to competitor` and `Resource / roundup page` tags. **Canonical URL list for building rows** — every URL that got body-crawled appears here, including ones that didn't yield a lead. |
| `*_authors.json` | AI Web Scraper sub-Actor (when `searchAuthorName: true`) | `Article Author`, `Author Source` (set to `searchAuthorName`). Note: this sub-Actor frequently TIMES-OUT at its 300s default — partial results are still saved. |

**What changed (vs. pre-2026-05-20 runs):**
1. No separate `MENTIONS` / `AUTHORS` / `DOMAINS_WITH_LEADS` named datasets — mention info is folded into `main_leads[i].source_url[]`.
2. No `SUB_ACTOR_RESULTS` record in the parent run's key-value store. Sub-Actor runIds are now only discoverable from the parent run log via regex `\[apify\. runId:([A-Za-z0-9]+)\]`. The runner script's `--fetch-sub-datasets` flag now falls back to log-parsing when the KV index is missing; the standalone `scripts/fetch_subactors_from_log.js` does the same for runs whose runner already exited.
3. The mentions schema reduced: `source_url[i]` carries only `{domain, brand_mentioned_in_source, url}` — no per-engine flags like the old `ChatGPT_mention` / `Perplexity_

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [apify](https://github.com/apify)
- **Source:** [apify/awesome-skills](https://github.com/apify/awesome-skills)
- **License:** Apache-2.0

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-apify-awesome-skills-apify-link-prospecting-outreach
- Seller: https://agentstack.voostack.com/s/apify
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
