---
name: what-changed-analysis-dashboard
description: Show what changed between the current and previous period for an Intentwise account — account totals, the campaigns that moved ad sales most, and the products that moved total sales most, delivered as a published dashboard. Use for "what changed", period-over-period change reports, or /what-changed-analysis-dashboard.
---

# What Changed Analysis

Current period vs the previous period, at three grains, delivered as a single published
dashboard with a short read-out in chat. Read-only — this skill never changes bids, budgets, or
campaign states.

This is a change report, not a leaderboard. The ranked sections answer **which rows moved most**,
in either direction, sorted by the size of the move. A campaign that spends a lot and spent the
same last period changed nothing and does not belong in the tables.

| # | Section | Grain | Sorted by | Rows |
|---|---|---|---|---|
| 1 | Account | account totals | — | all metrics |
| 2 | Campaigns driving the change | campaign | absolute change in ad sales | 10 |
| 3 | Products driving the change | ASIN / product | absolute change in total sales | 15 |

Increases and decreases sit in one table together, biggest move first, told apart by cell colour.
Keep the ranked tables to the columns listed in Step 5 — no rate columns, no contribution shares,
no extra metrics.

## Step 1 — Resolve the account

Call `get_organization`, then `get_intentwise_accounts`. One account → use it. More than one →
ask which, and ask nothing else. Use anything the user already said earlier in the conversation
— account, channel, dates — instead of asking again.

**Channel is `amazon`, ads data source is `ads`.** Never ask which platform. Only switch if the
user explicitly names a different one.

Check `available_data_sources` for `vendor_central` or `seller_central` — that tells you which
total-sales column to read: `product_sales` for seller, `shipped_revenue` for vendor. The calls
are still `data_source='ads'`. If neither is connected, the skill still runs: drop the total-sales
and TACOS cards, sort products by absolute change in ad sales instead, and say in one line that
total sales isn't available for this account.

## Step 2 — Set the window

Default to the **last 7 days vs the prior 7 days**. If the user names a period, use it and
compare against the immediately preceding window of equal length. State what you used; don't ask.

Use the same two windows for all three sections so the numbers reconcile.

## Step 3 — Pull the data

Three `get_insights` calls, issued **in parallel** in a single turn — none depend on each other.
Same account, same windows, all `data_source='ads'`. Express both periods in one call; the
insights service returns current and previous together, so never make two calls per period.

| Call | entity | Ask for |
|---|---|---|
| A. Account | `account` | Ad spend, ad sales **and** total sales, current and previous period |
| B. Campaigns | `campaign` | Per-campaign ad sales and ad spend for both periods, sorted by the **absolute change in ad sales**, limit 10 |
| C. Products | `product` | Per-ASIN total sales and ad spend for both periods, sorted by the **absolute change in total sales**, limit 15 |

**Ask the service to sort by the change.** It handles biggest-movers ranking on both periods in
one query, so the rows come back already ordered and the dashboard renders straight from the
response. Don't pull a wide candidate set to re-rank client-side, and don't ask for extra metrics
that won't appear in the output.

**Both the `account` and `product` entities carry advertising and total sales together.** That's
one call each, not two — ask for both sides in the same call and don't force a separate retail
source or join anything client-side.

Things that cost a retry if you skip them:

- If total sales comes back empty on A but the retail source is connected, re-run the `product`
  pull without a row limit and take its grand-total row as account total sales rather than
  summing the top rows — a top-N slice is a subset and won't tie out.
- **The first row of most results is a grand total** with null grouping columns. It's the sum
  across all groups, not an "unattributed" bucket. Use it for account totals if convenient,
  never show it as a ranked row.
- Products with total sales but no ad spend are real and worth keeping — unadvertised revenue is
  part of what changed. Don't filter them out.
- If a call returns nothing, don't retry with a different entity. Note the gap and present the rest.

## Step 4 — Finish the numbers (client-side)

Only what the response doesn't already give you:

- **Change** = current − previous, shown as both the absolute delta and the percent change.
- **ACOS and TACOS for the account section only**, derived from summed bases, never by averaging:
  ACOS = ad spend ÷ ad sales; TACOS = ad spend ÷ total sales.
- **Rate changes are percentage points.** ACOS moving 30% → 24% is **−6.0 pts**, not −20%.
- If a row has no previous-period value, show the change as `new` rather than a percentage.

