PivotPlanit
Lifecycle operations

The lifecycle operating model,
and what is still missing

Briefing for Njui · 24 September 2026 · draft for review

What already exists across both stacks, what is genuinely missing, and how lifecycle marketing gets run day to day once it is in place. Every status here was checked against a live system, a timestamped log or a git object.

6.5%
Hard bounce, sending now
Mike Intro, from the root domain
1,960
Suppressed but enrollable
Apollo reads a 44-day-old list
4
Suppression stores
None reconciled
6 of 12
Layers already in place
The build list is four

The short version

This is not an absence of programs. It is two systems that cannot see each other

Eleven lifecycle programs are sending today across four senders. They were each built standalone, so there is no shared audience layer, no shared frequency cap and no shared measurement. Separately, a full customer platform exists in dockblocks-data-ops with identity resolution, consent modelling, Customer 360 and RFM, and an activation layer with a shared eligibility gate. It gates one stack.

The whole proposal is one sentence: one question, asked by four senders, answered in one place.

Settled

The split is by audience temperature

Brevo carries mail the recipient expects. New burner domains carry mail that might generate complaints. The line is whether the person is warm, not whether the message is marketing.

TrafficSenderClassCapped
Order confirmationShopify nativeTransactionalExempt, never suppressed
Shipped notificationBrevoTransactionalExempt
Welcome, nurture, quote nurtureBrevoExpectedYes
Abandoned cart, checkout, browseBrevo, to buildMarketingYes, needs unsubscribe
Rep-signed campaigns, warm listsBrevo, three-domain splitMarketingYes
Cold and dormant reactivationDBOS, burner domainsHigh riskYes
Cold prospectingApollo, hello@ domainsHigh risk, not lifecycleYes
Rep digests, ops alertsBrevo, directInternalNo, different population
  • Order confirmation stays on Shopify. Brevo may still be suspended. Receipts are the one class where failure is worst.
  • Apollo is not a lifecycle program, but it is a governed sender and the Prospect-stage entry point.
  • Reply-To at the rep is correct. It carries no sending reputation, is never authenticated or scored, and it is what puts a real answer in front of the person who can close.

The model

Stage and path are two dimensions, not one sequence

An earlier draft had a single line where a quote preceded every purchase. That is wrong. Shopify is direct, and Matt is explicit that product type decides the route.

"If they select pontoons. No. If they select boats over 5,000. No... the only two are boats over 5,000 and pontoons and tritoons... And then on the floating dock side, helicopter docks, commercial, they're not going to be able to figure that out. Special event docks. I think everything else they can get in the online store." Matt West, 2026-08-04

So path is knowable at lead capture, from product interest. Today it is recorded on the sales order, which is after the only moment it is useful. That is the same thing as the action item already assigned on that call: exclude pontoons, CD switches, helicopter docks, commercial and solar from Shopify links in nurture.

PathEnters atNote
Self-serveProspectShopify direct, no quote
Rep-ledProspectQuote via Zoho
Distributor / dealerCustomerCSV upload, B2B reorder
Secondhand-owner partsCustomer, no prior stageBought the dock on eBay or Craigslist
Warranty / expansionCustomerLifetime warranty, damage or add-on
"the main reason people need parts is they buy a dock block on the aftermarket and then they take it home. And their boat is different. So we get tons of orders. We're like, we can't find the original customer in there." Matt West, 2026-08-04, on where: "I'm guessing eBay and Craigslist"
What that breaks, two layers down

Identity resolution is deterministic and anchored on Zoho. A secondhand owner has no Zoho anchor, so they either create a new identity each time or fail to resolve. By Matt's account this path generates "tons of orders", and he calls parts the most profitable end of the business. The highest-margin line is the one the data model serves worst.

Two fields the stage model needs and Zoho does not have. Repeat is only a tick box, and Dormant has no status at all. So the stage field has to be created, not just populated.

What the research changed

