Skip to main content

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.

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

SettingWhereEffect
Channel.couplingModePOST/PUT /api/v1/channelsDefault for the channel: "partial" (default) or "atomic".
DecisionFlow.couplingOverridePUT /api/v1/decision-flowsPer-flow override — beats the channel default when set. Useful when one channel serves both atomic (digest) and partial (transactional) flows.
placementResults[*].count === 0derivedThe trigger: zero offers in a placement, after group allocation.

Step 1 — Mark the email channel atomic

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

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:
{
  "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:
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.