> ## 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.

# Action Analysis (Why-Not)

> Understand exactly why a specific offer was or wasn't shown to a customer — full decision transparency.

Every decisioning engine needs to answer one question: **why did this customer see (or not see) this offer?** Action Analysis (also called "Why-Not") gives you a complete diagnostic breakdown of every decisioning gate and contact policy that determined the outcome.

This is essential for support teams investigating customer complaints, marketers debugging campaign reach, and compliance teams auditing decision fairness.

<Info>
  Action Analysis brings enterprise-grade decision transparency to every tenant. Access it from the Live Console or via the API at `GET /api/v1/customers/:customerId/why-not/:offerId`.
</Info>

***

## Business Value

| Scenario                  | How Why-Not Helps                                                                             |
| ------------------------- | --------------------------------------------------------------------------------------------- |
| **Customer complaints**   | Support agent looks up why a promotion was not shown, sees "frequency cap exceeded" instantly |
| **Campaign debugging**    | Marketer discovers a decisioning gate is blocking 40% of the target audience                  |
| **Compliance audits**     | Auditor verifies that exclusion decisions follow documented policy                            |
| **Enrichment validation** | Data scientist confirms customer enrichment data is flowing through the pipeline correctly    |

***

## How It Works

When you request a Why-Not analysis, KaireonAI re-evaluates the decisioning gates and contact policies for that specific customer-offer pair:

<Steps>
  <Step title="Load offer metadata">
    Fetches the offer, its category, sub-category, and active creatives.
  </Step>

  <Step title="Enrich customer data">
    Queries all schema tables for the customer's data (demographics, transactions, behavioral attributes).
  </Step>

  <Step title="Evaluate decisioning gates">
    Runs every active decisioning gate and reports whether it applies to this offer, whether it passed or blocked, and the exact reason.
  </Step>

  <Step title="Evaluate contact policies">
    Checks frequency caps, cooldown periods, and suppression rules against the customer's interaction history.
  </Step>

  <Step title="Return verdict">
    Produces a verdict of `eligible` or `blocked` with a human-readable summary and full detail breakdowns.
  </Step>
</Steps>

***

## Using Why-Not Analysis

### From the API

```bash theme={null}
curl https://playground.kaireonai.com/api/v1/customers/CUST-001/why-not/offer_summer_promo \
  -H "X-Tenant-Id: my-tenant" \
  -H "Authorization: Bearer <token>"
```

### Response Structure

The response includes four sections:

| Section         | Description                                                  |
| --------------- | ------------------------------------------------------------ |
| `verdict`       | `eligible` or `blocked` — the final outcome                  |
| `summary`       | Human-readable explanation of the verdict                    |
| `qualification` | Per-rule breakdown with pass/block/skip status and reasons   |
| `contactPolicy` | Per-policy breakdown with pass/block/skip status and reasons |

### Example Response

```json theme={null}
{
  "customerId": "CUST-001",
  "offerId": "offer_summer_promo",
  "offerName": "Summer Rewards Bonus",
  "offerStatus": "active",
  "category": "Retention",
  "verdict": "blocked",
  "summary": "Blocked by contact policy: Email Frequency Cap",
  "qualification": {
    "total": 3,
    "passed": 3,
    "blocked": 0,
    "skipped": 1,
    "details": [
      {
        "ruleId": "qr_001",
        "ruleName": "Gold Tier Only",
        "ruleType": "attribute_condition",
        "scope": "category",
        "applies": true,
        "result": "passed",
        "reason": "Passed"
      },
      {
        "ruleId": "qr_002",
        "ruleName": "Min Balance",
        "ruleType": "attribute_condition",
        "scope": "offer",
        "applies": false,
        "result": "skipped",
        "reason": "Rule scope offer:offer_winter_sale does not match this offer"
      }
    ]
  },
  "contactPolicy": {
    "total": 2,
    "passed": 1,
    "blocked": 1,
    "skipped": 0,
    "details": [
      {
        "policyId": "cp_001",
        "policyName": "Email Frequency Cap",
        "ruleType": "frequency_cap",
        "scope": "global",
        "applies": true,
        "result": "blocked",
        "reason": "Frequency cap exceeded: 5/3 (daily)"
      }
    ]
  },
  "customerData": {
    "age": 34,
    "income": 75000,
    "gender": "F",
    "membershipDays": 412
  },
  "interactionHistory": {
    "totalImpressions": 5,
    "lastContact": "2026-04-02T14:30:00.000Z",
    "summaryCount": 3
  }
}
```

<Warning>
  Why-Not analysis runs enrichment queries in real time. For customers with data across many schema tables, response times may be higher than typical API calls. Use this for diagnostic purposes, not in production recommendation paths.
</Warning>

***

## Understanding the Results

### Decisioning Gate Results

Each rule returns one of three statuses:

