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:
- All outbound sends still pass all 5 compliance gates from compliance-gates
- Service restarts for webhook handlers require explicit Henry auth (per feedback_action_gate_violation_repeated)
- SalesMsg uses query-param token (
?secret=...), NOT HMAC — never switch to HMAC without live verification- Stage 1 sends belong to outreach-stage1, not this hub
Quick reference
| Field | Value |
|---|---|
| Stages | Stage 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 chain | follow-up-scheduler → follow-up-engine (trigger eval) → compliance-gates → response-generator → dispo-send → [inbound] conversation-classifier → stage-sync → HubSpot |
| Compliance gates list | gate-computer, compliance-gate, blast-safety, thread-context, response-generator |
| Skills invoked | acquisitions-followup, messaging-compliance-gate |
| Success metrics | Follow-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 result | See follow-up-scheduler logs + openphone_events table most recent entries |
| Failure modes | Scheduler 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
| Trigger | Window | Template | Stage advance |
|---|---|---|---|
NO_REPLY_4H | 4-24h since Stage 1 send | detail-request.json | → “Reviewing Deal” |
NO_REPLY_24H | 24-72h since Stage 1 | gap-fill.json | (same) |
NO_REPLY_72H | 72h+ since Stage 1 | gap-fill.json (extended) | → “Stalled” |
PRICE_DROP | Any time CH price drops ≥5% | price-drop-engage.json | → “Re-engaged” |
NEW_DETAIL | When public record changes (occupancy, condition) | re-qualify.json | → “Reviewing Deal” |
STALLED_RECOVERY | Day 7+, no movement | offer-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.
| Category | Pattern | Routing |
|---|---|---|
| A. Price negotiation | ”your price is too high”, “what about $X” | response-generator.js → continue price thread |
| B. Detail provision | answers to gap-fill questions | parse-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-topic | unrelated content | flag for human review; do NOT auto-respond |
| F. Buyer interest | ”I’d buy this for $X” | route to dispo-blast |
| G. Multi-property thread | references multiple deals | thread-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 category | Acq pipeline stage | Dispo 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.
| Service | Port | Handler | Systemd unit | Auth method |
|---|---|---|---|---|
| OpenPhone inbound | 18792 | webhooks/quo-handler-enhanced.js | openphone-webhook | HMAC signature verify |
| SalesMsg inbound | 18793 | webhooks/salesmessage-handler-v4-complete.js | salesmsg-webhook | Query 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-webhookEntry 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 repliesworkspace/scripts/lib/thread-context.js— prior message aggregation for context injectionworkspace/scripts/lib/response-generator.js— v3 Dispo Conversion Agent (follow-up compose)workspace/scripts/lib/stage-sync.js— acq ↔ dispo HubSpot pipeline syncworkspace/scripts/lib/compliance-gate.js— same TCPA / quiet hours gate as Stage 1workspace/scripts/lib/blast-safety.js— cross-deal dedup + suppression checkworkspace/deal-qualification/ai/follow-up-generator-prompt.json— LLM system prompt for follow-up composeworkspace/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.jscron 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-webhookoropenphone-webhookservices 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
Cross-links
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
- acquisitions-followup — canonical wrapper for all Stages 2-4 and inbound handling
- dispo-blast — triggered by Category F (buyer interest) inbound replies
- messaging-compliance-gate — standalone compliance check
Plans that govern this
- osil-twilio-10dlc-resubmission-2026-05-03 — B14 10DLC; same carrier blocks apply to follow-up sends
- openclaw-fragmentation-fix-2026-05-01 — G-FAILED-SERVICE-MTTR: webhook services must be fixed or disabled within 24h of entering failed state
Feedback rules
- feedback_aurora_outbound_guardrails — outbound dispatch rules apply to all stages
- feedback_aurora_slack_behavior — escalation routing to Slack/Discord
- feedback_quo_lineid_required — lineId required for Quo internal notes; fail-closed
- feedback_salesmsg_api_first — SalesMsg API quirks (query-param token); check KB first
- feedback_no_em_dash — no em-dashes in outbound follow-up copy
- feedback_action_gate_violation_repeated — webhook service restarts require explicit Henry auth
- feedback_acq_thread_history_aware_gates — thread context injected before compose; skip satisfied gates
- feedback_never_send_without_auth — live batch sends require explicit auth
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
- vm-messaging-flow — full SMS send and receive flow
- vm-acq-agent-flow — acquisitions stage progression map
- vm-inbound-sms-flow — inbound reply routing detail
Related: SMS/Carrier compliance cluster
| Hub | Role |
|---|---|
| compliance-gates | Prerequisite — all 5 gates enforced on every follow-up send; same gates as Stage 1 |
| outreach-stage1 | Upstream — deals enter this hub after Stage 1 send |
| openphone-quo | OpenPhone webhook inbound (:18792) + outbound sends |
| salesmsg | SalesMsg webhook inbound (:18793); query-param auth |
| twilio | Carrier layer; 10DLC B14 affects delivery for all stages |
| deal-ingestion | Origin — 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_eventsdelivery rates. follow-up-schedulertimer interval: verifysystemctl --user status follow-up-scheduler.timerrunning 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.jsHubSpot ID drift: ifacquisition_deals.hubspot_deal_idbecomes null, stage transitions silently fail — periodic sync viaacq-deals-hs-syncer.jsrequired- SalesMsg P0 security:
salesmsg-gateway.servicehas hardcodedANTHROPIC_API_KEYin systemd unit (plaintext) — rotation pending per openclaw-fragmentation-fix-2026-05-01
Recent activity
- 2026-05-03: hub created (W1-S8)