# ManyChat parity gap — what he uses vs what we have

> **Purpose.** The operator's order, verbatim (2026-07-25): *"we need to at
> least make sure our system does exactly what manychat does without doing it
> cheaply like just make it work cheaply."* ManyChat is being replaced
> **gradually, piece by piece — there is no cutover day** (ledgered ruling,
> `docs/DECISIONS.md` 2026-07-23). This document is the honest per-capability
> answer, scoped to what he **actually uses** (`manychat-breakdown.md` §4 — his
> real flows), not the full theoretical builder catalog.
>
> **Sources.** The **live ManyChat API, read 2026-07-25** (read-only GETs only —
> no POST, no send, no mutation of any kind), plus
> `plans/rethink/manychat-breakdown.md` (2026-07-23 dashboard capture),
> `plans/rethink/manychat-capability-catalog.md` (the builder surface),
> `plans/rethink/channels-scope-5-kickoff.md` (our replacement package), the
> scope board, and the code as it stands on branch `rethink` at `ed253c9`.
>
> **Method.** Every BUILT / BUILT-UNGATED claim carries a `file:line`. Where a
> claim could not be verified it says so instead of guessing. Read-only survey;
> no code was changed and nothing in ManyChat was touched.

---

## 0. The live account, read from the API (2026-07-25)

`GET /fb/page/getInfo` → **Silver Oak Properties**, id `109939868479359`,
`@SOPUAE`, Pro, `Asia/Dubai`. `GET /fb/page/getFlows` → **26 flows in 5
folders** (20 real + 6 ManyChat system flows).

| Folder | Flows (live inventory, newest first) |
|---|---|
| **Comments-to-DMs** | Keyword **"YARDS"** (created 2026-07-23) · Comment "INVEST" · Keyword "SG" · Keyword "TORONTO" · Keyword "INVITE" |
| **client interaction** | facebook - Default Reply · Instagram - Job Seeker interaction · facebook - client interaction · **WIP** · Client Collector · Job Seeker Collector · Client Follow-up #1 · Default Reply |
| **functions** | facebook- Validate Phone Number · **Untitled** (2026-06-05) · instagram - Validate Phone Number |
| **DMs-automation** | DM Keyword trigger Job-seeker |
| **Templates** | **Untitled** (2026-07-01) · Give File Template · **Must-follow** (2026-04-18) |
| *(root, system)* | Subscribe/Unsubscribe from Bot · Subscribe/Unsubscribe from Instagram · Subscribe/Unsubscribe from TikTok |

**Live vs dormant.** `getFlows` returns **no status field** — live/stopped/draft
is knowable only from the 2026-07-23 dashboard capture. On that evidence:
**LIVE** = SG, TORONTO, INVITE, Client Collector, Job Seeker Collector,
Client Follow-up #1, Default Reply, facebook - client interaction,
Instagram - Job Seeker interaction, both Validate Phone Number flows.
**DORMANT** = Comment "INVEST" (stopped, 0 runs), facebook - Default Reply
(stopped), Give File Template (stopped), the two `Untitled` drafts.
**UNKNOWN** = **YARDS**, **WIP**, **Must-follow** — three flows the captured
doc never mentions, so nobody has established whether they are running.
*He should not be told he must rebuild an abandoned draft; equally, YARDS
cannot be dismissed as one.*

**Also read live:**
- `getGrowthTools` → **4** comment-trigger tools (`feed_comment_trigger`):
  `55193269`, `55748814`, `56090391`, and **`57517928` "Post or Reel Comments
  #17 copy copy"** — one more than the kickoff's 2026-07-22 export recorded.
- `getTags` → exactly 3: Job-seeker Flow Completion · Client Flow Completion ·
  Real Estate Professional. Matches the capture.
- `getCustomFields` → **13** fields with real types: 10 `text` (Budget,
  Feedback, Objective, Who are they?, Work Type, Comment, Location, Full Name,
  route, phone number), 2 `boolean` (Has property?, is phone number valid?),
  1 `datetime` (preferred_date_time). The capture's "14th" —
  `client_interaction` — is a **folder**, not a field.
- `getBotFields` → **empty**. `getOtnTopics` → **empty**. He uses neither.

