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)

TriggerWindowTemplateStage advance
NO_REPLY_4H4-24h since Stage 1detail-request.json→ “Reviewing Deal”
NO_REPLY_24H24-72hgap-fill.json(same)
NO_REPLY_72H72h+gap-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”

Inbound reply categories (from conversation-classifier.js)

CategoryPatternRouting
A. Price negotiation”your price is too high”, “what about $X”response-generator.js → continue thread
B. Detail provisionanswers to gap-fill questionsparse-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-topicunrelated contentflag for human review
F. Buyer interest”I’d buy this for $X”route to dispo-blast skill
G. Multi-property threadreferences multiple dealsthread-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)

ServicePortHandler file
OpenPhone webhook18792webhooks/quo-handler-enhanced.js
SalesMsg webhook18793webhooks/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 categoryacq stage transitiondispo 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

CapabilityLives in
Initial Stage 1 outreachacquisitions-outreach skill
HubSpot deal creationhubspot-deal-ingest skill
Buyer-side blastdispo-blast skill
Suppression list managementmessaging-compliance-gate skill (separate)
Webhook signature verificationwebhooks/ handlers (security baked in per FUNNEL-REGISTRY.md)

Failure modes

SymptomCauseRecovery
Webhook not processingservice downsystemctl --user restart openphone-webhook salesmsg-webhook
webhook signature invalidsecret rotation driftcheck FUNNEL-REGISTRY.md SalesMsg uses query param token, NOT HMAC
Follow-up scheduler skipping dealsgate failurecheck compliance log; quiet hours likely
429 from OpenPhoneline saturationrotate via LINE_DEAL_SOURCE_MAP
Stage transition not applyingHubSpot ID driftre-run sync; acq-deals-hs-syncer.js repopulates hubspot_deal_id
Categorizer mismatchingclassifier-LLM returned ambiguousflag for human review, don’t auto-respond

Memory cross-references

  • workspace/scripts/workers/follow-up-scheduler.js — main cron worker
  • workspace-solara/scripts/follow-up-scheduler.js — Solara variant
  • workspace/scripts/lib/follow-up-engine.js — trigger evaluation
  • workspace/scripts/lib/conversation-classifier.js — A-G categorizer
  • workspace/scripts/lib/thread-context.js — prior msg aggregation
  • workspace/scripts/lib/response-generator.js — v3 Dispo Conversion Agent
  • workspace/scripts/lib/stage-sync.js — acq ↔ dispo HS sync
  • workspace/deal-qualification/ai/follow-up-generator-prompt.json — LLM system prompt
  • workspace/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

Invokes / Invoked by

Invokes: SKILL, SKILL Invoked by: _summary, _summary, SKILL