> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Atomic channel coupling

> When one placement in a channel is empty, suppress every sibling placement in the same channel. Used for email digests where the hero and body placements must ship together — never one without the other.

## What this solves

A weekly digest email has three placements: hero, body card, footer. If contact-policy fatigue or qualification rules 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

| Setting                           | Where                        | Effect                                                                                                                                         |
| --------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `Channel.couplingMode`            | `POST/PUT /api/v1/channels`  | Default for the channel: `"partial"` (default) or `"atomic"`.                                                                                  |
| `DecisionFlow.couplingOverride`   | `PUT /api/v1/decision-flows` | Per-flow override — beats the channel default when set. Useful when one channel serves both atomic (digest) and partial (transactional) flows. |
| `placementResults[*].count === 0` | derived                      | The trigger: zero offers in a placement, after group allocation.                                                                               |

## Step 1 — Mark the email channel atomic

```bash theme={null}
curl -X PUT https://playground.kaireonai.com/api/v1/channels/<email-channel-id> \
  -H "Content-Type: application/json" -H "X-Requested-With: XMLHttpRequest" \
  -d '{ "couplingMode": "atomic" }'
```

## Step 2 — Fire a multi-placement recommend

```bash theme={null}
curl -X POST https://playground.kaireonai.com/api/v1/recommend \
  -H "Content-Type: application/json" -H "X-Requested-With: XMLHttpRequest" \
  -d '{
    "customerId": "cust-digest-001",
    "decisionFlowKey": "weekly-digest",
    "channelId": "<email-channel-id>",
    "placements": [
      { "placementId": "<email-hero>",      "limit": 1 },
      { "placementId": "<email-body-card>", "limit": 3 }
    ]
  }'
```

## What you'll see when coupling fires

Response body:

```json theme={null}
{
  "placements": {
    "<email-hero>":      { "count": 0, "offers": [] },
    "<email-body-card>": { "count": 0, "offers": [] }
  },
  "channelCoupling": [
    {
      "channelId":   "<email-channel-id>",
      "channelName": "email",
      "mode":        "atomic",
      "cascaded":    true,
      "emptyPlacements": ["<email-hero>"]
    }
  ]
}
```

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:

```bash theme={null}
curl -X PUT https://playground.kaireonai.com/api/v1/decision-flows \
  -H "Content-Type: application/json" -H "X-Requested-With: XMLHttpRequest" \
  -d '{
    "id": "<transactional-flow-id>",
    "name": "Transactional Alerts",
    "couplingOverride": "partial",
    "rowVersion": <current>
  }'
```

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.
