Overview
What it is: A follow-up recovery + re-engagement engine that prevents leads from going cold after the first touch.
Goal: Maximize replies/meetings without spamming by choosing the next best follow-up based on real engagement signals.
Key idea: Treat outreach like a lifecycle system (events → state → decision → action), not a fixed sequence.
Architecture (4-layer system)
- Event intake (signals + triggers)
- Collects events like: sent/delivered, open, click, reply, bounce, unsubscribe, calendar booked, resubmitted form.
- Sources: email provider webhooks, open/click tracking, CRM activity, Calendly.
- Normalizes into a single event format:
lead_id, event_type, timestamp, campaign_id, metadata (url clicked, message_id, etc.)
- State engine (lead lifecycle tracking)
- Maintains a lead state machine (examples):
sent_touch_1_waiting
opened_no_reply
clicked_high_intent
no_open_72h
soft_bounce_detected
reply_received_stop
booked_stop
- Responsibilities:
- Update state on every event
- Enforce stop rules
- Prevent duplicate sends
- Keep sequence context consistent
- Decision + sequencing logic (the brain)
- Stop / safety rules (hard rules)
- Stop immediately on reply, unsubscribe, hard bounce, calendar booked, or manual DNC flag.
- Behavior-based branching (soft rules)
- Opened no reply → follow up sooner, shorter, reference prior email, ask one direct question.
- Clicked → prioritize meeting ask, offer two paths, alert a rep.
- No opens → delay longer, change subject + angle, reduce links for deliverability.
- Multiple opens → “bump” email, optional alternate channel, flag as high interest.
- Deliverability guardrails
- Max touches per lead (ex: 5–7)
- Cooldowns + quiet hours
- Domain throttling + rate caps
- Spam-trigger checks
- Execution + logging (do & track)
- Sends follow-up emails (templates + variables + optional behavioral reference).
- Writes all activities to CRM.
- Updates pipeline stage, tags, and notes (ex: “opened 3x”, “clicked pricing link”).
- Pushes Slack/SMS alerts for warm intent.
- Tracks: reply rate by touch #, open/click distribution, meetings booked, deliverability metrics.
How it works (step-by-step)
- Lead enters sequence (outbound list, inbound that didn’t book, manual CRM add)
- Assigns
sequence_id, starting touch, and lead metadata (industry, role, company).
- Touch 1 sends + timer starts
- Creates a waiting window (ex: 48h) and begins monitoring events for that
message_id.
- Event listener updates state
- Every open/click/reply updates the state in real time.
- Decisions re-evaluate immediately (click →
clicked_high_intent → escalate).
- Dynamic follow-up selection
- Chooses follow-up type instead of a fixed “Touch 2”:
- Bump (1 line)
- Value add
- Direct question
- Breakup
- Alternate channel nudge (if phone exists)
- Assembles message using:
- Template + variables (name, company)
- Prior context snippet
- Optional behavioral reference
- Escalation + human handoff
- Triggers on warm intent: clicked, 3+ opens, or positive-intent keywords.
- Sends Slack alert with lead summary, assigns owner, creates CRM call task, pauses automation.
- End conditions + cleanup
- Marks outcome (no response / not interested / booked), archives state, stores stats.
Key components (conceptual)
followup_engine/event_listener.py
followup_engine/lead_state_store.py
followup_engine/decision_rules.py