Skip to main content
← Back to list
01Issue
FeatureOpenSwamp ClubPublic
AssigneesNone

Relationships

▾ parent of #2009▾ parent of #2010▾ parent of #2011▾ parent of #2012▾ parent of #2013▾ parent of #2014▾ parent of #2015▾ parent of #2016▾ parent of #2017▾ parent of #2018▾ parent of #2019▾ parent of #2020▾ parent of #2021▾ parent of #2022▾ parent of #2023▾ parent of #2024

#2008 Epic: Product nurture — lifecycle identification, dedupe-safe delivery, and operator control (email, inbox, CLI)

Opened by keeb · 9/4/2026

Six lifecycle moments deserve a message and currently get nothing. This epic builds the machinery to find the right operatives, decide once whether to contact them, and deliver on three channels — Resend email, the swamp-club inbox, and a waiting-notifications hint in the CLI.

It spans two repos: swamp-club (identification, ledger, admin control, email, inbox, API) and swamp (the CLI hint). Sub-issues are tagged by source.

The flows

From the whiteboard, with the segment name each one is really about:

# Trigger Segment Message
1 Signup Welcome, split by whether the CLI drove the signup
2 Signed up ≥30d ago, never ran the CLI ARRIVED, NEVER RAN Re-engage: install / next steps
3 Sustained model usage, zero workflow run BUILDING PRODUCT FEATURES Promote workflows + shareability
4 Published an extension GROW CHAMPION Celebrate, invite them to campaign for it
5 Was active, nothing for 7 days DRIFTED "Your quest is waiting"
6 A run failed, then nothing NEEDS HELP "Need help?"

Flow 1 already ships. sendWelcomeEmail (#1168) sends welcome-cli or welcome-web off the sc_cli_origin marker, once, behind an atomic claim. It is in scope only as the pattern to copy and as the thing flows 2–6 must not duplicate.

Order of work, and why it is this order

The ask was explicit: identification first, and prove we will not send duplicates, before a single message goes out. That is also the only order that is safe here, for one reason:

A time-triggered sweep, run for the first time against an existing population, finds the entire backlog at once and mails all of it.

trial-ended-watch.ts already hit this and solved it by refusing to run until a backfill marker exists. Nurture faces it six times over — every flow's first tick sees years of qualifying history. So:

  • Phase A builds identification and writes candidate rows that are visibly marked as never-to-be-sent. Nothing leaves the building. /admin/nurture is the verification surface: you look at real names, real evidence, and real counts, and only then decide the rule is right.
  • Phase B builds the delivery spine — suppression, an outbound transport that reports its outcome, the templates — and arms exactly one flow (extension published: event-triggered, no backlog, and a message nobody minds receiving).
  • Phase C arms the remaining flows one at a time. Each is its own issue because each has its own backlog size and its own false-positive story.
  • Phase D adds the inbox channel on the same decision record.
  • Phase E adds the CLI hint.

Non-negotiables

One decision, many channels. A nurture_send row is written at identification time with a deterministic _id of <flow>:<occurrenceKey>:<operativeId>. The unique index is the dedupe. Email and inbox are two deliveries of one row, never two producers — otherwise a Resend retry duplicates the inbox message.

Absent is not unknown. An operative with no bound device has unknown activity, not zero: someone who runs the CLI without swamp auth login is indistinguishable from someone who never installed it. Mailing them "you never got started" is a lie the recipient can see. Unresolvable operatives are skipped with a reason, never mailed — the #1589 rule, applied to people.

Devices are not people. ~11.6× more distinct_ids than humans. Every qualification is at the account grain, resolved device→owner through identity_map. collective:<id> grains are excluded outright — a team daemon is a credential, not a person (#1679).

Identification is a batch job and must never touch a request path. It may scan swamp.events for things cli_daily cannot answer (failures — cli_daily is result_status = 'success' only). That is fine at a nightly cadence and catastrophic on a page render (#1317, #1586). A static guard should pin it.

Every candidate carries its evidence. The numbers that made someone qualify are stored on the row and rendered in the panel. Without that, the panel is a list; with it, it is a diagnosis, and a wrong rule is visible before it mails anybody.

Known traps found during investigation

  1. ResendMailer.send swallows failures. A non-2xx is logged and the method returns void. You cannot record send status through it — B2.
  2. Resend suppression does not cover POST /emails. newsletter_subscriber.unsubscribedAt is mirrored from Resend broadcasts. A direct nurture send bypasses it entirely — B1.
  3. There is no per-operative product-email preference. Only the newsletter list has one. Nurture needs its own, with a footer link — B1.
  4. "Broadcast messages in the app" ≠ BroadcastMessage. In this codebase a broadcast goes to everyone. Nurture is targeted: sendSystemMessage({audience: {type: "users"}}) — D1.
  5. buildInboxView is too heavy to poll. It reads lab issues, broadcasts and colours tiers (a score read). The CLI needs a dedicated count endpoint, and that count must match what the inbox actually shows or the hint lies — E1.
  6. One human can hold several operatives. disableImplicitLinking mints a new account when a social email does not match. Per-operative dedupe will still mail the same person twice, so the frequency cap is keyed on the address, the way the Attio person grain is (#1665) — B1.
  7. An unnamed operative must hold, not send. SSO accounts are named milliseconds-to-minutes after creation; the announce path already learned this. Any copy rendering a username holds rather than going out blank.

Definition of done

All six flows armed in production; every send visible in /admin/nurture with its evidence, channel outcomes, and suppression reasons; the CLI showing a waiting-notifications line; and a documented, verified answer to "why did this person get this message, and can they get it twice?" — no.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

9/4/2026, 7:40:18 PM

No activity in this phase yet.

03Sludge Pulse
keeb linked parent of #20099/4/2026, 7:41:03 PM
keeb linked parent of #20109/4/2026, 7:41:04 PM
keeb linked parent of #20119/4/2026, 7:41:05 PM
keeb linked parent of #20129/4/2026, 7:41:07 PM
keeb linked parent of #20139/4/2026, 7:41:08 PM
keeb linked parent of #20149/4/2026, 7:41:09 PM
keeb linked parent of #20159/4/2026, 7:41:10 PM
keeb linked parent of #20169/4/2026, 7:41:11 PM
keeb linked parent of #20179/4/2026, 7:41:12 PM
keeb linked parent of #20189/4/2026, 7:41:13 PM
keeb linked parent of #20199/4/2026, 7:41:15 PM
keeb linked parent of #20209/4/2026, 7:41:16 PM
keeb linked parent of #20219/4/2026, 7:41:17 PM
keeb linked parent of #20229/4/2026, 7:41:18 PM
keeb linked parent of #20239/4/2026, 7:41:19 PM
keeb linked parent of #20249/4/2026, 7:41:20 PM

Sign in to post a ripple.