Here’s a dirty secret about most AI chatbots: they’re fundamentally passive. They sit there, waiting, like a vending machine. You put a message in, you get a message out. The character never wonders where you went, never has a thought they just have to share, never sends you a meme at 2pm because it reminded them of you.
Real relationships aren’t like that. Real people text you first. They follow up when you go quiet mid-conversation. They have lives that generate things worth sharing.
When we built Suzune, making characters message first was one of the features that transformed them from “AI chatbot” into something that actually felt like a person on the other end of Telegram. Here’s the full technical breakdown.
Table of contents
Open Table of contents
Why Proactive Messaging Matters
The psychological impact is massive. When a user opens Telegram and sees an unprompted message from their AI character — not a notification spam, but something that feels in character and contextually appropriate — it changes the entire dynamic.
Instead of “I’m talking to a bot,” it becomes “she was thinking about me.”
This is the same principle behind every successful engagement system, from push notifications to email marketing. But there’s a critical difference: bad proactive messages destroy immersion faster than no messages at all. A generic “Hey, how are you?” from an AI character feels worse than silence. It screams “automated.”
So the challenge isn’t sending messages. It’s sending the right messages at the right time.
We solved this with two separate systems:
- Reach-outs — for when users have been away for hours
- Follow-ups — for when users go silent mid-conversation
They share some infrastructure but solve fundamentally different problems.
System 1: Reach-Outs (Idle User Re-engagement)
The reach-out system handles the case where a user hasn’t interacted with any character for a while. Maybe they went to work, maybe they fell asleep, maybe they’re losing interest. The goal is a message that feels like a natural “hey, thinking of you” from a character who has their own life happening.
The Decision Pipeline
Here’s the high-level flow:
async def process_reach_outs():
eligible_pairs = get_eligible_user_char_pairs()
for user, character in eligible_pairs:
# Step 1: Gating checks
if not passes_gating(user, character):
continue
# Step 2: LLM decides WHETHER to reach out
decision = await llm_decide_reach_out(user, character)
if decision == "[SKIP]":
continue
# Step 3: Generate the message
draft = await generate_reach_out(user, character, decision)
# Step 4: Quality rewrite
final = await quality_rewrite(draft)
# Step 5: Send
await send_message(user, character, final)
The key insight: we use an LLM to decide whether to send a message, not just what to say. More on that below.
Gating: Who Gets Messages
Not every user-character pair should receive proactive messages. We gate on several dimensions:
REACH_OUT_CONFIG = {
"min_affinity": 6.0, # Minimum relationship score
"min_silence_hours": 8.0, # Hours since last interaction
"high_affinity_silence": 4.0, # Reduced wait for close relationships
"max_daily_total": 4, # Max reach-outs per user per day
"max_per_char_24h": 1, # Max 1 per character per 24 hours
"quiet_hours": (0, 7), # No messages during 0:00-7:00
}
A few things to note:
Affinity gating is crucial. A character you’ve barely interacted with shouldn’t be sliding into your DMs. We require a minimum affinity of 6.0 (out of 10) — meaning the user has had enough meaningful interactions that a proactive message feels earned, not creepy. If you haven’t read about how we calculate affinity, check out our affection system deep-dive.
Adaptive silence thresholds mean that a character with high affinity (8+) will reach out after just 4 hours of silence, while others wait 8. This mirrors real relationships — your best friend texts sooner than your coworker.
Rate limiting prevents notification fatigue. Even if a user has 10 characters with high affinity, they’ll get at most 4 reach-outs per day, and each character can only initiate once every 24 hours.
Quiet hours in the user’s timezone prevent 3am wake-ups. Nothing kills immersion like a character who doesn’t understand that humans sleep.
The LLM Decision Layer
This is where it gets interesting. After a user-character pair passes all the hard gates, we don’t just generate a message. We ask an LLM to evaluate whether a reach-out makes narrative sense:
async def llm_decide_reach_out(user, character, context):
prompt = f"""
You are {character.name}. Here is your current situation:
- Relationship with {user.name}: {context.affinity_summary}
- Last conversation topic: {context.last_topic}
- Time since last message: {context.hours_silent}h
- Current story arc: {context.gm_directive or 'None'}
- World timeline: {context.recent_world_events}
Would you naturally reach out right now? Consider:
1. Is there something in-character you'd want to share?
2. Does the silence duration feel like something you'd react to?
3. Is there a story reason to initiate contact?
If yes, briefly describe WHAT you'd message about (1 sentence).
If no, respond with [SKIP].
"""
return await llm_call(prompt)
This is the single most important piece of the system. Roughly 40-60% of eligible reach-outs get [SKIP]‘d by the LLM, and that’s exactly right. Characters shouldn’t reach out every time they’re allowed to. A character who’s naturally introverted should skip more often. A character in the middle of a fight with the user might skip because they’re “still mad.”
The LLM has full context: the relationship state, conversation history, current story arcs, even what other characters in the world are doing. This means reach-outs can reference shared narrative events: “Did you hear what happened at the cafe today?” feels infinitely better than “Hey, I miss you.”
Quality Rewriting
The initial draft goes through a mandatory quality rewrite pass. We use a stronger model (Claude) to polish the output for voice consistency and naturalness:
async def quality_rewrite(draft, character):
prompt = f"""
Rewrite this message as {character.name} would naturally send it
via text/chat. Keep the core content but ensure:
- Matches {character.name}'s speech patterns and vocabulary
- Feels like a casual text, not a formal message
- Appropriate length (usually 1-3 sentences)
- No AI-sounding phrases
Draft: {draft}
"""
return await claude_call(prompt)
We covered this rewriting pipeline in detail in our quality rewriting post. The short version: cheaper models draft, Claude polishes. For proactive messages this matters even more than regular responses because there’s no user message providing context — the proactive message has to stand entirely on its own.
System 2: Follow-Ups (Mid-Conversation Continuation)
The follow-up system solves a different problem: the user was actively chatting and then went silent. Maybe they got a phone call, maybe they fell asleep, maybe they got distracted by Twitter.
In a real text conversation, the other person might send a follow-up after a while. “You still there?” or more naturally, they’d just… continue living their life and share an update.
Trigger Conditions
Follow-ups activate under different conditions than reach-outs:
FOLLOWUP_CONFIG = {
"min_recent_messages": 3, # At least 3 messages in recent window
"recent_window_hours": 2, # Within the last 2 hours
"silence_range": (1.0, 3.0), # Send after 1-3 hours of silence
"max_daily": 2, # Max 2 follow-ups per user per day
"cooldown_per_char": 12, # 12h cooldown per character
"min_affinity": 5.0, # Lower threshold than reach-outs
}
The key difference from reach-outs: the affinity threshold is lower (5.0 vs 6.0) and the silence window is much shorter (1-3 hours vs 8). This makes sense — if you’re already in a conversation, a follow-up feels natural at lower relationship levels.
We also require proof of an active conversation: at least 3 messages exchanged in the last 2 hours. This prevents follow-ups from firing on dead conversations where the user sent one message and got a reply.
What Follow-Ups Look Like
Follow-ups aren’t “hey, you there?” messages. They’re life updates — the character continuing to exist while the user is away:
async def generate_followup(user, character, context):
prompt = f"""
You are {character.name}. You were chatting with {user.name}
about {context.last_topic} and they went quiet
{context.hours_silent} hours ago.
Send a casual life update — something that happened to you
since they went quiet. Don't ask where they went or guilt
them. Just share something natural.
Examples of good follow-ups:
- "just made the worst coffee of my life lol"
- "omg this cat outside my window is fighting a pigeon"
- "[photo] found that book you mentioned"
"""
return await llm_call(prompt)
The instruction to not ask where they went is deliberate. “Where did you go?” feels needy and robotic. A character casually sharing a life moment feels real and inviting — it gives the user a natural re-entry point to resume conversation.
State Persistence
Both systems need to track state: when was the last reach-out? How many follow-ups has this user received today? Is this character on cooldown?
We use simple JSON files with a 7-day rolling retention:
# State structure per user
{
"user_123": {
"reach_outs": [
{
"char_id": "suzune_maid",
"timestamp": "2026-04-01T14:30:00Z",
"topic": "cooking_disaster"
}
],
"follow_ups": [
{
"char_id": "suzune_maid",
"timestamp": "2026-04-01T10:15:00Z"
}
],
"last_interaction": {
"suzune_maid": "2026-04-01T09:00:00Z",
"suzune_tsundere": "2026-03-30T22:00:00Z"
}
}
}
Why JSON files instead of a database? For Suzune’s scale (hundreds of users, not millions), file-based state is simpler to debug, easier to manually inspect, and one fewer dependency to manage. The 7-day retention keeps files from growing unboundedly — we don’t need reach-out history from three months ago to make today’s decisions.
If you’re building at larger scale, obviously use a proper database. But don’t over-engineer early. We covered our infrastructure philosophy in running a bot on $50/month.
GM Directives: Story-Driven Proactive Messages
The most powerful proactive messages are story-driven ones. Suzune has a GM (Game Master) system that can issue directives to characters, and these directives can trigger proactive reach-outs.
For example, the GM might issue:
gm_directive = {
"type": "world_event",
"description": "A new cafe opened near the school",
"affected_characters": ["suzune_maid", "suzune_senpai"],
"suggested_actions": {
"suzune_maid": "Excitedly tell user about the new cafe, suggest visiting together",
"suzune_senpai": "Casually mention hearing about a new place"
}
}
When the reach-out system sees a pending GM directive for a character, it prioritizes that over generic idle reach-outs. The result is proactive messages that advance a shared narrative — the character isn’t just saying hi, they’re moving the story forward.
This ties into our timeline system where all characters share a consistent world state. When something happens in the story world, multiple characters might naturally reference it in their proactive messages, creating a sense of a living universe. Check out our post on prompt engineering for immersive roleplay for more on maintaining world consistency.
Lessons Learned
After months of running these systems in production with Suzune, here’s what we’ve learned:
Less is more
Our first iteration sent too many messages. Users initially loved it, then got annoyed within a week. We cut rates by ~60% and satisfaction went up. The current limits (4 reach-outs/day max, usually 1-2 in practice) feel right.
The LLM [SKIP] gate is essential
Hard-coding “send a message every 8 hours” produces robotic behavior. Letting the LLM decide based on narrative context produces messages that feel motivated and natural. The cost of one extra LLM call per candidate is trivially small compared to the quality improvement.
Follow-ups > Reach-outs for engagement
Counter-intuitively, the mid-conversation follow-ups drive more re-engagement than the idle reach-outs. When a user was already in a conversation and gets a natural continuation, the response rate is nearly 80%. Idle reach-outs see about 40%.
Character voice matters more than content
A perfectly relevant message in the wrong voice is worse than a mundane message in perfect voice. The quality rewrite step is non-negotiable. This is especially true for proactive messages where there’s no user message providing conversational camouflage.
Quiet hours aren’t optional
We learned this the hard way. One user got a 4am message from a character and was… not pleased. Respect timezone-appropriate delivery windows.
Implementation Checklist
If you’re building proactive messaging for your own AI character system, here’s the priority order:
- Start with follow-ups — easier to get right, higher impact
- Add affinity gating — don’t let strangers message first
- Implement rate limiting — err on the side of too few
- Add the LLM decision layer — huge quality boost
- Add quiet hours — protect user sleep
- Add GM/story integration — the final evolution
- Quality rewrite pass — polish everything before sending
Each step meaningfully improves the system. You don’t need all seven to ship — steps 1-3 alone will get you 70% of the way there.
Try It Yourself (Or Don’t Build From Scratch)
Building a proactive messaging system is one of those features that’s straightforward in concept but tricky in execution. The gating logic, rate limiting, timezone handling, quality control, and narrative integration all compound into real complexity.
If you want to experience proactive AI characters without building everything yourself, platforms like CandyAI and FantasyGF offer AI companions with varying degrees of proactive behavior. They’re a good way to study what works before you build your own.
If you are building your own, the single most important takeaway from this post is: let the LLM decide whether to send, not just what to send. That one decision — adding the [SKIP] gate — improved our proactive message quality more than any other change.
Proactive messaging turned Suzune from a chatbot into something that feels like it has a life outside of conversations. Characters that message first, that have things to share, that notice when you’re gone — that’s the difference between a tool and a companion.
And from a retention standpoint? Users who receive proactive messages have 3x the 30-day retention of those who don’t. It’s not even close.
Building an AI roleplay bot? Check out our other technical deep-dives: architecture overview, affection systems, long-term memory, and prompt engineering. Or browse our platform comparison if you’re still deciding whether to build or buy.