| Status    | Meaning                                                                |
| --------- | ---------------------------------------------------------------------- |
| `passed`  | Rule applies to this offer and the customer meets the criteria         |
| `blocked` | Rule applies and the customer fails the criteria — offer is ineligible |
| `skipped` | Rule does not apply to this offer (scope mismatch)                     |

### Contact Policy Results

| Status    | Meaning                                                                                 |
| --------- | --------------------------------------------------------------------------------------- |
| `passed`  | Policy applies and the customer is within limits                                        |
| `blocked` | Policy applies and the customer has exceeded the policy (e.g., frequency cap, cooldown) |
| `skipped` | Policy scope does not match this offer                                                  |

### Supported Rule Types

| Rule Type             | What It Checks                                                   |
| --------------------- | ---------------------------------------------------------------- |
| `attribute_condition` | Customer attribute against a threshold (e.g., `income >= 50000`) |
| `offer_attribute`     | Offer-level field value (e.g., `productType == "credit_card"`)   |
| `frequency_cap`       | Number of impressions within a time period                       |
| `cooldown`            | Hours since last contact                                         |

### Empty or short result set — what the trace tells you

Call `/recommend` with `debug: true` and read `debugTrace`. Beyond the funnel counts (`totalCandidates → afterQualification → afterSuppression → afterContactPolicy → afterConsent → afterGuardrails`), two fields explain results that would otherwise look like a silent failure:

| Field             | Meaning                                                                                                                                                                                                                                                                                                                                               |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inventoryNote`   | Present only when **zero** candidates were built for a structural reason. Distinguishes `"No offers exist"` vs `"N offer(s) exist but none are active"` (new offers and creatives default to `draft` — activate them) vs `"active offer(s) but no active creatives"`. This is the usual cause of "I created offers but `/recommend` returns nothing." |
| `truncatedByRank` | How many candidates the rank node's `maxCandidates` (topN, default **5**) and per-category/per-channel limits dropped. A short result set with `truncatedByRank: 12` means 12 offers *qualified and scored* but were cut by the topN slice — they were **not** disqualified. Raise `maxCandidates` to see more.                                       |

A fully-absent-data decisioning gate (e.g. `segment_required` with no segment data, or `propensity_threshold` on a cold model) now **skips** with a `Skipped: …` reason in `qualificationReasons` rather than silently blocking every offer.

### Per-request why-not: `rejectedOffers` on `/recommend?explain=true`

For a *live* request (rather than the re-evaluation this page's endpoint performs), call `/recommend` with `explain: true`. The response's `rejectedOffers[]` lists **every** offer that entered the pipeline but didn't make the response, each with the stage that dropped it and a self-contained human-readable reason. Coverage is complete — every candidate-dropping stage records a reason:

| Stage            | What dropped the candidate                                                                                      |
| ---------------- | --------------------------------------------------------------------------------------------------------------- |
| `eligibility`    | Decisioning gates / the qualify node's rule tree                                                                |
| `contact_policy` | Contact-policy suppression (frequency caps, cooldowns, DNC, …)                                                  |
| `consent`        | Customer consent for the channel type is revoked or not granted                                                 |
| `channel`        | Channel/creative/placement filters (no creative on the requested channel, placement mismatch, missing creative) |
| `suppression`    | An active suppression rule matched                                                                              |
| `frequency_cap`  | A tenant-wide frequency cap was exhausted                                                                       |
| `maturity_ramp`  | Below the cold-start exposure floor for this customer today                                                     |
| `guardrail`      | A `hard` guardrail rule fired                                                                                   |
| `coupling`       | Atomic channel-coupling cascade emptied the channel                                                             |
| `rank`           | The rank node's topN/limit cut — the offer qualified and scored but ranked below the cut                        |

The same merged list is persisted on `DecisionTrace.rejectedOffers` for sampled traces, so the answer to "why didn't customer X get offer Y on request Z" survives after the fact. See the [Recommend API `rejectedOffers` field](/api-reference/recommend) and [Decision Traces](/api-reference/decision-traces) for exact shapes.

***

## API Reference

See the full [Why-Not API Reference](/api-reference/why-not) for endpoint details, field descriptions, and error codes.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Decisioning Gates" icon="filter" href="/decisioning/qualification-rules">
    Configure the rules that determine offer eligibility.
  </Card>

  <Card title="Contact Policies" icon="shield-check" href="/decisioning/contact-policies">
    Set up frequency caps, cooldowns, and suppression rules.
  </Card>

  <Card title="Decision Traces" icon="magnifying-glass" href="/api-reference/decision-traces">
    View full pipeline execution traces for any decision.
  </Card>

  <Card title="Live Console" icon="monitor-waveform" href="/operations-reporting/ops-manager">
    Real-time monitoring console with built-in Why-Not access.
  </Card>
</CardGroup>
