---
name: attribution-anomaly-diagnosis
description: Diagnose "why" questions about attribution anomalies — attribution=0 (no attributed orders), and sudden drops, spikes or retroactive changes in attributed numbers. Do NOT use for "show me numbers" — that's attribution-data-query.
metadata:
category: attribution
risk: R0
version: 1.2.0
last-updated: 2026-09-23
references:
- references/basic-checks.md
- references/model-branches.md
- references/fallback-structure.md
- references/scenario-snippets.md
- references/output-template.md
- references/edge-cases.md
- references/failure-modes.md
templates:
- templates/01-scope-lookup.txt
- templates/02-spend-vs-attribution.txt
- templates/03-measurement-readiness.txt
- templates/04-utm-health.txt
- templates/05-click-distribution.txt
- templates/06-dda-vs-anyclick.txt
- templates/07-lift-test-results.txt
examples:
- examples/example-A-meta-roas-drop.md
---
## 1. Purpose
Diagnose **"why" questions** about attribution anomalies — specifically **attribution = 0** (a tactic shows no attributed orders) and **attribution swings** (sudden drops / spikes / retroactive changes to historical numbers). 5-step diagnostic tree → internal report + client-facing explanation.
> **Shared conventions (canonical elsewhere — don't re-derive):** attribution model default + aliases and sales-platform scope + measurement identity live in **`attribution-data-query`** (`references/attribution-model.md`, `references/sales-platform-scope.md`); follow those (load the `attribution-data-query` skill from this plugin if it isn't loaded). Note this skill reads MMM-calibrated measures (`calibrated_*`) for some checks — a *different* measure from the `attr_*` the other skills use; keep them clearly labeled so a $ figure here is never confused with an attributed-sales figure. (Open question flagged to DS — see the team doc.)
**Do NOT use this skill for "show me the numbers" queries** — those belong to `attribution-data-query`.
## 2. When to trigger
Trigger when user is asking **why** attribution looks wrong, not just **what** the numbers are.
**Do NOT trigger**:
- User just wants the numbers (no "why") → `attribution-data-query`
- User wants to compare two models on purpose → `attribution-model-comparison`
- Anomaly is on an out-of-scope channel/tenant → `attribution-edge-routing`
## 3. Inputs
| **Field** | **Required?** | **Notes** |
|-|-|-|
| `tenant_id` | Required | Injected by platform-mcp; never appears in SQL filters |
| `ads_platform` | Required | Meta / Google / TikTok / Pinterest / Snap / Amazon Ads... |
| `tactic_name` | Optional | If not given, scan all and pick biggest contributor to the anomaly |
| `sales_platform` | Required | Amazon Store / Shopify / TikTok Shop. Attribution is computed **per sales platform**; wrong combo (Amazon Ads → Shopify) always returns 0 by design. |
| `attribution_model` | Optional | `idda`(32) / `dda`(31) / `last_click`(1) / `first_click`(2) / `any_click`(21). If unspecified, Step 1 scans all to locate which is broken. SQL dimension: `attr_model_name`. |
| `metric` | Required | `roas` / `attr_orders` / `attr_new_customer_orders` / `cac` |
| `anomaly_period` | Required | Window where the user sees the anomaly |
| `baseline_period` | Optional | Comparison window. Default: same-length period immediately before `anomaly_period`. |
**Ask for missing required fields once before running diagnostics.**
## 4. SOP
**Step 0: `database-query-ask` (MANDATORY)** — produces the `ctx` timestamp `database-query-run` requires. Skipping fails at execution.
**Step 1: Lock the scope** — pinpoint *whose* attribution is broken before diagnosing why. If `attribution_model` is unspecified, compare all models in anomaly vs baseline windows to find which one diverged.
**HARD RULE — copy a template first, then fill placeholders**
BEFORE writing any SQL, **copy from `templates/`**:
- `templates/01-scope-lookup.txt` — Step 1 cross-model comparison
- `templates/02-spend-vs-attribution.txt` — Step 3a spend check
- `templates/03-measurement-readiness.txt` — Step 3c readiness
- `templates/04-utm-health.txt` — Branch A1 UTM
- `templates/05-click-distribution.txt` — Branch A2 click shift
- `templates/06-dda-vs-anyclick.txt` — Branch B1
- `templates/07-lift-test-results.txt` — Branch C3 lift-test recalibration
Copy template, fill placeholders (`{ads_platform}` / `{sales_platform}` / `{baseline_start}` / `{baseline_end}` / `{anomaly_start}` / `{anomaly_end}`). Do NOT write SQL from scratch.
After Step 1: **confirm with user** which `ads_platform` + `tactic` + `sales_platform` + `attribution_model` before continuing.
**Step 2: Classify — "attribution = 0" or "swing"?**
| **Type** | **How to detect** | **Next** |
|-|-|-|
| **Attribution = 0** | `attr_orders` / `attr_sales` exactly 0, or tactic row missing | Step 3b → 3c → 3d (config / product-design first) |
| **Attribution swing** | Sharp jump up/down, OR same window returns different numbers on different pull dates | Step 3a (spend check) → Step 5 (model routing) |
**Step 3: Basic checks** — rule out config / product-design causes before model branches. Detail → `references/basic-checks.md`
- **3a** Did spend change? (most "swings" are proportional spend drops)
- **3b** [=0 only] Is the platform × sales-platform combo valid? (Amazon Ads → Amazon Store only)
- **3c** [=0 only] Measurement Readiness
- **3d** [Row missing] Dashboard view & channel config
**Step 4: Attribution-model quick reference**
| **Model** | **model_id** | **Mechanism** |
|-|-|-|
| Last Click | 1 | Last click before conversion gets 100% credit |
| First Click | 2 | First click before conversion gets 100% credit |
| Any Click | 21 | Linear: every click in window gets equal share |
| DDA | 31 | Any Click + handling for Unmatched, VTA, PPS |
| iDDA | 32 | DDA + spend-change weighting + lift-test calibration |
**Step 5: Model-specific branches** — detail → `references/model-branches.md`
- **Branch A** Rule-based (Last Click / First Click / Any Click): UTM health → click distribution shift
- **Branch B** DDA: DDA vs Any Click → VTA → Unmatched → PPS
- **Branch C** iDDA: DDA trend → PPS → lift-test recalibration
**Don't take the bait — iDDA retroactive change is NOT a bug**
If the user says "last week it was X, this week the same date range shows Y", this is iDDA's **retro-recalibration mechanism**, not a data issue. When a new lift test is applied, the model rewrites attribution back over the historical window. **Diagnose it (Branch C3), don't treat it as broken.** Lead the client-facing explanation with "this is expected" before the mechanism, otherwise the customer assumes there's a bug.
**Don't take the bait — if spend dropped proportionally, STOP**
Step 3a is the fastest check for a reason. If spend ↓ X% and `attr_orders` ↓ \~X%, attribution dropping proportionally is **correct**, not an anomaly. Stop here and explain to the client. Do NOT continue down the model branches looking for a more "interesting" cause — most "swings" are just proportional spend changes.
**Step 6: Fallback — campaign-structure shift** — if all branches come up clean, check whether campaign mix actually changed (a competing tactic squeezed share, or upper-funnel campaigns flooded delivery). Detail → `references/fallback-structure.md`
## 5. Tools used
| **Tool** | **Required?** | **Purpose** |
|-|-|-|
| `database-query-ask` | Required (first) | Cube.dev schema patterns + `ctx` timestamp |
| `dashboard-metrics-list` | Required | Validate field names (e.g., `attr_model_name`, `calibrated_orders`) |
| `database-query-run` | Required | Execute Cube.dev SQL. |
| `lift-test-list` / `lift-test-get` | Conditional | Branch C / C3 — pull lift test metadata when iDDA retroactive change is suspected |
## 6. Output format
Diagnosis output is **customer-facing** — a "why did X drop" answer is a report a CSM
forwards to the client, so **produce it as a static HTML artifact** (a shareable
diagnosis page via the `artifacts` skill), not just a chat message.
The page:
- **Three sections only: What we found / Why it happened / What you can do.** Quantify
the change in the first line (X → Y, ±%). Concrete action items.
- **One small supporting visual** of the anomaly — the metric's before→after, or the
evidence the diagnosis rests on (e.g. spend flat while attributed sales fell). One
chart, per the `artifacts` chart method.
- **One root-cause story, not a buffet** — the decision tree + scenario snippets pick
it; on an unrecognized shape, say so, **never invent** a plausible cause.
- **Measurement identity in the footer:** sales-platform scope + attribution model +
data-as-of. (Diagnosis is **per sales platform**.)
- Business language only — no internal terms (`model_id`, `dws_view_*`, Branch A/B/C).
- **Static, not live:** a diagnosis is a point-in-time explanation — don't wire it to
auto-refresh, the numbers that prompted it would move out from under the story.
Full section template + 7 reusable scenario snippets → `references/output-template.md` + `references/scenario-snippets.md`
## 7. CRITICAL rules (top 8 — full list in references/failure-modes.md)
1. **Always copy a verified SQL template from `templates/`** before writing SQL — do not write from scratch
2. **Never skip `database-query-ask` before SQL** — `database-query-run` requires the `ctx`
3. **Never skip Step 3a (spend check)** — most "swings" are just proportional spend changes; stop there if proportional
4. **Never jump straight into lift-test diagnosis** on a rule-based or DDA anomaly — lift tests only affect iDDA
5. **Never treat iDDA retroactive change as a bug** — it's expected; lead the client explanation with "this is expected"
6. **Never conclude "WM bug"** without verifying against platform-reported orders — if Meta Ads Manager also shows the drop, it's real
7. **Never expose internal terminology** — no `model_id = 32`, no `dws_view_*` table names, no Branch A/B/C labels in customer output
8. **Never add `tenant_id` filters in SQL** — platform-mcp injects it
## 8. Edge cases
Full edge case & routing catalog → `references/edge-cases.md`
## 9. Related skills
- **Upstream**: `attribution-intent-clarification` (if the "why" question is too vague to scope)
- **Sibling**: `attribution-model-comparison` (user wants two models compared, not a diagnosis), `attribution-data-query` (user just wants numbers, no diagnosis)
- **Route to**: `attribution-edge-routing` (out-of-scope channel/tenant)