Three principles, each of which changed a decision

State-evaluated beats trigger-based. A trigger reacts to one event in isolation. State evaluation reads the person's whole current condition at the moment of acting. Scalero's example: do not fire the hour-four SMS if the hour-one email was already opened. The warehouse computes state; the senders are not consulting it at send time.

Frequency capping is a separate lever from the domain split. The split stops one reputation carrying every unsubscribe. A cap stops one person receiving too much. Braze's point applies exactly here: over-messaging comes from several senders each sending a defensible amount with no shared view of the total, so the cap has to be one cap across every sender and channel, segmented by engagement tier, transactional exempt. Scalero's published benchmark is 2 to 4 emails plus 1 to 2 SMS per week per subscriber.

Holdouts, carved at design time. An A/B test proves which variant did better. Only a true no-send control proves the program was worth running. There are no holdouts anywhere in the estate today, and they are expensive to retrofit because the counterfactual population no longer exists.

One habit underneath all three

Each is a defence against the same mistake: an eligibility fact read before the moment of sending is a guess about the moment of sending. It appears four times in this estate, and it is worth designing against once rather than catching four times.

Smaller research notes worth keeping
  • Braze Canvas evaluates its entry segment once and never re-checks. Whatever we build, decide deliberately whether enrollment re-checks. It matters most for win-back, where someone who buys mid-program should stop receiving it.
  • Cadence is event-triggered, not a standing meeting. Braze publishes no weekly ritual: per-launch and per-peak-season checklists, plus one explicitly periodic item, the frequency cap review. Scalero runs a quarterly or annual audit.
  • Scalero's audit shape is adoptable unchanged: journey and touchpoint map, content per stage, automated workflow performance, churn and win-back review, quantitative plus qualitative, and a written action plan with owners and dates.
  • Neither vendor keeps one stage model. Scalero contradicts itself across its own posts; Braze is consistent at four. The lesson is not which cut to copy, it is that there must be exactly one.

Twelve layers

#LayerWhere it standsAction
01Identity and profileBuilt: person_map, Customer 360Accept. Add a freshness check
02Event spine42 events documented, snapshot pinned 2026-07-02, drift CI never builtBridge
03Stage modelRFM in gold. No agreed business stagesDecide. Five, above
04Journey mapThree competing catalogsBuild. Map tab
05Segment registryBuilt in gold, not adopted by Stack ABridge
06Shared frequency capDoes not existBuild. Blocking
07Shared suppressionFour stores, one reconciler in report-only modeBuild. Blocking
08Content and templatesStack A only, recovery flows unbrandedExtend
09Measurement and holdoutsRill exists, no holdouts, ROAS blindBuild
10Write-back to ZohoDBOS ledger is Postgres onlyBuild
11Cadence and auditDoes not existAdopt
12Doc reconciliationThree claimants, ~25 unmarked filesBuild first

Six of twelve are accept or bridge. The build list is four items and two of them gate everything else.

Coverage

Where programs exist, and where nothing does

Stage is relationship depth. Path is how someone transacts, and it is a separate dimension because three of the five paths never pass through Prospect at all.

Lens:
Live, verified sendingBuilt, not sendingCosting something todayNothing built
PROSPECT IN-MARKET CUSTOMER REPEAT DORMANT Self-serve Shopify direct, no quote Rep-led quote via Zoho Distributor CSV upload, B2B reorder enters here ▸ Secondhand parts owns the dock, no CRM record enters here ▸ Warranty / expand existing customer enters here ▸ Welcome email per rep, Doug off ZOHO Welcome SMS automation 55, list 68 BREVO Product nurture 22 lists, tpl 188 BREVO Abandoned cart unbranded, to replace SHOPIFY SMS drip 2-4 coded, never sent BREVO Order confirmation stays on Shopify SHOPIFY Shipped notice shiplog BREVO Repeat / add-on Matt asked for this Win-back 1,798, shadow-gated DBOS Cold prospecting 6.5% bounce, breaching APOLLO Welcome email per rep ZOHO Quote follow-up ON, never fired ZOHO Stale quote sits for years Order confirmation SHOPIFY Reconversion sending since 08-15 BREVO Order receipt writes OFF since 09-13 BREVO Reorder prompt Parts order conf. no identity to match SHOPIFY Owner onboarding highest margin line Warranty / expand lifetime warranty

