Lifecycle tool, mockup

One app, two sections: Orchestrate (journeys, their nodes, their audiences) and Compose (messages, blocks). A client switcher sits above both, because this is not a Dock-Blocks-only tool. Control is three verbs, pause, resume and drain, and they live only on a journey row or the single-journey screen. Authoring stays a clone, a file and a review. There is no blank canvas anywhere in this app.

Journeys

7 defined, 3 live, 1 paused

Control is per row: pause, resume, drain. There is no button here that starts a journey from nothing: every row on this screen began as a clone of another row. Only rows with a Definition file are rendered from real data; the rest are illustrative catalogue rows and are labelled as such so they cannot be mistaken for built journeys.

JourneyNameStateAudienceChannelIn flightNoteDefinitionActions
winback-2026Dormant win-backpaused150email0ok, domain verified, warming upcampaigns/lifecycle/winback-2026.yaml
reconversionReconversionlivelive queryemail+smsunknownokno definition file yet
nurture-productProduct nurturelive22 listsemailunknownokno definition file yet
welcome-smsWelcome SMSlivelist 68smsunknownok, Brevo automation 55no definition file yet
dist-receiptDistributor receiptbuilton uploademail0okno definition file yet
sms-drip-2-4SMS drip 2 to 4builtnon-converterssms0held, nothing sentno definition file yet
c13-tradeshowTrade show re-engagedraftnot definedemail0no audienceno definition file yet

winback-2026

paused, 150 in audience, 0 in flight

Node graph: entry, message, wait, decision, branch, exit

Rendered from campaigns/lifecycle/winback-2026.yaml, not a hand-copied approximation.
entryentry
winback-2026.entry
audience: aud.winback.gate1_pilot; when: gate1b_dormant_365d; reentry: never; limit: 150
suppressionsuppression
winback-2026.suppression
stops: replied, unsubscribed, converted, stop_status; shared with: brevo, mailtrap
touch-1message
email
approved
winback-2026.touch-1
content: msg.winback-touch-1 (approved); if absent: abort
GATE: suppression
wait-1wait
winback-2026.wait-1
for 4d
touch-2message
email
approved
winback-2026.touch-2
content: msg.winback-touch-2 (approved); if absent: abort
GATE: suppression
wait-2wait
winback-2026.wait-2
for 4d
touch-3message
email
approved
winback-2026.touch-3
content: msg.winback-touch-3 (approved); if absent: abort
GATE: suppression
doneexit
winback-2026.done
Decisions in this file
decided #1Sending domain. DECIDED 2026-10-03 by Alecia ("yes to win-back" and "you have access to godaddy to update the spf record"): m.dock-blocks.com via Mailtrap, because a dormant audience carries the highest unsubscribe risk and must stay off the Brevo account (strike two) and off the root domain (Matt's mail and Shopify transactional). DNS VERIFIED COMPLETE, read live 2026-10-03: all five Mailtrap records exist. mt75.m is the domain-verification/return-path CNAME, pointing at smtp.mailtrap.live, which publishes Mailtrap's SPF, so SPF passes on the return path and aligns under the root's adkim=r/aspf=r; Mailtrap's own docs say domain verification covers SPF and a separate SPF record should not be added. rwmt1/rwmt2._domainkey.m are the DKIM records, mt-link.m is the tracking record, and _dmarc.m exists with p=none. A PREVIOUS version of this line said SPF was MISSING; that was WRONG and is corrected here. The one oddity left, not to be changed: _dmarc.m's rua/ruf point at smtp-staging.mailtrap.net. The domain has never sent, so the 150-person pilot is also its warm-up.
decided #2The open/no-open fork and the "did not open" message (msg.winback-touch-2b) are RESOLVED 2026-10-03, Alecia's decision: dropped. Win-back is a straight line of three touches, matching Njui's approved campaign copy (gate1b, touches n=1/2/3). Everyone gets all three unless the suppression gate refuses them, or they reply or unsubscribe. There is no touch-2a/touch-2b distinction and no decision node; this is the permanent design, not a placeholder.
open #3Shared suppression across Brevo and Mailtrap is written here as a target (shared_with: [brevo, mailtrap]) but is not actually enforced: Mailtrap sends bypass the eight refusals in scripts/lib/brevo_send.py. Held by Alecia with Njui (prototype doc Part 7, "the one that cannot be deferred").
decided #4Content-type classification for frequency purposes. RESOLVED 2026-10-03, Alecia's decision: EDITORIAL. brevo-sending-limits-as-built.md gates per-person frequency by content type: promo 30 days, editorial 7 days, SMS 14 days, with a 3-in-30 backstop across any kind. This journey reads as a personal check-in from the CEO or the owning rep, carrying no offer, so it is classified editorial rather than falling to the promo default.
open #5The "one send per person per 90 days" rule is RECONVERSION's rule (returning leads), not this journey's. Confirmed by checking: lifecycle-surfaces-2026-10-01.html ties that 90-day language to "returning leads / Reconversion" specifically, and nothing in the win-back sources repeats it for gate1/ gate2/gate3b. Not applied here; see #4 for the rule that actually governs win-back's own cadence.
open #6gate2 (stalled prospects) has no SQL in export_audience.py per lifecycle-surfaces-2026-10-01.html ("gate2: not exported, no SQL exists"). This file only runs gate1_pilot. A gate2 wave is a clone, not an extension of this file, and cannot be built until that audience is exported.
The gate runs before EVERY message. Not once at entry. Somebody can unsubscribe between step two and step three, so an eligibility answer from five days ago is a guess about today. Njui raised this and the vendors agree: Braze rechecks the cap at the actual send. There is no holdout node. A holdout is a branch whose path does nothing, computed as an ordinary audience in the warehouse, same as any other segment, never a special node type.

