Follow-Up Suggestions
The /suggestions page presents AI-generated recommendations for who to reach out to and why. Suggestions help you maintain relationships proactively rather than letting connections go cold.
How Suggestions Are Generated
The follow-up engine runs daily via Celery beat. It selects contacts across two pools with a combined budget of 5 suggestions per cycle.
Pool A — Active Relationships (3 slots)
Four independent triggers feed into Pool A. A contact only needs to match one:
| Trigger | Condition | Example |
|---|---|---|
| Time-based | No interaction in 90+ days, score > 0 | "Haven't talked to Alex in 3 months" |
| Event-based | DetectedEvent in last 7 days, confidence > 70% | Bio change, tweet about fundraising |
| Scheduled | Past the priority interval since last follow-up | Medium priority set to 60 days, last follow-up was 65 days ago |
| Birthday | Birthday in next 3 days | High priority (1500), bypasses dormancy filter |
Priority intervals are configurable per priority level in Settings (default: high=30d, medium=60d, low=180d).
Pool B — Dormant Revival (2 slots)
Surfaces contacts you've lost touch with but had meaningful past engagement. Three sub-triggers:
| Trigger | Condition |
|---|---|
| B1 — Deep dormant | No interaction in 365+ days, 2+ past interactions, score >= 3, history spans 30+ days |
| B2 — Mid dormant | No interaction in 180-365 days, 3+ past interactions, score >= 4 |
| B3 — Event revival | Dormant 180+ days but had a recent detected event (job change, etc.) |
Cooldowns and Guards
All triggers respect these guards — including event-based triggers:
| Guard | Rule |
|---|---|
| Interaction cooldown | Skip if last interaction was within 14 days |
| Follow-up cooldown | Skip if last follow-up suggestion was within 14 days |
| Already queued | Skip if a pending or snoozed suggestion already exists |
| Archived | Skip archived contacts |
| 2nd-tier | Skip contacts labelled "2nd-tier" |
| No channel | Skip contacts with no email, Telegram, or Twitter handle |
| Ghosting (3+) | Skip if last 3+ consecutive interactions are all outbound with no reply |
| Ghosting (2) | Reduce priority by 50% if last 2 consecutive interactions are outbound |
| Unread outbound | Skip if last outbound Telegram message hasn't been read by recipient |
| Read no reply | Boost priority +100 if last outbound was read but no inbound reply |
Priority Scoring
Within each pool, candidates are ranked by priority score:
- Interaction count — richer history = higher priority
- Days since last contact — longer gap = higher priority
- Event trigger bonus — +200 for event-based triggers (ensures timely events surface quickly)
- Birthday bonus — +1500 for upcoming birthdays (highest priority)
Relationship Score
The relationship score (0-10) is computed from five weighted dimensions plus a tenure bonus:
| Dimension | Weight | What it measures |
|---|---|---|
| Reciprocity | 30% | Balance of inbound vs outbound messages |
| Recency | 25% | How recently you interacted (exponential decay) |
| Frequency | 20% | How often you interact |
| Breadth | 15% | Number of platforms you communicate on |
| Tenure | 10% | How long you've known the contact |
Tenure bonus: 0-2 extra points based on relationship duration (caps at 2 years).
Extended decay: Interactions from 1-2 years ago still contribute at 0.05x weight; 2-5 years at 0.02x. This prevents long-term contacts from dropping to zero.
Event Classification
Bio changes detected during the daily Twitter profile poll create DetectedEvent records. When composing follow-up suggestions, the engine also fetches recent tweets on-demand via Bird CLI (cached in Redis for 12h) and classifies them with Claude into categories like job changes, fundraising, product launches, etc.
Tweet fetching and LLM classification are not run on the daily cron — they happen lazily only for contacts that actually need a suggestion.
AI Message Composer
Each suggestion includes an AI-drafted message generated by Claude. The draft takes into account:
- The contact's profile, company, and bio information
- The last 5 interactions (for tone analysis: formal vs casual)
- Any detected events (job change, fundraising, etc.)
- Recent Twitter activity (bio + last 5 tweets, fetched on-demand for every trigger type, 12h Redis cache)
- The contact's preferred communication channel (based on most recent interaction platform)
When the last conversation is 90+ days old and a tweet from the last 30 days exists, the prompt instructs the LLM to anchor the message on the fresh Twitter signal rather than reopening the stale thread.
Contact Avatars
When a contact has an avatar available (from Google Contacts, Telegram, Twitter, or LinkedIn), it is displayed alongside the suggestion for quick visual identification.
Actions
For each suggestion, you can:
-
Edit and Send -- modify the AI-drafted message and send it through the appropriate channel.
-
Snooze -- defer the suggestion for a set period:
- 2 weeks
- 1 month
- 3 months
Snoozed suggestions are reactivated automatically via an hourly Celery beat task.
-
Dismiss -- remove the suggestion entirely. The contact will not be suggested again until a new trigger occurs.