Read this before using the map

This is how people agree coverage, ownership and gaps. It is not the execution model. Execution is rule-based and evaluates state at send time, so do not read the columns as a sequence anybody walks. The runtime lives in the Rules tab.

What the map shows

Three gaps are structural, not oversights

  • Repeat is empty on every path. Matt raised this himself: "how many repeat customers come back and change their docks and add on?" There is no program and no field.
  • Secondhand-owner parts has one node, and it is a receipt. No identity, no onboarding, no path into lifecycle, on what Matt calls the most profitable end of the business.
  • Stale quotes have no exit. They sit for years rather than resolving to Dormant, so win-back never sees them.

Two nodes are marked as costing something today rather than merely missing: Apollo cold prospecting at 6.5% hard bounce from the root domain, and the abandoned-cart flow running unbranded out of Shopify.

Execution model

The runtime has no sequence. It has rules

This is the half the coverage map deliberately cannot show. Every program is an eligibility question evaluated at send time, not a step on a path.

Authority

Eligibility is a property of the person: may this person be contacted at all, on this channel, right now, given everything that already touched them. One answer, so one owner: the gold layer, because it is the only component that sees all four senders plus Zoho plus Shopify. Enrollment is a property of the program and stays with whichever engine runs it. This asks for an interface, not for control of anybody's engine.

ProgramEntry conditionExitClassHoldout
WelcomeLead createdSequence complete or quote requestedExpectedNo, and should not
Product nurtureProduct interest set, path self-serveOrder, or quote requestedExpectedYes
Quote nurtureQuote issued, no orderOrder, or quote aged outExpectedYes
Abandoned cartCart abandoned, no order in windowOrder placedMarketingYes
Stale quoteQuote aged past threshold, no movementOrder, or moves to DormantMarketingYes
Rep campaignSegment from gold, warm onlyPer sendMarketingYes
Win-backDormant, 365 days no order and no engagementAny order or engagementHigh riskRequired
ReconversionReturning person detectedOne send per person per 90 daysHigh riskRequired
Cold prospectingApollo filter, verified emailReply, bounce or optoutHigh risk, not lifecycleNo
Order confirmationOrder placedSentTransactionalNever

The eligibility gate, in order

Every enrollment answers these before anything sends. Order matters: the cheapest and most absolute refusals come first.

1  suppressed?        one shared store. Unsubscribe, hard bounce, complaint, manual.
                       Missing store is a HARD FAILURE, empty store is a BUILD FAILURE.
2  consent?           opt-in, not not-opted-out. Per channel.
3  transactional?     if yes, stop here and send. Never capped, never suppressed.
4  frequency?         ONE cap across every sender and channel, by engagement tier.
                       benchmark 2-4 email + 1-2 SMS per week per person.
5  temperature?       computed from gold. A warm person must not be on a burner domain,
                       and a cold one must not be on a protected domain.
6  priority?          if two programs both qualify, the higher-intent one wins and the
                       other defers rather than both sending.
7  holdout?           if the person is in the control group, record the suppression as
                       a measurement event, not as a failure.

on gold unreachable:   marketing FAILS CLOSED. transactional is unaffected, which is the
                       other reason order confirmation stays on Shopify native.
Why the gate cannot live in a sender

Eligibility depends on facts no sender can see. Whether someone is over their weekly cap depends on what the other senders sent. Whether an audience is genuinely dormant depends on order history the sender does not hold. A sender asking itself "may I send this?" can only answer from its own records, which is exactly how the failures in the Findings tab happened.

