DEV Community

Ujjwal Dubey
Ujjwal Dubey

Posted on

Follow-Up Timers for Property Leads: A Small Scheduler With Stop Rules

Follow-up is where property leads are won, and where bots turn into spam. The difference is almost always the stop rules. This post shows a small scheduler pattern for AI automation for real estate agencies with follow-up timers that stop: a data model, a sequence, the stop logic, and a worker loop.

Data model

One row per lead in an active sequence.

from dataclasses import dataclass
from datetime import datetime
from typing import Optional

@dataclass
class FollowUp:
    lead_id: str
    agent_id: str
    step: int                  # 0 = first reply already sent
    next_at: datetime
    status: str                # "active" | "paused" | "stopped"
    stop_reason: Optional[str] = None
Enter fullscreen mode Exit fullscreen mode

The sequence

Keep it short and explicit. Each step says what kind of message it is, because that decides who sends it.

SEQUENCE = [
    {"after_hours": 2,   "kind": "agent_draft"},      # AI drafts, agent sends
    {"after_hours": 48,  "kind": "template_nudge"},   # pre-approved template
    {"after_hours": 120, "kind": "related_listing"},  # pre-approved template
]
Enter fullscreen mode Exit fullscreen mode

Stop rules (the important part)

def should_stop(lead, last_msg) -> Optional[str]:
    if last_msg and last_msg.from_lead:
        return "replied"            # a human takes over, timers end
    if lead.status in ("not_interested", "bought_elsewhere", "under_offer"):
        return "closed"
    if lead.opted_out:
        return "opt_out"
    if lead.step >= len(SEQUENCE):
        return "sequence_done"
    return None
Enter fullscreen mode Exit fullscreen mode

A reply from the lead always stops automation and alerts the agent. That single rule prevents most of the embarrassing cases, like a nudge arriving an hour after the buyer booked a viewing.

The worker

def tick(now):
    for f in due_followups(now):
        lead = get_lead(f.lead_id)
        reason = should_stop(lead, last_message(f.lead_id))
        if reason:
            stop(f, reason)
            continue
        step = SEQUENCE[f.step]
        if step["kind"] == "agent_draft":
            queue_for_agent(f, draft_from_history(f.lead_id))
        else:
            send_template(f, step["kind"])
        advance(f)   # step += 1, compute next_at from the next step
Enter fullscreen mode Exit fullscreen mode

Run tick from a scheduler every few minutes. Make advance and stop atomic with the send, or use an outbox table, so a crash cannot send the same nudge twice.

Notes from practice

  • Pause, do not delete. If an agent is on leave, pause their sequences and resume them, rather than letting them run unattended.
  • Templates outside the window. On WhatsApp, business-initiated messages outside the customer service window need pre-approved templates, so keep the nudge steps as templates.
  • Log every stop reason. Over a few weeks it tells you which listings and which messages actually get replies.
  • Quiet hours. Clamp next_at to sensible local hours for the lead, not the server.

Disclosure: I co-founded NxFlowAI (Mumbai). When we scope a follow-up workflow like this, the first audit takes 72 hours.

Top comments (0)