# Campaign Follow-Up Flow Audit

Date: 2026-04-02

## Scope

Reviewed the active campaign follow-up runtime against the expected flow:

- If a lead replies, pending follow-ups should stop.
- If a lead does not reply, configured follow-ups should continue.
- After AI sends a new outbound reply, the inactivity timer should restart from that latest outbound.

## Active Runtime Path

The worker starts the newer campaign automation cron, not the legacy `leadFollowUp` cron.

- Worker boot: `worker.js`
- Active job registration: `utils/workerRuntime.js`
- Active follow-up scheduler: `corns/followUpCron.js`
- Active campaign send engine: `services/sendEngine.js`
- Lead outcome/reset logic: `utils/leadOutcome.js`
- Inbound email path: `services/imapResponse.js`, `services/emailProcessor.js`
- Inbound WhatsApp path: `routes/waba.route.js`

The older file `corns/leadFollowUp.js` exists, but it is not started by the worker.

## What Matches The Diagram

### 1. Campaign launch moves leads into In-progress

When a campaign is launched, the lead is assigned campaign automation state and an outbound send sets the timer baseline.

- Lead assignment snapshot: `utils/campaignAutomation.js`
- Initial send starts timer from latest outbound: `services/sendEngine.js`

### 2. No reply causes follow-up automation to start

The active follow-up cron checks `lastOutboundAt` plus `timerBeforeFollowUp`. If that timer expires and follow-ups are enabled, the lead moves from `In-progress` to `Follow-up`.

- Decision logic: `utils/campaignAutomation.js`
- Cron transition into follow-up mode: `corns/followUpCron.js`

### 3. Lead reply cancels remaining follow-ups

When an inbound reply is processed, `applyLeadOutcomeFromIntent()` clears follow-up state:

- `followUpStep -> 0`
- `nextFollowUpAt -> null`
- `followUpStartedAt -> null`
- `awaitingFinalTimeoutUnassign -> false`
- lead returns to `In-progress` for neutral replies, or moves to terminal statuses when intent is qualified / not interested / human intervention / booked meeting

This is the core cancellation behavior shown in your image.

- Reset logic: `utils/leadOutcome.js`
- Email inbound usage: `services/imapResponse.js`, `services/emailProcessor.js`
- WhatsApp inbound usage: `routes/waba.route.js`

### 4. Timer restarts after the AI sends again

After the system sends an outbound email or WhatsApp reply, `lastOutboundAt` is updated again. This means the inactivity timer restarts from the newest AI outbound message, which matches the "AI responds / timer resets" branch in the diagram.

- Email / WhatsApp campaign and follow-up sends: `services/sendEngine.js`
- WhatsApp conversational sends: `routes/waba.route.js`

### 5. No reply after final follow-up returns lead to In-progress, then eventually Unassigned

After the last configured follow-up is sent, the lead returns to `In-progress` with `awaitingFinalTimeoutUnassign = true`. If the timer expires again with no reply, the lead moves to `Unassigned`.

- Final follow-up completion update: `utils/campaignAutomation.js`
- Final timeout to unassigned: `corns/followUpCron.js`

This matches the bottom-right branch in the diagram.

## Differences / Approval Items

### A. First follow-up is not always immediate after the In-progress timer

Current runtime behavior:

1. Wait for `timerBeforeFollowUp`
2. Move lead to `Follow-up`
3. Wait for follow-up step 1 delay
4. Send follow-up #1

This is implemented by scheduling `nextFollowUpAt` using step 1's configured delay when follow-up mode starts.

- `buildFollowUpStartUpdate()`: `utils/campaignAutomation.js`

Impact:

- If step 1 delay is `0`, the first follow-up is effectively immediate.
- If step 1 delay is `1 day`, the first follow-up happens `timerBeforeFollowUp + 1 day` after the last outbound.

Approval option:

- Keep current behavior if step 1 delay is intended to be explicit.
- Change behavior so follow-up #1 sends immediately once the In-progress timer expires, and only later steps use per-step delays.

### B. Legacy unused cron has different behavior and can confuse maintenance

`corns/leadFollowUp.js` contains an older follow-up engine with different semantics, including immediate first follow-up handling and channel-specific attempt counters. It is not wired into the worker, but it is still present in the codebase.

Impact:

- Runtime is safe today because this file is not started.
- Future maintenance is riskier because engineers can read the wrong implementation.

Approval option:

- Keep it as historical code.
- Archive or remove it after review so the active flow is unambiguous.

## Focused Verification

Executed:

- `node campaignAutomationTestRunner.js`
- `node leadOutcomeTestRunner.js`
- `node whatsappCampaignDeliveryTestRunner.js`

Result:

- All focused tests passed.
- Existing tests explicitly confirm that replies during follow-up cancel later follow-ups and return the lead to `In-progress`.

## Recommendation

Recommended approval items:

1. Confirm whether follow-up #1 should be immediate after the In-progress timer, or whether step 1 delay should remain configurable.
2. Remove or archive `corns/leadFollowUp.js` after approval so the active flow is clear.

No runtime code changes were applied in this audit.
