# Template — N-stage Follow-Up Sequence

> Copy-paste skeleton for a multi-stage follow-up (FU) sequence built to the doctrine in
> `knowledge/follow-up-architecture.md`. Fill in the stamps, offsets, and stop set for your funnel,
> then create one `upsert_workflow` (type `FOLLOW_UP`) per row. **Read
> `follow-up-architecture.md §2/§3/§5` first** — this template only works if you understand the
> stage-N invariant and the coupled-set retrofit rule.

---

## Step 0 — define stamps and stops (boolean custom properties)

The evaluator sets all of these; the conversational agent sets none of them. All boolean, all
`check_until_match = true` with terminal target `true`.

**Stamps** (one per funnel stage the agent completes, in order):

| Stamp | Meaning (evaluator sets `true` when…) |
|---|---|
| `welcome_sent`        | the agent delivered the opening/value message |
| `age_qualified`       | the lead confirmed they qualify (edit per funnel) |
| `registration_link_sent` | the agent sent the registration/booking link |
| `deposit_confirmed`   | terminal — the lead paid/booked (doubles as a stop) |

**Stops** (disqualifiers — kill the whole sequence, excluded by *every* FU):

`bad_fit`, `lead_declined`, `escalation_needed`, `is_minor`, `deposit_confirmed`.

> Reuse existing `ninjo_*` auto-generated properties where they exist (e.g. `call_booked`) instead of
> minting duplicates — divergent duplicates are a known production bug (`prompt-antipatterns.md`).

---

## Step 1 — the exclusion matrix (fill this in before creating any workflow)

| FU stage | entry stamp `= true` | downstream exclusions (`NOT_EQUALS true`) | stops (`NOT_EQUALS true`) |
|---|---|---|---|
| Etapa 0 | `welcome_sent` | `age_qualified`, `registration_link_sent` | all stops |
| Etapa 1 | `age_qualified` | `registration_link_sent` | all stops |
| Etapa 2 | `registration_link_sent` | *(none)* | all stops |

**Retrofit rule:** adding a stage below forces adding its entry stamp to the downstream-exclusion
column of *every* row above it. (`follow-up-architecture.md §3`.)

---

## Step 2 — one workflow per FU (with same-stage stagger)

Each FU is a `FOLLOW_UP` workflow. Same-stage FUs (e.g. a 20-min and a 2-h nudge off `welcome_sent`)
each get their **own** anchor so they don't burst.

```
# ── Etapa 0 · nudge #1 (20 min after welcome) ────────────────────────────
Type:  FOLLOW_UP
name:  "Etapa 0 – 20min"
intervalMinutes: 30
minMinutesSinceLastInbound: 20          # silence gate
executionWindowStart/End: 09:00 / 21:00
onlyRunOnce: true
delayTriggerPropertyId: welcome_sent    # anchor: clock starts when stage entered
delayMinutesAfterPropertySet: 20        # this FU's offset (staggers same-stage FUs)
Conditions:
  welcome_sent            EQUALS      true    # entry
  age_qualified           NOT_EQUALS  true    # downstream exclusion
  registration_link_sent  NOT_EQUALS  true    # downstream exclusion
  bad_fit                 NOT_EQUALS  true    # stop
  lead_declined           NOT_EQUALS  true    # stop
  escalation_needed       NOT_EQUALS  true    # stop
  is_minor                NOT_EQUALS  true    # stop
  deposit_confirmed       NOT_EQUALS  true    # stop
Prompt: "Warm nudge — the lead saw the opener but went quiet. Re-open, no pressure."

# ── Etapa 0 · nudge #2 (2 h after welcome) ───────────────────────────────
# Identical conditions to nudge #1; ONLY the anchor offset changes:
delayMinutesAfterPropertySet: 120
onlyRunOnce: true

# ── Etapa 1 · nudge (30 min after age_qualified) ─────────────────────────
delayTriggerPropertyId: age_qualified
delayMinutesAfterPropertySet: 30
Conditions:
  age_qualified           EQUALS      true    # entry
  registration_link_sent  NOT_EQUALS  true    # downstream exclusion
  <all stops>             NOT_EQUALS  true

# ── Etapa 2 · nudge (1 h after link sent) ────────────────────────────────
delayTriggerPropertyId: registration_link_sent
delayMinutesAfterPropertySet: 60
Conditions:
  registration_link_sent  EQUALS      true    # entry (no downstream stamps)
  <all stops>             NOT_EQUALS  true
```

---

## Step 3 — day-scale stages need an approved Meta template

Any FU that fires **> 24h** after the lead's last inbound must deliver via a `WHATSAPP_TEMPLATE`
destination (approved Meta template), not free text — free text outside the window silently fails.
Wire it via the workflow → notification → destination chain in `outbound-whatsapp-playbook.md`.

---

## Step 4 — conformance before you ship

- Record the sequence in the agent's `fu-model.json`.
- Run `fu_conformance.py` — it enforces the stage-N invariant, the downstream-exclusion (R2), the
  per-FU anchor/stagger, boolean-gating, `check_until_match`-on-stops, and `NOT_EQUALS true`-not-`=
  false`. (Tooling side — `cortex-gateway-service/docs/followup-mcp-spec.md`.)
- Verify sends off `messages.source_workflow_id IS NOT NULL`, never `workflow_runs`
  (`follow-up-architecture.md §8`).

---

## Common mistakes this template prevents

| Mistake | Symptom | Guarded by |
|---|---|---|
| `= false` instead of `NOT_EQUALS true` | FU fires for almost nobody (NULL block) | Step 2 conditions |
| Missing downstream exclusion | cross-stage duplicate (advanced lead gets old stage) | Step 1 matrix + retrofit rule |
| Same-stage FUs share one anchor | same-stage burst (nudges land minutes apart) | Step 2 per-FU `delayMinutesAfterPropertySet` |
| String stop property | stop never gates | Step 0 (all boolean) |
| `check_until_match` off on a stop | early `false` freezes; agent chases converted lead | Step 0 |
| Free text > 24h | day-scale FU silently undelivered | Step 3 |
</content>
