Backlink Email Outreach Pipeline
Status: [Live]
The email outreach pipeline is a 3-agent BullMQ chain: researcher → crawler → email writer. Each stage enqueues the next on completion. A Campaign wraps the entire run; a CampaignEmail is created per prospect once the outreach email is drafted.
Pipeline Stages
backlink-researcher
└── [for each prospect] → website-crawler
└── [on crawl success] → backlink-outreach-writer
└── [all emails drafted] → Campaign: dm_reviewStage 1 — backlink-researcher
Trigger: Manual or from a deliverable run (DM initiates the campaign).
Input: Agent prompt with client context, target niche, and any seed domains or competitor URLs.
Process:
- LLM generates a JSON list of link-building prospects.
- Prospects are parsed and domain-normalised via
backlink-researcher.utils.ts. - One
Campaign(type:email_outreach, status:draft) is created. - For each prospect: a
Backlinkrow is created withoutreachStatus: "prospecting", then awebsite-crawlerjob is enqueued.
Output fields written to Backlink:
sourceDomain,sourceUrl,prospectType,relevanceScore,pitchRationale
Credit type: backlink_research
Stage 2 — website-crawler
Trigger: Enqueued by backlink-researcher per prospect.
Input: { backlinkId, tenantId, campaignId, domain }
Process:
- Check
CrawlCachefor a valid (<14 days old) entry on this domain. - On cache miss: spawn a Claude sub-process to crawl the domain’s contact page.
- Upsert
CrawlCachewith:contactPageUrl,recipientEmail,recipientName,rawExtract,isReachable,hasCaptcha,isBlocked. - Update
Backlink.sourceUrlwith the contact page URL. - Auto-create a
Contactrecord ifrecipientEmailwas found. - Enqueue
backlink-outreach-writer.
On failure or unreachable site: Decrement Campaign.totalEmails; skip the prospect (no email generated).
Timeout: 300 s (5 min per domain)
Stage 3 — backlink-outreach-writer
Trigger: Enqueued by website-crawler on success.
Input: { backlinkId, tenantId, campaignId }
Process:
- Load
Backlink,CrawlCache,AgentConfig,ClientContext,Campaign,Tenant. - Build prompt using: agent system prompt, client name, prospect domain,
prospectType,pitchRationale,contactPageUrl, crawl extract, and client context. - Reserve credits, execute Claude, parse JSON output
{ subject, body }. - Create/update
CampaignEmail(status:draft) with recipient info from the crawl cache. - Consume credits, publish
agent:completedevent. - Check campaign completion: if all emails are drafted → set
Campaign.status = "dm_review".
On failure: Decrement Campaign.totalEmails, release credits, log error.
Concurrency: 3 jobs
Lock duration: 180 s
Credit type: backlink_outreach
Status Lifecycles
Backlink outreachStatus
prospecting → outreach_sent → responded → agreed → published
↘ rejected
↘ no_responseCampaign status
draft → dm_review → dm_approved → sentCampaignEmail status
draft → approved → sentApproval Gates
| Gate | Who | Action |
|---|---|---|
| Campaign review | DM | Review all generated emails; approve individual CampaignEmail rows |
| Send individual | DM | POST /dm/v1/campaigns/:id/send-email/:emailId |
| Send all approved | DM | POST /dm/v1/campaigns/:id/send-all |
Clients are not in the approval chain for outreach emails — DMs own the full send workflow.
Prospect Types
prospectType | Description | Outreach angle |
|---|---|---|
resource-page | Site has a curated resource list; client content fits | ”Add our resource to your list” |
guest-post | Site accepts contributor articles | ”We’d love to contribute a post” |
broken-link | Site has a broken outbound link the client’s content can replace | ”We noticed a broken link — here’s a replacement” |
competitor-backlink | Site links to a competitor; client is a strong alternative | ”We do the same, and here’s why we’re a better fit” |
mention | Site mentioned the client without a link | ”You mentioned us — would you mind adding a link?” |
Crawl Cache
CrawlCache is tenant-agnostic — the same crawl result is reused across all tenants for 14 days. This avoids hammering the same domains repeatedly when multiple clients target overlapping prospect lists.
Fields stored: domain (unique), contactPageUrl, recipientEmail, recipientName, rawExtract, isReachable, hasCaptcha, isBlocked, crawledAt.