**What the API will NOT give, confirmed by probing:** `getFlow`,
`getFlowContent`, `getFlowFields`, `getWidgets`, `getGrowthTool`, `getWidget`,
`getSequences`, `getKeywords` all return **404**. So flow content, trigger
keyword lists and the sequence schedule remain **dashboard-only reads** — the
kickoff's 2026-07-22 research verdict (`:383-388`) still holds, now re-proven.
**Consequence for the board's TORONTO item:** whether the live trigger still
carries the 7 misspelling variants **cannot be checked from the API**. It stays
an unverified dashboard-only item. Nothing was changed.

## Status vocabulary

| Status | Means |
|---|---|
| **BUILT** | Exists and is live — an operator can use it today. |
| **BUILT-UNGATED** | Code exists and is **deployed to prod** (`HANDOFF.md:15-18`), but has never run in front of a customer: every `FunnelFlow.state` is `off` and **nothing is seeded**. |
| **PARTIAL** | Some of the capability, with a named piece missing. |
| **SPEC'D** | Planned in `channels-scope-5-kickoff.md`, not built. |
| **MISSING** | Nothing, anywhere. |

**One standing caveat that colours every BUILT-UNGATED row.** The funnel engine
has **exactly one wired sender, WhatsApp**
(`packages/shared/src/funnel-flows/send-capability.ts:17`), and WhatsApp's
number is disconnected; Instagram delivers **zero** `messaging` webhooks
(measured in prod, `HANDOFF.md:40-45`). So today no channel can both receive a
DM and answer it. "Deployed" is not "proven".

---

## The table

### A. Triggers

