sparkx-weekly-ads-report · v1.0.5 · 2026-09-17 · sha256 80628a7caa4df4ef
sparkx-weekly-ads-report v1.0.5A
Immutable. This exact content never changes and is served at /api/v1/blob/80628a7caa4df4ef.
---
name: sparkx-weekly-ads-report
description: >-
Generate a weekly ad performance recap report for ads ops / customer-facing teams to use every Monday.
Includes a KPI card (with WoW comparison), 7-day trend, anomaly summary, top-mover lists, and next-week
action recommendations. Use when the user says "出个周报", "周复盘", "上周表现如何", "weekly report".
Not for monthly/quarterly reports (use sparkx-monthly-ads-report / quarterly-ads-report, not yet built),
nor for deep-dive analysis on a specific question (use the relevant analysis skill instead).
metadata:
version: 1.0.5
---
# Weekly Ads Report
## Scope: This Is an Orchestration Skill, Not a New MCP Tool
This skill does **not** map to a single MCP tool of its own — it orchestrates three existing read tools, each already covered by its own skill:
- `get_ads_perf` → [sparkx-query-ads-performance](../sparkx-query-ads-performance/SKILL.md)
- `get_operation_log` → [sparkx-query-operation-log](../sparkx-query-operation-log/SKILL.md)
- `get_entity_metadata` → [sparkx-query-entity-metadata](../sparkx-query-entity-metadata/SKILL.md)
> **Dependency — install these three base skills alongside this one:** `sparkx-query-ads-performance`, `sparkx-query-entity-metadata`, `sparkx-query-operation-log`. Skills install as sibling directories under your current agent's skills root (`.claude/skills/<name>/` for Claude Code; the equivalent root for other agents — see the [supported-agents table](../../README.md)), which is exactly what the `../query-.../…` links above (and elsewhere in this doc) resolve against — if a base skill isn't installed there, those links won't resolve and this skill can't run. Install all three first.
**Read those three SKILL.md files first** (parameter formats, field-naming rules, the Ratio Metric Display Rule) plus [`references/platform-notes.md`](references/platform-notes.md) (auth flow, error handling, pagination, date-range limits, currency rules — shared by all three underlying tools). This document only covers logic specific to the weekly-report use case; it doesn't repeat what the three base skills already document.
## Output Language: Always Match the User, Never Hard-Code Chinese or English
**The worked examples and output templates below are written in Chinese** (matching this product's primary customer base), but they are **structural illustrations, not literal text to copy verbatim**. Generate the actual report in whatever language the user asked in — section headers, KPI labels, the one-line summary, everything. A customer who asks in English should get an English report with English headers ("Weekly KPI Overview", not "本周 KPI 概览"); a customer who asks in Japanese should get one in Japanese. Do not assume the audience is Chinese-speaking just because this skill's own documentation and templates are.
When calling `get_ads_perf`, also set the `language` parameter (`zh`/`en`/`ja`) to match the customer's language — this controls the human-readable enum companion text the tool returns (e.g. `campaignTypeText`, `targetMatchTypeText`), so a customer reading an English report doesn't see a Chinese enum label embedded in an otherwise-English table. **`get_entity_metadata` has no `language` parameter** — it's not in that tool's documented parameter list. Localize its enum values using the returned `{field}Text` companion fields, or `sparkx-query-entity-metadata`'s own [`enum-i18n.md`](../sparkx-query-entity-metadata/references/enum-i18n.md) if a given enum has no `Text` companion — don't pass `language` to `get_entity_metadata` expecting it to have the same effect as on `get_ads_perf`.
## When to Use
**Trigger phrases**: 周报, 周复盘, 上周表现, weekly report, recap last week, 周会材料
**User roles**: ads ops, customer-facing AM/CSM
**Core question**: what happened last week, what to do this week
**Not for**:
- Monthly/quarterly view → `sparkx-monthly-ads-report` / `quarterly-ads-report` (not yet built)
- A specific question the user brings ("is my SBV structure right") → `sparkx-ads-structure-analysis`
- Day-level monitoring → a dedicated alerting skill (not yet built)
## Input Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `profileIds` | array[long] | No | — | Target profile IDs. See "Resolving `profileIds`" below for the exact selection rules — don't default to "all stores" without checking those rules first |
| `weekEnding` | date (`YYYY-MM-DD`) | No | Last Sunday (local timezone) | The end date (Sunday) of the week this report covers |
| `compareBaseline` | enum | No | `wow` | `wow` \| `wow+4w_avg` (the latter also compares against the trailing-4-week average) |
| `outputFormat` | enum | No | `markdown` | `markdown` \| `structured_report` |
| `includeAiSection` | `bool` \| `"auto"` | No | `auto` | Whether to include an AI-managed summary section (`auto` = include if the store has AI management enabled) |
### Resolving `profileIds`
Always call `get_user_authorized_context` first to get the authorized `profiles` list, then apply exactly one of these cases:
1. **The user named specific store(s)** ("Demo US", "my UK store") → resolve the name(s) against the authorized list and query only the matching profile(s).
2. **The user explicitly said "all stores" / "全账号" / equivalent** → query all authorized `profileIds`.
3. **The user didn't name a store, and there's only one authorized profile** → use it directly. There's nothing to ask — one option is not a choice.
4. **The user didn't name a store, and there are multiple authorized profiles**:
- **5 or fewer** → you may default to using all of them.
- **More than 5** → do not silently roll everyone up into one report. Hand the user the `profiles` list and ask them to pick one, several, or explicitly confirm "all stores" before proceeding.
Case 4's threshold exists because this is a customer-facing report, not an internal dashboard: a silent multi-store aggregate can hide a single store's problem behind a healthy-looking blended number, and it forces an unannounced USD conversion (see "Multi-Store and Currency" below) the customer didn't ask for.
**Determining `includeAiSection=auto`**: call `get_entity_metadata` (`entity: aiGroup`, filtered to the relevant `profileIds`; **add `filters: {"aiStatus": 1}`, or fully paginate — default page size is 100, so an account with more managed groups than one page could otherwise have its only running group sitting past page 1 and be missed**). If any returned row has `aiStatus: 1` (running), include the full AI-managed summary section. If none are currently running but Step 2f's AI/automation action summary still turns up rows for the week (a group may have been toggled off partway through), include a brief one-line mention instead of a full section — the activity is still worth surfacing, just not as prominently as an actively-running group.
## Workflow
### Step 1 · Define Time Windows
- **This week**: `[weekEnding − 6d, weekEnding]` (7 days inclusive)
- **Last week**: `[weekEnding − 13d, weekEnding − 7d]`
- **4-week average baseline** (when `compareBaseline=wow+4w_avg`): trailing **weekly** average over `[weekEnding − 34d, weekEnding − 7d]` (28 days = 4 weeks) — see Step 2g and Step 3 for why this must be a weekly figure, not a daily one
These three windows are 7 / 7 / 28 days respectively — all comfortably within `get_ads_perf`/`get_operation_log`'s 90-day single-call cap, so no window-splitting is needed.
**⚠️ T+2 data-delay check (matters especially because this report is meant to run every Monday)**: ad performance data has a T+2 processing delay. If `weekEnding` (default: last Sunday) is less than 2 days before today — e.g. the report runs Monday morning and `weekEnding` is yesterday or the day before — the last 1-2 days of "this week" may not have finished processing yet. Check the gap between `weekEnding` and today before generating the report; if it's under 2 days, add an explicit note at the top of the report ("the last X day(s) of this week's data may not be fully processed yet and could shift slightly") rather than presenting an incomplete number as final.
### Step 2 · Pull Data
The calls below are independent and can be issued in parallel. Every call needs `userContext` (non-empty string, ≤100 chars). None of them add a `campaign.campaignState_` filter — the WoW comparison needs to see a campaign that was running last week but paused this week (see Step 5); adding a state filter would make that campaign disappear from both periods instead of showing up as a real finding.
**a. This week, daily granularity (account-level trend)**
```json
{
"profileIds": [4404871489220462],
"factEntity": "campaign",
"dateStart": "{this Monday}",
"dateEnd": "{weekEnding}",
"select": ["date"],
"metrics": ["Impressions", "Clicks", "Spend", "Sales", "Conversions", "ACOS", "ROAS", "CTR", "CVR"],
"pageSize": 500,
"userContext": "Weekly report - this week's daily trend"
}
```
No campaign dimension in `select` — just `["date"]` — gives account-level daily totals across all campaigns, blended across every profile in `profileIds`.
**⚠️ Overall trend vs. per-store trend — don't blend these two shapes.** The call above (`select: ["date"]`) is for the single blended trend line covered by "## 2. 7-Day Trend" below. If the customer instead wants to see **each store's trend separately** (not one blended line), that's a different call: add `profile.profileId_`/`profile.profileName_` to `select` alongside `date` — but doing so changes the row granularity from **one row per day** to **one row per day × per profile**. Don't apply the "Multi-Store and Currency" section's advice to add `profile.profileId_` to *this* call by reflex when you actually want the single blended trend — that advice is for the campaign-level calls (c/d) where per-store attribution is the point; here it would silently fragment the trend into N per-store lines instead of the one overall line the KPI section expects. If you need both, issue both shapes as separate calls rather than trying to compute one from the other after the fact.
**b. Last week, daily granularity** — identical shape, `dateStart`/`dateEnd` shifted back to last week's window.
**c. This week, campaign-level (feeds Top Movers)**
```json
{
"profileIds": [4404871489220462],
"factEntity": "campaign",
"dateStart": "{this Monday}",
"dateEnd": "{weekEnding}",
"select": ["campaign.campaignId_", "campaign.campaignName_"],
"metrics": ["Impressions", "Clicks", "Spend", "Sales", "Conversions"],
"pageSize": 500,
"userContext": "Weekly report - this week's campaign performance for WoW comparison"
}
```
**`select` must include `campaign.campaignId_`** (not just the name) — Step 5 joins by ID, not name.
**d. Last week, campaign-level** — same shape as c, `dateStart`/`dateEnd` shifted back a week; everything else (`select`/`metrics`/filters) must match c exactly, or the two periods aren't comparable.
**e. This week, Top 10 ASINs**
```json
{
"profileIds": [4404871489220462],
"factEntity": "asin",
"dateStart": "{this Monday}",
"dateEnd": "{weekEnding}",
"select": ["asin.asin_", "asin.asinTitle_"],
"metrics": ["TotalSalesAmount", "OrderCount", "TACOS"],
"orderBy": [{"field": "TotalSalesAmount", "direction": "DESC"}],
"pageSize": 10,
"userContext": "Weekly report - top 10 ASINs by sales"
}
```
**When `profileIds` spans more than one store, decide which of two different questions you're actually answering — the same call doesn't serve both:**
- **One cross-store blended Top 10** ("top 10 ASINs by sales across all these stores combined"): do **not** add `profile.profileId_` to `select` — same as the single-store call above, `factEntity: asin` with `select: ["asin.asin_", "asin.asinTitle_"]` already aggregates `TotalSalesAmount` across every profile in `profileIds` per ASIN, and `orderBy`+`pageSize:10` gives you the correct blended top 10 directly.
- **Each store's own Top 10** ("top 10 ASINs per store"): add `profile.profileId_`/`profile.profileName_` to `select`. This changes the grouping to ASIN × profile, and **`orderBy`+`pageSize:10` at that point returns the top 10 ASIN×profile combinations *overall*** (by global rank across every store's ASINs mixed together) — it does **not** give you 10 rows for each store. To get a genuine per-store Top 10, either issue one call per `profileId`, or raise `pageSize` enough to pull the full set and then group by `profile.profileId_` client-side, sorting and truncating to 10 within each group.
Don't reach for `profile.profileId_` by default "for attribution" here — decide which of the two questions above the customer actually asked first.
**f. This week's AI/automation action summary** (using `get_operation_log`, which is real — not a subscription alert feed)
```json
{
"profileIds": [4404871489220462],
"dateStart": "{this Monday}",
"dateEnd": "{weekEnding}",
"changeBy": {"operator": "IN", "values": ["ai", "automation"]},
"pageSize": 200,
"userContext": "Weekly report - AI/automation actions this week"
}
```
**Check `truncated`**: if `true`, this week had more than 200 matching AI/automation ops — follow `sparkx-query-operation-log`'s "Getting a Complete Count" procedure (split into non-overlapping sub-windows and recurse) before summing; don't treat this one page as the full week.
**Step 2f as a whole is best-effort, not a hard dependency for the rest of the report.** If splitting into sub-windows is still timing out, or the call keeps erroring, don't let it block Steps 3/5/6 (KPI overview, Top Changes, action recommendations) from completing. Produce the rest of the report normally and mark this section explicitly as "AI/automation activity data incomplete this week" (or similar, in the report's own language) rather than failing the entire report over one section.
**g. 4-week average baseline** (only when `compareBaseline=wow+4w_avg`)
```json
{
"profileIds": [4404871489220462],
"factEntity": "campaign",
"dateStart": "{weekEnding − 34d}",
"dateEnd": "{weekEnding − 7d}",
"select": ["date"],
"metrics": ["Impressions", "Clicks", "Spend", "Sales", "Conversions"],
"pageSize": 500,
"userContext": "Weekly report - trailing 4-week average baseline"
}
```
Same shape as call (a) (account-level, `date`-only `select`), just a 28-day window instead of 7.
**Only these 5 base metrics are requested — deliberately no `ACOS`/`ROAS`/`CTR`/`CVR` in this call.** Sum each across the 28 days, then divide by 4 (not 28) to get a trailing-4-week *weekly* average. Dividing by 28 would give a *daily* average, which is not directly comparable to "this week"'s 7-day total (that mismatch would make a perfectly flat week look like a several-hundred-percent spike) — both figures compared in Step 3 must be on the same "per week" scale.
**Then recompute the ratio metrics from those summed base metrics** using the standard formulas — `ACOS = Spend/Sales×100`, `ROAS = Sales/Spend`, `CTR = Clicks/Impressions×100`, `CVR = Conversions/Clicks×100`. Ratio metrics were left out of `metrics` above specifically so there's no 28-daily-values version of them lying around to be misused — summing or averaging 28 individual days' ACOS/ROAS/CTR/CVR is never valid (same principle as never averaging pre-computed ratios across a split window — see the aggregation-over-time example in `sparkx-query-ads-performance`), so don't request them here in the first place. Whether you apply the formulas to the raw 28-day sums or to the ÷4 weekly averages makes no difference to the result (the formulas are scale-invariant).
Skip this call entirely when `compareBaseline=wow` (the default) — don't issue it if the report doesn't need it.
### Step 3 · Compute Core Metrics
**Total KPIs (8)**: Spend, Sales, ACOS, ROAS, Impressions, Clicks, CTR, CVR
**For each**: this-week value, last-week value, WoW % (`(this week − last week) / last week × 100`; undefined when last week is 0 — see below), direction arrow (↑↓ + color in a rendering context that supports them; in a plain-text-only context — e.g. a CLI that won't reliably render emoji or color — spell it out as "up"/"down"/"flat" instead)
**When `compareBaseline=wow+4w_avg`**: additionally include the trailing-4-week **weekly** average from Step 2g and a `vs. 4-week avg` % computed the same way as WoW (`(this week − 4-week avg) / 4-week avg × 100`, same zero-denominator handling). Per Step 2g: the 5 base metrics use 28-day sum ÷ 4 (**not** ÷ 28, which would be a daily figure not comparable to this week's 7-day total); the ratio metrics (`ACOS`/`ROAS`/`CTR`/`CVR`) are **not** divided by anything directly — they're recomputed from those summed base metrics via their standard formulas. Add the baseline as a third column alongside This Week / Last Week in both output templates below — don't compute this baseline without displaying it, and don't display the column without actually computing it.
**Ratio-metric display rule** (same rule shared across the three base skills): confirmed percentage metrics such as `ACOS`, `CTR`, `CVR`, `TACOS`, and the derived `*Rate` metrics explicitly listed in Platform Notes are pre-scaled ×100 (the tool returns `17.61` meaning 17.61%) — **append `%` directly, do not multiply/divide by 100 again**. Do not infer an unlisted field's scale from its name alone; `ROAS`/`CPC`/`CPA` are not percentages, they're plain ratios/unit-costs — no `%`.
**"Significant change" thresholds** (used for the initial screen in Step 5's Top Changes section — not the final sort criterion, which still follows Step 5's full procedure):
- Spend WoW absolute value ≥ 20%
- ACOS WoW absolute value ≥ 15%
- Last week's spend < 5% of last week's average spend → treat as too small to matter; apply Step 5's denominator floor to filter this out, so tiny-base noise doesn't get reported as a "spike"
### Step 4 · Identify Anomalies
**There is no independent alert/subscription data source** — `get_operation_log` is a change-history query tool, not a subscription-based alerting system; there is no `log_type=alert` parameter. Anomaly identification is entirely built from these two pieces:
**a. Built-in rule scan over this week's campaign data** (using Step 2c's result):
- High spend, zero orders: `Spend` exceeds a meaningful floor for the row's currency **and** `Conversions = 0`. **Don't hardcode a bare "100" and assume it's USD.** For a multi-profile report (aggregated to USD per "Multi-Store and Currency" below), `Spend > 100` (USD) is a reasonable floor. For a single-profile report in a non-USD currency, a raw "100" is meaningless in some currencies (e.g. JPY has no cents, and typical campaign spend is often in the thousands of yen) — use a floor that's actually meaningful in that store's local currency, and if you're not sure what "high spend" means for that store, say so rather than silently applying the USD figure to a different currency.
- `This week's ACOS > last week's ACOS × 1.5 AND this week's ACOS > 30%` → ACOS spike. **Step 2c/2d's `metrics` list doesn't include `ACOS` directly** — compute it per campaign as `Spend/Sales×100` from each period's own `Spend`/`Sales`. **When a campaign's `Sales = 0`**, its ACOS is undefined (division by zero), not "infinite" — exclude that campaign from the ACOS-spike rule entirely rather than computing a fabricated huge number (a zero-Sales, nonzero-Spend campaign is already caught by the high-spend/zero-orders rule above, so it isn't silently missed).
- `This week's Impressions < last week's Impressions × 0.5` → impression collapse
**b. Step 2f's AI/automation action summary**: tally the top 3 most-frequent action categories this week (`actionType`/`operationType` distribution) + the top 3 highest-spend-impact records (needs cross-referencing that campaign's spend from Step 2c). This is a real change record, not an anomaly verdict — present it as "what AI/automation did this week," not conflated with "anomaly detected."
If neither part turns up anything notable, write "no significant anomalies this week."
### Step 5 · Extract Top Changes (Follow the Period-over-Period Procedure Strictly — Don't Simplify)
**Must follow** [sparkx-query-ads-performance's Period-over-Period and Top Movers example](../sparkx-query-ads-performance/references/example-period-comparison-top-movers.md) in full — don't simplify by intuition. This is the section most prone to error and most likely to be challenged by a customer:
1. **Join by `campaign.campaignId_`, never by name** — names can be duplicated or renamed between periods.
2. **Bucket into three groups before ranking**: new this week (present in Period A only), disappeared (present in Period B only — don't show these as unlabeled zeros), and normal comparisons (present in both). New/disappeared entities have no valid `% change` (division by zero) — don't force them into a percentage leaderboard.
3. **Spend increase Top5**: sort by absolute spend change, floor the population by `max(this week's spend, last week's spend)` — not just this week's value alone — so a campaign that shrank from big to small isn't missed just because its *current* number looks small.
4. **ACOS worsening Top5**: ACOS increase magnitude × spend weight; each period's ACOS is requested/computed independently — never average the two periods' ACOS before computing the change. **If either period's `Sales = 0` for a campaign, its ACOS is undefined for that period** — exclude it from this ranking rather than computing against a zero denominator; a campaign with zero Sales and nonzero Spend belongs in the high-spend/zero-orders anomaly (Step 4) instead, not the ACOS worsening list.
5. **Sales increase Top5**: same treatment as spend increase, positive direction.
6. **The floor for % change goes on last week's (Period B) value, not this week's** — e.g. last week's spend $1 → this week's spend $101 looks like "+10,000%" and is misleading unless floored; the floor must be applied to the prior-period (denominator) value.
7. **Every Top list shows both periods' raw values plus the absolute delta** — not just the percentage, since "+45%" is meaningless without knowing whether that's $10 or $10,000.
8. **If full pagination is impractical at scale** (e.g. `profileIds` spans dozens of stores and paging both periods to `hasNextPage: false` would take many round-trips), you may fall back to a spend-ranked sample: pull only the top N campaigns by `Spend` per period (via `orderBy` + a capped `pageSize`) instead of the full population. **This fallback must be disclosed explicitly in the report** — label the Top Changes section as based on a top-spend sample, not the full population (in whichever language the report is written), and state concretely what was actually covered (how many profiles, how many pages, and whether the underlying calls reported `hasNextPage: true` when you stopped) — never silently present a sampled result as if it were the complete Top Movers analysis.
9. **A degraded or failed Top Changes section must not fail the whole report.** If pagination times out or a call keeps erroring even after retrying/splitting, still produce Steps 3 and 6 (KPI overview, action recommendations) from whatever data did come back, and present the Top Changes section as partial/unavailable rather than aborting report generation entirely.
**Top 10 ASINs this week**: use Step 2e's result directly, no WoW comparison needed (if the customer wants one, pull the same-shape ASIN data for last week and join by ID the same way).
### Step 6 · Generate Next-Week Action Recommendations (3-5 items)
Auto-derive "what to do" from the data above, e.g.:
- Found "high spend, zero orders" → recommend "pause or lower bids on Campaign X (spent $Y with zero orders this week)"
- Found "ACOS spike" → recommend "investigate Campaign X's keyword bids — cross-reference with `get_operation_log` for bid/budget changes in this window"
- Found "sales-growth standout" → recommend "scale Campaign X's structure to similar campaigns"
**Every recommendation must have**: action + target (specific campaign/ASIN name) + evidence (data). **Correlation, not causation** — if a recommendation is based on something like "ACOS rose + there was a bid change in the same window," say explicitly that it's a correlation, not "caused by," unless `get_operation_log` has pinpointed the exact change and the timing lines up precisely.
## Multi-Store and Currency
When `profileIds` spans multiple stores:
- All monetary metrics from `get_ads_perf` auto-aggregate to **USD** — any dollar amount in the report should be explicitly labeled "(USD)" so the customer doesn't assume local currency.
- Steps 2c/2d (campaign-level) need `profile.profileId_` (usually also `profile.profileName_`) added to `select`, or multi-store rows can't be attributed to a specific store. Steps 2a/2b (the blended daily trend) and 2g (the blended 4-week baseline) deliberately do **not** get profile fields added — see the warning under Step 2a for why adding them there would change the trend's row granularity instead of just attributing it. Step 2e (Top 10 ASINs) is conditional on which question is being asked — see the two cases spelled out under Step 2e; don't add `profile.profileId_` there by default.
- `asin.asinPrice_` (if used) stays in local currency and is not affected by the USD-aggregation rule above — this report doesn't currently use that field, but note the exception if the report is later extended to include it.
## Output Format
### Naming entities in this report
Nobody reading this report knows an internal id, and the ids `get_ads_perf` returns are
Amazon's anyway. So:
- **Every entity column and every sentence uses the name** — `campaignName`, `adGroupName`,
`asinTitle`. A `Campaign` column holds a campaign name, never a number.
- **Keep ids in your working data regardless.** Period-over-period joins must be by id,
because names get duplicated and renamed — that requirement does not change. What changes
is only what reaches the page.
- **An export gets both.** If the user asked for CSV / Excel / "something I can reconcile",
add an id column next to the name. The id to publish is the **Amazon** one, which is what
`get_ads_perf` already gives you.
- In `structured_report` mode the entity/target fields carry the **name**; when ids were
asked for, add a separate key for the Amazon id rather than gluing both into one string.
Full convention, including the managed-group and product-ad exceptions:
[`references/platform-notes.md`](references/platform-notes.md) -> "Naming things in your
answer".
Remember: **the section headers and labels below are structural placeholders (shown in English here) — always regenerate them in the user's own language**, not copy the English (or any other) wording verbatim (see "Output Language" above).
**Plain-text fallback**: the 🔴/🟡 severity markers and ↑/↓ direction arrows below are for rendering contexts that support them (chat UIs, rendered markdown). In a plain-text-only output context, replace them with words instead — e.g. "High:"/"Medium:" for severity, "up"/"down"/"flat" for direction — rather than relying on a glyph or color the target surface might not render.
### markdown mode (default)
```markdown
# Weekly Ads Report · {profile_name}
**Period**: {week_start} ~ {weekEnding}
**Baseline**: {last week / last week + 4-week average}
{If data may be incomplete due to T+2 delay, add a note here}
## 1. This Week's KPI Overview
{If compareBaseline=wow (default), omit the "4-Week Avg" column entirely — only include it when compareBaseline=wow+4w_avg}
| Metric | This Week | Last Week | WoW | 4-Week Avg | vs. 4-Week Avg |
|---|---|---|---|---|---|
| Spend | ${spend} | ${prev_spend} | {wow%} {arrow} | ${avg4w_spend} | {vs4w%} {arrow} |
| Ad Sales | ${sales} | ${prev_sales} | {wow%} {arrow} | ${avg4w_sales} | {vs4w%} {arrow} |
| ACOS | {acos}% | {prev_acos}% | {wow%} {arrow} | {avg4w_acos}% | {vs4w%} {arrow} |
| ROAS | {roas} | {prev_roas} | {wow%} {arrow} | {avg4w_roas} | {vs4w%} {arrow} |
| Impressions | {imp} | {prev_imp} | {wow%} {arrow} | {avg4w_imp} | {vs4w%} {arrow} |
| Clicks / CTR / CVR | {clk} / {ctr}% / {cvr}% | ... | ... | ... | ... |
**One-line summary**: {a qualitative read auto-generated from the KPI changes, e.g. "Spend up 15% this week, ACOS held steady — healthy growth"}
## 2. 7-Day Trend
{Embed a trend chart or describe key inflection points in text}
**Key inflection points**:
- {date}: {what changed and a possible reason — label as correlation, not confirmed cause, unless verified via get_operation_log}
## 3. This Week's Anomalies / AI-Automation Activity
{If none, write "no significant anomalies this week"}
- 🔴 **{event type}**: {target} - {specific evidence}
- 🟡 **{event type}**: ...
AI/automation made {N} operations this week: {brief breakdown by actionType}
## 4. Top Changes
### Spend Increase Top5
| Campaign | This Week's Spend | Last Week's Spend | Change |
|---|---|---|---|
| ... | ... | ... | ... |
### ACOS Worsening Top5
{same format as above}
### Sales Increase Top5
{same format as above}
### New / Disappeared Campaigns
{listed separately — not mixed into the three percentage leaderboards above}
### Top 10 ASINs This Week
{table}
## 5. Next Week's Action Recommendations
1. **{action}** {target}
- Evidence: {data}
2. ...
---
_Generated: {timestamp} · Data source: SparkX AI MCP · Skill: sparkx-weekly-ads-report v{version}_
```
### structured_report mode
Output JSON schema:
```json
{
"period": {"start": "...", "end": "..."},
"dataFreshnessWarning": null,
"kpiCards": [{"metric": "spend", "current": ..., "previous": ..., "wowPct": ..., "fourWeekAvg": null, "vsFourWeekAvgPct": null}],
"trend": {"daily": [{"date": "...", "spend": ..., "sales": ...}]},
"anomalies": [{"severity": "...", "type": "...", "entity": "...", "evidence": "..."}],
"aiAutomationSummary": {"totalOps": ..., "byActionType": {...}},
"topChanges": {
"spendUp": [...],
"acosWorse": [...],
"salesUp": [...],
"new": [...],
"disappeared": [...]
},
"topAsins": [...],
"actions": [{"priority": 1, "action": "...", "target": "...", "evidence": "..."}]
}
```
`kpiCards[].fourWeekAvg`/`vsFourWeekAvgPct` are only populated when `compareBaseline=wow+4w_avg`; leave them `null` for the default `wow` baseline.
## Data Boundaries
- **Paid Sponsored Ads data only** — no DSP, no AMC
- **No year-over-year comparison** — YoY belongs at the monthly/quarterly report level
- **No structural/share-of-spend deep analysis** — that's `sparkx-ads-structure-analysis`'s job
- **Anomaly rules are a lightweight built-in version** — there's no independent deep-alerting subscription capability
- **Cannot reliably compute "real-time remaining budget"** — `dailyBudget`/`currentBudget` are configuration values, not a live spend ledger, and today's `Spend` is usually incomplete due to the T+2 delay. If a customer asks this in the weekly-report context, say plainly it can't be computed reliably — don't present a subtraction as if it were precise.
- **`get_entity_metadata` has no historical-snapshot capability** — if this report needs to show "this week's config vs last week's config" (as opposed to performance data), that can only come from `get_operation_log`'s change records, not from querying `get_entity_metadata` for a past point in time.
## Example Call
**User input**:
> Give Demo US a report for last week
**Skill flow**:
1. Resolve the profile: `get_user_authorized_context` → find Demo US's `profileId`
2. `weekEnding` = last Sunday; check the T+2-delay window
3. Run Steps 2-6
4. Output the markdown report — in English, since the user asked in English
## Error Handling
Follows the shared error envelope used by all three underlying tools (`isError`/`errorType`, see [`references/platform-notes.md`](references/platform-notes.md)). Specific handling for the weekly-report use case:
| Situation | Handling |
|---|---|
| `profileIds` is empty | Follow "Resolving `profileIds`" above — call `get_user_authorized_context` first, then apply those rules; ask the user to pick only when those rules actually require it (not automatically) |
| Any call returns `isError: true` | Handle per `errorType` (`invalid_params` is usually a construction bug in this skill's own call — check parameter names and date ranges first; `rate_limited` → wait `retryAfterSeconds` before retrying, don't retry immediately) — don't treat a tool error as "no data for this period" |
| `effectiveProfileIds` doesn't match the requested `profileIds` | Explain that the actual scope differs from expected — check the token's authorized scope first, don't just treat it as "this store has no data" |
| A time window is genuinely missing data (not an error) | State clearly "data for X-Y is missing," and still produce the report with what's available |
| `get_operation_log` returns `truncated: true` | Follow its "Getting a Complete Count" procedure (split into sub-windows) rather than treating one page as the full week; note in the report that the AI/automation op count is a partial/lower-bound figure |
| All KPIs show WoW change < 5% | Change the one-line summary to "stable week, no significant swings"; merge the Top Changes section into a brief note |
## Not Covered by This Skill
Don't do these in the weekly report — point the user to the corresponding capability instead. Where a dedicated skill exists, name it; the rest still require manually combining the three base tools:
- Deep root-cause attribution ("why did ACOS go up") → see `sparkx-query-ads-performance`'s [ACOS root-cause investigation example](../sparkx-query-ads-performance/references/example-acos-root-cause-investigation.md)
- Full product-level diagnosis → `sparkx-product-diagnosis`
- Structural/share-of-spend analysis → `sparkx-ads-structure-analysis`
- Search-term mining → a future dedicated skill