Path is computed, not declared

A guard can prove a send left from a burner domain. It cannot prove the audience belonged there. Nothing in a payload distinguishes a correctly-routed dormant list from a warm list misrouted off Brevo. So temperature and path are computed attributes from gold, asserted at enrollment, never a label someone types into a campaign builder.

path := f(product_interest)   set at LEAD CAPTURE, carried forward
  pontoons, boats over 5,000 lb, tritoons   -> rep-led, no Shopify link in nurture
  CD switches                               -> rep-led, no Shopify link
  helicopter, commercial, solar, special    -> rep-led, no Shopify link
  everything else                           -> self-serve, Shopify link from email 2 or 3
  distributor account flag                  -> distributor
  parts order, no prior record              -> secondhand owner
  existing customer + warranty or add-on    -> warranty / expansion

Today this lives on the SALES ORDER, which is after the only moment it is useful.

Findings

Found while mapping every sending path on 2026-09-24, recorded with paths, values and dates so each can be checked. Ordered by urgency.

1. Two active cold sequences send from the root domain, above the kill rule

bounce_watchdog   threshold 2.0% hard bounce | 25 sequences
                  breaching 2   ok 6   not measurable 17
BREACHING
   6.5%    25/382   ACTIVE   Mike Intro Sequence
   2.3%    13/577   ACTIVE   Jim Intro Sequence

apollo_bounce_watchdog.json   every entry: "last_alert": "2026-08-28T17:56:06Z"
                              detected, alerted once, silent for 27 days since
both send FROM  matt@dock-blocks.com   the root domain, not a subdomain

The root domain carries Matt's live Proofpoint mail, the Shopify transactional flow, and is the parent of one of the three protected Brevo sending domains. Apollo sequences pause only in the UI; there is no API kill switch. This is a larger active threat than anything the burner-domain plan addresses and it is fixable in minutes.

2. A subdomain is not isolation

dock-blocks.com
  DMARC   v=DMARC1; p=quarantine; adkim=r; aspf=r      relaxed alignment
  MX      mx1/2/3-usg2.ppe-hosted.com                Proofpoint, Matt's live mail
  DKIM    brevo1 / brevo2 present

Relaxed alignment means a DKIM signature from any subdomain aligns to the organizational domain. So sequences.dock-blocks.com, the documented unsubscribe route for the DBOS engine, does not sit near the root's reputation. It authenticates as it.

The pattern to copy already exists. Five separate registrable domains are in use for Apollo, each authenticated, with a warm-up ramp codified at 10/20/30/50 per mailbox per day and assignment fixed per industry so bounce stays attributable to one list.

dock-blocksonline.com   SPF ok  MX Outlook   DMARC p=quarantine   Jim: waterfront, hospitality
dock-blocksdirect.com   SPF ok  MX Outlook   DMARC p=quarantine   Mike: boat owners, dealers
dock-blocksweb.com      SPF ok  MX Proofpnt  DMARC p=quarantine   Mark: commercial, municipal
dock-blocksgov.com      SPF ok  MX Outlook   DMARC p=quarantine   marine law enforcement
dock-blockspro.com      reserved-mailbox system, 2026-09-16, built not active

Recommendation is still to buy two new domains for reactivation rather than reuse these, since the Apollo five carry a working revenue engine. Copy the pattern, not the domains.

3. Apollo enrolls against a suppression list 44 days stale

suppression_list.csv     16,299 addresses   built 2026-08-11   read by apollo/enroll.py
global-suppression.csv    3,221 addresses   built 2026-09-07   read by the live Brevo/Zoho path
in global but NOT in Apollo's list:  1,960
nothing schedules .claude/scripts/build_suppression_list.py

Neither file is a superset of the other. The gate design is the best in the estate: multi-source, missing file is a hard failure, empty file is a build failure, fails closed. The input is stale.