Launch preflight, re-run on every refresh

CheckNowRead
Audience pinnedpass150 rows
Suppression freshpass18,390, read now
Consent opt-inpassper person
Frequencypasssee cap panel
Domain authenticatedpassSPF via domain verification + DKIM live, DNS verified 2026-10-03
Domain warmfailno history
Stranded checkmissingsensor built, needs DSN

Frequency, both cap layers

Cap layerWindowNowRead
Email, per channel30d promo cappass3 of 30 used
SMS, per channel7d capwarn2 of 3 used
Combined, all channelsstricter cap, evaluated lastpass4 of 6 touches in 14d
Combined is stricter than either channel alone and is evaluated last. Someone can sit inside the email cap and inside the SMS cap individually and still be over the combined cap the moment both channels have touched them this window.

Propose an edit: agents propose, a human approves

journey winback-2026 · proposed by agent:cleanup-bot · status pending approval
- entry.limit: 150
+ entry.limit: 300
- wait-1.for: 4d
+ wait-1.for: 7d
A tool edit produces a versioned artifact: a diff against the journey file. Agents propose changes as a diff and never act. A named human approves it before it is live. Approved by: (not yet)

Audiences

6 defined
Filter by channel: All channelsEmailSMS
AudienceSizeChannelComputedUsed byNote
aud.winback.gate1-pilot150email02:30 todaywinback-2026pinned
aud.winback.gate1-rest1,648email02:30 todaywave 2not released
aud.winback.gate2no sizeemailnevernothingno SQL exists
aud.reconversion.returninglive queryemail+smson triggerreconversion90d rule
aud.nurture.by-product7 to 38 eachemailunknownnurture-product22 lists
aud.welcome.sms-opted-in221smson lead createwelcome-smsBrevo list 68
Segments are computed in the warehouse, nightly, and the tool only selects them. Not defined in Brevo, not defined twice. Two definitions of dormant that disagree is the documented failure, and we already have three rival eligibility functions.

Compose · Messages

3 from winback-2026.messages.yaml, 3 illustrative

Compose is content, not control. Nothing on this screen pauses, resumes or drains anything: those verbs live on the journeys that use these messages, not here. The msg.winback-* rows are loaded from campaigns/lifecycle/winback-2026.messages.yaml: three touches, all status approved, sourced from Njui's approved campaign copy. The open/no-open fork and its unapproved msg.winback-touch-2b are RESOLVED (2026-10-03, Alecia's decision) and no longer exist in this registry.

MessageStageBlocksChannelNeedsIf absentVariants
msg.winback-touch-1approvednoneemail2 attrsabort1
msg.winback-touch-2approvednoneemail2 attrsabort1
msg.winback-touch-3approvednoneemail2 attrsabort1
msg.cart.abandoneddraft7 blocksemail3 attrsomit6
msg.ambassador.invitedraft6 blocksemail2 attrsrefuse2
msg.seasonal.roundupnot startedlist shapeemailunknownomitunknown
Variants is the column that earns this screen. An attribute-driven message is a different body per person, so the defect that ships is in the variant nobody rendered. Six defects passed every automated check on Labor Day and were caught by eye.

