Follow-up Stages 2-3-4

After Stage 1 (initial outreach via outreach-stage1), all subsequent engagement lives here. Stage 2 handles no-reply nudges (NO_REPLY_4H). Stage 3 handles price-drop re-engagement (PRICE_DROP) and extended no-reply (NO_REPLY_72H). Stage 4 handles stalled deal recovery (STALLED_RECOVERY, Day 7+). Inbound replies from both OpenPhone (:18792) and SalesMsg (:18793) route through the A-G conversation classifier before AI response generation. Owner: acquisitions agent + Aurora.

Critical rules — do not skip:

Quick reference

FieldValue
StagesStage 2 (NO_REPLY_4H) → Stage 3 (PRICE_DROP / NO_REPLY_72H) → Stage 4 (STALLED_RECOVERY)
Primary agent_summary
Supporting agents_summary, _summary, _summary
Agent handoff chainfollow-up-scheduler → follow-up-engine (trigger eval) → compliance-gates → response-generator → dispo-send → [inbound] conversation-classifier → stage-sync → HubSpot
Compliance gates listgate-computer, compliance-gate, blast-safety, thread-context, response-generator
Skills invokedacquisitions-followup, messaging-compliance-gate
Success metricsFollow-up sent within trigger window; conversation-classifier categorizes ≥90% of replies; stage-sync updates both pipelines; no orphaned stalled deals
Cost per stage~0.0075/SMS segment; classifier ~$0.001
Throughput~50 follow-up candidates evaluated every 30 min; ~10-30 follow-ups/day after gate filtering
Last run resultSee follow-up-scheduler logs + openphone_events table most recent entries
Failure modesScheduler service down; webhook not processing (service restart needs explicit auth); signature drift (SalesMsg query-param); 429 from OpenPhone; classifier mismatching

Architecture — outbound (follow-up scheduling)

acquisition_deals rows (Stage 1 sent ≥4h ago, no reply)
        │
        ▼
[follow-up-scheduler.js] — runs every 30 min via systemd cron
        │
        ▼
[follow-up-engine.js] — trigger evaluation
        │
        ├─ NO_REPLY_4H  (4-24h since Stage 1)  → detail-request.json
        ├─ NO_REPLY_24H (24-72h)                → gap-fill.json
        ├─ NO_REPLY_72H (72h+)                  → gap-fill.json (extended)
        ├─ PRICE_DROP   (CH price drops ≥5%)    → price-drop-engage.json
        ├─ NEW_DETAIL   (public record changes) → re-qualify.json
        └─ STALLED_RECOVERY (Day 7+)            → offer-inquiry.json
        │
        ├─→ [response-generator.js] — LLM compose (follow-up-generator-prompt.json)
        ├─→ [compliance-gate.js] — same 5 gates as Stage 1
        ├─→ [thread-context.js] — inject prior message history
        │
        ▼
[dispo-send.js] — send chokepoint (OpenPhone OR SalesMsg depending on line routing)
        │
        ▼
OpenPhone (acq primary) | SalesMsg (acq secondary / overflow)

Architecture — inbound (reply handling)

CH replies via SMS/call
        │
        ├─ OpenPhone webhook → [quo-handler-enhanced.js:18792]
        └─ SalesMsg webhook  → [salesmessage-handler-v4-complete.js:18793]
                │
                ▼
        [conversation-classifier.js] — A-G categorizer
                │
                ▼
        [response-generator.js] — AI response compose
                │
                ▼
        [stage-sync.js] — update both HubSpot deal stages
                │
                ▼
        openphone_events (audit trail) + Discord #ops alert (if escalation)

Follow-up trigger windows

TriggerWindowTemplateStage advance
NO_REPLY_4H4-24h since Stage 1 senddetail-request.json→ “Reviewing Deal”
NO_REPLY_24H24-72h since Stage 1gap-fill.json(same)
NO_REPLY_72H72h+ since Stage 1gap-fill.json (extended)→ “Stalled”
PRICE_DROPAny time CH price drops ≥5%price-drop-engage.json→ “Re-engaged”
NEW_DETAILWhen public record changes (occupancy, condition)re-qualify.json→ “Reviewing Deal”
STALLED_RECOVERYDay 7+, no movementoffer-inquiry.json→ “Stalled”