And the evergreen loop does not consult it at all. The auto-prospecting workflows are Apollo-native, proven read-only to our API key. Live filters on all three active loops reference only Apollo-internal checks plus verified email status. A hard bounce is excluded; a consent withdrawal elsewhere is invisible.

16 rule_configs, 6 active, 3 are prospecting loops, all ran 2026-09-24:
  Mark Government    created 2026-08-27   10/day   last ran 14:22Z
  Mike Intro         created 2026-08-24   10/day   last ran 13:11Z
  Jim Intro          created 2026-03-31   10/day   last ran 14:22Z

4. The cross-stack reconciler runs daily and writes nothing

com.dockblocks.suppression-reconcile   loaded, 07:00 daily
log written 2026-09-24 08:23, 30KB
final line: "REPORT ONLY. 19 target(s). Add --apply to write (ledger mode)."
"14 of them are still members of a sending list. Removing them is a separate
 decision and is not done here."
  [redacted]@gmail.com    emailBlacklisted  STILL IN LIST(S) [29, 56, 59, 64, 177, 178, 220]
  [redacted]@verizon.net  emailBlacklisted  STILL IN LIST(S) [59, 63, 137, 154, 162, 168, ...]

Brevo's own blocklist should stop marketing reaching these people, so this is a record-integrity problem rather than a proven send. The exception matters: Brevo's emailBlacklisted does not block transactional, proven on 2026-08-25 when a blacklisted address received, opened and clicked. That is why abandoned cart must not be built on the transactional endpoint for convenience.

5. Attribution is the gap with a measured revenue consequence

"Leads are down maybe 20%, but revenue is down 50%." Matt West, 2026-09-22. 158 orders against 468 a year earlier, ad spend flat
"our coverage has increased from as low as 0 to at least we're able to attribute at least 30% of all the forms... We need to get this number to over 90." Kinyanjui Njoroge, 2026-08-12

The gap register already ranked gclid and UTM capture the highest-ROI item. It now has a business consequence attached, and it has been open across five months with no owner. The funnel Njui names in the same passage is the real object model: click, visit, lead, deal or quote, sales order. Also recorded: 500 to 600 PPC-sourced phone numbers never logged in Zoho.

6. Three documents each claim to be the source of truth

DocumentDatedStanding
docs/workflows/LIFECYCLE_COMMS_MAP.md2026-08-26Most current. Gap register G1 to G19. Has self-corrected three times
docs/architecture/eventcatalog-catalog-as-built.md2026-07-07Pinned 2026-07-02, drift CI never built. Do not cite on reconversion
docs/lifecycle_comms/lifecycle_comms_catalog.json2026-09-2362 rows, in data-ops

The sharpest conflict: the EventCatalog declared reconversion live on 2026-07-07 and was never revisited. Sending did not run until 2026-08-15, because the flag was set in Dokploy but never listed in the compose file, and Compose injects only yaml-referenced variables. Detection ran throughout. Sending never did.

Two more, smaller but live

lc-52 quoteFollowUp is marked LIVE in the data-ops catalog while a sensor reading live Zoho last_executed_time lists it among nine rules switched on that have never fired once.

The 99-path comms register is not missing, it is stranded. It lives only on branch docs/lifecycle-master-consolidation, last commit 2026-07-07, 196 rows. Read it with git show docs/lifecycle-master-consolidation:docs/workflows/comms-paths-inventory-and-test-matrix.md. Do not merge that branch to recover it: it is 14 commits ahead while main is 1,766 ahead and would add 7,922 lines, mostly superseded files.

7. A weekly count of customer messages is not producible today

Three structural reasons: the ledger does not record which population a message reached, Zoho-native rules produce no ledger row at all, and Shopify's native sends are invisible from the repo. The cap cannot be verified until population is a recorded field. The clearest example: the Apollo click webhook, in one call, queues an external SMS to the lead and writes the field that fires an internal notification to the rep plus two hardcoded addresses. One call, both populations, one log line.

