acquisitions-followup skill
Role: the second-touch+ outreach lane. Handles cadence escalation for non-replies and AI-driven response to inbound replies.
This skill wraps live follow-up workers + webhook handlers. Pairs with acquisitions-outreach (which handles Stage 1) — this skill handles Stage 2/3/4 and inbound.
⚡ Execution-mode model routing (G-MODEL-ROUTING-AT-EXEC)
This skill is execution-mode by definition. Per CLAUDE.md “MANDATORY: Execution-Mode Model Routing” + feedback_model_routing_at_exec.md:
Step 1 of every invocation: decompose the work and dispatch via the Agent tool with model: "sonnet" using the 8-element prompt contract (task goal, evidence, tool to use, test requirements, acceptance criteria, rollback command, rule constraints, output format).
Opus (main conversation) role = dispatcher + verifier + Henry-gate. Direct Opus script invocation / code edits = governance violation unless the work fails the Sonnet rubric (novel architecture, rule arbitration, Henry decision gate, first instance of a new pattern). Action-gate per CLAUDE.md still requires Henry approval for any live send regardless of which model drafted the call. Verify each subagent report against acceptance criteria before marking complete.
Architecture
acquisition_deals row (Stage 1 SMS sent ≥4h ago, no reply)
│
▼
[follow-up-scheduler.js] — runs every 30 min via cron
│
├─→ [follow-up-engine.js] — trigger evaluation
│ ├─ NO_REPLY_4H — first nudge
│ ├─ NO_REPLY_24H — second nudge
│ ├─ NO_REPLY_72H — gap-fill template
│ ├─ PRICE_DROP — re-engage when CH lowers
│ ├─ NEW_DETAIL — re-engage when public record changes
│ └─ STALLED_RECOVERY — Day-7+ re-engage
│
├─→ [response-generator.js] — LLM compose (per follow-up-generator-prompt.json)
├─→ [compliance-gate.js] — same gates as Stage 1
├─→ [thread-context.js] — inject prior message history
│
▼
[dispo-send.js] — same chokepoint as outreach
│
▼
OpenPhone / SalesMsg
INBOUND PATH (when CH replies):
OpenPhone webhook → [quo-handler-enhanced.js:18792] ─┐
SalesMsg webhook → [salesmessage-handler-v4-complete.js:18793] ─┤
▼
[conversation-classifier.js] — A-G category
│
▼
[response-generator.js] — AI response
│
▼
[stage-sync.js] — update HS dealstage
Follow-up triggers (from follow-up-engine.js)
| Trigger | Window | Template | Stage advance |
|---|---|---|---|
NO_REPLY_4H | 4-24h since Stage 1 | detail-request.json | → “Reviewing Deal” |
NO_REPLY_24H | 24-72h | gap-fill.json | (same) |
NO_REPLY_72H | 72h+ | 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” |
Inbound reply categories (from conversation-classifier.js)
| Category | Pattern | Routing |
|---|---|---|
| A. Price negotiation | ”your price is too high”, “what about $X” | response-generator.js → continue thread |
| B. Detail provision | answers to gap-fill questions | parse-deal-message → update acquisition_deals |
| C. Showing request | ”can I see it”, “when can I view” | escalate to dispo handler |
| D. Stop / opt-out | ”STOP”, “remove me”, “fuck off” | suppression list → no further sends |
| E. Off-topic | unrelated content | flag for human review |
| F. Buyer interest | ”I’d buy this for $X” | route to dispo-blast skill |
| G. Multi-property thread | references multiple deals | thread-context aggregate, smart-reply |
Capabilities (entry points)
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 Solara agent for orchestration-level follow-ups; less aggressive cadence.
Inbound handlers (services, not direct invocation)
| Service | Port | Handler file |
|---|---|---|
| OpenPhone webhook | 18792 | webhooks/quo-handler-enhanced.js |
| SalesMsg webhook | 18793 | webhooks/salesmessage-handler-v4-complete.js |
These run as systemd services (openphone-webhook, salesmsg-webhook) and are triggered by inbound webhooks from the SMS vendors. Don’t invoke directly.
Stage transitions (HubSpot deal pipeline)
When conversation-classifier categorizes an inbound, stage-sync.js updates the HubSpot deal:
| Inbound category | acq stage transition | dispo stage transition (mirror) |
|---|---|---|
| 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” |
Triggers
User says any of:
- “run follow-up worker”
- “schedule follow-ups for today”
- “handle inbound reply for deal X”
- “re-engage stalled deals”
- “trigger price-drop re-engagement”
For Stage 1 (initial outreach), route to acquisitions-outreach skill.
Capabilities NOT in this skill
| Capability | Lives in |
|---|---|
| Initial Stage 1 outreach | acquisitions-outreach skill |
| HubSpot deal creation | hubspot-deal-ingest skill |
| Buyer-side blast | dispo-blast skill |
| Suppression list management | messaging-compliance-gate skill (separate) |
| Webhook signature verification | webhooks/ handlers (security baked in per FUNNEL-REGISTRY.md) |
Failure modes
| Symptom | Cause | Recovery |
|---|---|---|
| Webhook not processing | service down | systemctl --user restart openphone-webhook salesmsg-webhook |
webhook signature invalid | secret rotation drift | check FUNNEL-REGISTRY.md SalesMsg uses query param token, NOT HMAC |
| Follow-up scheduler skipping deals | gate failure | check compliance log; quiet hours likely |
429 from OpenPhone | line saturation | rotate via LINE_DEAL_SOURCE_MAP |
| Stage transition not applying | HubSpot ID drift | re-run sync; acq-deals-hs-syncer.js repopulates hubspot_deal_id |
| Categorizer mismatching | classifier-LLM returned ambiguous | flag for human review, don’t auto-respond |
Memory cross-references
- feedback_aurora_outbound_guardrails.md — outbound rules
- feedback_aurora_slack_behavior.md — escalation routing
- feedback_quo_lineid_required.md — quo_phone_number_id required for Aurora postInternalNote
- feedback_salesmsg_api_first.md — SalesMsg API quirks
- feedback_no_em_dash.md — no em-dashes in outbound copy
Related code (the implementation)
workspace/scripts/workers/follow-up-scheduler.js— main cron workerworkspace-solara/scripts/follow-up-scheduler.js— Solara variantworkspace/scripts/lib/follow-up-engine.js— trigger evaluationworkspace/scripts/lib/conversation-classifier.js— A-G categorizerworkspace/scripts/lib/thread-context.js— prior msg aggregationworkspace/scripts/lib/response-generator.js— v3 Dispo Conversion Agentworkspace/scripts/lib/stage-sync.js— acq ↔ dispo HS syncworkspace/deal-qualification/ai/follow-up-generator-prompt.json— LLM system promptworkspace/webhooks/quo-handler-enhanced.js— OpenPhone inbound (port 18792)workspace/webhooks/salesmessage-handler-v4-complete.js— SalesMsg inbound (port 18793)workspace/knowledge-base/openphone/CONVERSATION-PATTERNS.md— 6-month analysis, A-G examples