An email in Cleo is sent to an audience. That audience can be specific people, a segment defined by rules, or everyone on the list. For a long time the code that resolved an audience worked through those options in order: if the email named specific contacts, use them; otherwise, if it had a segment, use that; otherwise, send to all active contacts.
Each step was reasonable. The last one was a hazard.
Absence is not intent
The trouble with falling through to "everyone" is that it cannot tell two very different situations apart. An owner who deliberately chose to email their whole list, and an email whose audience was simply never set, look identical to the resolver. Both have no contacts and no segment. Both go to everyone.
An email drafted for one person is the dangerous case. If the step that binds that person to the email is skipped, whether by a path that forgets to pass the contact along or a draft created before the person was mentioned, the email does not fail. It succeeds, widely. And an email sent to a whole list cannot be called back.
I found a second instance of the same shape inside the A/B testing path, which resolved audiences separately and ignored named recipients altogether. A test meant for a handful of people would have fanned out to the full list under the test's split. Same assumption, different door.
Make the broadcast a choice
The fix was to make "everyone" an explicit value rather than the meaning of nothing.
Every email now carries an audience scope. It is one of three things: specific contacts, a segment, or all active contacts, and the last is only ever set by a deliberate action. A single function computes the audience from that scope, and every path that resolves recipients goes through it, A/B tests included. If an email has no contacts, no segment and no explicit broadcast scope, every resolver refuses to send. So does the step that submits an email for approval, so the refusal arrives before anyone is asked to sign anything off.
When Cleo drafts an email for a named person, that person is bound to the draft when it is created, and a segment is never quietly applied over the top of named recipients. When Cleo drafts a broadcast, the draft says so in a field rather than by leaving one empty.
The refusal is written to be recovered from. It says the email has no audience and needs one, which leads to a fix on the next turn rather than a loop.
Show the audience at the moment of approval
The engine change closes the hole. The interface change makes what remains visible.
Before an email goes out, someone approves it. Previously the approval showed the email and a button. It now opens a confirmation that leads with the number of people the email will reach, in a plain sentence, followed by the actual list of recipients. Large sends carry a gentle caution. Nothing is sent until the confirmation is pressed.
Building that confirmation surfaced one more mismatch. The function that listed recipients for display also ignored named contacts, so an email to one person would have shown the whole list in its confirmation. It now resolves recipients exactly as the send does. A preview that disagrees with the action it previews is worse than no preview at all.
Defaults with a blast radius
I am generally in favour of strong defaults. Most settings should not exist, and the product should decide on the user's behalf wherever it reasonably can. The exception is any default whose failure is both irreversible and wide. Sending to everyone is the clearest case in a marketing product. It deserves to be a decision someone makes, recorded in the data, and shown back to them before it happens.
A test now checks that no path can reach the full list without that explicit scope. I would much rather a future change break a test than send an email to a list of people who were never meant to receive it.