Skip to Content
PlansOutreach Pipeline — Design Spec (Step 9)

Outreach Pipeline — Design Spec (Step 9)

Source: plans/pipeline-9-outreach-design.md · synced 2026-06-12

Date: 2026-06-11 Covers steps: 9.1 (Personalisation engine) · 9.2 (Email send) · 9.3 (LinkedIn sequencing) · 9.4 (Response handling) Status: Draft — awaiting greenlight. Depends on: ✅ Step 8.1 verdict synthesis (Strong yes + Soft yes flow here). ⏳ Step 5.0 candidate-merge (CandidateRecord carries the email + LinkedIn URL this pipeline writes to). ⏳ Calibre brand-voice / tone guide reference for prompt templates. Grant mapping: Annex 3 Deliverable AD3 — “AI-Driven Candidate Outreach”. See grant/scope.md §“AI-Driven Candidate Outreach”.


1. Context

Step 8.1 emits a shortlist verdict per (JD, candidate) pair. Strong yes + Soft yes flow downstream to outreach. Step 9 turns each shortlisted candidate into a personalised, multi-channel message sequence and captures responses back into the candidate database.

Four sub-steps:

  • 9.1 Personalisation — LLM generates per-candidate email subject + body + LinkedIn message body grounded in (NormalisedJD, CandidateRecord, MatchRecord) evidence.
  • 9.2 Email send — Outbound delivery via Instantly or Smartlead (deliverability + tracking).
  • 9.3 LinkedIn sequencing — Connection request → wait → InMail → follow-up. Tool TBD (LinkedHelper / Phantombuster / manual queue).
  • 9.4 Response handling — Webhook listeners auto-tag candidate records on signal (opened, replied, deferred, declined, unreachable). Feeds re-pool logic for parked candidates.

2. Architectural Decisions

DecisionChoiceRationale
Personalisation groundingEvidence-grounded LLM — every personalisation pulls from Step 7.2 verbatim quotes + JD requirementsAuditable. Recruiter can verify “why did this email mention X.” Same hallucination defence as Step 7.2.
Voice + tonePer-vertical templates loaded from reference/verticals/ §“Outreach tone notes”Calibre’s brand voice preserved per vertical. Top-tier shop alumni get a different register than a back-office reach-out.
Channel cadenceEmail FIRST, LinkedIn SECOND. 48h gap unless email bounces.Email is higher signal (verified address); LinkedIn is supplementary. Avoids double-touch fatigue.
Per-tenant scopeEach tenant brings own Instantly/Smartlead workspace + own LinkedIn accountMulti-tenancy hook for Step 10. Outreach quotas + warm-up live in tenant scope.
Email toolDecision pending: Instantly vs SmartleadBoth viable. Greenlight item.
LinkedIn toolDecision pending: LinkedHelper / Phantombuster / Calibre-managed manual queueAll three carry ToS risk. Greenlight item.
Response signal taxonomyLocked 5 states: interested, deferred, declined, unreachable, no_response_yetBounded set. Each maps to a candidate-record action (recycle / cold / decline / dead / nudge).
Re-pool logicdeferred → re-pool for matching against future JDs in 90 days. declined → permanent skip.Salvages otherwise-good fits whose timing was off.
Audit logEvery email + LinkedIn message persisted with (jd_id, candidate_id, template_version, llm_prompt_hash, sent_at, response_signal)Grant-defensible. Recruiter can replay any sequence.

3. End-to-End Flow

Step 8.1 Shortlist (Strong yes + Soft yes) │ ▼ 9.1 Personalise ─→ EmailDraft + LinkedInDraft (per candidate) │ ▼ (recruiter approve / edit / skip) ▼ 9.2 Email send via Instantly|Smartlead ─→ delivered, opened, clicked │ ▼ (48h gap if no response) ▼ 9.3 LinkedIn — connect → InMail → follow-up │ ▼ 9.4 Response handler — webhook → signal taxonomy → CandidateRecord enrichment │ ▼ Re-pool decision (deferred → 90d wait; declined → permanent)

4. Sub-step Specs

9.1 — Personalisation engine

Input: (NormalisedJD, CandidateRecord, MatchRecord) per shortlisted candidate. MatchRecord carries Step 7.2 evidence quotes (which CV passages won which rubric dimension).

Output:

interface OutreachDraft { candidate_id: string; jd_id: string; email_subject: string; // ≤ 60 chars email_body: string; // ≤ 180 words, evidence-cited linkedin_connect_note: string; // ≤ 300 chars, LinkedIn's limit linkedin_inmail_body: string; // ≤ 1900 chars template_version: string; // for audit replay llm_prompt_hash: string; // SHA256 of the prompt that generated this draft evidence_anchors: string[]; // verbatim CV quotes the email references generated_at: string; status: "drafted" | "approved" | "edited" | "skipped" | "sent"; }

LLM: Claude tool-use call per shortlisted candidate.

Code target: calibre-app/src/lib/outreach/personalisation.ts.

9.2 — Email send

Input: OutreachDraft[] with status: "approved" or "edited".

Tool decision pending — Instantly vs Smartlead. Both support webhook-style delivery callbacks.

Output: EmailSendRecord[] — one per dispatched email with (message_id, delivered, opened, clicked, bounced, replied) state stream.

Code target: calibre-app/src/lib/outreach/email-send.ts.

Warm-up + domain rotation: per-tenant config. Calibre’s existing domain is already warm.

9.3 — LinkedIn sequencing

Input: OutreachDraft[] where the candidate hasn’t replied to email within 48h.

Tool decision pending — LinkedHelper / Phantombuster / Calibre-managed manual queue.

Cadence:

  1. Send linkedin_connect_note (connection request).
  2. Wait 72h for accept.
  3. If accepted: send linkedin_inmail_body.
  4. Wait 7d for response.
  5. If no response: one follow-up (50-word, short).
  6. Stop.

Output: LinkedInTouchRecord[] — (touch_number, sent_at, response_received_at, response_kind).

Code target: calibre-app/src/lib/outreach/linkedin-sequence.ts.

9.4 — Response handling

Input: Webhook callbacks from email tool + LinkedIn tool.

Signal taxonomy:

SignalDetectionCandidateRecord action
interestedReply text contains accept signals (per LLM classifier)Tag outreach_status: interested. Notify Calibre recruiter inbox.
deferredReply text contains “not now”, “later”, “Q3” etc.Tag + re-pool eligible after 90 days.
declinedReply text contains “no thanks”, “not interested”Tag + permanent skip.
unreachableEmail bounce + LinkedIn connection rejectedTag + flag email/LinkedIn as stale.
no_response_yetDefault stateContinue waiting until end of LinkedIn cadence.

Code target: calibre-app/src/lib/outreach/response-handler.ts.

LLM classifier: light tool-use call per inbound reply — (reply_text) → signal.


5. Implementation Plan (separate file pending greenlight)

plans/pipeline-outreach-impl.md. Outline:

TaskSub-stepFile
19.1outreach/types.ts — schemas above
29.1outreach/personalisation.ts — Claude call + tone-guide loader
39.2outreach/email-send.ts — chosen tool client (Instantly OR Smartlead)
49.3outreach/linkedin-sequence.ts — chosen tool client + cadence state machine
59.4outreach/response-handler.ts — webhook handler + LLM classifier
6AllDB tables: outreach_drafts, email_send_records, linkedin_touch_records, response_signals
7AllOrchestrator: ingests Step 8.1 shortlists; dispatches 9.1 → 9.2 → 9.3 in cadence
8AllAudit-replay CLI: outreach-replay --candidate-id=... reconstructs the sequence
9AllEnd-to-end test: synthetic shortlist of 5 → email → LinkedIn → mock responses

6. Cost + Risk

ItemEstimate
Claude personalisation per candidate~USD 0.05
Email send (Instantly/Smartlead)~USD 0.001 per email (within plan)
LinkedIn tool (any)~USD 50–150/month per LinkedIn account
LLM response classifier per reply~USD 0.01
Per-shortlist (assume 6 shortlisted/JD, 4 touches each)~USD 0.50 + email-tool plan amortisation

Risk: LinkedIn ToS. All third-party LinkedIn automation tools violate ToS. Mitigation: per-tenant warm-up + low daily caps + fallback to Calibre-managed manual queue if account flagged.

Risk: Deliverability. Cold-email open rates depend on domain warm-up. Mitigation: use Calibre’s warm domain for tenant 1; new tenants get warm-up guidance + cost transparency.


7. Dependencies

  • Step 8.1 must ship before Step 9 has shortlists to consume.
  • Step 5.0 ships CandidateRecord.primary_email + linkedin_url — the addressing fields.
  • Step 7.2 ships verbatim CV quotes — the evidence anchors for personalisation.
  • reference/verticals/ §“Outreach tone notes” — domain content Daniel + Jon need to ratify.

8. Greenlight Checklist for Ben

  • Approve email-first / LinkedIn-second cadence (48h gap)
  • Decide: Instantly vs Smartlead for 9.2
  • Decide: LinkedHelper / Phantombuster / manual queue for 9.3
  • Approve 5-signal response taxonomy + re-pool logic
  • Authorise impl plan draft (pipeline-outreach-impl.md)
  • Schedule Calibre team session to populate per-vertical tone-guide content
Last updated on