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
| Decision | Choice | Rationale |
|---|---|---|
| Personalisation grounding | Evidence-grounded LLM — every personalisation pulls from Step 7.2 verbatim quotes + JD requirements | Auditable. Recruiter can verify “why did this email mention X.” Same hallucination defence as Step 7.2. |
| Voice + tone | Per-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 cadence | Email FIRST, LinkedIn SECOND. 48h gap unless email bounces. | Email is higher signal (verified address); LinkedIn is supplementary. Avoids double-touch fatigue. |
| Per-tenant scope | Each tenant brings own Instantly/Smartlead workspace + own LinkedIn account | Multi-tenancy hook for Step 10. Outreach quotas + warm-up live in tenant scope. |
| Email tool | Decision pending: Instantly vs Smartlead | Both viable. Greenlight item. |
| LinkedIn tool | Decision pending: LinkedHelper / Phantombuster / Calibre-managed manual queue | All three carry ToS risk. Greenlight item. |
| Response signal taxonomy | Locked 5 states: interested, deferred, declined, unreachable, no_response_yet | Bounded set. Each maps to a candidate-record action (recycle / cold / decline / dead / nudge). |
| Re-pool logic | deferred → re-pool for matching against future JDs in 90 days. declined → permanent skip. | Salvages otherwise-good fits whose timing was off. |
| Audit log | Every 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:
- Send
linkedin_connect_note(connection request). - Wait 72h for accept.
- If accepted: send
linkedin_inmail_body. - Wait 7d for response.
- If no response: one follow-up (50-word, short).
- 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:
| Signal | Detection | CandidateRecord action |
|---|---|---|
interested | Reply text contains accept signals (per LLM classifier) | Tag outreach_status: interested. Notify Calibre recruiter inbox. |
deferred | Reply text contains “not now”, “later”, “Q3” etc. | Tag + re-pool eligible after 90 days. |
declined | Reply text contains “no thanks”, “not interested” | Tag + permanent skip. |
unreachable | Email bounce + LinkedIn connection rejected | Tag + flag email/LinkedIn as stale. |
no_response_yet | Default state | Continue 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:
| Task | Sub-step | File |
|---|---|---|
| 1 | 9.1 | outreach/types.ts — schemas above |
| 2 | 9.1 | outreach/personalisation.ts — Claude call + tone-guide loader |
| 3 | 9.2 | outreach/email-send.ts — chosen tool client (Instantly OR Smartlead) |
| 4 | 9.3 | outreach/linkedin-sequence.ts — chosen tool client + cadence state machine |
| 5 | 9.4 | outreach/response-handler.ts — webhook handler + LLM classifier |
| 6 | All | DB tables: outreach_drafts, email_send_records, linkedin_touch_records, response_signals |
| 7 | All | Orchestrator: ingests Step 8.1 shortlists; dispatches 9.1 → 9.2 → 9.3 in cadence |
| 8 | All | Audit-replay CLI: outreach-replay --candidate-id=... reconstructs the sequence |
| 9 | All | End-to-end test: synthetic shortlist of 5 → email → LinkedIn → mock responses |
6. Cost + Risk
| Item | Estimate |
|---|---|
| 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