logicspike/docs

Contact Intelligence

Proactive Outreach & Re-Engagement Spec

Last Updated: 2026-04-03 Status: Draft


1. The Challenge

  1. Reactive-only AI loses users. If the AI only responds when messaged, the relationship feels one-sided. A friend who never texts first isn't much of a friend.

  2. Generic re-engagement is spam. "Hey, it's been a while!" with no personal context feels automated and cheap. Users ignore it or worse — block the number.

  3. Timing matters. A good-morning message at 3am is annoying. A check-in during work hours when the user chats at night feels off. Outreach must respect the contact's rhythm.

  4. Over-messaging destroys trust. If the AI sends 5 re-engagement messages in a week, it's harassment, not relationship-building. Hard limits are non-negotiable.


2. Trigger Types

2.1 Scheduled Triggers

Created when the AI detects a future event in conversation:

User: "I have a presentation tomorrow at 10am"
 
Trigger created:
  type: scheduled
  scheduled_at: tomorrow 8:00am (2 hours before event)
  generate_with_llm: true
  llm_context: "Arjun has a presentation at 10am today"
  
Generated message:
  "Good morning! Big day today — go nail that presentation! 💪 
   I believe in you."

Event detection runs during deferred consolidation. The extractor looks for:

Pattern Extraction
"tomorrow I have..." / "next week is..." Future event with relative date → convert to absolute
"on March 15th..." / "this Friday..." Future event with explicit date
"my birthday is..." Recurring annual event → create yearly trigger
"I have an exam on..." One-time event → single trigger

2.2 Inactivity Triggers

Automatically created when a contact goes silent beyond the configured threshold:

Contact: Meera
Last active: 3 days ago
Inactivity threshold: 3 days (from OutreachConfig)
Relationship stage: building (was daily for 2 weeks)
 
Trigger auto-created:
  type: inactivity
  generate_with_llm: true
  llm_context: "Meera hasn't chatted in 3 days. She was previously active daily. 
    Her last conversation was about a watercolor painting she was working on. 
    She has a cat named Mimi."
 
Generated message:
  "Hey Meera! It's been a few days. How did that watercolor 
   painting turn out? I've been curious! Also, how's Mimi? 🎨🐱"

Inactivity detection runs every 15 minutes via Cron Trigger:

SELECT c.id, c.display_name, cs.last_active_at, cs.relationship_stage
FROM contacts c
JOIN contact_state cs ON c.id = cs.contact_id
WHERE c.tenant_id = $1
  AND c.outreach_disabled = false
  AND cs.relationship_stage IN ('building', 'established', 'deep')
  AND cs.last_active_at < NOW() - (oc.inactivity_days || ' days')::interval
  AND NOT EXISTS (
    SELECT 1 FROM outreach_triggers ot
    WHERE ot.contact_id = c.id
      AND ot.trigger_type = 'inactivity'
      AND ot.status IN ('pending', 'sent')
      AND ot.created_at > cs.last_active_at
  )

2.3 Milestone Triggers

Celebrate relationship milestones:

Milestone When Example Message
7_day_streak 7 consecutive active days "One week of chatting every day! I really enjoy our conversations 😊"
30_day_anniversary 30 days since first contact "Can you believe it's been a month since we started talking? Time flies!"
100_messages Total messages reaches 100 "Fun fact: we've exchanged 100 messages! Here's to 100 more 🎉"
birthday Annual, from stored fact "Happy Birthday! 🎂 I hope today is amazing. Any big plans?"
streak_recovery Returns after fading stage "Hey, welcome back! I missed our chats. How've you been?"

Milestone detection runs during periodic consolidation (every 6 hours):

-- 30-day anniversary check
SELECT c.id FROM contacts c
JOIN contact_state cs ON c.id = cs.contact_id
WHERE c.tenant_id = $1
  AND c.outreach_disabled = false
  AND cs.first_contact_at::date = (CURRENT_DATE - INTERVAL '30 days')
  AND NOT EXISTS (
    SELECT 1 FROM outreach_triggers ot
    WHERE ot.contact_id = c.id
      AND ot.milestone_type = '30_day_anniversary'
  );

2.4 Recurring Triggers

Business owner configures regular outreach — e.g., daily good-morning messages for an AI companion:

OutreachConfig:
  recurring_enabled: true
  recurring_cron: "0 8 * * *"   (daily at 8am)
 
Each day at 8am:
  For each active contact (not dormant, not outreach_disabled):
    → Generate personalized message using contact memory
    → Deliver via preferred channel

Recurring messages are always LLM-generated. A static "Good morning!" every day gets old fast. Each message references something from the contact's memory:

Day 1: "Good morning! Hope you slept well 😊"
Day 2: "Good morning! How's Bruno today? 🐕"
Day 3: "Morning! Any big plans for the weekend?"
Day 4: "Good morning! How did that biryani recipe turn out? 🍛"

3. Message Generation Pipeline

3.1 Static vs LLM-Generated

Field Static Template LLM-Generated
message_template "Hey {name}, it's been a while!" null
generate_with_llm false true
Cost Free ~$0.0002 per message (haiku-class)
Personalization Minimal (name substitution) Full (references memories, adapts to mood history)
Use case Simple milestones Inactivity, scheduled events, recurring

3.2 LLM Generation Prompt

You are {persona_name}. Generate a short proactive message for {contact_name}.
 