A-G conversation classifier

Every inbound reply is categorized by conversation-classifier.js before any AI response is generated. Never skip the classifier — miscategorized replies cause wrong-stage transitions and wrong AI responses.

CategoryPatternRouting
A. Price negotiation”your price is too high”, “what about $X”response-generator.js → continue price thread
B. Detail provisionanswers to gap-fill questionsparse-deal-message → update acquisition_deals fields
C. Showing request”can I see it”, “when can I view”escalate to dispo handler; CC _summary
D. Stop / opt-out”STOP”, “remove me”, “fuck off”messaging_suppression insert → no further sends from any stage
E. Off-topicunrelated contentflag for human review; do NOT auto-respond
F. Buyer interest”I’d buy this for $X”route to dispo-blast
G. Multi-property threadreferences multiple dealsthread-context aggregate; smart-reply with combined context

Classifier ambiguity rule: if classifier returns ambiguous result (multiple categories scoring close), flag for human review. Do NOT auto-respond on ambiguous classification.

Stage transitions (HubSpot pipeline sync)

When inbound reply is classified, stage-sync.js updates both HubSpot pipelines:

Inbound categoryAcq pipeline stageDispo pipeline stage
A. Price negotiation→ “Negotiating”→ “Active negotiation”
B. Detail provided→ “Collecting Details”(no change)
C. Showing request→ “Requesting Access”→ “Showing scheduled”
D. Stop/opt-out→ “Dead”→ “Closed lost”
F. Buyer interest→ “Ready for Dispo”→ “Buyer engaged”

Acq pipeline: 877963314 | Dispo pipeline: 816046

Webhook handler services

Both handlers run as systemd services. Do NOT invoke directly — they are triggered by inbound webhooks.

ServicePortHandlerSystemd unitAuth method
OpenPhone inbound18792webhooks/quo-handler-enhanced.jsopenphone-webhookHMAC signature verify
SalesMsg inbound18793webhooks/salesmessage-handler-v4-complete.jssalesmsg-webhookQuery param token (?secret=...) — NOT HMAC

SalesMsg auth warning: SalesMsg uses query-param token, NOT HMAC. The WEBHOOKS.md KB once said otherwise — the KB was wrong. Live behavior is correct. Never switch SalesMsg to HMAC without verifying via live event headers first.

Service restart protocol: requires explicit Henry auth per feedback_action_gate_violation_repeated:

# Only with explicit Henry authorization:
systemctl --user restart openphone-webhook salesmsg-webhook

Entry points (CLI)

Cron worker — follow-up-scheduler.js

node /home/opsadmin/.openclaw/workspace/scripts/workers/follow-up-scheduler.js \
  [--limit=50] \
  [--dry-run]

Runs every 30 min via systemd timer. Evaluates all eligible deals; sends follow-ups that pass compliance.

Solara variant (alternate cadence)

node /home/opsadmin/.openclaw/workspace-solara/scripts/follow-up-scheduler.js \
  [--limit=20] \
  [--dry-run]

Used by _summary for orchestration-level follow-ups; less aggressive cadence.

Components

  • workspace/scripts/workers/follow-up-scheduler.js — main cron worker (runs every 30 min)
  • workspace-solara/scripts/follow-up-scheduler.js — Solara variant (less aggressive cadence)
  • workspace/scripts/lib/follow-up-engine.js — trigger evaluation (NO_REPLY_4H/24H/72H, PRICE_DROP, etc.)
  • workspace/scripts/lib/conversation-classifier.js — A-G categorizer for inbound replies
  • workspace/scripts/lib/thread-context.js — prior message aggregation for context injection
  • workspace/scripts/lib/response-generator.js — v3 Dispo Conversion Agent (follow-up compose)
  • workspace/scripts/lib/stage-sync.js — acq ↔ dispo HubSpot pipeline sync
  • workspace/scripts/lib/compliance-gate.js — same TCPA / quiet hours gate as Stage 1
  • workspace/scripts/lib/blast-safety.js — cross-deal dedup + suppression check
  • workspace/deal-qualification/ai/follow-up-generator-prompt.json — LLM system prompt for follow-up compose
  • workspace/webhooks/quo-handler-enhanced.js — OpenPhone inbound handler (port :18792)
  • workspace/webhooks/salesmessage-handler-v4-complete.js — SalesMsg inbound handler (port :18793)
  • workspace/knowledge-base/openphone/CONVERSATION-PATTERNS.md — 6-month analysis, A-G examples