| ManyChat capability | What it does for him | Our status | Evidence (file:line) | Gap |
|---|---|---|---|---|
| **A1. Post/Reel comment keyword trigger** | TORONTO, SG, INVEST — a comment on any post starts a flow | **BUILT-UNGATED** | new: `packages/shared/src/funnel-flows/types.ts:308`, `apps/api/src/funnel-flows/funnel-trigger.service.ts:344`, wired at `apps/api/src/instagram/comment-pipeline.ts:1133` · legacy: `apps/api/src/instagram/comment-pipeline.ts:472`, seeds `packages/shared/src/agent/triggers.ts:77` | Two parallel implementations coexist (legacy `Trigger` rows + funnel triggers); Stage F is meant to retire the legacy one. Neither has ever fired with its gate on. Comments **do** arrive (`HANDOFF.md:42`), so this is the one trigger class that could be witnessed today. **Our seed covers 3 of his 5 comment flows — `Keyword "YARDS"` (live inventory §0) is absent from `triggers.ts:77-105` entirely**, and `Comment "INVEST"` was deliberately not migrated. |
| **A2. Keyword *variants* per trigger** | TORONTO + 7 deliberate misspellings — a live workaround for imperfect matching | **PARTIAL** | legacy seeds carry all 8: `packages/shared/src/agent/triggers.ts:86-95` · funnel takes exact keyword lists only: `types.ts:279-281` | **The docs and the code disagree — see §4.1.** The funnel side deliberately dropped variants; the deployed legacy seed still carries them. Whichever survives Stage F decides this. |
| **A3. Randomized public comment replies** | "Yes, random multiple replies" — a list, one picked per comment, to dodge Meta's spam-pattern detection | **PARTIAL** | list + picker exist: `types.ts:316`, `packages/shared/src/funnel-flows/trigger-match.ts:257` · picked and **logged, never posted**: `apps/api/src/instagram/comment-pipeline.ts:1164` · legacy `canned` rule posts **one fixed** string: `apps/api/src/instagram/triggers-admin.controller.ts:26-30` | Nothing posts a funnel trigger's public reply. The legacy path posts a single text with no randomization. The anti-spam property he relies on does not exist on either path. |
| **A4. Comment → private-reply DM opener** | The one unprompted DM a commenter gets ("You're officially invited…") | **BUILT-UNGATED** | `apps/api/src/instagram/comment-pipeline.ts:1339` (`sendDmOpener`), gate ships OFF: `comment-pipeline.ts:1326`, seeded inert: `apps/api/src/instagram/ig-comment-admin.service.ts:99-114` | Gate `instagram:comment_to_dm` has never been flipped (`HANDOFF.md:82`). Never witnessed. |
| **A5. DM keyword trigger** | "Message contains `close`" → Job Seeker Collector | **BUILT-UNGATED** | `types.ts:347` (`dm` trigger, 6 match modes at `types.ts:265-275`), IG seam `apps/api/src/instagram/ig-dm.service.ts:145-153`, WhatsApp seam `apps/api/src/flows/basic-flow-engine.ts:208,300` | No channel currently delivers a DM: IG `messaging` webhooks are zero, the WhatsApp number is dead. Facebook Messenger has **no receive call site at all** (channels-scope-4). |
| **A6. AI-intent DM trigger** | "Person wants to buy/rent/invest in property" / "person wants a job" — intent, not keywords | **BUILT-UNGATED** | `types.ts:283-287` (`is_about`), classifier `packages/shared/src/funnel-flows/intent-policy.ts`, fail-safe to no-match; thresholds + rate caps are seeded settings (`packages/shared/src/funnel-flows/settings.ts`) | Same delivery gap as A5. Never run against a real inbound. |
| **A7. Default Reply (unmatched DM fallback)** | Instagram-side fallback, 25 runs — routes into Client Collector / Job Seeker Collector | **PARTIAL** | Deliberately **omitted as a trigger**: `channels-scope-5-kickoff.md:18-19` — "an unmatched DM is the AI conversation". The agent exists and is the substitute (`apps/api/src/agent/agent.service.ts`, precedence chain `apps/api/src/flows/basic-flow-engine.ts:208`) | The substitution is a ruling, not a gap in itself — but it is unproven end-to-end, because no DM channel is delivering. His ManyChat Default Reply routes to a *scripted collector*; ours routes to a conversational agent. Different behaviour, accepted by ruling. |
| **A8. "Contact subscribed to a sequence" trigger** | The only trigger on Client Follow-up #1 | **MISSING** | The event name exists in the vocabulary (`types.ts:405`) but there is **no `Sequence` model** (`grep "model Sequence" schema.prisma` → none) and **no emitter or consumer for any `contact_event` trigger** in `apps/api` (single hit: `funnel-trigger.service.ts:222`, classification only) | A trigger for an object that does not exist. See D1 and §2. |
| **A9. Story-reply auto-responder** (Basic tab, on) | Auto-answers story replies | **PARTIAL** | Full config schema incl. his 5 delay values and auto-❤️: `types.ts:359-390` · **no webhook ingest** — `channels-scope-5-kickoff.md:629-630` says story-reply and live-comment triggers are "config + matcher only" | Config that nothing can fire. |
| **A10. Reaction + story-mention auto-responder** (Basic tab, on) | Auto-responds to reactions and story mentions | **MISSING** | Not in `FUNNEL_TRIGGER_KINDS` (`types.ts:485-491`); kickoff rules "**No Basics tab**" (`channels-scope-5-kickoff.md:19`) and carries the capability nowhere else | A live ManyChat behaviour with no home in any plan. See §2. |
| **A11. Built-in Subscribe / Unsubscribe flows** | Six ManyChat system flows (Bot · Instagram · TikTok), read live at §0 — the opt-in/opt-out confirmations | **BUILT** | `packages/shared/src/contacts/opt-out.ts` (`detectOptSignal`, both text and button payload), consumed at `apps/api/src/flows/basic-flow-engine.ts` before anything else, with a self-healing sweep (`channels-scope-5-kickoff.md:879-891`) | Ours is stricter than his: it outranks the flow and releases the session. Per-channel opt-out semantics for **Instagram** are undecided — the IG DM path has no opt-keyword handling at all (`apps/api/src/instagram/ig-dm.service.ts`, disclosed at `kickoff:1121-1129`). |

### B. Message content and branching