8. Two guards fixed while writing this

bf08a612  the domain-split check gated on /emailCampaigns BEFORE looking at senders, so
           it would print success having examined zero files. Added a denominator floor.
0d1bc1ea  the one-door guard needed "brevo.com/v3" and the send path on ONE line, so a
           file holding the base in a constant was a second door for as long as the guard
           existed. Baseline 8 to 11, all pre-existing internal digests, no customer mail.

Neither exposed a customer. Both are in the suite, which now runs 204 checks.

Running it

Who owns what

ConcernOwnerNote
Customer platform, gold, eligibility gateNjuiOwns it because he owns the layer that can compute it
Apollo outbound enginePivotPlanIt33 scripts. Unchanged but for one pre-enrollment check
Brevo execution, templates, automationsPivotPlanItAutomations are hand-built in the UI from versioned specs
Approval for anything externalAleciaIncluding removing shadow mode
Stage model and journey mapShared, Alecia decidesOne model, everything tags against it

Standing rules every send obeys

  • Marketing splits across all three protected domains, keyed on the owning rep. Never a hardcoded sender. Enforced by a commit-blocking test.
  • No marketing send uses a rep's own mailbox as the From address. Reply-To at the rep stays.
  • Every Brevo send goes through the one door, which refuses on eight conditions. Transactional is exempt structurally, so a flag cannot slip marketing past.
  • Drafts are appended, never sent. A deliverability gate runs before every send.
  • Consent is opt-in, not not-opted-out.
  • Money never appears on a shared board, in a task or in a comment.

Definition of done

"Built" is not a state anybody can act on. Anything above built must name a re-runnable sensor.

StateMeansSensor
scopedTrigger, audience, goal and exit written downThe spec exists
builtCode or automation exists, tests passTest in the suite
deployedRunning where it will run, not on a laptopContainer or job
scheduledFires on its own. A disabled job is not scheduledSchedule plus a recent log
sendingA real message reached a real personLedger row, not an HTTP 200
measuredEnrolled, delivered, engaged, converted, unsubscribed, with a holdoutDashboard panel
Two rules from expensive experience

A 200 means the request was understood, not that it happened: read the outcome, not the status. And zero findings over zero input is the same document as a clean estate, so any check that can report no problems having examined nothing must fail instead.

Cadence

WhenWhatOutput
WeeklyPer-program performance. Enrollment queue. Gate and deliverability check against thresholds. Anomaly check on anything that moved without a changeShort written read, nothing if nothing moved
Per launchBefore: audience verified and dated, every variant reviewed by eye, suppression fresh, holdout carved, cap checked. After, at 24h and 7d: delivery, complaints, unsubscribes, replies, conversionsLaunch record in the program folder
MonthlyFrequency cap review, since volume and engagement tiers move. Stage boundary checkCap changes, if any
QuarterlyThe audit: journey map, content per stage, automation performance, churn and win-back, quantitative plus qualitative, prioritised plan with ownersThe audit document
Event-triggeredAfter peak season, a launch, a reputation change or any suspension warningOff-cycle note
Look at every variant before calling anything ready

On the Labor Day send, six defects passed every automated check and were caught by eye: a Memorial Day photograph, a duplicate headline, a debug chip, an unsigned body, a signature in the wrong rep's block, and a product row below the footer. Automated checks catch shape. A person catches sense.

Sequence

PhaseWorkGate to the next
0One canonical map from the three catalogs plus the stranded 196-row register. Archive the restNo competing claimants
1Shared suppression, shared cap, agreed stage model. Refresh the Apollo suppression input and schedule itUnsubscribe a seed in one stack, prove another refuses it
2Burner domains registered, authenticated, warmed on a seed. Dormant win-back as pilot, holdout carved at the startShadow mode comes off only after the Phase 1 test passes
3Brevo audiences come from gold. Abandoned cart, checkout and browse move off Shopify nativeShopify off and Brevo on in the same change, or customers get both or neither
4Per-program reporting across both stacks. Holdout lift. gclid and UTM captureThe audit produces a plan somebody acts on