Compose · Blocks

view only: marketers view, engineering authors

Engineering owns the gate, adapters, schema, validator and block authoring. Marketers can view a block's rendered output and where it is used, and cannot edit it here. Editing a block is a code change and a review, same as the gate.

BlockRoleUsed byAccess
mastheadheader8 messagesview only
hero-2primary offer3 messagesview only
item_rowproduct line5 messagesview only
ctacall to action8 messagesview only

Architecture

LimeJourney's published backend components, mapped to ours

Boxes and labelled arrows, the shape of LimeJourney's own backend diagram: event sources into webhook intake, into the warehouse (bronze, silver, gold, where segments are computed), into the DBOS journey engine, through the send-time GATE before every send, out to Brevo and Mailtrap. Dashed boxes are GAPs: nothing is invented to fill them.

bronze to goldrefuse/sendWeb client / SDKZoho, Shopify, Brevo,ApolloREST APIWebhook intakeGAP: Kafka eventqueuenone; nightly batchPostgresDBOS state + warehouseJourney Engine(orchestrator +Temporal)DBOS TransactGAP: Redisjourney/triggermapnothing needs itGAP: ClickHousegold answers nightlySegmentationEngineWarehouse gold, nightlyExternal action(email / push)Brevo email + SMS,MailtrapSend-time gate(ours)before every message

The table below is evidence under the diagram, not the view: each row traces to the repo doc or code line that verified it.

Web client / SDK
Event sources: Zoho CRM, Shopify, Brevo + Apollo, GA4 / Ads / Aircall
docs/planning/lifecycle-inventory-reconciled-2026-10-01.md source list (1)-(4); LIFECYCLE_COMMS_MAP.md system column
REST API
Webhook intake: scripts/webhooks/zoho_to_brevo.py, scripts/webhooks/shopify_to_zoho.py
scripts/webhooks/zoho_to_brevo.py, scripts/webhooks/shopify_to_zoho.py (code read)
Kafka event queue
GAP GAP: no event queue. Nightly batch via Dagster is right-sized for one client.
scripts/deliverables/build_lifecycle_surfaces.py: "No Kafka, one client, nightly batch is right-sized"
Postgres
DBOS system tables (durability) plus the warehouse Postgres (medallion bronze/silver/gold)
scripts/monitoring/dbos_version_drift.py DBOS_SYSTEM_DB_URL; build_lifecycle_surfaces.py warehouse box
Journey Engine (orchestrator + Temporal)
DBOS Transact: durable per-person execution, replacing Temporal
docs/architecture/eventcatalog-catalog-as-built.md:55 "DBOS callback"; docs/planning/lifecycle-inventory-reconciled-2026-10-01.md dormant-winback-2026.yaml executed by DBOS
Redis journey/trigger map
GAP GAP: no Redis. Nothing in this estate needs a sub-second lookup.
scripts/deliverables/build_lifecycle_surfaces.py: "No Redis, nothing needs sub-second lookup"
ClickHouse
GAP GAP: no ClickHouse. The gold layer (person_lifecycle, consent_state) answers these questions nightly, not in real time.
scripts/deliverables/build_lifecycle_surfaces.py: "No ClickHouse, gold already answers these questions"
Segmentation Engine
Warehouse gold layer, computed nightly by Dagster; the tool only SELECTS a segment, it never defines one
docs/planning/lifecycle-inventory-reconciled-2026-10-01.md: "Segments are computed in the warehouse and the tool only selects them"
External action (email / push)
Brevo (lifecycle email + SMS) and Mailtrap (volume sends, e.g. dormant win-back)
docs/planning/lifecycle-inventory-reconciled-2026-10-01.md: "DBOS via Mailtrap, sender matt@m.dock-blocks.com"
The send-time gate (ours; no LimeJourney equivalent in their published diagram)
Re-checked before every message: suppression, consent, frequency, ramp
docs/workflows/lifecycle-comms-senders-as-built.md gate logic traced in zoho_to_brevo.py; every message node in the Orchestrate canvas carries this as a GATE badge

Naming, applied where a name is typed

Naming is not a screen in this tool. It is a validator that runs the instant a journey, node, audience, message, module or list gets a name: in the Clone modal, on the Journeys screen. See it there rather than as a separate stop nobody makes before naming something.