| ManyChat capability | What it does for him | Our status | Evidence (file:line) | Gap |
|---|---|---|---|---|
| **B1. Send Message (text)** | Every step of every flow | **BUILT-UNGATED** | `apps/api/src/funnel-flows/funnel-engine.service.ts:793`, gate `:856-888`, dispatch `:675` | Only sends on WhatsApp (`send-capability.ts:17`); IG/FB are a logged `skipped_unsupported_channel` no-op. |
| **B2. Tappable row buttons** | "I'm interested" on the SG/TORONTO invites (SG logged 100% CTR) | **BUILT** (ORD-038, verified 2026-07-29) | `buildFunnelInteractive` sends a real WhatsApp `type:"button"` interactive payload: `packages/shared/src/funnel-flows/render-message.ts:109`, called on every message-node send by `funnel-engine.service.ts:809`; `supportsTappableButtons` now defaults TRUE for `whatsapp`: `render-message.ts:60-63` | Built and live on WhatsApp. IG/FB still fall back to text link-lines pending their own send enablement (unrelated to the sender itself). |
| **B3. URL button ("Register →")** | The registration link on both expo flows | **PARTIAL** | Same rendering path: `render-message.ts:44` emits `Title: url` | The URL survives; the button does not. His seeded openers already fold the CTA into text (`packages/shared/src/agent/triggers.ts:70-72`). |
| **B4. Quick-reply pills** | "Still working it out" on the budget question | **PARTIAL** | Schema `types.ts:124` (Meta caps enforced) · rendered as `a \| b \| c` text: `render-message.ts:62` · inbound payloads **are** parsed (`quick_reply.payload`, kickoff:811-813) | We can read a quick reply we can never send. |
| **B5. Branch-per-button routing** | Budget tiers (AED 500K–1M / 1M–2M / 2M+) each set a different field | **PARTIAL** | Ports exist on edges (`types.ts:698-704`); the engine routes on `inbound.interactiveId` (`funnel-engine.service.ts:466`) | **Because B2/B4 send plain text, no funnel send can ever produce an `interactiveId` — so a button port is unreachable in practice.** The branch mechanism is real in the graph and dead on the wire. Not stated in any stage log. |
| **B6. Image / file / video in a message** | Not confirmed in his live flows (breakdown §4 does not say any current flow sends media) | **PARTIAL** | Blocks are modelled (`types.ts:45-90`) but render as text markers: `render-message.ts:15-24` emits `[image] <url>` | If any seeded flow carries media, the customer receives a URL string, not the media. |
| **B7. Personalization token in message text** | Client Follow-up #1's apology: "`full name`, Rest assured, we've notified your assigned agent…" | **MISSING** | `renderContentBlock` returns `block.text` verbatim (`render-message.ts:13-14`). `{{field}}` interpolation exists **only** for external-request/dynamic bodies (`funnel-engine.service.ts:1396`) and LeadRat action fields (`types.ts:550`) | A message cannot address the contact by name. Not mentioned in any stage. See §2. |
| **B8. Data Collection / question node** | Full name · role & experience · phone number, across all three collectors | **BUILT-UNGATED** | `types.ts:159-182` (11 reply types — a superset of his 8), executor `funnel-engine.service.ts:1103`, validation `packages/shared/src/funnel-flows/question-validate.ts:36` | Never run against a real reply. |
| **B9. Data-Collection timeout branch** | "expires in 30 minutes" → its own next step | **BUILT-UNGATED** | `types.ts:172`, timer job `apps/api/src/funnel-flows/internal-funnel-flows.controller.ts:44`, arming rules reworked over two review rounds (kickoff:908-927) | Never fired against a real clock. |
| **B10. Phone-number validation sub-flow** | `instagram - Validate Phone Number` (64 runs) — called from 3 parent flows | **BUILT-UNGATED** | Implemented as a **reply type + retry**, not a sub-flow: `types.ts:162,169`, `question-validate.ts:48-53` | Shape divergence, probably an improvement (one node instead of a called flow). His exact retry wording was never captured (kickoff:375-377), so seeding it is guesswork unless he re-reads it. |

### C. Actions and integrations

| ManyChat capability | What it does for him | Our status | Evidence (file:line) | Gap |
|---|---|---|---|---|
| **C1. Set User Field** | Objective · Budget · Has property? on every branch | **BUILT-UNGATED** | `types.ts:518`, executor `funnel-engine.service.ts:1315-1322` | — |
| **C2. Add Tag** | "Client Flow Completion" · "Job-seeker Flow Completion" | **BUILT-UNGATED** | `types.ts:520`, executor `funnel-engine.service.ts:1323-1330`, `Tag` model `packages/db/prisma/schema.prisma:339` | Tag **folders** (his Auto-DM / Grow-followers folders) do not exist. |
| **C3. External Request → n8n webhook** | "POST client data" ends every collector | **BUILT-UNGATED** | `types.ts:522-531`, executor `funnel-engine.service.ts:1349` | Works, **but the locked frame forbids using it this way**: "Leads DIRECT to LeadRat; n8n never fed" (`channels-scope-5-kickoff.md:20`). Seeding his flows as-captured would violate it — see §4.2. No SSRF guard yet (pinned to Stage E, kickoff:171-172). |
| **C4. Start Automation (flow calls flow)** | The reusability mechanism — `functions/` folder; INVEST → Client Collector | **BUILT-UNGATED** | As a **node**: `types.ts:646-650`, executor `funnel-engine.service.ts:711`, returns control via `funnelFlowSpeaks` recursion (kickoff:940-952) | **As an *action* it is a silent no-op**: `types.ts:532` defines a `start_flow` action, `funnel-engine.service.ts:1305` previews it in shadow, and `executeOneAction`'s switch (`funnel-engine.service.ts:1314-1341`) has **no `start_flow` case** — in live it falls through and does nothing, with no run-log line. Latent (everything is gate-off); disclosed here, not fixed (another seat owns the file). |
| **C5. Lead handoff to the CRM** | ManyChat POSTs to n8n and never learns the outcome | **BUILT-UNGATED** | Four LeadRat actions with a **measured** contract: `apps/api/src/funnel-flows/funnel-crm.service.ts`, `types.ts:547-567`; upsert/fallback/failure all land on the run row + a Needs-action item | Strictly better than his current setup (see §3). Never run from a flow; the contract itself was proven live against a sandbox lead (kickoff:1274-1320). |

