Module: Outreach (Phase 2)
The platform builds the sender layer — it does not rent one. Emails go out from VirtualCompany / VirtualEmployee synthetic sender identities that the platform manages, delivered through Microsoft Graph / Exchange Online mailboxes, with inboxes warmed by Trulyinbox. We own sequencing, ordering by benchmark, variant selection, and translating delivery/reply events into lifecycle transitions.
Prerequisite: GDPR/UWG shape the outreach design, but a full compliance layer is explicitly out of scope for v1. Treat legal as a design consideration and get a written data-protection/UWG opinion before building this module (see 00-overview).
Responsibilities
- Send benchmark-ordered outreach to targets from managed VirtualCompany / VirtualEmployee identities via Microsoft Graph / Exchange Online, with merge fields (contact, company, product).
- Keep sender inboxes healthy with Trulyinbox warmup running continuously in the background.
- Orchestrate a 3-mail sequence per target.
- Generate AI email content into versioned, hashed
StepVersions (each audited), and let the Bandit pick the best-performing version at send time. - Receive/parse inbound mail and delivery events, classify replies, and advance the target lifecycle. Non-responders are handed off to Connect.
Integration boundary
- Microsoft Graph / Exchange Online — mail send + receive, behind a
MailTransportinterface (its own NestJS module, mockable, retried, timed-out, cost-tracked). - Trulyinbox — inbox warmup, behind a
WarmupProviderinterface; runs as a continuous background concern, not per-send. - The platform manages sender reputation itself via VirtualCompany/VirtualEmployee rotation, randomized sick days / vacations for virtual employees, and warmup — this is deliberately built, not offloaded to a third-party sending SaaS.
Sender identities
- A VirtualCompany owns 1–3 VirtualEmployees; each VirtualEmployee maps to an Exchange
Online mailbox and is the
fromidentity on anOutreachMessage. - Scheduled jobs randomize virtual-employee sick days and plan vacations so sending looks human and volume stays under per-mailbox caps.
Content, StepVersions & the Bandit
- An OutreachVariant holds the sequence; each OutreachStep (mails 1–3) has multiple StepVersions — AI-generated content variants that are hashed and audited; editing content never rewrites history, it produces a new version.
- Every LLM response is schema-validated, and generated mail passes a plausibility check (required fields present, no hallucinated prices) before send. User/research content is XML-isolated in prompts (prompt-injection defense).
- The Bandit — a multi-armed bandit, Thompson Sampling by default with an epsilon-greedy
alternative — selects the best
StepVersionat send time and learns from reply outcomes:- Success = a genuine reply (not spam / out-of-office).
- Failure = no reply within the wait window.
- Every decision/outcome is recorded append-only in
bandit_events(never UPDATE/DELETE).
Flow
Qualified targets (benchmark desc)
→ Bandit picks StepVersion for mail 1
→ send via Microsoft Graph from a VirtualEmployee → OutreachMessage(sent) → target: Emailed 1
→ [no reply, wait window] → mail 2 (Bandit-selected) → target: Emailed 2
→ [no reply, wait window] → mail 3 (Bandit-selected) → target: Emailed 3
→ genuine reply → classify → target: Interested (route toward Connect/appointment)
→ bounce / hard failure → flag the contact/email (drop or re-enrich)
→ no reply after sequence → hand off to Connect (enters the connect pipeline)
Inbound processing (signed webhooks / mailbox polling)
- Reply → classify inbound mail (genuine / out-of-office / auto-reply / bounce);
a genuine reply sets
outreach_messages.status = replied, records a successbandit_event, and transitions the target toward Interested. - No reply in the wait window → records a failure
bandit_eventand advances the sequence (or hands the target to Connect after mail 3). - Bounce / delivery failure → mark bounced; flag the contact/email; no success signal.
- Webhook payloads are signature-verified before any business logic runs.
Key services / jobs
SendOutreachStep— Bandit-select aStepVersion, render merge fields, send via theMailTransport, write theOutreachMessage.ProcessInboundMail— verify signature, classify the reply, updateOutreachMessage, write thebandit_event, transition the target.AdvanceSequence— schedule the next mail after the configured wait window; hand non-responders to Connect after mail 3.GenerateStepContent— AI draft into a new hashedStepVersion(schema-validated, plausibility-checked).- Warmup and virtual-employee sick-day/vacation randomization run as scheduled background jobs.
Async: outreach sends run on the outreach-mail BullMQ queue; inbound events on webhooks.
Business logic lives in NestJS services; input is validated via DTOs (class-validator) / zod.
Acceptance
- Qualified targets receive mail in benchmark order, from a managed VirtualEmployee identity via Microsoft Graph, with correct merge fields.
- The 3-mail sequence advances; each
OutreachMessagereflects its send state. - The Bandit selects a
StepVersionper step and records abandit_eventper outcome (genuine reply = success, no reply in window = failure). - Reply classification handles genuine / OOO / auto-reply / bounce; forged webhook signatures are rejected.
- No-reply targets hand off to Connect after the full sequence.
- Tests use a faked
MailTransportandWarmupProvider— no live sends, no real API calls in CI.
Open questions
- ⚠️ Needs client sign-off: written GDPR/UWG opinion before enabling live sending.
- Whether AI content generation is enabled from day one or the sequence ships with hand-authored
StepVersions first.