No re-sorting, no contribution shares, no per-row rates. Compute every number here, before the
dashboard is written — the dashboard renders finished values and never derives, re-aggregates,
or recalculates anything.

## Step 5 — Build the dashboard

One self-contained HTML file written to `/mnt/user-data/outputs/`, then published with the
Artifact tool. The published link is the deliverable.

**Data.** Embed the computed rows inline as a JavaScript object literal at the top of the script.
No fetch, no external data file, no API calls — every number is already in hand from Step 4.
Nothing enters the dashboard that didn't come out of a `get_insights` call.

**Structure**, in this order:

1. **Header** — account name, channel, both date ranges spelled out, and which total-sales column
   was used. One line each, no hero image.
2. **KPI cards** — one per account metric: Ad spend, Ad sales, Total sales, ACOS, TACOS. Each card
   shows the current value large, the previous value small beneath it, and the change.
3. **Campaigns driving the change** — 10 rows in service order.
   Columns: `Campaign · Ad sales · Ad sales Δ · Spend · Spend Δ`.
4. **Products driving the change** — 15 rows in service order.
   Columns: `ASIN · Product · Total sales · Total sales Δ · Ad spend · Ad spend Δ`.
   Truncate titles to about 40 characters with the full title in a `title` attribute.

Both tables are sortable by clicking a column header, and re-sorting never changes the colours.
Default order is the order the service returned.

### Colouring

**Every delta cell gets a coloured background**, not just coloured text — a tinted fill across
the whole cell so the shape of the change is readable while scanning a column, without reading a
single number.

- Positive → green fill. Negative → red fill. Zero or `new` → neutral fill.
- Define the fills as tokens on `:root`: a green and a red background plus a matching foreground,
  redefined for dark mode so the tint stays subtle on a dark page rather than glowing. Both
  themes need text that still passes contrast against the tint — tint the background lightly and
  keep the text near-full-strength, rather than using a saturated fill with pale text.
- **Colour is never the only signal.** Keep the sign on the number in every coloured cell, and add
  ▲ or ▼ ahead of it. Anyone who can't separate the two hues still reads the table correctly.
- Only delta cells are filled. Leave the value columns, the campaign name, and the ASIN uncoloured
  so the page doesn't turn into a heat map.
- **KPI cards colour by meaning rather than by sign**: falling ACOS and TACOS are good and take
  the green treatment despite a negative number. Everywhere else, positive is green. The ranked
  tables carry no rate columns, so sign and meaning never diverge there.

**Build constraints:**

- One file. Inline all CSS and JavaScript; no external stylesheets, no remote images.
- No chart library. Use CSS or inline SVG for any bar or sparkline.
- No browser storage. There is no per-viewer state worth keeping.
- Responsive: relative units, wide tables inside their own `overflow-x: auto` container so the
  page body never scrolls sideways.
- Theme-aware: define colours as tokens on `:root`, redefine under
  `@media (prefers-color-scheme: dark)` guarded as `:root:not([data-theme="light"])`, again under
  `:root[data-theme="dark"]`, and give `body` an explicit background.
- Sections that had no data are omitted from the dashboard, with a one-line note in their place
  saying what was unavailable. Never render an empty table or a placeholder row.

Publish it with a title naming the account and the current period, and a single emoji favicon.
Publishing is the delivery step — don't also present the file.

## Step 6 — Read out in chat

Keep the chat short; the dashboard holds the detail. Open with one line naming the account,
channel, and both date ranges. Then the account table only:

`Metric · Current · Previous · Change · % Change`

One row each: Ad spend, Ad sales, Total sales, ACOS, TACOS. Money as `$15,846`, ACOS and TACOS as
`21.8%`, percent changes as `+12.4%`, rate changes as `−6.0 pts`.

The campaign and product tables stay in the dashboard — don't repeat them in chat.

Close with two or three short bullets on what actually moved — which rows carry most of the
account change, whether the increases and decreases offset or compound, and whether total sales
moved with ads or independently of them. Tie it to outcomes, not to the mechanics of the pull,
and don't restate the table or narrate how the dashboard was built.

## Rules

- Never invent a row. A missing section is a note, not a guess.
- Sort on the change, never on current-period size — and let the service do the sorting.
- Ranked tables carry only the columns in Step 5.
- The dashboard displays only what Step 4 computed — no recalculation in the page.
- One account per run.
- Same two date windows across all three sections.
- Read-only.
