# Runbook — the follow-up never fires (or fires when it shouldn't)

> **Symptom.** *"el follow up no se envía nunca"* · *"my follow up never gets sent"* · *"se manda a
> todos, incluso a los que ya compraron"* · *"llegan dos follow-ups iguales"*.
>
> Split it first: **never fires** → H1–H4. **Fires too much or to the wrong people** → H5–H7. The
> two halves share almost no causes, and guessing which one the operator means is the fastest way to
> waste the session.

## What you need first

- The agent id and the workflow name.
- One conversation where the follow-up should have gone out and didn't (or shouldn't have and did).

---

## It never fires

### H1 — The 24-hour window closed

On Meta channels a follow-up can only go out when the last message is the agent's **and** the lead's
last inbound is under 24 hours old. Most "low send rate" reports are this, and it is not a bug — it
is the platform doing what it must.

- **Test:** for the conversations you expected to receive one, check who sent last and when the lead
  last replied.
- **Stop condition:** the window had closed on most of them → **you're done**. Report the real
  eligible count, not the raw one.
- **Fix:** nothing here. To reach leads outside the window you need an approved WhatsApp template,
  which is a different mechanism — see `knowledge/outbound-whatsapp-playbook.md`.

### H2 — A stop property is silently killing every rung

A check-until-match property used as an exclusion on **every** stage of a sequence is a permanent
kill switch: one false positive and that lead never receives any follow-up again, at any stage,
forever. It looks like the sequence "just doesn't work for some people".

- **Test:** `list_workflows`, read the property conditions on each stage, and check how many of them
  share the same exclusion property.
- **Stop condition:** one latching property excludes every rung → **you're done**.
- **Fix:** keep the hard stop on terminal rungs only, or use a non-latching property for the soft
  cases. A single evaluator false positive should cost one message, not the lead.

### H3 — The conditions were wiped by a partial write

Workflow writes are replace, not merge. An upsert without the complete field set, or a rename, will
drop `workflow_property_conditions` and can null the prompt and model config too. The workflow still
exists and looks configured.

- **Test:** read the workflow's conditions back from storage after any recent edit — do not trust
  the tool's success response.
- **Stop condition:** conditions are empty on a workflow you know was gated → **you're done**.
- **Fix:** re-apply the full set and re-read to confirm.

*Evidence: a rename generated a new workflow with none of its conditions, because the deploy script
does not carry `workflow_property_conditions` — documented in its own config and still surprising in
practice.*

### H4 — The workflow is gated on a property that never becomes true

Same failure as `agent-wont-stop.md` H2/H3, seen from the other side: if the gating property never
flips, the follow-up is correctly suppressed forever.

- **Test:** `list_custom_properties`, then check the property's value on a lead who should have been
  followed up.
- **Fix:** fix the property, not the workflow. If the property is fed by an integration, confirm the
  integration is actually delivering — a dead webhook makes every workflow gated on it inert.

---

## It fires when it shouldn't

### H5 — The workflow has no property conditions at all

An ungated follow-up fires on every conversation regardless of stage: leads who already converted,
leads who were disqualified, leads mid-conversation.

- **Test:** `list_workflows` and look for stages with an empty condition set.
- **Stop condition:** empty conditions → **you're done**.
- **Fix:** add them. At minimum: exclude converted, exclude disqualified, and exclude anyone already
  in a later stage of the same sequence.

*Evidence: a post-opener follow-up with zero conditions, firing on every conversation regardless of
stage. Separately, a qualification gate implemented only as prompt text let a clearly disqualified
lead through, because the follow-up never saw the disqualification as a condition.*

### H6 — Duplicates, because the next stage does not exclude the previous one

If stage 2 does not exclude leads who already received stage 3, both can fire. The operator sees the
same lead getting two messages.

- **Test:** for each stage, check that it excludes the stages after it.
- **Stop condition:** any stage missing a later-stage exclusion → **you're done**.
- **Fix:** add the exclusions. This is a condition problem, never a timing problem — do not fix it
  by spacing the stages further apart.

### H7 — The gate is an LLM judgment on a weak model

A follow-up whose condition is "did the conversation close?" is asking a model to make a judgment
call every run. On a small model this fails in both directions at once, and rephrasing the prompt
just moves which direction it fails in.

- **Test:** read the workflow's model config. If a small model is pinned there, check its decisions
  against a handful of conversations you have read yourself.
- **Stop condition:** the model is wrong on cases you can judge instantly → **you're done**.
- **Fix:** replace the judgment with a property condition wherever the signal is structural
  (converted, disqualified, stage reached). Reserve LLM gating for genuinely fuzzy cases, and do not
  pin a small model to it.

*Evidence: a follow-up re-created with a small model pinned in its config could not reliably
distinguish an open conversation from a closed one; across three different prompt structures it
over-fired on closed conversations or under-fired on open ones, never neither.*

---

## Two things that look like bugs and aren't

- **Re-evaluation noise.** The runner re-invokes the model on conversations with no new inbound, so
  evaluation counts massively exceed send counts. A high evaluation count with few sends is not a
  malfunction, and `action = 'SENT'` in the logs counts evaluations, not deliveries. Never quote it
  as a send count.
- **Silence the follow-up cannot see.** When the agent deliberately stays silent, the follow-up
  receives no signal that a decision was made — it sees an idle conversation. If a sequence must not
  run after a deliberate silence, that has to be an explicit property, because nothing infers it.

## When to stop

If the window is open, the conditions are intact and correct, and the property has the value you
expect, capture it: the workflow, the conversation, the property values at the time, and what you
expected to be sent. Send that with `report_feedback`.