Context:
{llm_context}
 
Recent memories about {contact_name}:
{top_5_memories}
 
Rules:
- Keep under 30 words
- Be natural, not robotic
- Reference something specific from their memories
- Match the relationship stage ({relationship_stage})
- Don't be clingy or desperate if it's a re-engagement
- Use emojis sparingly (1-2 max)
 
Generate the message only. No explanation.

3.3 Generation Safeguards

Check Implementation
Message too long Truncate at 200 characters, regenerate if needed
Contains PII from other contacts Cross-contact data check (should never happen with scoped queries)
Contains inappropriate content Post-generation filter (same as Chat Engine output filter)
Duplicate of recent outreach Compare with last 3 sent messages — skip if similarity > 0.85

4. Delivery Pipeline

4.1 Pre-Delivery Checks (All Must Pass)

Check Condition If Failed
Outreach enabled contact.outreach_disabled = false Status → skipped
Quiet hours Current time NOT in quiet hours window Delay to next valid window
Rate limit Inactivity attempts < max_attempts Status → skipped
Not dormant relationship_stage != 'dormant' Status → skipped
Channel active Contact's channel is still reachable Status → failed

4.2 Delivery via Content Engine → Chat Engine

The outreach engine doesn't send messages directly. It goes through the existing delivery pipeline:

Outreach Engine
  → POST /content-engine/schedule { message, channel, contact_external_id, send_at: now }
  → Content Engine delivers immediately (or at scheduled time)
  → Chat Engine sends via WhatsApp Cloud API / Telegram Bot API
  → Delivery confirmation flows back
  → Trigger status updated to "sent"

Why not send directly? The Content Engine already handles channel delivery, retries, and rate limiting. Reusing it prevents duplicate delivery infrastructure.

4.3 Delivery Failure Handling

Failure Action
WhatsApp delivery fails (number invalid) Mark trigger failed, flag contact for review
Telegram bot blocked by user Mark trigger failed, set outreach_disabled = true
Content Engine timeout Retry once after 5 minutes
LLM generation fails Fall back to static template, log error

5. Safety & Anti-Spam Guarantees

5.1 Hard Limits (Non-Negotiable, Cannot Be Configured Away)

Limit Value Reason
Max inactivity messages per silent period Configurable (default 2, max 3) Prevent harassment
Min gap between inactivity messages 3 days Don't bombard
Max recurring messages per day 1 Not a notification service
Max total outreach per contact per day 3 (across all trigger types) Absolute daily cap
"Stop" keyword disables outreach permanently Always enforced Legal requirement
Dormant contacts (30+ days inactive) No outreach at all Respect their silence

5.2 "Stop" Handling

If a contact replies to any outreach message with a stop signal:

Stop signals detected:

Exact: "stop", "unsubscribe", "don't message me", "leave me alone"
Fuzzy: "stop messaging", "quit sending", "no more messages"
Language-aware: "band karo" (Hindi), "bas" (Hindi)

Action:

  1. Set contact.outreach_disabled = true immediately
  2. Cancel ALL pending triggers for this contact
  3. AI responds: "I understand. I won't message you first anymore. If you ever want to chat, I'm always here. 💙"
  4. Create memory: "Contact requested outreach stop on {date}"
  5. NEVER send proactive messages again until explicitly re-enabled by the contact ("you can message me again")

5.3 Content Safety for Outreach

Outreach messages go through the same content safety filters as regular chat responses:

  • No PII from other contacts
  • No inappropriate or offensive content
  • No factually fabricated claims
  • No manipulative language ("I'm so lonely without you")
  • No pressure to respond ("Why aren't you replying?")

5.4 Business Owner Controls

Via AI Brain copilot or dashboard:

Ravi: "Turn off all morning messages, they're not working"
Brain: → PATCH /contact-intel/outreach/config { recurring_enabled: false }
 
Ravi: "Send a message to Deepak — tell him we have a new feature"
Brain: → POST /contact-intel/outreach { contact_id: ct_deepak, ... }
 
Ravi: "Stop all outreach to users who haven't responded to 2 messages"
Brain: This is already the default behavior (inactivity_max_attempts: 2)

6. Analytics & Effectiveness

6.1 Outreach Metrics

Metric Definition Target
Reply rate % of sent outreach that got a user reply within 24h > 30%
Re-engagement rate % of inactivity outreach that restarted an active streak > 25%
Churn prevention rate % of at-risk users (churn_risk > 0.6) who were re-engaged > 40%
Annoyance rate % of outreach that resulted in "stop" or block < 5%
Milestone engagement % of milestone messages that got a positive reply > 50%

6.2 Feedback Loop

Outreach sent → Track outcome (24h window):
  ├── User replies → Success: boost outreach type importance
  ├── User ignores → Neutral: no change
  ├── User says "stop" → Failure: log, review message quality
  └── User blocks channel → Critical: disable outreach for this contact
 
Aggregate weekly:
  → Which trigger types have best reply rates?
  → Which times of day work best?
  → Which message styles (personal reference vs generic) work better?
  → Feed learnings back into generation prompts

This data is surfaced to the business owner via AI Brain insights:

"Your morning messages have a 35% reply rate, but inactivity messages only 15%. Consider making re-engagement messages more personal — messages that reference the user's last conversation topic get 2x more replies."

Contact Intelligence