How it’s used

  • Trigger (outbound): follow-up-scheduler.js cron evaluates all deals with Stage 1 sent; fires follow-up for any deal matching a trigger window (NO_REPLY_4H, PRICE_DROP, etc.)
  • Trigger (inbound): CH replies to either OpenPhone or SalesMsg → webhook fires → classifier → AI response → stage-sync
  • Workflow: scheduler → trigger evaluation → compliance 5-gate → compose → send → audit log; OR: webhook inbound → classify → respond → sync stages
  • Agents involved: _summary (outbound), _summary (inbound + Quo notes), _summary (showing-request escalation), _summary (oversight)
  • Handoff from Stage 1: deal enters this pipeline when either (a) 4h passes with no reply, OR (b) CH sends any inbound reply via OpenPhone or SalesMsg
  • Failure mode: if salesmsg-webhook or openphone-webhook services are down, inbound replies queue at carrier and are delivered on service restart; requires explicit auth for restart
  • Success criteria: follow-up sends tracked in openphone_events; inbound replies classified within 30s of webhook receipt; HubSpot pipeline stages updated within 60s of classification

Agents that touch this

  • _summary — primary owner; runs follow-up-scheduler; handles outbound
  • _summary — inbound reply processing; posts internal Quo notes; manages escalations
  • _summary — receives escalation on Category C (showing request) and F (buyer interest)
  • _summary — runs Solara variant follow-up-scheduler (less aggressive cadence)
  • _summary — deal oversight; monitors stalled deals; STALLED_RECOVERY audit

Skills that invoke this

Plans that govern this

Feedback rules

KB / source docs

  • README — OpenPhone API, conversation patterns, quiet hours
  • README — SalesMsg webhook auth (query-param), API quirks
  • README — Carrier compliance, 10DLC (B14 blocker)

System maps

HubRole
compliance-gatesPrerequisite — all 5 gates enforced on every follow-up send; same gates as Stage 1
outreach-stage1Upstream — deals enter this hub after Stage 1 send
openphone-quoOpenPhone webhook inbound (:18792) + outbound sends
salesmsgSalesMsg webhook inbound (:18793); query-param auth
twilioCarrier layer; 10DLC B14 affects delivery for all stages
deal-ingestionOrigin — deals ingested here before Stage 1 or follow-up can fire

B14 blocker cross-link (10DLC): osil-twilio-10dlc-resubmission-2026-05-03

Open issues / TODOs

  • B14: 10DLC blocks carrier deliverability for follow-up sends same as Stage 1. Monitor openphone_events delivery rates.
  • follow-up-scheduler timer interval: verify systemctl --user status follow-up-scheduler.timer running every 30 min
  • Solara variant (workspace-solara/scripts/follow-up-scheduler.js) — confirm cadence settings haven’t drifted from main scheduler config
  • Category E (off-topic) and ambiguous classifier results: currently flagged for human review but no Discord alert fires — alert needs wiring
  • stage-sync.js HubSpot ID drift: if acquisition_deals.hubspot_deal_id becomes null, stage transitions silently fail — periodic sync via acq-deals-hs-syncer.js required
  • SalesMsg P0 security: salesmsg-gateway.service has hardcoded ANTHROPIC_API_KEY in systemd unit (plaintext) — rotation pending per openclaw-fragmentation-fix-2026-05-01

Recent activity

  • 2026-05-03: hub created (W1-S8)