### D. Sequences

| ManyChat capability | What it does for him | Our status | Evidence (file:line) | Gap |
|---|---|---|---|---|
| **D1. Sequence: "Lead Qualifer Follow-up"** | 3 subscribers, one step: **after 1 day → send Client Follow-up #1**. His entire post-lead follow-up motion. | **MISSING** | No `Sequence` model, no subscribe/unsubscribe action in `FunnelActionSchema` (`types.ts:517-568`), no scheduler. Named only as "Sequences fast-follow" (`channels-scope-5-kickoff.md:19`) and "out of scope" (`:194`); **zero occurrences of "sequence" in `plans/rethink/scope-board.md`** | Not built, not spec'd, not boarded. The largest single hole. See §1 and §2. |

### E. Product surfaces

| ManyChat capability | What it does for him | Our status | Evidence (file:line) | Gap |
|---|---|---|---|---|
| **E1. Canvas flow builder** | Where he edits every flow | **SPEC'D** | Stage E, `channels-scope-5-kickoff.md:99-113` + primitives `:115-141`. No `apps/web` route, no component: the only funnel controller in the API is the worker-callback one (`apps/api/src/funnel-flows/funnel-flows.module.ts:18`, `internal-funnel-flows.controller.ts:16`) | **There is no way to create a flow.** Held on a live witness (`HANDOFF.md:103-107`). |
| **E2. Draft vs published + explicit Publish** | Edit safely, publish deliberately | **SPEC'D** | Model exists (`schema.prisma:1058-1073`, `status` at `:1062`, at most one draft + one published) — **no write path anywhere** | Stage E is the first graph-WRITE path; its two prerequisites (zod on every write, SSRF guard) are pinned to it. |
| **E3. Flow list with folders** | 5 folders organize 25 flows | **SPEC'D** | `FunnelFlowFolder` model `schema.prisma:1043-1053`; no API, no UI | — |
| **E4. Per-flow on/off** | LIVE / STOPPED per flow | **PARTIAL** | Richer than his: `off · shadow · live` (`schema.prisma:1022`), honoured at every send (`funnel-engine.service.ts:864-888`) and at arming (`send-capability.ts:19-21`) — **no operator surface to flip it** | Today the state can only be changed by SQL. |
| **E5. Per-node analytics overlay** | Sent / Delivered / Opened / Clicked stamped on each node; click-through funnel panel | **SPEC'D + a hole** | Stage E promises the stats strip (`channels-scope-5-kickoff.md:99-101`, `:123`). `FunnelFlowRunEvent` (`schema.prisma:1111-1124`) records node **outcomes**; **nothing anywhere records delivered / opened / clicked** | The counters themselves are unplanned — see §2. |
| **E6. Flow-level Runs / CTR columns** | How he judges which flow works | **PARTIAL** | `runCount` / `completedCount` on `FunnelFlow` (`schema.prisma:1027-1028`); no CTR, no surface | CTR needs click data we do not collect (E5). |
| **E7. "Called by" cross-reference** | Shows which flows call a given sub-flow | **MISSING** | No reverse index; `start_flow` targets resolve at runtime only | Small, but it is how he navigates `functions/`. |
| **E8. User Fields settings screen** | 14 custom fields, in a `client_interaction` folder | **BUILT** | `ContactFieldDefinition` `schema.prisma:355-367`, `apps/api/src/contacts/contact-fields.controller.ts`, surfaced in `apps/web/src/routes/Settings.tsx` | No field **folders**. Types: ours vs his 6 (Text/Number/Date/Date&Time/True-False/Array) not compared here. |
| **E9. Tags settings screen** | 3 tags, in folders | **BUILT** | `apps/api/src/contacts/tags.controller.ts:47`, `apps/web/src/lib/tags-api.ts`, `Settings.tsx` | No tag folders; no per-tag subscriber counts. |
| **E10. Contacts list + contact profile** | Who came through | **BUILT** | `apps/web/src/App.tsx:78-79` → `Contacts.tsx`, `ContactDetail.tsx` | — |
| **E11. Live Chat / Inbox** | Human takeover | **BUILT** | `apps/web/src/App.tsx:77` → `Inbox.tsx:40-46` (multi-channel: WhatsApp + Instagram DMs) | Inbound is dead on both channels today, so it has nothing to show. |