Phase 2 can buy domains and warm a seed in parallel with Phase 1, since that is safe and takes 5 to 7 days. Nothing leaves shadow mode early.

Open questions

For Njui

  • sequences.dock-blocks.com inherits the root domain's reputation through adkim=r. Is that route load-bearing, or can it move before anything sends?
  • Does activation (P3) intend to write suppression fields directly, and what reconciles it against suppress_contact.py? Two writers with no contract is how the 7.82% Zoho-versus-Brevo divergence happened the first time.
  • Six campaign cards went past due on 2026-09-18, abandoned cart among them. Is any of that already started in data-ops?
  • Would the DBOS audience binding move from a frozen CSV to a query evaluated at send time?
  • lc-52 quoteFollowUp is LIVE in the catalog and has never fired.

For Alecia

  • The two Apollo sequences breaching the bounce rule from the root domain: pause now, or fix the list first?
  • Rebuild and schedule the Apollo suppression input. It reads Zoho and Brevo at scale.
  • The 14 blocked contacts still in sending lists. The reconciler deliberately refuses to decide.
  • Two burner domain names, and approval to register.
  • The five-stage model and the five paths.

Genuinely undecided

  • Apollo and DBOS both do cold. Apollo prospects people who have never heard of DockBlocks; DBOS reactivates people already in the database. Different audiences, same risk class, and nothing stops a person being in both.
  • Does enrollment re-check? The Braze Canvas behaviour is a choice we make rather than inherit. It matters most for win-back.
  • Brevo could still be suspended. The split moves complaint traffic away, but if it goes, welcome, nurture and the shipped notification go with it.
  • The PivotPlanit checkout of data-ops is three months behind and 23 of roughly 560 commits since June are ours.

Reference

Key code paths

scripts/lib/brevo_send.py                  the one door, eight refusal conditions
scripts/brevo/sending_domains.py           three-domain split, keyed on rep
scripts/brevo/suppression_reconcile.py     cross-stack reconciler, report-only
scripts/lead-pipeline/suppress_contact.py  writes Brevo + Zoho + local ledger
.claude/scripts/build_suppression_list.py  builds the Apollo input, unscheduled
scripts/apollo/senders.py                  mailbox and domain policy, weaning the primary
scripts/apollo/enroll.py                   manual enrollment, gates on suppression, fails closed
scripts/apollo/prospecting_workflow.py     evergreen rule_configs, read-only to our key
scripts/apollo/bounce_watchdog.py          the 2% kill rule
scripts/monitoring/gate_integrity.py       asserts the sending plan daily
services/dbos-workflows/                   DBOS engine, data-ops origin/main

Constraints worth knowing before designing around them

  • Brevo automations cannot be built by API. All three endpoints 404. An automation is a browser job and cannot be read back to verify. A scheduled campaign is not a substitute: one sender, one fire, no trigger, no exits.
  • An automation step renders its own template copy, not the template named after the campaign.
  • A campaign is immutable. Delete and rebuild rather than patch.
  • Brevo rewrites utm_campaign from the campaign name, so the name is the analytics key and must be the slug.
  • Apollo enrollment returns skipped_contact_ids: a 200 is not an enrolment.
  • Apollo rule_configs live on app.apollo.io, behind Cloudflare, and the collection is a POST to /rule_configs/search.

Sources

Research: scalero.io blog and services pages, braze.com resources and documentation, 18 URLs recorded. Transcripts: docs/meetings/transcripts/, pulled from Fireflies this session. Note the Fireflies titles are unreliable, because the same Zoom link is reused, so a recording titled "Digital Marketing Call" often holds a rep one-to-one instead.