Skip to main content

What this solves

A weekly digest email has three placements: hero, body card, footer. If contact-policy fatigue or decisioning gates empty the hero, you don’t want to send a footer-only email — that’s worse than not sending. Atomic coupling lets you say “this channel is all-or-nothing within itself” without forcing every flow on that channel to be atomic.

Why this works

Coupling is per-channel, not global. Each Channel.couplingMode defaults to "partial" (any non-empty placement ships). Switching it to "atomic" makes the post-group coupling pass scan that channel’s placement results; if any one is empty, the rest are emptied with a cascade record in trace.channelCoupling[]. Cross-channel coupling is intentionally not supported — different channels are different attention surfaces, and an empty SMS shouldn’t suppress an email.

Three knobs

Step 1 — Mark the email channel atomic

The channel id goes in the body — PUT /api/v1/channels updates by id, there is no /channels/{id} sub-route.

Step 2 — Fire a multi-placement recommend

What you’ll see when coupling fires

Response body:
Both placements are empty even though <email-body-card> would have had offers — because the hero was empty and the channel is atomic. The cascaded: true flag in channelCoupling[] is your audit trail: this wasn’t “no offers were available,” this was “we deliberately suppressed.”

Why not just use Group.allowPartial?

Group.allowPartial was the old switch. It’s deprecated and no-op now (the field still parses to keep legacy IRs valid). It conflated two distinct cases:
  1. “Operator chose to require all placements filled” (now: Channel.couplingMode = "atomic")
  2. “Contact policy fatigue happened to empty everything” (now: surfaces in the trace’s contactPolicyResults[] regardless)
The per-channel model lets you couple email but leave web partial; legacy allowPartial could only do flow-wide all-or-nothing.

Per-flow override

A weekly-digest flow needs atomic; a same-channel transactional alert flow needs partial. Set the channel default to atomic, then override on the transactional flow:
Now the transactional flow sends whichever placements have offers; the digest flow on the same channel still goes all-or-nothing.

Proof reference

T8 of the proof bundle: channelCoupling.cascaded = true with both placements empty, when the hero is fatigued out by a frequency cap.