**Row counts — BUILT 5 · BUILT-UNGATED 13 · PARTIAL 11 · SPEC'D 4 · MISSING 5** (38 rows).

---

## 1. THE CRITICAL PATH

The ordered minimum set of gaps that must close before **any** real conversation
can move off ManyChat. Ordered by dependency, not by size.

1. **A DM must reach us at all.** Instagram delivers **zero** `messaging`
   webhooks (measured in prod, `HANDOFF.md:40-45`) and the WhatsApp number is
   disconnected. Every reply-driven capability — question nodes, button
   branches, timeouts, the advance loop — is unobservable until one DM channel
   is alive. This gates everything below and is the operator's own flip
   (`HANDOFF.md:63-65`).
2. **Identity: does an IG comment author id resolve to the same `Contact` row as
   a DM IGSID?** Unanswerable from code — the codebase asserts they are *not*
   interchangeable (`channels-scope-5-kickoff.md:711-741`). If they differ,
   every comment-triggered flow is structurally broken: the session hangs on one
   row and the reply arrives on another. Needs one live comment + one live DM
   from the same person. Depends on (1).
3. **A graph-WRITE path and the canvas (Stage E).** There is no API and no UI to
   create a flow — the only funnel controller is the worker callback
   (`funnel-flows.module.ts:18`). Nothing in section B or C can be used by
   anybody until a flow can exist. Its two pinned prerequisites land with it:
   zod/`validateFunnelGraph` on every write, and an SSRF guard on
   dynamic-block/external-request URLs.
4. **Tappable buttons on the send side.** His three highest-value live flows
   (SG, TORONTO, and the budget-tier qualification) are button-driven; today
   buttons render as text (`render-message.ts:41-53`) and, because of that, the
   button branch ports are unreachable (`funnel-engine.service.ts:466`). This
   needs the IG button spike **and** a sender that emits an interactive payload.
   Depends on (1) for the spike.
5. **Message-text personalization (`{{field}}`).** His live follow-up addresses
   the contact by name; ours cannot (`render-message.ts:13-14`). Small, but it
   is visible in the first message a customer reads.
6. **Seed his real flows (Stage F)** — **re-read the live inventory first (§0),
   not the frozen capture**: our seed covers 3 of his 5 comment flows and misses
   `Keyword "YARDS"` entirely, and three flows (YARDS, WIP, Must-follow) have no
   status ruling. Resolve the n8n-vs-LeadRat divergence (§4.2) and the
   keyword-variant divergence (§4.1) explicitly, not silently. Depends on (3).
7. **Sequences.** Client Follow-up #1 is triggered *only* by a sequence
   subscription (breakdown §4). Without a sequence engine his entire
   post-qualification follow-up motion stays on ManyChat, whatever else moves.
   Independent of (1)-(6) — it can be built in parallel, and it is the one thing
   on this list that is not started at all.

---

## 2. WHAT MANYCHAT DOES THAT WE HAVE NOT PLANNED AT ALL

Absent from every stage of `channels-scope-5-kickoff.md` **and** from
`plans/rethink/scope-board.md`. Not softened.

1. **Sequences — the whole engine.** No model, no action, no scheduler, no
   kickoff, **and zero occurrences of the word "sequence" in the scope board.**
   The kickoff calls it a "fast-follow package" (`:19`) and then lists it under
   "Out of scope" (`:194`) — which means it was named once and never boarded.
   Meanwhile `contact.sequence_subscribed` and `contact.sequence_unsubscribed`
   already sit in our shipped trigger vocabulary (`types.ts:405-406`) pointing at
   an object that does not exist. This is his live follow-up motion.
