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.
Journey
Name
State
Audience
Channel
In flight
Note
Definition
Actions
winback-2026
Dormant win-back
paused
150
email
0
ok, domain verified, warming up
campaigns/lifecycle/winback-2026.yaml
reconversion
Reconversion
live
live query
email+sms
unknown
ok
no definition file yet
nurture-product
Product nurture
live
22 lists
email
unknown
ok
no definition file yet
welcome-sms
Welcome SMS
live
list 68
sms
unknown
ok, Brevo automation 55
no definition file yet
dist-receipt
Distributor receipt
built
on upload
email
0
ok
no definition file yet
sms-drip-2-4
SMS drip 2 to 4
built
non-converters
sms
0
held, nothing sent
no definition file yet
c13-tradeshow
Trade show re-engage
draft
not defined
email
0
no audience
no definition file yet
Clone from template: naming is validated here, inline
Every journey clone, node tag, audience, message, module and list name is
checked against a pattern THE MOMENT it is typed or cloned, not on a separate screen nobody
opens before naming something.
Thing
Pattern
Example
Journey
<program>-<year>
winback-2026
Node tag
<journey>.<node>
winback-2026.touch-1
Audience
aud.<program>.<cut>
aud.winback.gate1-pilot
Message
msg.<program>.<role>
msg.cart.abandoned
Module
<role>, never client-prefixed
hero-2
Brevo list
<program>.<segment>
nurture.boat-lifts
Inline check, live as you type
Typed name
Result
Why
winback-2027
✓ valid
lowercase, program-year, matches the journey pattern
Winback_2027
✗ invalid
uppercase and an underscore; pattern is lowercase, hyphens only
hero-2
✓ valid
module role name, no client prefix (correct, it is shared across clients)
dockblocks-hero-2
✗ invalid
module names are never client-prefixed; this breaks sharing with every other client
A clone inherits the template's exits, suppression and gates. There is no
blank canvas: a journey that did not come from a clone does not exist in this tool.
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
Check
Now
Read
Audience pinned
pass
150 rows
Suppression fresh
pass
18,390, read now
Consent opt-in
pass
per person
Frequency
pass
see cap panel
Domain authenticated
pass
SPF via domain verification + DKIM live, DNS verified 2026-10-03
Domain warm
fail
no history
Stranded check
missing
sensor built, needs DSN
Frequency, both cap layers
Cap layer
Window
Now
Read
Email, per channel
30d promo cap
pass
3 of 30 used
SMS, per channel
7d cap
warn
2 of 3 used
Combined, all channels
stricter cap, evaluated last
pass
4 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
Audience
Size
Channel
Computed
Used by
Note
aud.winback.gate1-pilot
150
email
02:30 today
winback-2026
pinned
aud.winback.gate1-rest
1,648
email
02:30 today
wave 2
not released
aud.winback.gate2
no size
email
never
nothing
no SQL exists
aud.reconversion.returning
live query
email+sms
on trigger
reconversion
90d rule
aud.nurture.by-product
7 to 38 each
email
unknown
nurture-product
22 lists
aud.welcome.sms-opted-in
221
sms
on lead create
welcome-sms
Brevo 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.
Message
Stage
Blocks
Channel
Needs
If absent
Variants
msg.winback-touch-1
approved
none
email
2 attrs
abort
1
msg.winback-touch-2
approved
none
email
2 attrs
abort
1
msg.winback-touch-3
approved
none
email
2 attrs
abort
1
msg.cart.abandoned
draft
7 blocks
email
3 attrs
omit
6
msg.ambassador.invite
draft
6 blocks
email
2 attrs
refuse
2
msg.seasonal.roundup
not started
list shape
email
unknown
omit
unknown
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.
Block
Role
Used by
Access
masthead
header
8 messages
view only
hero-2
primary offer
3 messages
view only
item_row
product line
5 messages
view only
cta
call to action
8 messages
view 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.
The table below is evidence under the diagram, not the view: each row
traces to the repo doc or code line that verified it.
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.