The defect lives
in the overlap.
Each flow passes review on its own. Together, two of them reach the same audience and charge a weekly pressure of 9.5 to the same contact, against a ceiling of 6.0. That sum appears in neither file.
Sections on this page
Narrative
A journey with a construction defect keeps running. The flow fires, the messages go out, the report closes with a positive number, and whoever already converted keeps receiving the sequence built for those who did not. The effect shows up weeks later, in unsubscribes and in domain deliverability.
Declare the journey in YAML and audit the file before it ships. Exit criteria, suppression list, frequency cap and control group are four fields the automation interface never asks for, which is why they stay empty. In a declared format, their absence becomes a failure with an id and an exit code.
Twelve rules. Ten read one journey at a time, and two read the portfolio of active journeys. The two portfolio rules are why the tool exists. L011 measures the overlap between audience definitions, L012 sums the weighted weekly pressure those journeys charge to the same contact and compares it with the configured ceiling.
The first version of L011 used the Jaccard index and flagged nothing in the examples. It returned 0.5 on exactly the case that motivated the rule, because one of the sets carries an extra filter. A rule that misses the case that created it is worse than no rule, because it reports green.
The audit in action
The same command that runs locally runs in CI. The output stays in Portuguese, the language of the operation the tool was written for, in both versions of this page.
CLI runs
Tacit operational knowledge does not scale through manual review. It scales when it becomes an executable rule, with an id, a severity and an exit code.
The defect no file contains
The two portfolio rules read what individual reviews cannot see. Step through the four stages to follow where exit code 1 comes from.
Each file passes individual review. Nothing here is wrong when you read one flow at a time.
L011 compares the audience definitions. The checkout one is contained in the winback one, and the overlap coefficient returns 1.0 against a threshold of 0.6.
L012 sums the weighted load of both flows over the seven day window: 5.5 plus 4.0, that is 9.5 against a ceiling of 6.0.
A portfolio level error returns exit code 1, and the merge does not pass. The defect was in neither file.
Weekly pressure
Each journey on its own fits under the journey ceiling. Summed over the same contact, they blow past the portfolio ceiling. These are two different limits, and only the second requires reading the set.
abandonado winback_
inativos aggregate
(L012)
scale 0 to 10 · channel weights: email 1.0 · push 1.5 · SMS and WhatsApp 2.0
weekly_pressure = load × 7 / max(duration_days, 7)
A plain message count does not work, because push, SMS and email carry different attention costs. Each channel got a weight reflecting perceived intrusiveness, and journey pressure is the weighted load over a seven day window, with a seven day floor in the denominator. Without the floor, a checkout flow with three touches in 24 hours would read as 21 touches per week, a number that never happens because the journey ends first. False positives are the most common reason a linter gets abandoned, and calibrating that formula took the longest in the project.
# limites são decisão de operação, versionados no repositório
max_weekly_pressure: 6.0
max_journey_pressure: 4.0
audience_overlap_threshold: 0.6
holdout_required_above: 5000
disabled_rules: []
downgrade_to_warning: [L007]
The calculation decisions
"Inactive for 60 days" and "inactive for 60 days who abandoned checkout" reach the same group of people: the second set is contained in the first. Same numerator, two denominators, two verdicts.
Divides by the union. Since the contained set is smaller, the union grows and the result falls below the threshold. Green on the very case that created the rule.
Divides by the smaller set's cardinality. Under containment, the intersection is the smaller set itself, so the coefficient is 1.0 and the rule finally sees the conflict.
The twelve rules
They encode CRM operational practice, not software engineering convention. Scope is the column that matters: ten read one journey at a time, and two read the whole portfolio.
| ID | Scope | Rule |
|---|---|---|
| L001 | journey | Active journey with no exit criterion |
| L002 | journey | Branch with no default path |
| L003 | journey | Loop with no iteration cap |
| L004 | journey | Audience with no suppression list |
| L005 | journey | Segment with no time decay |
| L006 | journey | High message pressure within the journey |
| L007 | journey | Intrusive channel with no quiet hours |
| L008 | journey | Active journey with no control group |
| L009 | journey | Success metric missing or a vanity metric |
| L010 | journey | Re-entry with no cooldown period |
| L011 | portfolio | Active journeys competing for the same audience |
| L012 | portfolio | Aggregate pressure above the per contact ceiling |
Where the model comes in
The --explain flag sends the already closed findings to an AI assistant and prints a prose summary to stderr, to take into the review meeting. It is optional. The diagnosis is entirely deterministic: none of the twelve rules calls a model, and with no API key the linter runs the same, with the flag reporting that the explanation is unavailable.
An LLM on the critical path of an audit trades a reproducible result for one that varies between runs, and an audit with that property cannot serve as a publication criterion. The criterion I applied: where a rule can be determined, I write the rule. The model comes in at synthesis and at translating the finding into the language of whoever decides.
The suite
Three repositories on the same thesis: a customer journey is a versionable artifact. If it can be declared in a file, it can be read, audited and reviewed in a pull request, with history and peer review. Today it lives inside the automation tool's interface, where there is no diff between versions and no record of why that delay is 48 hours.
The output of one is the input of the next, which makes the agent's acceptance criterion objective: the proposed journey has to pass the linter.
Reads journeys declared in YAML and flags the defect before publication, including the one that only exists between two flows.
This pageReads product events and returns the segment brief with the proposed journey, already validated by the linter.
Holds the versioned reading prompts, with verifiable assertions and a scoreboard that measures regression in CI.