2. **Message-text personalization.** `{{field}}` resolves inside HTTP bodies and
   CRM payloads only. No stage mentions rendering it into message text. His live
   Client Follow-up #1 uses it.
3. **Real media sends.** Image/file/audio/video blocks are modelled and then
   rendered as `[image] <url>` text (`render-message.ts:15-24`). No stage says a
   real media send is owed; Stage B's oracle never covered one.
4. **A sender that can emit a tappable button or quick-reply at all.** Stage E is
   scoped as *the canvas UI*; Stage B explicitly deferred button rendering to
   "Stage E/live-sender" (`funnel-engine.service.ts:775-782`), and the IG spike
   was re-pinned to Stage D and then declared BLOCKED (`kickoff:621-622`). No
   stage owns building the interactive sender itself. Consequence, stated in no
   log: **button branch ports in the graph are currently unreachable.**
5. **Delivery analytics — Sent / Delivered / Opened / Clicked.** Stage E promises
   the per-node stats strip and the funnel panel, but nothing anywhere records
   delivery, open or click events. `FunnelFlowRunEvent` stores node outcomes.
   Flow-level CTR — the number he actually steers by — has no data source.
6. **Reaction and story-mention auto-responders.** Both are ON in his account's
   Basic tab. The kickoff rules "No Basics tab" (`:19`) and no other stage picks
   the capability up, so it is a silent deletion, not a deferral.
7. **The "called by" cross-reference.** How he navigates the `functions/` folder.
   No reverse index exists or is planned.
8. **Folders for tags and user fields.** His account uses both
   (`client_interaction` field folder; Auto-DM / Grow-followers tag folders).
   `FunnelFlowFolder` covers flows only.
9. **Contact-event triggers have no emitter.** The entire Contact-events tab is
   configurable and can never fire — the Stage-D log concedes it in a
   parenthetical ("the contact-event bus emission points are not wired (Stage
   E/F)", `kickoff:630-631`), which is the closest thing to a plan it has.

---

## 3. WHAT WE DO THAT MANYCHAT CANNOT

1. **Off · Shadow · Live per flow, where shadow is propose-and-approve.** Every
   step drafts a review item the operator approves before a customer sees it
   (`funnel-engine.service.ts:869-888`). ManyChat is binary live/stopped.
2. **Direct, verified CRM writes.** Four LeadRat actions (save, update fields,
   append note, set status) against a contract that was **measured against the
   live API**, not inferred (`kickoff:1292-1320`): partial-merge semantics, leaf
   status ids, refusal detection on HTTP 200. Failure is data — it lands on the
   run row and raises a Needs-action item. ManyChat POSTs a webhook into n8n and
   never learns whether the lead landed.
3. **A real AI agent as the default reply**, with a hard no-conflict mutex: one
   active session per contact, and the agent is muted for the whole session
   (`schema.prisma:1079-1090`, agent check before every LLM call). ManyChat's
   fallback is another scripted flow.
4. **Comment moderation ahead of everything.** A hide/delete policy with
   confidence thresholds, escalation alerts and an audit spine runs **before** any
   flow may claim a comment (`comment-pipeline.ts:524-580`). ManyChat has no
   moderation surface at all.
5. **Compliance that outranks the flow.** A `STOP` is handled at the boundary
   before the graph sees it, releases the session and records consent
   (`packages/shared/src/contacts/opt-out.ts`); consent is re-checked before
   trigger start and before every send; a sweep heals any release that did not
   land (`kickoff:879-891`). ManyChat's opt-in/opt-out are ordinary actions a
   flow may or may not call.
6. **A node-granular run log with typed outcomes** (`schema.prisma:1111-1124`) —
   including honest partial successes like `crm_updated_note_failed`. ManyChat
   shows aggregate percentages, never why one contact stalled.
7. **WhatsApp as a first-class channel.** His ManyChat account is
   Instagram/Facebook/TikTok only; our trigger catalog carries WhatsApp from day
   one (`trigger-catalog.ts:47-53`).
8. **Everything an operator might change is data.** Keywords, intent thresholds,
   per-inbound and hourly AI caps, question retry counts and timeouts, CRM
   missing-lead fallbacks and re-entry note text are all seeded settings, not
   constants (`packages/shared/src/funnel-flows/settings.ts`, `types.ts:510-566`).
9. **Deferred triggers are visible with their reason** rather than absent — Ref
   URL / QR / Ads are greyed in the picker with an honest explanation
   (`trigger-catalog.ts:56-57`). ManyChat simply offers what it has.

---

## 4. Where the documents and the code disagree

Surfaced, not reconciled (project hard rule). Each names both sides.

**4.1 — TORONTO's misspelling variants.** The kickoff's locked frame says
"**Exact keywords only (no misspelling variants)**"
(`channels-scope-5-kickoff.md:31`) and the scope board records the same as a
small task, asserting "our seed uses exact keywords"
(`plans/rethink/scope-board.md:393-395`). The **deployed code seeds all eight
variants**: `packages/shared/src/agent/triggers.ts:86-95`, iterated at
`apps/api/src/instagram/ig-comment-admin.service.ts:99`. Two docs claim a
behaviour the shipped seed contradicts.

**4.2 — n8n in the lead path.** `manychat-breakdown.md` §4 records that every
collector ends with "POST client data" to
`https://n8n.silveroakglobal.ae/w…`, and the kickoff's own Stage-2 capture
names the endpoint (`:363-365`). The kickoff's locked frame says "**Leads
DIRECT to LeadRat; n8n never fed**" (`:20`). Both are true statements about
different systems — but Stage F is ordered to seed "the operator's real flows
(breakdown §4)" (`:108`), and seeding them as captured would wire n8n back in.
The conflict is unresolved in writing; it will bite at seed time.

**4.3 — The captured breakdown is already incomplete. RESOLVED AGAINST IT by
the live API.** `manychat-breakdown.md` §4's Comments-to-DMs folder lists three
flows (TORONTO, SG, INVEST). `GET /fb/page/getFlows` returns **five** in that
folder: it also holds **Keyword "INVITE"** — which the breakdown omits entirely
while the kickoff's Stage-2 export (`:316`) and dashboard read (`:354-367`)
both carry it, and our code seeds it (`packages/shared/src/agent/triggers.ts:100-104`)
— and **Keyword "YARDS"**, created 2026-07-23, which appears in **no document
and no seed**. Three further flows are missing from the breakdown: **WIP**
(client interaction), **Must-follow** (Templates), one **Untitled** (functions),
plus all **6 ManyChat system flows**. And `getGrowthTools` shows a **fourth**
comment-trigger tool the kickoff's export does not list. Conclusion for the
operator: **the frozen capture is a floor, not an inventory** — any parity or
seeding work must re-read the live list, and three flows (YARDS, WIP,
Must-follow) need a status ruling before anyone decides whether to rebuild them.

**4.4 — The TORONTO misspelling item cannot be closed from the API.**
`getFlow`, `getKeywords`, `getGrowthTool` and `getWidgets` all 404 (probed
2026-07-25), so a trigger's keyword list is unreadable programmatically — the
kickoff's own research verdict (`:383-388`), re-proven. The board item
(`scope-board.md:393-395`) therefore stays open on a dashboard read, and §4.1's
code-vs-docs conflict stays unresolved on our side too.

**4.5 — Tappable buttons on Instagram.** `packages/shared/src/agent/triggers.ts:70-72`
asserts IG private replies carry no tappable buttons, so CTAs fold into text
links. The kickoff's red-team says Meta's docs support quick replies and button
templates on IG (13 × 20 chars) and **his own live SG flow shows a tappable
button with 100% CTR** (`:255-259`). Flagged as a ⚠ CONFLICT since 2026-07-23;
the deciding spike is BLOCKED on live Meta calls (`:621-622`). Our code takes the
conservative side by default (`render-message.ts:47-51`).

**4.6 — Is "Start Automation" a node or an action?** `manychat-breakdown.md` §2
lists it as an Actions sub-item; `manychat-capability-catalog.md` §2 says it is a
peer top-level node and states outright that "the breakdown's node-type table
conflated the two" (`:287-290`). Our code implements **both**: a `start_flow`
node (`types.ts:646`, executed at `funnel-engine.service.ts:711`) **and** a
`start_flow` action (`types.ts:532`) — and the action has **no live executor**
(§C4). Accidentally matching both documents produced one dead code path.

---

*Read-only survey, branch `rethink` @ `ed253c9`. The ManyChat side was read from
the live API on 2026-07-25 with GET requests only — nothing was created,
updated, deleted or sent. Our side has **not** been verified against live
traffic: no funnel flow has ever run.*
