Adobe CJA
Adobe v2.0.0
Adobe Customer Journey Analytics helps authorized users discover data views, dimensions, metrics, segments, date ranges, projects, and audiences; run ranked CJA reports; inspect component definitions; and create or update reusable CJA components and Workspace projects through ChatGPT. It requires Adobe IMS OAuth and Customer Journey Analytics product access, and tool calls use the signed-in user's IMS organization, sandbox or product context, and CJA permissions.
Language: English · Automatically detected from descriptions.
Package details
Publisher declarations from the archived package. These are separate from our research and the live service's terms.
- Package author
- Adobe
Package observed Sep 30, 2026.
Files & skills
File archives
Skill instructions
cja-dimension-analysis12.8 KB
---
name: cja-dimension-analysis
description: >
Comprehensive dimension analysis and reporting for CJA. Use this skill whenever the user
wants to analyze one or more dimensions — including cardinality, distribution/skew, trends,
anomalies, data quality errors, comparisons, and forecasting. Also trigger when someone
asks "what are the top values for...", "dimension health", "explore this dimension",
"dimension dashboard", "dimension statistics", "data quality check on a dimension",
"dimension cardinality", "dimension trends", "dimension skew", "dimension anomalies",
"compare dimensions", or any similar request to understand what's inside a CJA dimension.
Produces an interactive HTML dashboard or a markdown report. Works with the CJA MCP server.
license: Apache-2.0
metadata:
author: Adobe
version: "1.0"
---
# CJA Dimension Analysis
Analyze one or more CJA dimensions to understand their cardinality, distribution, trends,
anomalies, data quality issues, and forecasts. Produces an actionable report that helps
teams understand what's inside their dimensions and where to focus attention.
## Workflow
Execute phases in order. Each phase is **selectable** — the user can ask for a subset
(e.g., "just cardinality and errors") or the full analysis. Default is all phases.
### Phase 0 — Setup
1. Call `findDataViews` to list available data views. If the user hasn't specified one,
ask which data view to analyze. Set it with `setDefaultSessionDataViewId`.
2. Ask which dimensions to analyze. Options:
- **Named dimensions**: "Analyze Page Name and Browser Type"
- **By ID**: user provides dimension IDs directly
- **All dimensions**: warn that this may be slow; ask for a limit (default: top 50 by name)
3. Ask which analyses to run (or confirm "all" as the default):
- Cardinality, Distribution/Skew, Trends, Anomalies, Data Quality, Comparisons, Forecasting
4. Ask for the date range. If the user hasn't specified one, test a few ranges to find data:
- Try last 30 days, last 90 days, last 6 months, last year — use the first that returns rows.
5. Ask for the primary metric to use for distribution/skew (default: occurrences or visits).
6. Confirm the plan with the user before proceeding.
### Phase 1 — Cardinality
For each dimension:
1. Call `searchDimensionItems(dimensionId, limit: 50000)` to estimate unique value count,
or `runReport` with the dimension as rows and a count metric to get row count.
2. Classify cardinality:
| Level | Threshold |
|-------|-----------|
| LOW | < 100 unique values |
| MEDIUM | 100 – 1,000 |
| HIGH | 1,000 – 10,000 |
| VERY HIGH | > 10,000 |
3. Track cardinality over time (optional): `runReport` with dimension + date breakdown;
count unique dimension values per day/week to see cardinality growth trend.
4. Flag HIGH and VERY HIGH dimensions with performance recommendations.
Store: `{dimensionId, name, uniqueValueCount, cardinalityLevel, cardinalityTrend}`
### Phase 2 — Distribution & Skew
For each dimension:
1. `runReport` with dimension as rows + primary metric (e.g., occurrences/visits).
Request at least 50 rows to capture the distribution shape.
2. Compute top-N % share (top 1, 5, 10), Gini coefficient, and cumulative distribution.
3. Classify skew:
| Label | Condition |
|-------|-----------|
| **Extreme skew** | Top 1 value > 50% of total |
| **High skew** | Top 1 value > 30% of total |
| **Moderate** | Top 5 values < 70% of total |
| **Long tail** | Top 10 values < 50% of total |
4. Note: per-value breakdown with percentage and cumulative %.
Store: `{dimensionId, distribution: [{value, metric, pct, cumulative}], gini, skewLabel, top1Pct, top5Pct, top10Pct}`
### Phase 3 — Trends
For each dimension:
1. `runReport` with dimension + date granularity (day or week depending on range).
Compare two periods: first half vs second half of the selected date range.
2. Identify:
- **New values**: appeared in period 2 but not period 1
- **Disappeared values**: present in period 1, absent in period 2
- **Growth**: metric in period 2 > metric in period 1 by > 10%
- **Decline**: metric in period 2 < metric in period 1 by > 10%
- **Stable**: < 10% change between periods
3. Assign trend badges per value: 🟢 Growing | 🔴 Declining | 🟡 Stable | 🆕 New | ⬜ Disappeared
Store: `{dimensionId, periodComparison: {period1, period2, changes: [{value, p1Metric, p2Metric, pctChange, badge}]}, newValues: [], disappearedValues: []}`
### Phase 4 — Anomalies
For each dimension:
1. From the Phase 3 time-series, compute rolling mean and stddev per dimension value.
2. **Z-score detection**: flag (value, date) pairs where the z-score exceeds the threshold
(default: 2.0; sensitive: 1.5; conservative: 3.0).
3. **Threshold alerts**:
- Any single value holding > 50% of total metric on a given day
- Value count that is > 2× the rolling average for that value
4. **New/disappeared alerts**: flag values that appear or disappear mid-period (from Phase 3).
5. Collect: anomaly type (spike, drop, new, disappeared, threshold), dimension value, date, magnitude.
Store: `{dimensionId, anomalies: [{value, date, type, magnitude, zScore}]}`
### Phase 5 — Data Quality / Errors
For each dimension:
1. Search for known bad values using `searchDimensionItems`:
- `"Unspecified"`, `"None"`, `"(empty)"`, `""`, `"null"`, `"undefined"`, `"N/A"`, `"unknown"`
2. Count occurrences with `runReport` filtering to each known bad value.
3. Compute: missing data % = (sum of bad value occurrences) / total occurrences.
4. Flag: dimensions where missing data > 5% (warning), > 20% (critical).
5. If the dimension has an expected format (URL, email, date), note it — but don't auto-validate
patterns unless the user asks.
Store: `{dimensionId, errorPatterns: [{pattern, count, pct}], missingDataPct, missingDataSeverity}`
### Phase 6 — Comparisons (multi-dimension or time-period)
This phase runs when the user is analyzing 2+ dimensions OR requests period comparison.
**Side-by-side (2–3 dimensions):**
1. For each dimension pair, compare cardinality level, skew, top-5 values, error rate.
2. Produce a comparison table: dimension A vs B vs C on each metric.
**Time-period comparison (single dimension):**
1. Compare two custom date ranges provided by the user (or auto-detect: first half vs second half).
2. For each value: metric in period 1, metric in period 2, delta, % change.
3. Surface the biggest movers (top 5 growing, top 5 declining).
Store: `{comparisons: [{type, dimensions or periods, table}]}`
### Phase 7 — Forecasting
For each dimension with sufficient time-series data (>= 7 data points):
1. For the top 5–10 values by metric, fit a linear regression to the time series.
2. Project 7 periods forward.
3. Report:
- Trend direction: Upward / Downward / Flat (based on slope)
- Confidence: High (R² > 0.7), Medium (0.4–0.7), Low (< 0.4)
- Projected value at end of forecast window
4. Flag values with strong upward trend (might become dominant) or strong downward trend
(might disappear soon).
Store: `{dimensionId, forecasts: [{value, slope, r2, direction, confidence, projectedValues: []}]}`
### Phase 8 — Report Generation
After all analysis phases complete:
1. Save all collected data to a JSON file:
`dimension_analysis_results_YYYY-MM-DD_HH-MM.json`
(in a temp output directory, e.g. `/tmp/cja-dimension-analysis/`, or a path the user specifies)
2. Run the Python report generator:
```bash
python3 scripts/cja_dimension_analysis.py \
<analysis_json> \
"<data_view_name>" \
"<data_view_id>" \
[output_directory] \
[--format=html|markdown] \
[--keep-analyses=N]
```
**Options:**
- `--format=html` (default): Interactive HTML dashboard with Chart.js visualizations
- `--format=markdown`: Comprehensive text-based report with tables
- `--keep-analyses=N` (default: 0 = keep all): Auto-cleanup of old analysis files
3. The script generates a second output file: the report (HTML or markdown).
4. Open with `open <output_directory>/dimension_analysis_report_*.html`
5. Present the report path to the user and summarize key findings:
- Dimensions with HIGH/VERY HIGH cardinality
- Dimensions with extreme or high skew
- Any anomalies found
- Data quality issues above warning threshold
- Forecast trends worth watching
## CJA MCP Tools Used
| Tool | Phase | Purpose |
|------|-------|---------|
| `findDataViews` | 0 | List available data views |
| `setDefaultSessionDataViewId` | 0 | Set active data view for session |
| `findDimensions` | 0 | Discover dimensions by name/search |
| `describeDimension` | 0 | Get dimension metadata and ID |
| `searchDimensionItems` | 1, 5 | Count unique values; search for specific items (error patterns) |
| `runReport` | 1–7 | Primary data engine: dimension rows + metric, with optional date breakdown |
## Output Format
### HTML Dashboard (default)
Interactive report with:
- Executive summary cards (total dimensions, flagged dimensions, critical issues)
- Per-dimension sections: cardinality badge, distribution chart (Chart.js bar), skew metrics,
trend table, anomaly list, data quality indicators
- Comparison section (if multiple dimensions or period comparison requested)
- Forecast section (if forecasting was run)
- Recommendations panel: grouped by priority (critical → warning → info)
- Design: dark navy-to-blue gradient header, full-width, card-based layout, collapsible sections
### Report HTML Style — Required
The generated HTML **must** use the editorial design system shared across all skills:
warm off-white surface, serif display title, red-on-black gradient header, and
underline-on-hover text-link nav. Do **not** introduce corporate-blue chrome,
centered headers, or alternative gradients.
Read [`template.html`](template.html) and use it verbatim. It contains the
Google Fonts `<link>` tags, the full CSS block, and the `<header>` structure.
Paste the `<head>` block into the generated report's `<head>`, paste the
`<header>` block at the top of `<body>`, and fill in the `{ORG_NAME}`,
`{DIMENSION_COUNT}`, `{DATE_RANGE}`, `{DATA_VIEW_NAME}`, and `{DATE}`
placeholders. Do not improvise the styling.
Where `{ORG_NAME}` is the customer's brand name (with technical suffixes like
` — Prod`, ` - Demo`, ` MCP`, ` Stage` stripped). Never substitute a vendor or
product name into the title. The title is all white — do not color any word red.
For single-dimension reports, replace the h1 with `{ORG_NAME} {DIMENSION_NAME} Report`.
**Section titles — no phase prefix**: Section headings in the HTML report must **not** include
the phase number. Use the plain section name only:
- ✅ "Cardinality" — not "Phase 1 — Cardinality"
- ✅ "Distribution & Skew" — not "Phase 2 — Distribution & Skew"
- ✅ "Trends" — not "Phase 3 — Trends"
- ✅ "Data Quality" — not "Phase 5 — Data Quality / Errors"
### Markdown Report
Text-based report with:
- Summary table across all dimensions
- Per-dimension deep-dive sections with inline tables
- Anomaly log
- Recommendations with rationale
The JSON schema consumed by `scripts/cja_dimension_analysis.py` is derived from the
`Store: {...}` shapes in each phase above. The script knows its own input contract;
build the JSON to match the per-phase Store entries.
## Example Interaction
> "Can you analyze how our 'Marketing Channel' dimension is performing and break it down by device type?"
1. **Setup:** Confirm the data view with `findDataViews`. Call `setDefaultSessionDataViewId`.
2. **Dimension discovery:** Call `findDimensions` to locate the 'Marketing Channel' dimension and its ID. Confirm it exists and has data with `searchDimensionItems`.
3. **Analysis:** Run `runReport` for Marketing Channel performance over the last 30 days (visits, conversions, revenue). Identify top and bottom performers.
4. **Breakdown:** Run a second report cross-tabbing Marketing Channel by Device Type dimension to surface mobile vs. desktop patterns.
5. **Report:** Run the Python analysis script to generate an interactive HTML report. Open it. Summarize top findings: "Email drives 38% of conversions despite only 12% of traffic. Paid Search converts 2× better on mobile than desktop."
## Important Guardrails
- Never modify dimension definitions or project data. This is read-only analysis.
- If a dimension returns no data for the selected date range, try a broader range before giving up.
- For VERY HIGH cardinality dimensions (> 50k values), note that full distribution analysis
may be truncated — use sampled top-N values.
- If `runReport` times out on a dimension, reduce the row limit and note the limitation.
- Always tell the user which analyses are being run and which were skipped.
- For large dimension sets (> 20 dimensions), run phases 1–2 first and ask if the user wants
to proceed with deeper analysis on a subset.
- Let the user know progress as you move through phases: "Phase 1 complete (cardinality for 5
dimensions). Running Phase 2 (distribution)..."
Referenced files: 4
cja-executive-briefing33.8 KB
---
name: cja-executive-briefing
description: >
Generates a polished, leadership-ready performance briefing with KPI tiles,
executive narrative bullets, and a driver analysis — all as a print-ready HTML
document. Always use this skill when someone asks for an executive summary,
performance briefing, leadership readout, stakeholder update, or business review
— even if they don't say "executive" explicitly. Trigger phrases include:
"write a summary of last week's performance," "create a briefing for our
leadership team," "produce a monthly business review," "what should I tell
executives about our metrics," "generate a performance narrative," "QBR summary,"
"weekly business review," "board update," "stakeholder briefing," "how did we
do last week," or "give me a performance snapshot." When in doubt, use this skill
— it is always better to produce a polished briefing than a raw data dump.
license: Apache-2.0
metadata:
author: Adobe
version: "1.0"
---
# Executive Briefing (Customer Journey Analytics)
Produce a polished, leadership-ready performance document. Executives do not
read data tables — they read narratives that answer "are we growing?", "what
changed?", and "what do we do about it?"
This skill converts CJA data into a briefing that a VP or C-suite leader can
read in under 3 minutes, with supporting data available for those who want to
dig deeper.
Tone: business impact, not technical metrics. Write "Revenue grew 12% driven by
a strong Paid Search week" not "metrics/revenue increased by 0.12 per metrics/sessions."
---
## CJA MCP Tools Used
- `describeCja(DATAVIEW_CONTEXT_GUIDE)` — understand the business context of the data view
- `listComponentUsage` — identify the north-star and supporting KPIs
- `findMetrics` — resolve user-specified or discovered metric IDs
- `findCalculatedMetrics` — include custom business KPIs
- `runReport` — pull KPI values for current and comparison period
- `searchDimensionItems` — identify top driver dimension values
---
## Phase 0 — Setup
1. Call `findDataViews` to list available data views.
2. If the user hasn't specified a data view, present the list and ask which to use.
3. Call `setDefaultSessionDataViewId` with the chosen ID.
4. Confirm the reporting period (default: last 7 days) and the audience for the briefing (executive, board, team lead).
---
## Phase 1 — Establish Context
### 1.1 Load data view context
Call `describeCja("DATAVIEW_CONTEXT_GUIDE")` to understand:
- What business the data view represents (e-commerce, media, SaaS, etc.)
- The primary conversion event and revenue metric
- **Calendar conventions and timezone.** Record these values — they are
inputs to every date computation in 1.2:
- `WEEK_START_DOW` — day of week each week starts on (Sunday, Monday, …).
Default: **Monday** (ISO 8601) if the context guide doesn't specify.
- `FISCAL_YEAR_START_MONTH` — month the fiscal year begins. Default:
**January** (calendar year) if the context guide doesn't specify.
- `TIMEZONE` — for example, `America/Los_Angeles`.
- `CALENDAR_SOURCE` — one of `"context guide"` (values came from `describeCja`),
`"default fallback"` (the context guide didn't expose them and you used the
defaults above), or `"user override"` (the user explicitly specified them).
This context determines what counts as the "north-star metric" and what
language to use in the narrative (e.g., "subscribers" vs "customers" vs "users").
### 1.2 Determine reporting period
Infer the period type from the user's request. Do not stop to ask — proceed immediately.
#### The principle
Two runs of this skill on the same data view, period type, and prompt MUST
produce identical `current` and `comparison` date ranges. Determinism comes
from (a) reading calendar conventions from 1.1 instead of improvising, and
(b) applying the alignment rule for the period type without taste calls.
#### Period type → alignment rule
Pick exactly one period type from the user's request:
| User request | `PERIOD_TYPE` | Current period | Comparison period |
|---|---|---|---|
| "last week" / unspecified | `weekly` | Most recent full week ending before today, aligned to `WEEK_START_DOW` (exactly 7 days) | The week immediately before, same alignment |
| "this month" / "MTD" | `month-to-date` | 1st of current month → today | 1st of prior month → same day-of-month as today |
| "last month" | `monthly` | Prior full calendar month (1st → last day) | The month before that |
| "this quarter" / "QTD" | `quarter-to-date` | Start of current fiscal quarter → today; fiscal quarters derived from `FISCAL_YEAR_START_MONTH` | Same days into the prior fiscal quarter |
| "last quarter" / "Q[N]" | `quarterly` | Prior full fiscal quarter | The fiscal quarter before that |
| Custom date range | `custom` | Use as specified | Equal-length window ending the day before `current.startDate` |
#### Universal invariants (must hold for every period type)
Before calling `runReport`, verify all six:
1. `current.startDate < current.endDate`
2. `comparison.startDate < comparison.endDate`
3. `comparison.endDate < current.startDate` (no overlap)
4. The day after `comparison.endDate` equals `current.startDate` (contiguous)
5. `current` and `comparison` have the **same length in days**
6. The alignment rule for `PERIOD_TYPE` is satisfied:
- `weekly`: both `startDate`s fall on `WEEK_START_DOW`
- `monthly`: both `startDate`s fall on the 1st of a month
- `month-to-date`: both `startDate`s fall on the 1st; both `endDate`s have the same day-of-month
- `quarterly`: both `startDate`s fall on the first day of a fiscal quarter
- `quarter-to-date`: both `startDate`s fall on a fiscal quarter start; both `endDate`s are the same number of days into the quarter
- `custom`: lengths match; contiguity holds
If ANY invariant fails, recompute the dates. **Never** paper over a mismatch by editing the footer.
#### Worked examples — today is Tuesday, May 26, 2026
These examples assume the data view's context guide returns `WEEK_START_DOW = Sunday` and `FISCAL_YEAR_START_MONTH = January`. Numbers change for other calendars — that's exactly the point.
| User request | `PERIOD_TYPE` | Current | Comparison |
|---|---|---|---|
| "last week" | `weekly` | May 17 (Sun) – May 23 (Sat) | May 10 (Sun) – May 16 (Sat) |
| "this month" / "MTD" | `month-to-date` | May 1 – May 26 | Apr 1 – Apr 26 |
| "last month" | `monthly` | Apr 1 – Apr 30 | Mar 1 – Mar 31 |
| "this quarter" / "QTD" | `quarter-to-date` | Apr 1 – May 26 | Jan 1 – Feb 24 |
| "last quarter" | `quarterly` | Jan 1 – Mar 31 | Oct 1 – Dec 31 (2025) |
| Custom: "May 15–22" | `custom` | May 15 – May 22 (8 days) | May 7 – May 14 (8 days) |
If `WEEK_START_DOW = Monday` instead, the weekly row becomes `May 18 (Mon) – May 24 (Sun)` vs `May 11 (Mon) – May 17 (Sun)`. The other rows are unchanged.
#### A common AI failure mode
The AI may "know" from training data that weeks are Mon–Sun (ISO 8601) or that
quarters are Q1=Jan–Mar (calendar). Silently overriding the context guide with
those defaults is exactly the determinism bug this section exists to prevent.
The footer's methodology line MUST accurately describe the dates you computed
— if footer says "weeks start Sunday" but `current.startDate` is a Monday,
that's a bug to fix in the dates, not in the footer.
### 1.3 Audience assumption
Default to **internal leadership** (VPs, directors, senior managers). This means:
- Include specific dimension values (e.g., channel names) in the narrative
- Surface both positive and negative findings with equal directness
- The reader understands your business — no need to define basic terms
If the user says "external," "board," or "investors," shift to higher-level
business outcomes, remove any internal channel naming that could be sensitive,
and lead with the most positive finding.
---
## Phase 2 — Discover North-Star Metrics
Pull the most-used metrics to identify what the org actually tracks as success.
Run both calls in parallel — this is fast and sets the foundation for every KPI
decision downstream:
```
listComponentUsage(componentType: "metric")
listComponentUsage(componentType: "calculatedMetric")
```
### Deterministic selection — do not improvise
The KPI set MUST be reproducible across runs for the same data view. Two runs of
this skill on the same period must produce the same metric values, which requires
selecting the **same metric IDs** every time. Follow this algorithm exactly:
1. Combine both `listComponentUsage` results into a single ranked list.
2. Sort by `usageCount` descending. Break ties by metric ID alphabetically
(stable secondary sort).
3. Resolve metric IDs to human-readable display names with `describeMetric` or
`describeCalculatedMetric`.
4. Take the top **6 metric IDs** from this sorted list. That is the KPI set.
Do NOT cherry-pick by metric "type" (volume vs conversion vs revenue) and do NOT
swap in an alternative metric because its name reads better in a narrative.
Different runs picking `metrics/orders` vs `metrics/orders_1_1` produce wildly
different numbers and break trust in the briefing.
### When the user specifies metrics explicitly
If the user names metrics in their request ("give me a briefing on revenue,
orders, and conversion rate"), resolve each name to **one** specific metric ID
via `findMetrics`. If multiple metrics match a name (e.g., "Orders" matches both
`metrics/orders` and a calculated metric called "Orders"), pick the one with
the highest `usageCount` and document the choice.
### Always disclose the metric IDs used
Include a small "Metrics included" line in the briefing artifact's footer
listing the resolved metric IDs (e.g., `metrics/orders_1_1`, `metrics/visits`,
`metrics/page_views`). This makes the report auditable and lets the user
confirm a re-run is using the same metrics.
Cap at 6 KPIs. More is noise.
---
## Phase 3 — Pull KPI Data
Run both reports in parallel — current period and comparison period — with all
selected metrics. Use a date dimension (e.g., `variables/daterangeday`) so the
report returns summary totals across the full period. The `summaryData.totals`
array in the response contains the aggregate values you need.
```
runReport(
startDate: "<current period start>T00:00:00",
endDate: "<current period end>T00:00:00",
dimensionIds: "variables/daterangeday",
metricIds: "metrics/visits,metrics/orders_1_1,metrics/revenue_1,...",
page: 0,
limit: 1
)
runReport(
startDate: "<prior period start>T00:00:00",
endDate: "<prior period end>T00:00:00",
dimensionIds: "variables/daterangeday",
metricIds: "metrics/visits,metrics/orders_1_1,metrics/revenue_1,...",
page: 0,
limit: 1
)
```
Read `summaryData.filteredTotals` from each response — the values correspond to
the metrics in the order they were specified in `metricIds`.
Compute for each KPI:
- `current` value
- `prior` value
- `delta` = current − prior
- `pctChange` = delta / prior × 100
- `direction`: up / down / flat (using ±3% threshold)
- `polarity`: positive if higher is better, negative if lower is better
- `signal`: green (favorable), red (unfavorable), yellow (mixed/flat)
---
## Phase 4 — Find What Drove the Movement
For the 2 KPIs with the largest absolute % change, find the top driver:
Run current and prior period breakdown reports in parallel:
```
runReport(
startDate: "<current period start>",
endDate: "<current period end>",
dimensionIds: "variables/marketing_channel",
metricIds: "<top-moved metric id>",
limit: 5
)
runReport(
startDate: "<prior period start>",
endDate: "<prior period end>",
dimensionIds: "variables/marketing_channel",
metricIds: "<top-moved metric id>",
limit: 5
)
```
Compare channel-level values between periods to find which dimension value's delta
most closely mirrors the overall metric change. This becomes the "driven by" phrase
in the narrative.
Only run dimension breakdowns for the 2 most-moved metrics (by absolute % change).
This constraint exists because: (a) more breakdowns add latency without adding
executive value, and (b) the two largest movers are what leadership will ask about
first. The remaining KPIs get context from the narrative without needing attribution.
If `variables/marketing_channel` returns no useful breakdown (e.g., only one
channel has data), try `variables/page_type` or `variables/product_category`
as alternative driver dimensions.
---
## Phase 5 — Write the Executive Narrative
Compose 3–5 bullet points as the narrative. Each bullet follows this structure:
**[Signal emoji] [Metric]: [Value] ([+/-pctChange%] vs [prior period]) — [Plain-English context]**
Guidelines:
- Open with the most important finding (highest-impact metric, positive or negative)
- Use business language: "revenue," "customers," "conversions" — not "metrics/revenue"
- Name the driver when known: "driven by Paid Search," "led by Product Page improvements"
- Include one forward-looking note if relevant: "This trend should be monitored..."
- For negative trends: be direct but not alarming. Suggest investigation, not panic.
Examples of well-written bullets:
- "Revenue reached $1.24M last week, up 8.2% vs prior week — Paid Search drove the majority of the gain (+$82K)."
- "Conversion Rate declined to 2.1% (−0.4pp), primarily on mobile. Desktop conversion remained stable at 3.4%."
- "Sessions were flat at 540K (+1.1%) — organic and direct traffic offset a reduction in email campaign sends."
- "Bounce Rate ticked up to 43% (+2pp). No single driver identified; worth monitoring through end of month."
---
## Phase 6 — Generate the HTML Briefing Document
Generate the briefing inline. Write to `/tmp/cja_executive_briefing_<YYYY-MM-DD_HHMMSS>.html`.
This document should look polished enough to share with leadership — not a
developer tool output.
### Rendering rules — apply consistently across runs
Two runs of this skill on the same data view + period must render identically
(modulo the generation timestamp). The rules below pin the formatting choices
that the AI would otherwise drift on.
#### Number formatting
- **KPI values** (the big number in each tile) — use full digits with
thousands separators (`8,160`, `77,584`, `1,250,000`). Do **NOT** use SI
suffixes like `K` or `M`, even for large values. Executives want exact
numbers, not abbreviations.
- **Percent change** (in pills and narrative bullets) — always one decimal
place, rounded **half-away-from-zero**. For example, `−23.55%` displays as
`−23.6%`, never `−23.5%`. Compute on full-precision values; round only at
display time.
- **Percentage-point change** (for already-percentage metrics like Conversion
Rate or Bounce Rate) — same rounding, suffix `pp`. Example: `+0.40 pp`.
- **Currency** — `$` prefix with thousands separators and no decimals for
values ≥ $100 (`$1,240,000`); cents only when value < $100 (`$45.20`).
#### Null / missing data handling
A KPI tile must reflect what the data view actually returned. The AI must
**not** silently substitute a different metric or hide a tile to make the
briefing look cleaner.
- **Both periods return 0 or NULL** for a metric in the resolved set: render
the tile with `kpi-value` = `Data unavailable`, pill class `flat`, pill text
`⚠ N/A`, and `prior` text = `Both periods returned no data — validate
instrumentation`. The tile stays in the grid; do not omit it.
- **One period returns valid data, the other 0 / NULL**: render the tile with
the valid value as `kpi-value`, pill class `flat`, pill text `⚠ N/A`, and
`prior` text = `Prior {period_noun}: no data`.
- **Never** substitute a derived metric (e.g., adding "Conversion Rate"
because Revenue came back $0). The visible KPI set MUST match the resolved
metric IDs disclosed in the footer.
- If a Revenue / monetary metric is unavailable, surface a `.callout.note`
above the KPI grid explaining the gap. Do not invent a value.
### Template variables
Populate every `{PLACEHOLDER}` in the HTML template below using these rules.
The briefing belongs to the customer — never substitute Adobe, CJA, or any
vendor language into customer-visible fields.
- **`{ORG_NAME}`** — The customer's business or brand name, derived from the
data view context loaded in Phase 1.1. Strip technical/environment suffixes
like ` — Prod`, ` - Demo`, ` Stage`, ` Test`, ` MCP`. If the data view name
has no clean brand label, fall back to the data view display name with
suffixes removed. Do **not** invent a name and do **not** substitute a
vendor name.
- **`{PERIOD_TYPE}`** — One of `Weekly`, `Monthly`, `Quarterly`, or
`Performance` (default), chosen from the period inferred in Phase 1.2.
- **`{LEDE_SENTENCE}`** — Use exactly this pattern, with no vendor names:
`Leadership readout for the {period type lowercase} of {PERIOD_LABEL} compared to {COMPARISON_LABEL}.`
Example: `Leadership readout for the week of May 12–18, 2026 compared to May 5–11, 2026.`
- **`{DATA_VIEW}`** — Data view display name as returned by `findDataViews`.
Suffixes are acceptable here — this row is the technical identifier line,
not the title.
- **`{PERIOD_LABEL}`** / **`{COMPARISON_LABEL}`** — Human-readable date ranges,
e.g., `May 12–18, 2026`.
- **`{GENERATED_DATE}`** — Today's date in the same human format,
e.g., `May 23, 2026`.
- **`{METRIC_LABEL}` / `{FORMATTED_VALUE}` / `{PCT_CHANGE}` / `{PRIOR_VALUE}`** —
Per-KPI values from Phase 3. Use the metric's customer-facing display name,
not its internal ID.
### HTML Template
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Performance Briefing — {ORG_NAME} — {PERIOD}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f4f1;
--surface: #ffffff;
--ink: #1a1a1a;
--ink-muted: #6b6b6b;
--border: #e5e2dc;
--header-bg: #0e0e10;
--header-warm: #3a1010;
--accent-red: #c8312f;
--accent-red-bright: #ff6b68;
--accent-red-soft: #fdecea;
--accent-green: #1f7a4d;
--accent-yellow: #d4a017;
}
body { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg); color: var(--ink); line-height: 1.5;
-webkit-font-smoothing: antialiased; }
/* === Header === */
header { background: linear-gradient(120deg, var(--header-bg) 0%, #1a0d0d 55%, var(--header-warm) 100%);
color: #fff; padding: 56px 56px 44px; position: relative; overflow: hidden; }
header::after { content: ""; position: absolute; right: -140px; top: -140px;
width: 460px; height: 460px;
background: radial-gradient(circle, rgba(200,49,47,.35) 0%, transparent 70%);
pointer-events: none; }
.header-inner { max-width: 1080px; margin: 0 auto; position: relative; z-index: 1; }
.eyebrow { display: inline-flex; align-items: center; gap: 8px;
padding: 6px 14px; border: 1px solid rgba(255,107,104,.55);
border-radius: 999px; color: var(--accent-red-bright);
font-size: 11px; font-weight: 600; letter-spacing: 1.2px;
text-transform: uppercase; margin-bottom: 24px;
background: rgba(200,49,47,.10); }
.eyebrow::before { content: ""; width: 6px; height: 6px;
background: var(--accent-red-bright); border-radius: 50%; }
header h1 { font-family: "Playfair Display", Georgia, serif;
font-size: 56px; font-weight: 700; letter-spacing: -1.5px;
line-height: 1.05; margin-bottom: 14px; }
header .lede { font-size: 16px; max-width: 560px;
color: rgba(255,255,255,.80); margin-bottom: 24px;
line-height: 1.55; }
header .meta { display: flex; flex-wrap: wrap; gap: 22px;
font-size: 13px; color: rgba(255,255,255,.60); }
header .meta span { display: inline-flex; align-items: center; gap: 6px; }
header .meta .icon { opacity: .8; }
/* === Tabs === */
nav { background: var(--surface); border-bottom: 1px solid var(--border);
padding: 0 56px; display: flex; gap: 28px;
position: sticky; top: 0; z-index: 50; }
nav a { display: block; padding: 16px 0; font-size: 14px;
color: var(--ink); text-decoration: none;
border-bottom: 2px solid transparent;
transition: border-color .15s ease; }
nav a:hover { border-bottom-color: var(--accent-red); }
/* === Container === */
.container { max-width: 1080px; margin: 0 auto; padding: 36px 56px 60px; }
/* === Callout === */
.callout { background: var(--accent-red-soft);
border-left: 4px solid var(--accent-red);
border-radius: 6px;
padding: 18px 22px; margin-bottom: 32px;
display: flex; gap: 14px; align-items: flex-start; }
.callout .warn-icon { font-size: 22px; line-height: 1; flex-shrink: 0;
color: var(--accent-yellow); }
.callout-title { font-weight: 700; color: var(--accent-red);
margin-bottom: 4px; font-size: 15px; }
.callout-body { font-size: 14px; color: #4a2222; line-height: 1.55; }
.callout.note { background: #fdf6e7; border-left-color: var(--accent-yellow); }
.callout.note .callout-title { color: #8a5a08; }
.callout.note .callout-body { color: #5a4108; }
/* === Section label === */
.section-label { font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 1.4px;
color: var(--ink-muted); margin-bottom: 14px;
padding-bottom: 10px; border-bottom: 1px solid var(--border); }
/* === KPI grid === */
.kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px; margin-bottom: 36px; }
.kpi-tile { background: var(--surface); border-radius: 8px;
padding: 22px 22px 20px;
border-top: 3px solid #b9b6ae;
box-shadow: 0 1px 3px rgba(0,0,0,.05); }
.kpi-tile.down { border-top-color: var(--accent-red); }
.kpi-tile.up { border-top-color: var(--accent-green); }
.kpi-tile.flat { border-top-color: #b9b6ae; }
.kpi-head { display: flex; justify-content: space-between;
align-items: center; margin-bottom: 10px; }
.kpi-label { font-size: 11px; font-weight: 700;
text-transform: uppercase;
color: var(--ink-muted); letter-spacing: 1px; }
.kpi-value { font-family: "Playfair Display", Georgia, serif;
font-weight: 700; font-size: 38px;
line-height: 1; color: var(--ink);
margin-bottom: 12px; }
.pill { display: inline-flex; align-items: center; gap: 4px;
padding: 3px 9px; border-radius: 4px;
font-size: 12px; font-weight: 600; line-height: 1.4; }
.pill.down { background: var(--accent-red-soft); color: var(--accent-red); }
.pill.up { background: #ebf5ef; color: var(--accent-green); }
.pill.flat { background: #f1efea; color: var(--ink-muted); }
.prior { display: block; margin-top: 10px;
font-size: 12px; color: var(--ink-muted); }
/* === Narrative === */
.narrative-card { background: var(--surface); border-radius: 8px;
padding: 32px 36px;
box-shadow: 0 1px 3px rgba(0,0,0,.04);
margin-bottom: 24px; }
.narrative-card h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 24px; font-weight: 700;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border); }
.bullet-list { list-style: none; }
.bullet-list li { padding: 14px 0; font-size: 15px; line-height: 1.65;
border-bottom: 1px solid #f1eeea; }
.bullet-list li:last-child { border-bottom: none; }
.bullet-list .signal { margin-right: 6px; }
.bullet-list .metric-highlight { font-weight: 700; }
.bullet-list .driver { color: var(--accent-red); font-style: italic; }
/* === Sections (collapsible tables) === */
.section { background: var(--surface); border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,.04);
margin-bottom: 22px; overflow: hidden; }
.section-header { padding: 18px 28px; border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between;
align-items: center; cursor: pointer; }
.section-header h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 18px; font-weight: 700; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th { background: #faf8f4; padding: 12px 22px;
text-align: left; font-weight: 600;
text-transform: uppercase; letter-spacing: .6px;
font-size: 11px; color: var(--ink-muted);
border-bottom: 1px solid var(--border); }
tbody td { padding: 12px 22px; border-bottom: 1px solid #f4f1eb; }
tbody tr:last-child td { border-bottom: none; }
.badge { display: inline-block; padding: 3px 9px; border-radius: 4px;
font-size: 11px; font-weight: 600; }
.badge.green { background: #ebf5ef; color: var(--accent-green); }
.badge.red { background: var(--accent-red-soft); color: var(--accent-red); }
.badge.yellow { background: #fef6e3; color: #b67a08; }
.badge.grey { background: #f1efea; color: var(--ink-muted); }
footer { text-align: center; padding: 32px 24px;
font-size: 12px; color: var(--ink-muted); }
/* === Print === */
@media print {
nav { display: none; position: static; }
header { padding: 36px 32px 28px; }
header h1 { font-size: 42px; }
.section-header { cursor: default; }
.kpi-row { page-break-inside: avoid; }
.kpi-tile, .narrative-card, .section {
box-shadow: none; border: 1px solid var(--border);
}
}
</style>
</head>
<body>
<header>
<div class="header-inner">
<div class="eyebrow">{PERIOD_TYPE} Performance Briefing</div>
<h1>{ORG_NAME} Performance</h1>
<p class="lede">{LEDE_SENTENCE}</p>
<div class="meta">
<span><span class="icon">📅</span> {PERIOD_LABEL}</span>
<span><span class="icon">📊</span> {DATA_VIEW}</span>
<span><span class="icon">🕔</span> Prepared {GENERATED_DATE}</span>
</div>
</div>
</header>
<nav>
<a href="#highlights">Highlights</a>
<a href="#narrative">Summary</a>
<a href="#data">KPI Detail</a>
<a href="#drivers">Drivers</a>
</nav>
<div class="container">
<!--
Optional critical callout. Include ONLY when the data has a
notable finding worth raising above the fold (e.g., a metric
moved >25% week-over-week, or data is missing/anomalous).
Use the .note variant (yellow) for non-critical context;
omit entirely if there is nothing noteworthy.
-->
<!--
<div class="callout">
<span class="warn-icon">⚠</span>
<div>
<div class="callout-title">Critical: {ONE_LINE_HEADLINE}</div>
<div class="callout-body">{ONE_PARAGRAPH_CONTEXT}</div>
</div>
</div>
-->
<!-- Headline KPI Tiles -->
<div class="section-label">Key Performance Indicators</div>
<div id="highlights" class="kpi-row">
<!-- Repeat for each KPI (4-6 tiles), class = up | down | flat:
<div class="kpi-tile down">
<div class="kpi-head">
<div class="kpi-label">{METRIC_LABEL}</div>
</div>
<div class="kpi-value">{FORMATTED_VALUE}</div>
<span class="pill down">▼ {PCT_CHANGE}%</span>
<span class="prior">Prior week: {PRIOR_VALUE}</span>
</div>
-->
</div>
<!-- Executive Narrative -->
<div id="narrative" class="narrative-card">
<h2>Executive Summary</h2>
<ul class="bullet-list">
<!--
<li>
<span class="signal">{EMOJI}</span>
<span class="metric-highlight">{Metric}:</span> {value} ({delta} vs prior period) —
<span class="driver">{plain-English driver or context}</span>
</li>
-->
</ul>
</div>
<!-- Supporting Data Table -->
<div id="data" class="section">
<div class="section-header" onclick="toggle('data-body')">
<h2>Full KPI Detail</h2>
<span id="data-body-icon">▾</span>
</div>
<div id="data-body">
<table>
<thead><tr>
<th>Metric</th>
<th>{PERIOD_LABEL}</th>
<th>{COMPARISON_LABEL}</th>
<th>Change</th>
<th>% Change</th>
<th>Signal</th>
</tr></thead>
<tbody>
<!-- One row per KPI with delta and badge -->
</tbody>
</table>
</div>
</div>
<!-- Key Drivers -->
<div id="drivers" class="section">
<div class="section-header" onclick="toggle('drv-body')">
<h2>What Drove the Movement</h2>
<span id="drv-body-icon">▾</span>
</div>
<div id="drv-body">
<table>
<thead><tr>
<th>Metric</th>
<th>Top Driver</th>
<th>Value This Period</th>
<th>Value Prior Period</th>
<th>Contribution</th>
</tr></thead>
<tbody>
<!-- One row per metric with top dimension driver -->
</tbody>
</table>
</div>
</div>
</div>
<footer>
Performance Briefing — {ORG_NAME} — Generated {GENERATED_DATE}<br>
<span style="opacity:.7">
Methodology: {PERIOD_TYPE}
· current {CURRENT_START_ISO}–{CURRENT_END_ISO}
· comparison {COMPARISON_START_ISO}–{COMPARISON_END_ISO}
· week starts {WEEK_START_DOW}
· fiscal year starts {FISCAL_YEAR_START_MONTH}
· tz {TIMEZONE}
· calendar source: {CALENDAR_SOURCE}
· metrics: {METRIC_IDS_CSV}
</span>
</footer>
<script>
function toggle(id) {
var el = document.getElementById(id);
var ic = document.getElementById(id + '-icon');
if (el.style.display === 'none') { el.style.display=''; ic.textContent='\u25be'; }
else { el.style.display='none'; ic.textContent='\u25b8'; }
}
</script>
</body></html>
```
---
## Phase 7 — Deliver the Briefing
1. Write the HTML to `/tmp/cja_executive_briefing_<YYYY-MM-DD_HHMMSS>.html`
2. Open with `open /tmp/cja_executive_briefing_<YYYY-MM-DD_HHMMSS>.html`
3. Output the narrative bullets directly in chat so the user can copy-paste
them into an email or slide deck immediately — the HTML is the "appendix."
In-chat format:
```
Performance Briefing — Last Week vs Prior Week
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📈 Revenue: $1.24M (+8.2%) — Paid Search drove the majority of the gain.
📉 Conversion Rate: 2.1% (−0.4pp) — Mobile checkout declined; desktop stable.
➡ Sessions: 540K (+1.1%) — Essentially flat; organic offset email decline.
📈 Orders: 11,340 (+6.7%) — Product page improvements appear to be contributing.
⚠️ Bounce Rate: 43% (+2pp) — No clear driver identified; monitor next week.
Full briefing document: /tmp/cja_executive_briefing_<YYYY-MM-DD_HHMMSS>.html
```
---
## Tone and Style Rules
1. **Write outcomes, not activities.** "Revenue grew" not "metrics/revenue increased."
2. **Name the driver concisely.** "Paid Search drove the gain" not "Marketing
Channel = Paid Search had a higher value in period A vs period B."
3. **One sentence per bullet.** Two at most. Executives skim.
4. **Lead with the most important finding.** Do not build to a conclusion.
5. **Be direct about bad news.** "Conversion Rate declined" not "Conversion Rate
saw some movement in a downward direction."
6. **Quantify everything.** Every bullet must have a number. Opinions without
data are not executive communication.
7. **Avoid jargon.** No "dimensions," "metrics IDs," "data view context," or
"MCP tool calls" in the output.
## Important Guardrails
- **Read-only reporting.** Never modify metrics, segments, or projects.
- **Use business language, not technical IDs.** Replace dimension IDs (e.g., `variables/evar5`) with their display names. Never expose internal IDs in the narrative.
- **Always state the date range prominently.** Executives need context — "last week" is ambiguous; write "April 7–13, 2026."
- **Don't invent data.** If a KPI is unavailable or the report returns no data, say so explicitly rather than omitting or estimating.
- **Cap the briefing to the most impactful findings.** 3–5 KPI tiles + 2–3 driver bullets is ideal; more than 8 KPIs dilutes the message.
- **Note significant context.** Mention known external factors (campaigns, holidays, product releases) that affect the metrics when relevant.
- **Validate numbers before presenting.** Cross-check KPI values against a second `runReport` call if they look anomalous.
- **Customer-branded output, never vendor-branded.** The briefing is the customer's document about their own business. Never use "Adobe", "CJA", "Customer Journey Analytics", or any vendor/product name in the customer-visible briefing (title, lede, narrative, tables, footer, or in-chat output). The red/black header palette is the only Adobe-branded element allowed in the artifact.
---
## Example Interaction
> "Write me an executive briefing for last week's performance."
1. **Setup:** Confirm data view with `findDataViews`. User selects their primary data view. Call `setDefaultSessionDataViewId`.
2. **Scope:** Confirm reporting period (last 7 days, April 7–13, 2026) and key KPIs (Revenue, Conversion Rate, Sessions, AOV).
3. **Data pull:** Run `runReport` for each KPI: current week vs. prior week and vs. same week last year.
4. **Narrative draft:** Compose 3–4 executive bullets in business language: "Revenue of $2.4M was up 8% week-over-week, driven by a 15% increase in Paid Search conversions. Conversion Rate dipped 2 points to 3.1%, consistent with the product page redesign rollout on April 9."
5. **HTML report:** Generate the print-ready HTML briefing with KPI tiles, narrative section, and driver table. Open the file. Present a 2-sentence summary to the user and offer to adjust tone or scope.
Referenced files: 1
cja-funnel-health-check23.9 KB
---
name: cja-funnel-health-check
description: >
Analyzes a multi-step conversion funnel to find where users drop off and which
steps have the worst leakage. Use this skill when someone describes a journey
or funnel and asks about conversion rates, drop-off, fallout, or step completion.
Trigger for phrases like "analyze our onboarding funnel," "where are users
dropping off," "what's our checkout conversion rate," "funnel analysis,"
"show me fallout between these steps," or "which step loses the most users."
license: Apache-2.0
metadata:
author: Adobe
version: "1.0"
---
# Funnel Health Check (Customer Journey Analytics)
Turn a plain-English funnel description into a quantified step-by-step
conversion analysis. The output identifies the biggest leakage point in the
funnel and provides dimension-based breakdowns to show which audience or channel
has the worst drop-off.
Funnel health checks are most valuable when a team suspects a specific step is
broken but hasn't quantified it. This skill does the quantification in a single
conversation turn.
---
## CJA MCP Tools Used
- `findDimensions` — resolve page name, event, or other dimensions for step filtering
- `findMetrics` — get the base metric to measure (sessions, visitors, events)
- `searchDimensionItems` — find exact dimension values for step names
- `runReport` (with `adhocSegments`) — measure visitor counts at each funnel step
- `findSegments` — use existing segments as funnel step filters if applicable
- `describeSegment` — understand segment logic before applying as a funnel step
---
## Phase 0 — Setup
1. Call `findDataViews` to list available data views.
2. If the user hasn't specified a data view, present the list and ask which to use.
3. Call `setDefaultSessionDataViewId` with the chosen ID.
4. Ask the user to define the funnel stages if not already specified (e.g., "What are the steps in the funnel you want to analyze?").
## Phase 1 — Define Funnel Steps
### 1.1 Parse the user's funnel description
Extract the step sequence from the user's plain-English description:
- "Homepage → Product Page → Cart → Purchase"
- "Registration → Onboarding Step 1 → Onboarding Step 2 → Activated"
- "Landing Page → Lead Form → Thank You Page"
Each step must resolve to a measurable condition in CJA. A step can be:
- **Page view**: user viewed a specific page (resolved via page name dimension)
- **Event/metric**: user triggered a specific action (e.g., "Added to Cart")
- **Segment membership**: user meets a pre-built segment condition
### 1.2 Clarify ambiguous steps
If any step is vague (e.g., "checkout" without a page name), ask one question:
> "For the 'Checkout' step — should I look at people who visited a page
> containing 'checkout' in the URL, or those who triggered a specific event
> like 'Cart Add'?"
Do not ask more than one clarifying question at a time.
### 1.3 Resolve page names to dimension values
For page-based steps, call `searchDimensionItems` to find the exact dimension
values that match the step name. First call `findDimensions` with a semantic
search like `"page name url"` to confirm the correct dimension ID — in most
CJA data views this is `variables/web.webPageDetails.name`, not `variables/page`:
```
searchDimensionItems(
dimensionId: "variables/web.webPageDetails.name",
searchAnd: "<step page name>",
startDate: "<period start>",
endDate: "<period end>",
page: 0,
limit: 10
)
```
Present matches to the user if there are multiple candidates:
> "I found these pages matching 'checkout': /checkout/start, /checkout/payment,
> /checkout/review. Should I use '/checkout/start' as the entry to the checkout
> step?"
---
## Phase 2 — Measure Each Step
For each funnel step, construct an ad hoc segment that filters to visitors/sessions
that reached that step. Then run a report measuring the base metric (usually
Unique Visitors or Sessions) with that segment applied.
### 2.1 Construct ad hoc segments for each step
A "reached step N" ad hoc segment is:
- Container: Visit or Person (use Visit for session-level funnels, Person for
cross-visit journeys)
- Condition: Page Name equals "<step page value>" OR Event occurred
### 2.2 Run reports for each step
Run one `runReport` per step with the ad hoc segment applied. The `adhocSegments`
parameter takes fully-formed CJA segment definition objects — NOT a simplified
shorthand. The correct structure is:
```
runReport(
dimensionIds: "variables/web.webPageDetails.name",
metricIds: "metrics/visitors",
startDate: "<period start>",
endDate: "<period end>",
page: 0,
limit: 1,
adhocSegments: [{
"func": "segment",
"version": [1, 0, 0],
"container": {
"func": "container",
"context": "visitors",
"pred": {
"func": "streq",
"val": { "func": "attr", "name": "variables/web.webPageDetails.name" },
"str": "<step page value>"
}
}
}]
)
```
Use `context: "visitors"` (person-level) for cross-visit funnels. Use
`context: "visits"` (session-level) for within-session funnels. The metric
value comes from `summaryData.filteredTotals[0]` in the response.
The dimension and value used in the predicate should match what was found via
`searchDimensionItems` in Phase 1 — use the exact `value` string returned.
**Important**: Each step uses a cumulative filter — measure "visitors who
EVER reached this step in the period," not "visitors who ONLY visited this
page." This produces the classic funnel waterfall.
Capture `stepCount[i]` for each step i from 1 to N.
---
## Phase 3 — Compute Funnel Metrics
For each step-to-step transition:
- `stepConversionRate[i→i+1]` = stepCount[i+1] / stepCount[i] × 100
- `dropOff[i→i+1]` = stepCount[i] − stepCount[i+1]
- `dropOffRate[i→i+1]` = 100 − stepConversionRate[i→i+1]
Overall funnel:
- `overallConversionRate` = stepCount[N] / stepCount[1] × 100
- `biggestDropOffStep` = argmax(dropOff[i→i+1]) — the step with the most
visitors lost
---
## Phase 4 — Optional: Segment the Funnel
If the user wants to compare funnel performance across audiences or channels,
run the same step reports filtered by a dimension or segment:
```
runReport(
dimensionIds: "variables/device_type",
metricIds: "metrics/visitors",
startDate: "<range start>",
endDate: "<range end>",
page: 0,
limit: 10,
adhocSegments: [{ /* step N filter — same structure as Phase 2 */ }]
)
```
Note: use `findDimensions` with `searchQuery: "device type mobile desktop"` to
confirm the device dimension ID for the data view (often `variables/device_type`).
This shows step-N visitor counts broken down by device type (or channel,
or country). If one segment has a dramatically lower conversion through the
worst step, that's the target for optimization.
Common comparisons to suggest:
- Device type (mobile vs desktop conversion often differs significantly)
- Marketing channel (paid vs organic users may convert differently)
- New vs returning visitors
---
## Phase 5 — Generate HTML Funnel Report
Generate the funnel report inline and write to
`/tmp/cja_funnel_health_check_report_<YYYY-MM-DD_HHMMSS>.html`.
### HTML Template
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Funnel Health Check — {ORG_NAME} — {FUNNEL_NAME}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f4f1;
--surface: #ffffff;
--ink: #1a1a1a;
--ink-muted: #6b6b6b;
--border: #e5e2dc;
--header-bg: #0e0e10;
--header-warm: #3a1010;
--accent-red: #c8312f;
--accent-red-bright: #ff6b68;
--accent-red-soft: #fdecea;
--accent-green: #1f7a4d;
--accent-yellow: #d4a017;
}
body { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg); color: var(--ink); line-height: 1.5;
-webkit-font-smoothing: antialiased; }
/* === Header === */
header { background: linear-gradient(120deg, var(--header-bg) 0%, #1a0d0d 55%, var(--header-warm) 100%);
color: #fff; padding: 56px 56px 44px; position: relative; overflow: hidden; }
header::after { content: ""; position: absolute; right: -140px; top: -140px;
width: 460px; height: 460px;
background: radial-gradient(circle, rgba(200,49,47,.35) 0%, transparent 70%);
pointer-events: none; }
.header-inner { max-width: 1080px; margin: 0 auto; position: relative; z-index: 1; }
.eyebrow { display: inline-flex; align-items: center; gap: 8px;
padding: 6px 14px; border: 1px solid rgba(255,107,104,.55);
border-radius: 999px; color: var(--accent-red-bright);
font-size: 11px; font-weight: 600; letter-spacing: 1.2px;
text-transform: uppercase; margin-bottom: 24px;
background: rgba(200,49,47,.10); }
.eyebrow::before { content: ""; width: 6px; height: 6px;
background: var(--accent-red-bright); border-radius: 50%; }
header h1 { font-family: "Playfair Display", Georgia, serif;
font-size: 56px; font-weight: 700; letter-spacing: -1.5px;
line-height: 1.05; margin-bottom: 14px; color: #fff; }
header .lede { font-size: 16px; max-width: 560px;
color: rgba(255,255,255,.80); margin-bottom: 24px;
line-height: 1.55; }
header .meta { display: flex; flex-wrap: wrap; gap: 22px;
font-size: 13px; color: rgba(255,255,255,.60); }
header .meta span { display: inline-flex; align-items: center; gap: 6px; }
header .meta .icon { opacity: .8; }
/* === Tabs === */
nav { background: var(--surface); border-bottom: 1px solid var(--border);
padding: 0 56px; display: flex; gap: 28px;
position: sticky; top: 0; z-index: 50; }
nav a { display: block; padding: 16px 0; font-size: 14px;
color: var(--ink); text-decoration: none;
border-bottom: 2px solid transparent;
transition: border-color .15s ease; }
nav a:hover { border-bottom-color: var(--accent-red); }
/* === Container === */
.container { max-width: 1080px; margin: 0 auto; padding: 36px 56px 60px; }
/* === Section label === */
.section-label { font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 1.4px;
color: var(--ink-muted); margin-bottom: 14px;
padding-bottom: 10px; border-bottom: 1px solid var(--border); }
/* === KPI grid === */
.kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px; margin-bottom: 36px; }
.kpi-tile { background: var(--surface); border-radius: 8px;
padding: 22px 22px 20px;
border-top: 3px solid #b9b6ae;
box-shadow: 0 1px 3px rgba(0,0,0,.05); }
.kpi-tile.down { border-top-color: var(--accent-red); }
.kpi-tile.up { border-top-color: var(--accent-green); }
.kpi-tile.flat { border-top-color: #b9b6ae; }
.kpi-head { display: flex; justify-content: space-between;
align-items: center; margin-bottom: 10px; }
.kpi-label { font-size: 11px; font-weight: 700;
text-transform: uppercase;
color: var(--ink-muted); letter-spacing: 1px; }
.kpi-value { font-family: "Playfair Display", Georgia, serif;
font-weight: 700; font-size: 38px;
line-height: 1; color: var(--ink);
margin-bottom: 12px; }
.pill { display: inline-flex; align-items: center; gap: 4px;
padding: 3px 9px; border-radius: 4px;
font-size: 12px; font-weight: 600; line-height: 1.4; }
.pill.down { background: var(--accent-red-soft); color: var(--accent-red); }
.pill.up { background: #ebf5ef; color: var(--accent-green); }
.pill.flat { background: #f1efea; color: var(--ink-muted); }
.prior { display: block; margin-top: 10px;
font-size: 12px; color: var(--ink-muted); }
/* === Funnel chart wrap === */
.chart-wrap { background: var(--surface); border-radius: 8px;
padding: 28px 32px;
box-shadow: 0 1px 3px rgba(0,0,0,.04);
margin-bottom: 22px; }
.chart-wrap h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 18px; font-weight: 700;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border); }
/* === Sections (collapsible tables) === */
.section { background: var(--surface); border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,.04);
margin-bottom: 22px; overflow: hidden; }
.section-header { padding: 18px 28px; border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between;
align-items: center; cursor: pointer; }
.section-header h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 18px; font-weight: 700; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th { background: #faf8f4; padding: 12px 22px;
text-align: left; font-weight: 600;
text-transform: uppercase; letter-spacing: .6px;
font-size: 11px; color: var(--ink-muted);
border-bottom: 1px solid var(--border); }
tbody td { padding: 12px 22px; border-bottom: 1px solid #f4f1eb; }
tbody tr:last-child td { border-bottom: none; }
.badge { display: inline-block; padding: 3px 9px; border-radius: 4px;
font-size: 11px; font-weight: 600; }
.badge.green { background: #ebf5ef; color: var(--accent-green); }
.badge.red { background: var(--accent-red-soft); color: var(--accent-red); }
.badge.yellow { background: #fef6e3; color: #b67a08; }
.badge.grey { background: #f1efea; color: var(--ink-muted); }
.highlight-row td { background: var(--accent-red-soft) !important;
font-weight: 700; }
.progress-bar-wrap { background: #f1efea; border-radius: 4px;
height: 8px; overflow: hidden;
width: 100%; min-width: 80px; }
.progress-bar { height: 100%; border-radius: 4px;
background: linear-gradient(90deg, var(--accent-red), #8a1d1c); }
.rec-item { display: flex; gap: 12px; align-items: flex-start;
padding: 14px 28px; border-bottom: 1px solid #f1eeea;
font-size: 14px; line-height: 1.55; }
.rec-item:last-child { border-bottom: none; }
.back-top { position: fixed; bottom: 24px; right: 24px;
background: var(--accent-red); color: #fff;
width: 44px; height: 44px; border-radius: 50%;
border: none; font-size: 20px; cursor: pointer;
box-shadow: 0 4px 12px rgba(200,49,47,0.30); }
footer { text-align: center; padding: 32px 24px;
font-size: 12px; color: var(--ink-muted); }
/* === Print === */
@media print {
nav { display: none; position: static; }
header { padding: 36px 32px 28px; }
header h1 { font-size: 42px; }
.section-header { cursor: default; }
.kpi-row { page-break-inside: avoid; }
.kpi-tile, .chart-wrap, .section {
box-shadow: none; border: 1px solid var(--border);
}
.back-top { display: none; }
}
</style>
</head>
<body>
<header>
<div class="header-inner">
<div class="eyebrow">Funnel Health Report</div>
<h1>{ORG_NAME} Funnel Health</h1>
<p class="lede">Step-by-step conversion for the {FUNNEL_NAME} journey across {DATE_RANGE}, with the biggest leakage point surfaced.</p>
<div class="meta">
<span><span class="icon">📅</span> {DATE_RANGE}</span>
<span><span class="icon">📊</span> {DATA_VIEW}</span>
<span><span class="icon">🕔</span> Prepared {GENERATED_DATE}</span>
</div>
</div>
</header>
<nav>
<a href="#overview">Overview</a>
<a href="#chart">Funnel Chart</a>
<a href="#steps">Step Detail</a>
<a href="#segments">Segment Breakdown</a>
<a href="#recs">Recommendations</a>
</nav>
<div class="container">
<!-- Summary KPI Tiles -->
<div class="section-label">Funnel Summary</div>
<div id="overview" class="kpi-row">
<div class="kpi-tile flat">
<div class="kpi-head"><div class="kpi-label">Entered Funnel</div></div>
<div class="kpi-value">{STEP_1_COUNT}</div>
<span class="prior">Step 1 visitors</span>
</div>
<div class="kpi-tile flat">
<div class="kpi-head"><div class="kpi-label">Completed Funnel</div></div>
<div class="kpi-value">{STEP_N_COUNT}</div>
<span class="prior">Reached final step</span>
</div>
<div class="kpi-tile flat">
<div class="kpi-head"><div class="kpi-label">Overall Conversion</div></div>
<div class="kpi-value">{OVERALL_CVR}%</div>
<span class="prior">End-to-end rate</span>
</div>
<div class="kpi-tile down">
<div class="kpi-head"><div class="kpi-label">Biggest Drop-Off Step</div></div>
<div class="kpi-value" style="font-size:22px;">{WORST_STEP_NAME}</div>
<span class="pill down">▼ Worst leakage</span>
</div>
<div class="kpi-tile down">
<div class="kpi-head"><div class="kpi-label">Worst Step Drop-Off</div></div>
<div class="kpi-value">{WORST_STEP_DROPOFF}%</div>
<span class="prior">Of visitors lost at this step</span>
</div>
</div>
<!-- Funnel Bar Chart -->
<div id="chart" class="chart-wrap">
<h2>Funnel Visualization</h2>
<canvas id="funnelChart" height="100"></canvas>
</div>
<!-- Step-by-Step Table -->
<div id="steps" class="section">
<div class="section-header" onclick="toggle('steps-body')">
<h2>Step-by-Step Analysis</h2>
<span id="steps-body-icon">▾</span>
</div>
<div id="steps-body">
<table>
<thead><tr>
<th>Step</th>
<th>Visitors</th>
<th>% of Step 1</th>
<th>Step Conversion</th>
<th>Drop-Off</th>
<th>Drop-Off Rate</th>
<th>Visual</th>
</tr></thead>
<tbody>
<!-- For each step i:
<tr class="{highlight-row if worst step}">
<td>{STEP_NAME}</td>
<td>{STEP_COUNT}</td>
<td>{PCT_OF_STEP_1}%</td>
<td>{STEP_CVR}% {badge}</td>
<td>{DROPOFF}</td>
<td>{DROPOFF_RATE}%</td>
<td>
<div class="progress-bar-wrap">
<div class="progress-bar" style="width:{PCT_OF_STEP_1}%"></div>
</div>
</td>
</tr>
-->
</tbody>
</table>
</div>
</div>
<!-- Segment Breakdown (optional) -->
<div id="segments" class="section">
<div class="section-header" onclick="toggle('seg-body')">
<h2>Segment Breakdown at Worst Step</h2>
<span id="seg-body-icon">▾</span>
</div>
<div id="seg-body">
<table>
<thead><tr>
<th>Segment / Dimension</th>
<th>Entered Worst Step</th>
<th>Passed Worst Step</th>
<th>Conversion</th>
<th>vs. Average</th>
</tr></thead>
<tbody>
<!-- Fill with dimension breakdown at the worst step -->
</tbody>
</table>
</div>
</div>
<!-- Recommendations -->
<div id="recs" class="section">
<div class="section-header" onclick="toggle('rec-body')">
<h2>Optimization Recommendations</h2>
<span id="rec-body-icon">▾</span>
</div>
<div id="rec-body">
<!-- 2-3 recommendation items as .rec-item -->
</div>
</div>
</div>
<button class="back-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">↑</button>
<footer>Funnel Health Check — {ORG_NAME} — Generated {GENERATED_DATE}</footer>
<script>
// Funnel bar chart — recolor via design tokens.
// Stage colors fade from accent-red (worst leakage upstream) to a deeper red downstream.
const ctx = document.getElementById('funnelChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: {STEP_NAMES_JSON},
datasets: [{
label: 'Visitors',
data: {STEP_COUNTS_JSON},
backgroundColor: [
'rgba(200,49,47,0.90)',
'rgba(200,49,47,0.72)',
'rgba(200,49,47,0.54)',
'rgba(200,49,47,0.38)',
'rgba(200,49,47,0.22)'
],
borderRadius: 6
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: '#eee9df' },
ticks: { color: '#6b6b6b' } },
x: { grid: { display: false },
ticks: { color: '#1a1a1a', font: { weight: '600' } } }
}
}
});
function toggle(id) {
var el = document.getElementById(id);
var ic = document.getElementById(id + '-icon');
if (el.style.display === 'none') { el.style.display=''; ic.textContent='\u25be'; }
else { el.style.display='none'; ic.textContent='\u25b8'; }
}
</script>
</body></html>
```
---
## Workflow Summary
1. Parse funnel steps from user description.
2. Clarify any ambiguous steps (one question at a time).
3. Resolve page names with `searchDimensionItems`.
4. Run one `runReport` per step with ad hoc segment filter; capture visitor counts.
5. Compute step conversion rates, drop-off counts, and rates.
6. Identify the biggest drop-off step.
7. Optionally run dimension breakdown at the worst step.
8. Generate HTML report with Chart.js funnel visualization.
9. Write to `/tmp/cja_funnel_health_check_report_<YYYY-MM-DD_HHMMSS>.html`.
10. Open with `open /tmp/cja_funnel_health_check_report_<YYYY-MM-DD_HHMMSS>.html`.
11. Summarize inline: "Overall conversion: X%. Biggest drop-off at [Step N]:
Y% of users abandon. Mobile users drop off at a 2× higher rate than desktop."
---
## Important Guardrails
- **Read-only analysis.** Never modify segments, calculated metrics, or project definitions automatically.
- **Confirm funnel stages before running.** Ambiguous stage definitions produce misleading results — clarify with the user first.
- **Note attribution model.** Funnel conversion rates depend on the attribution model in the data view; mention it in the report.
- **Flag incomplete data.** If any stage returns zero or suspiciously low counts, note possible tracking gaps before drawing conclusions.
- **Cap date range.** Funnel analysis over very long date ranges (>90 days) can be slow; suggest 30-day windows as default.
- **Never assume stage order.** Confirm with the user that the stages are sequential and mutually exclusive before calculating drop-off rates.
## Example Interaction
> "Check the health of our checkout funnel — I want to see where people are dropping off."
1. **Setup:** Call `findDataViews`, user selects their e-commerce data view. Call `setDefaultSessionDataViewId`.
2. **Define funnel:** Ask "What are the checkout stages?" User replies: "Product View → Add to Cart → Checkout Start → Purchase."
3. **Analysis:** Run `runReport` for each stage transition over the last 30 days. Calculate drop-off rates: Product View→Cart 22%, Cart→Checkout 58%, Checkout→Purchase 71%.
4. **Findings:** Identify the Cart→Checkout step as the highest drop-off (78% fall off). Segment by device type to find mobile conversion is 40% lower than desktop.
5. **Report:** Present a funnel visualization with drop-off rates per stage, top exit segments, and 3 prioritized recommendations.
## Recommendations Logic
- **Worst step drop-off > 60%**: "Critical leakage — this step is broken or
the user expectation is misaligned. Prioritize UX investigation."
- **Mobile drop-off > 2× desktop**: "Mobile UX at this step needs attention —
consider a dedicated mobile flow or simplified form."
- **Specific channel drop-off > 40% worse than average**: "Users from this
channel may have mismatched intent — review landing page alignment."
- **Step 1 count < 1,000**: "Funnel entry volume is too low for statistical
confidence. Check the step definition or expand the date range."
Referenced files: 1
cja-kpi-pulse11.8 KB
---
name: cja-kpi-pulse
description: >
Produces a compact KPI digest showing how key metrics changed over a period and
what's driving the movement. Use this skill when someone asks for a performance
summary, a weekly recap, a morning briefing, a KPI update, or any variation of
"how did we do this week/month." Also trigger for requests like "give me a
performance overview," "what moved in the last 7 days," "pull our KPI report,"
or "summarize our metrics."
license: Apache-2.0
metadata:
author: Adobe
version: "1.0"
---
# KPI Pulse (Customer Journey Analytics)
Produce a compact KPI digest in under 2 minutes. The goal is a crisp answer to
"how did we do?" — not a deep-dive, not a data dump. Each KPI gets a scorecard
showing current value, period-over-period change, trend direction, and the top
dimension breakdown that explains any movement.
---
## CJA MCP Tools Used
- `describeCja(DATAVIEW_CONTEXT_GUIDE)` — understand the data view context
- `listComponentUsage` — find the most-used metrics (the org's real KPIs)
- `findMetrics` — resolve metric IDs from user-specified names
- `findCalculatedMetrics` — include custom KPIs if present
- `runReport` — pull metric values for current and prior periods
- `searchDimensionItems` — top dimension breakdown for movers
---
## Phase 0 — Setup
1. Call `findDataViews` to list available data views.
2. If the user hasn't specified a data view, present the list and ask which to use.
3. Call `setDefaultSessionDataViewId` with the chosen ID.
4. Call `describeCja("DATAVIEW_CONTEXT_GUIDE")` to load data view context.
Record the data view's first-day-of-week as `WEEK_START_DOW` and timezone
as `TIMEZONE`. If the context guide does not return a week-start value,
default to **Monday** (ISO 8601). You will use both in Phase 1.1.
5. Clarify the monitoring scope: which KPIs to track and the comparison period (e.g., WoW, MoM, vs. target).
## Phase 1 — Clarify Scope
### 1.1 Determine the reporting period
If the user did not specify a period, ask one question:
> "What time window would you like? Options: last 7 days, last 30 days, this
> week vs last week, this month vs last month, or a custom range."
Default to **this week vs last week** if no answer is given.
Map the answer to two date ranges:
- **Period A** (current): e.g., "thisWeek", "thisMonth", last 7 days
- **Period B** (comparison): e.g., "lastWeek", "lastMonth", prior 7 days
**Calendar rule (mandatory):**
Use `WEEK_START_DOW` from Phase 0 to define what "week" means. The current
period (Period A) and the comparison period (Period B) MUST use the same
first-day-of-week — i.e., both periods' `startDate` fall on the same
day-of-week, both are exactly equal length, and the comparison period ends
immediately before the current period starts. Never mix conventions
(e.g., a Mon–Sun current with a Sun–Sat prior) within the same pulse run.
Pick the boundary once, then derive both periods from it. For custom date
ranges, compute Period B as the equal-length window ending immediately
before Period A starts.
**Sanity check before calling `runReport`:** confirm `periodA.startDate` and
`periodB.startDate` are the same day-of-week and that
`periodA.startDate - periodB.endDate == 1 day`. If not, recompute.
### 1.2 Determine the metrics
If the user named specific metrics, resolve them with `findMetrics` or
`findCalculatedMetrics`. Otherwise, discover the top 5–8 KPIs automatically:
```
listComponentUsage(componentType: "metric")
listComponentUsage(componentType: "calculatedMetric")
```
Note: `listComponentUsage` may return an empty list for data views with no
usage history. If it returns empty, fall back to:
```
findMetrics(searchQuery: "sessions visits revenue orders")
findMetrics(searchQuery: "page views cart conversion")
```
Pick the most business-relevant metrics from the results (sessions, orders,
revenue, product views, cart views, people — in that priority order).
Deduplicate: if a built-in metric and a calculated metric measure the same
thing, keep only the calculated metric (it's more intentional).
Final list: 5–8 metrics. More than 8 KPIs in a pulse report is noise.
---
## Phase 2 — Pull Current and Prior Period Data
Run a single `runReport` call per period with all KPI metrics included.
Use one call for Period A and one for Period B to minimize round-trips.
Use a summary dimension (e.g., `variables/daterangeday`) and limit: 1 to
get aggregate totals from `summaryData.totals` in the response.
```
runReport(
dimensionIds: "variables/daterangeday",
metricIds: "metrics/visits,metrics/visitors,metrics/orders_1_1,metrics/productListItems.priceTotal,metrics/cart_views",
startDate: "<periodA start>T00:00:00",
endDate: "<periodA end>T23:59:59",
page: 0,
limit: 1
)
```
```
runReport(
dimensionIds: "variables/daterangeday",
metricIds: "metrics/visits,metrics/visitors,metrics/orders_1_1,metrics/productListItems.priceTotal,metrics/cart_views",
startDate: "<periodB start>T00:00:00",
endDate: "<periodB end>T23:59:59",
page: 0,
limit: 1
)
```
Read aggregate totals from `summaryData.totals` (not row data), which
gives you the full-period sum for each metric in the order they were listed.
Capture for each metric:
- `valueA` (current period)
- `valueB` (comparison period)
- `delta` = valueA − valueB
- `pctChange` = (delta / valueB) × 100, rounded to 1 decimal
---
## Phase 3 — Classify Trends
For each KPI, assign a trend indicator:
- **↑ Up** if pctChange > +3%
- **↓ Down** if pctChange < −3%
- **→ Flat** if −3% ≤ pctChange ≤ +3%
Assign a signal color:
- For "higher is better" metrics: ↑ = green, ↓ = red, → = grey
- For "lower is better" metrics (bounce rate, error rate): ↑ = red, ↓ = green
---
## Phase 4 — Top Mover Drill-Down
For the 1–2 metrics with the largest absolute % change, find what's driving
the movement. Run a dimension breakdown for the current period:
```
runReport(
dimensionIds: "variables/marketing_channel",
metricIds: "<moving metric id>",
startDate: "<periodA start>T00:00:00",
endDate: "<periodA end>T23:59:59",
page: 0,
limit: 5
)
```
Note: Use `variables/marketing_channel` (not `variables/marketingchannel`) —
verify the exact dimension ID with `findDimensions(searchQuery: "marketing channel")`
if unsure.
Compare dimension values between Period A and Period B to identify the top
contributor to the change. This becomes the "What drove it" entry in the report.
---
## Phase 5 — Generate HTML Report
Generate the KPI Pulse HTML report INLINE — do not use a Python script.
Build the HTML string directly from the collected data and output it as a
code block the user can save, or write it to `/tmp/cja_kpi_pulse_report_<YYYY-MM-DD_HHMMSS>.html`
using a one-line bash command.
### Rendering rules — apply consistently across runs
Two runs of this skill on the same data view + period must render identically
(modulo the generation timestamp). The rules below pin the formatting choices
that the AI would otherwise drift on.
#### Number formatting
- **KPI values** (the big number in each tile) — use full digits with
thousands separators (`8,160`, `77,584`, `1,250,000`). Do **NOT** use SI
suffixes like `K` or `M`, even for large values. Executives want exact
numbers, not abbreviations.
- **Percent change** (in pills and narrative bullets) — always one decimal
place, rounded **half-away-from-zero**. For example, `−23.55%` displays as
`−23.6%`, never `−23.5%`. Compute on full-precision values; round only at
display time.
- **Percentage-point change** (for already-percentage metrics like Conversion
Rate or Bounce Rate) — same rounding, suffix `pp`. Example: `+0.40 pp`.
- **Currency** — `$` prefix with thousands separators and no decimals for
values ≥ $100 (`$1,240,000`); cents only when value < $100 (`$45.20`).
#### Null / missing data handling
A KPI tile must reflect what the data view actually returned. The AI must
**not** silently substitute a different metric or hide a tile to make the
report look cleaner.
- **Both periods return 0 or NULL** for a KPI being rendered: render the tile
with `kpi-value` = `Data unavailable`, pill class `flat`, pill text
`⚠ N/A`, and `prior` text = `Both periods returned no data — validate
instrumentation`. The tile stays in the grid; do not omit it.
- **One period returns valid data, the other 0 / NULL**: render the tile with
the valid value as `kpi-value`, pill class `flat`, pill text `⚠ N/A`, and
`prior` text = `Prior {period_noun}: no data`.
- **Never** substitute a derived metric (e.g., adding "Conversion Rate"
because Revenue came back $0). The visible KPI set MUST match the metrics
selected for this run.
### HTML Template
Read [`template.html`](template.html) and use it verbatim. Do not improvise the
HTML structure or CSS — only fill in the `{PLACEHOLDER}` tokens (`{ORG_NAME}`,
`{PERIOD_LABEL}`, `{COMPARISON_LABEL}`, `{DATA_VIEW}`, `{GENERATED_DATE}`,
`{METRIC_NAME}`, `{FORMATTED_VALUE_A}`, `{FORMATTED_VALUE_B}`, `{PCT_CHANGE}`,
`{VALUE_A}`, `{VALUE_B}`, `{DELTA}`, `{ARROW}`) and repeat the KPI tile / detail
row / mover row blocks once per data item. Preserve the `.up | .down | .flat`
and `.green | .red | .yellow | .grey` modifier classes per the trend rules in
Phase 3.
---
## Phase 6 — Deliver the Report
After generating the HTML:
1. Write it to `/tmp/cja_kpi_pulse_report_<YYYY-MM-DD_HHMMSS>.html`
2. Open with `open /tmp/cja_kpi_pulse_report_<YYYY-MM-DD_HHMMSS>.html`
3. Provide a 3–5 line text summary inline in the chat:
```
KPI Pulse — This Week vs Last Week
↑ Revenue: $1.24M (+8.2%) — Paid Search drove most of the gain
↓ Conversion Rate: 2.1% (−0.4pp) — Drop in mobile checkout
→ Sessions: 540K (+1.1%) — Flat week-over-week
↑ Orders: 11,340 (+6.7%) — Product page improvements appear to be working
↓ Bounce Rate: 43.2% (+2.1pp) — Worth monitoring next week
```
The text summary gives immediate value even without opening the HTML file.
---
## Important Guardrails
- **Read-only monitoring.** Never modify metrics, segments, or projects.
- **Use consistent date ranges.** Week-over-week and month-over-month comparisons must use equal-length periods.
- **Flag anomalies, don't diagnose them.** The pulse report surfaces significant deviations — deep root cause analysis belongs in the anomaly triage skill.
- **Respect business calendar.** Holiday periods, campaigns, and seasonal patterns affect normal variance — note context when flagging anomalies.
- **Cap metric count.** Monitor up to 10–15 KPIs per pulse; more than that dilutes focus. Ask the user to prioritize if they specify too many.
- **Note data freshness.** If the most recent data point is older than expected, warn the user before presenting the pulse.
## Example Interaction
> "Give me a quick pulse on our key metrics for this week."
1. **Setup:** Confirm data view with `findDataViews`. User selects their main data view. Call `setDefaultSessionDataViewId`.
2. **Scope:** Ask "Which KPIs should I include?" User says: "Sessions, Revenue, Conversion Rate, and Average Order Value."
3. **Data pull:** Run `runReport` for current week vs. prior week for all four metrics.
4. **Analysis:** Sessions +8% WoW (within normal range). Revenue +3% WoW. Conversion Rate -12% WoW — flagged as anomalous. AOV +17% WoW — notable positive.
5. **Summary:** Present a KPI scorecard with traffic-light status (green/yellow/red), highlight the Conversion Rate drop as needing investigation, and note that the AOV increase partially offsets it.
## Error Handling
- If `runReport` returns no data for Period B (comparison is too far in the
past or data view lacks history), show "N/A" for the delta and flag it with
a grey badge.
- If a metric returns null, display "—" rather than 0 to avoid false
impressions of zero performance.
- If fewer than 3 metrics are available, warn the user that the pulse may be
incomplete and suggest they verify the data view is correctly configured.
Referenced files: 2
cja-segment-performance-comparator10.4 KB
---
name: cja-segment-performance-comparator
description: >
Compares the performance of two or more audience segments across key metrics
side by side. Use this skill when someone wants to compare audiences, cohorts,
or groups — for example, "how do mobile users compare to desktop users on
conversion," "compare new vs. returning visitors," "show me the difference
between these two segments," "compare these audiences on our KPIs," or
"which segment performs better." Also trigger for "segment comparison,"
"audience comparison," or "cohort comparison."
license: Apache-2.0
metadata:
author: Adobe
version: "1.0"
---
# Segment Performance Comparator (Customer Journey Analytics)
Compare 2–5 audience segments across a set of key metrics in a side-by-side
matrix. The output tells the user not just what each segment looks like in
isolation, but which segment wins or loses on each metric — and which
differences are large enough to act on.
This skill answers the question "which audience should we focus on?" with data.
Segment comparisons drive product decisions, personalization strategy, and
budget allocation — so clarity and actionability matter more than exhaustive data.
---
## CJA MCP Tools Used
- `findSegments` — search for segments by name or keyword
- `describeSegment` — understand the logic of candidate segments before using them
- `findMetrics` — resolve base metric IDs
- `findCalculatedMetrics` — include custom KPIs in the comparison
- `listComponentUsage` — identify the most-used metrics as default comparison set
- `runReport` (with `segmentIds` or `adhocSegments`) — pull metric values per segment
---
## Phase 0 — Setup
1. Call `findDataViews` to list available data views.
2. If the user hasn't specified a data view, present the list and ask which to use.
3. Call `setDefaultSessionDataViewId` with the chosen ID.
4. Ask the user which segments to compare if not already specified. Confirm the metrics to compare them on.
---
## Phase 1 — Identify Segments to Compare
### 1.1 From user description
If the user named specific segments, resolve them:
```
findSegments(search: "<segment name>")
```
For each match, call `describeSegment` to verify it is the correct one:
```
describeSegment(segmentId: "<id>")
```
Show the segment definition summary to the user if there is ambiguity:
> "I found two segments matching 'mobile users': **Mobile Visitors (All Devices)**
> and **Mobile App Users**. Which do you want to compare?"
### 1.2 From plain-English descriptions
If the user says "compare mobile vs desktop users" but there are no matching
segments, offer to create ad hoc segments inline for the comparison:
> "I don't see pre-built segments for mobile and desktop. I can create
> temporary ad hoc segments for this comparison using device type. Should I
> proceed with ad hoc segments, or would you like to create permanent segments
> first?"
Ad hoc segments are constructed using `adhocSegments` in `runReport` — no
save required for the comparison itself.
### 1.3 Segment count limit
Maximum 5 segments for a single comparison. More than 5 creates a matrix
that is too wide to read meaningfully. If the user requests more, say:
> "I'll limit to the 5 most relevant segments for readability. Would you like
> me to prioritize by usage count or stick with your list order?"
---
## Phase 2 — Identify Metrics to Compare
### 2.1 From user specification
Resolve named metrics via `findMetrics` and `findCalculatedMetrics`.
### 2.2 Default metric discovery
If the user did not specify metrics, pull the top metrics by usage. The
`listComponentUsage` tool does not support a `limit` parameter — it returns all
components ranked by usage count; take the top 6–8 from the result:
```
listComponentUsage(componentType: "metric")
listComponentUsage(componentType: "calculatedMetric")
```
Prefer calculated metrics over raw base metrics when they measure the same
thing — calculated metrics reflect intentional KPI definitions.
### 2.3 Metric selection for a comparison
Good comparison metrics should be meaningful across all segments. For example,
"Revenue" is meaningful for both mobile and desktop users; "App Installs" is
only meaningful for mobile. Remove metrics that would be trivially zero for
one segment.
If unsure, ask: "Should I use your standard KPI set, or focus on specific
metrics like conversion rate, revenue, and engagement?"
---
## Phase 3 — Run the Comparison
For each segment, run a `runReport` with that segment applied and all
comparison metrics included. Note that `runReport` takes `metricIds` as a
comma-separated string, `startDate`/`endDate` (not `dateRange`), and a
`dimensionIds` (required even for summary-only reports — use a low-cardinality
dimension like `variables/daterangeday` or `variables/web.webPageDetails.name`).
The summary totals for all metrics are in `summaryData.filteredTotals`:
```
runReport(
dimensionIds: "variables/web.webPageDetails.name",
metricIds: "metrics/visits,metrics/revenue_1,metrics/orders_1_1",
startDate: "<period start>T00:00:00",
endDate: "<period end>T23:59:59",
page: 0,
limit: 1,
segmentIds: "<segment id>"
)
```
For ad hoc segments, use the full CJA segment definition object:
```
runReport(
dimensionIds: "variables/web.webPageDetails.name",
metricIds: "metrics/visits,metrics/orders_1_1",
startDate: "<period start>T00:00:00",
endDate: "<period end>T23:59:59",
page: 0,
limit: 1,
adhocSegments: [{
"func": "segment",
"version": [1, 0, 0],
"container": {
"func": "container",
"context": "visitors",
"pred": {
"func": "streq",
"val": { "func": "attr", "name": "variables/device_type" },
"str": "Mobile Phone"
}
}
}]
)
```
Read metric totals from `summaryData.filteredTotals[i]` where `i` is the
0-based index of the metric in the `metricIds` string.
Run one report per segment. Collect all results into a matrix:
- Rows = metrics
- Columns = segments
---
## Phase 4 — Build the Comparison Matrix
For each cell (metric × segment):
- `value[metric][segment]` = raw metric value from `runReport`
For each metric row:
- `winner` = segment with the highest value (or lowest, for "lower is better" metrics)
- `loser` = segment with the lowest value (or highest, for inverse metrics)
- `range` = (max − min) / max × 100 — the spread across segments as a percentage
- `significant` = true if range > 10% (a meaningful difference worth acting on)
---
## Phase 5 — Generate HTML Comparison Report
Generate the report inline and write to
`/tmp/cja_segment_performance_comparator_report_<YYYY-MM-DD_HHMMSS>.html`.
### HTML Template
Read [`template.html`](template.html) and use it verbatim. Do not improvise the
HTML structure or CSS — only fill in the `{PLACEHOLDER}` tokens (`{ORG_NAME}`,
`{DATE_RANGE}`, `{DATA_VIEW}`, `{GENERATED_DATE}`, `{SEGMENT_NAMES_SUMMARY}`,
`{SEGMENT_NAME}`, `{COLOR}`, `{VISITOR_COUNT}`, `{NUM_SEGMENTS}`, `{NUM_METRICS}`,
`{NUM_SIGNIFICANT}`, `{OVERALL_WINNER}`, `{METRIC_NAME}`, `{VALUE}`,
`{WINNER_SEGMENT}`, `{SPREAD}`, `{INSIGHT_TEXT}`) and repeat segment chips,
matrix rows, and insight boxes once per data item. Use the `cell-winner` /
`cell-loser` classes per Phase 4 winner/loser rules.
---
## Phase 6 — Narrative Insights
After building the matrix, generate 3–5 insight bullets for the Insights section:
1. **Overall Winner**: "Returning Visitors outperform New Visitors on 5 of 7
metrics, with the largest gap in Revenue per Session (+82%)."
2. **Most Significant Difference**: "The biggest gap is Conversion Rate: Mobile
converts at 1.2% vs Desktop at 3.8% — a 68% gap worth prioritizing."
3. **Surprising Parity**: "New vs Returning Visitors show nearly identical
Bounce Rates (42% vs 44%), suggesting landing page quality is consistent."
4. **Actionable Signal**: "Paid Search visitors have 2.3× higher Revenue per
Session than Direct visitors — consider shifting budget toward Paid Search."
5. **Anomaly**: "One segment shows near-zero values across all metrics — verify
that the segment definition is correct and matches the current data view."
Insights should be plain English, not metric IDs. Name the specific segments
and metric values.
---
## Workflow Summary
1. Resolve 2–5 segments (by name or ad hoc definition).
2. Identify 5–8 comparison metrics (from user or top usage).
3. Run one `runReport` per segment with all metrics; collect results.
4. Build comparison matrix: rows = metrics, columns = segments.
5. Mark winner/loser per row; compute spread; flag significant differences.
6. Generate HTML report with matrix and insight bullets.
7. Write to `/tmp/cja_segment_performance_comparator_report_<YYYY-MM-DD_HHMMSS>.html`.
8. Open with `open /tmp/cja_segment_performance_comparator_report_<YYYY-MM-DD_HHMMSS>.html`.
9. Deliver inline summary: which segment wins overall, biggest gap metric,
one actionable recommendation.
---
## Important Guardrails
- **Read-only analysis.** Never delete or modify segments or calculated metrics.
- **Always confirm segments before running.** Ambiguous segment names (e.g., "Mobile" could be several) should be resolved by showing the user the matched segment IDs and definitions.
- **Use the same date range for all segments.** Comparisons across different time windows are misleading.
- **Note overlap between segments.** If two segments share substantial audience overlap, note it — the "difference" may be exaggerated.
- **Cap the number of segments compared.** Comparing more than 5–6 segments in a single report makes the output unreadable; ask the user to prioritize.
- **Distinguish statistical significance from practical significance.** A 0.1% difference is rarely actionable — focus on differences of 5%+ unless the user specifies otherwise.
---
## Example Interaction
> "Compare our mobile vs. desktop segment performance for last quarter."
1. **Setup:** Confirm data view. Call `findDataViews`, user selects. Call `setDefaultSessionDataViewId`.
2. **Segment resolution:** Call `findSegments` to locate the "Mobile Users" and "Desktop Users" segments. Show matched names and IDs to confirm. User approves.
3. **Metrics:** Ask "Which metrics should I compare?" User: "Sessions, Conversion Rate, Revenue, and Average Order Value."
4. **Analysis:** Run `runReport` for Q1 2026 with both segments applied. Tabulate results side-by-side.
5. **Findings:** Mobile: 45% of sessions, 2.1% CVR, $0.84 RPV. Desktop: 55% of sessions, 4.8% CVR, $2.10 RPV. Desktop converts 2.3× better. Present a comparison table and 3 recommended next steps.
Referenced files: 2
cja-top-movers-watchlist23.8 KB
---
name: cja-top-movers-watchlist
description: >
Identifies which items (pages, campaigns, products, channels, regions) had the
biggest increases or decreases for a key metric between two time periods. Use
this skill when someone asks "what's up and what's down," "which campaigns
moved the most," "top gainers and losers," "what pages are trending," "show me
what changed by channel," or any variation of identifying the biggest movers
and decliners for a metric.
license: Apache-2.0
metadata:
author: Adobe
version: "1.0"
---
# Top Movers Watchlist (Customer Journey Analytics)
Surface the biggest gainers and decliners for a metric across any dimension in
two time periods. The output tells the user exactly what moved, by how much, and
whether items appeared or disappeared entirely — which is often the most
interesting signal.
This skill is a faster, more targeted alternative to a full anomaly triage.
Use it when the user wants to scan the landscape of changes rather than drill
into a single anomaly.
---
## CJA MCP Tools Used
- `describeCja(DATAVIEW_CONTEXT_GUIDE)` — load data view calendar/timezone
- `findMetrics` — resolve the metric being watched
- `findCalculatedMetrics` — if the metric is a custom KPI
- `findDimensions` — resolve the dimension to break down by
- `runReport` — pull dimension-metric data for both periods
- `searchDimensionItems` — validate dimension values if user specifies names
---
## Phase 0 — Setup
1. Call `findDataViews` and `setDefaultSessionDataViewId` as needed.
2. Call `describeCja("DATAVIEW_CONTEXT_GUIDE")` to load data view context.
Record the first-day-of-week as `WEEK_START_DOW` and timezone as
`TIMEZONE`. If the context guide does not return a week-start value,
default to **Monday** (ISO 8601). You will use both in Phase 1.3.
---
## Phase 1 — Clarify Inputs
### 1.1 Metric
If the user specified a metric, resolve it:
```
findMetrics(search: "<user's metric name>")
```
If not specified, suggest the top 3 metrics from usage:
> "Which metric would you like to track? I can suggest: Sessions, Revenue,
> Orders based on what your team uses most."
### 1.2 Dimension (what to break down by)
Common dimension choices and their typical use cases:
| Dimension | Use Case |
|--------------------|-----------------------------------------|
| Marketing Channel | "Which channels moved?" |
| Page Name | "Which pages are trending?" |
| Campaign | "Which campaigns improved?" |
| Product | "Which products gained/lost traction?" |
| Country / Region | "Which markets moved?" |
| Device Type | "Did mobile or desktop shift?" |
| Referring Domain | "Which referrers changed?" |
If the user did not specify, ask:
> "Which dimension should I break down by — for example, marketing channel,
> page, campaign, country, or product?"
Call `findDimensions(search: "<dimension keyword>")` to resolve the dimension ID.
### 1.3 Periods
Define Period A (current) and Period B (comparison). Defaults:
- **Period A**: this week (or last 7 days)
- **Period B**: last week (or the 7 days before that)
If the user specifies "this month vs last month" or a custom range, map
accordingly. Always confirm the periods before running reports:
> "I'll compare **this week (Mar 13–19)** vs **last week (Mar 6–12)**. Sound right?"
**Calendar rule (mandatory):**
Use `WEEK_START_DOW` from Phase 0 to define what "week" means. Period A and
Period B MUST use the same first-day-of-week — i.e., both periods'
`startDate` fall on the same day-of-week, both are exactly equal length,
and Period B ends immediately before Period A starts. Never mix conventions
(e.g., a Mon–Sun Period A with a Sun–Sat Period B) within the same run.
Pick the boundary once, then derive both periods from it. For custom date
ranges, compute Period B as the equal-length window ending immediately
before Period A starts.
**Sanity check before calling `runReport`:** confirm `periodA.startDate`
and `periodB.startDate` are the same day-of-week and that
`periodA.startDate - periodB.endDate == 1 day`. If not, recompute.
---
## Phase 2 — Pull Data for Both Periods
Run two reports — one per period — with the same dimension breakdown:
```
runReport(
dimensionIds: "<dimension id>",
metricIds: "<metric id>",
startDate: "<period A start>T00:00:00",
endDate: "<period A end>T23:59:59",
page: 0,
limit: 50
)
```
```
runReport(
dimensionIds: "<dimension id>",
metricIds: "<metric id>",
startDate: "<period B start>T00:00:00",
endDate: "<period B end>T23:59:59",
page: 0,
limit: 50
)
```
Use `limit: 50` to capture enough items to surface meaningful movers.
If the user's dimension has thousands of values (e.g., page names), limit to
top 100 by Period A volume to keep the comparison meaningful.
Row data is in the `rows` array — each row has `value` (dimension item name)
and `data[0]` (the metric value). There is no limit on dimension cardinality
but results default to sorted by metric descending, which is what you want.
Always verify the dimension ID with `findDimensions(searchQuery: "<name>")`
before running — dimension IDs can vary from what you might guess
(e.g., `variables/marketing_channel` not `variables/marketingchannel`).
---
## Phase 3 — Compute Rankings
Build a unified table joining both result sets on dimension value:
For each dimension value present in either period:
- `valueA` = metric value in Period A (0 if not present)
- `valueB` = metric value in Period B (0 if not present)
- `delta` = valueA − valueB
- `pctChange` = (delta / valueB) × 100 if valueB > 0, else "New Entry"
- `status`:
- Present in A but not B → **New Entry** (appeared this period)
- Present in B but not A → **Disappeared** (dropped out this period)
- Both present → normal mover
Sort by:
1. **Top Gainers**: sort by delta descending (biggest absolute gains first)
2. **Top Decliners**: sort by delta ascending (biggest absolute drops first)
3. **% Gainers**: sort by pctChange descending
4. **% Decliners**: sort by pctChange ascending
Limit each list to **Top 10**. New Entries and Disappeared items get their
own sections regardless of count (they are always interesting signals).
---
## Phase 4 — Generate HTML Report
Generate the movers report inline and write to
`/tmp/cja_top_movers_report_<YYYY-MM-DD_HHMMSS>.html`.
### Rendering rules — apply consistently across runs
Two runs of this skill on the same data view + metric/dimension + period must
render identically (modulo the generation timestamp). The rules below pin the
formatting choices that the AI would otherwise drift on.
#### Number formatting
- **KPI values** (the big number in each summary tile, and per-row mover
values) — use full digits with thousands separators (`8,160`, `77,584`,
`1,250,000`). Do **NOT** use SI suffixes like `K` or `M`, even for large
values. Stakeholders want exact numbers, not abbreviations.
- **Percent change** (in pills and narrative bullets) — always one decimal
place, rounded **half-away-from-zero**. For example, `−23.55%` displays as
`−23.6%`, never `−23.5%`. Compute on full-precision values; round only at
display time.
- **Percentage-point change** (for already-percentage metrics like Conversion
Rate or Bounce Rate) — same rounding, suffix `pp`. Example: `+0.40 pp`.
- **Currency** — `$` prefix with thousands separators and no decimals for
values ≥ $100 (`$1,240,000`); cents only when value < $100 (`$45.20`).
#### Null / missing data handling
A KPI tile or mover row must reflect what the data view actually returned.
The AI must **not** silently substitute a different metric or hide a tile to
make the report look cleaner.
- **Both periods return 0 or NULL** for the tracked metric in a summary tile:
render the tile with `kpi-value` = `Data unavailable`, pill class `flat`,
pill text `⚠ N/A`, and `prior` text = `Both periods returned no data —
validate instrumentation`. The tile stays in the grid; do not omit it.
- **One period returns valid data, the other 0 / NULL**: render the tile with
the valid value as `kpi-value`, pill class `flat`, pill text `⚠ N/A`, and
`prior` text = `Prior {period_noun}: no data`.
- **Never** substitute a different metric (e.g., switching from Revenue to
Orders because Revenue came back $0). The metric being analyzed MUST be the
metric rendered.
### HTML Template
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Top Movers — {ORG_NAME} — {METRIC_NAME} by {DIMENSION_NAME}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f4f1;
--surface: #ffffff;
--ink: #1a1a1a;
--ink-muted: #6b6b6b;
--border: #e5e2dc;
--header-bg: #0e0e10;
--header-warm: #3a1010;
--accent-red: #c8312f;
--accent-red-bright: #ff6b68;
--accent-red-soft: #fdecea;
--accent-green: #1f7a4d;
--accent-yellow: #d4a017;
}
body { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg); color: var(--ink); line-height: 1.5;
-webkit-font-smoothing: antialiased; }
/* === Header === */
header { background: linear-gradient(120deg, var(--header-bg) 0%, #1a0d0d 55%, var(--header-warm) 100%);
color: #fff; padding: 56px 56px 44px; position: relative; overflow: hidden; }
header::after { content: ""; position: absolute; right: -140px; top: -140px;
width: 460px; height: 460px;
background: radial-gradient(circle, rgba(200,49,47,.35) 0%, transparent 70%);
pointer-events: none; }
.header-inner { max-width: 1080px; margin: 0 auto; position: relative; z-index: 1; }
.eyebrow { display: inline-flex; align-items: center; gap: 8px;
padding: 6px 14px; border: 1px solid rgba(255,107,104,.55);
border-radius: 999px; color: var(--accent-red-bright);
font-size: 11px; font-weight: 600; letter-spacing: 1.2px;
text-transform: uppercase; margin-bottom: 24px;
background: rgba(200,49,47,.10); }
.eyebrow::before { content: ""; width: 6px; height: 6px;
background: var(--accent-red-bright); border-radius: 50%; }
header h1 { font-family: "Playfair Display", Georgia, serif;
font-size: 56px; font-weight: 700; letter-spacing: -1.5px;
line-height: 1.05; margin-bottom: 14px; color: #fff; }
header .lede { font-size: 16px; max-width: 560px;
color: rgba(255,255,255,.80); margin-bottom: 24px;
line-height: 1.55; }
header .meta { display: flex; flex-wrap: wrap; gap: 22px;
font-size: 13px; color: rgba(255,255,255,.60); }
header .meta span { display: inline-flex; align-items: center; gap: 6px; }
header .meta .icon { opacity: .8; }
/* === Tabs === */
nav { background: var(--surface); border-bottom: 1px solid var(--border);
padding: 0 56px; display: flex; gap: 28px;
position: sticky; top: 0; z-index: 50; }
nav a { display: block; padding: 16px 0; font-size: 14px;
color: var(--ink); text-decoration: none;
border-bottom: 2px solid transparent;
transition: border-color .15s ease; }
nav a:hover { border-bottom-color: var(--accent-red); }
/* === Container === */
.container { max-width: 1080px; margin: 0 auto; padding: 36px 56px 60px; }
/* === Section label === */
.section-label { font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 1.4px;
color: var(--ink-muted); margin-bottom: 14px;
padding-bottom: 10px; border-bottom: 1px solid var(--border); }
/* === KPI grid === */
.kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px; margin-bottom: 36px; }
.kpi-tile { background: var(--surface); border-radius: 8px;
padding: 22px 22px 20px;
border-top: 3px solid #b9b6ae;
box-shadow: 0 1px 3px rgba(0,0,0,.05); }
.kpi-tile.down { border-top-color: var(--accent-red); }
.kpi-tile.up { border-top-color: var(--accent-green); }
.kpi-tile.flat { border-top-color: #b9b6ae; }
.kpi-head { display: flex; justify-content: space-between;
align-items: center; margin-bottom: 10px; }
.kpi-label { font-size: 11px; font-weight: 700;
text-transform: uppercase;
color: var(--ink-muted); letter-spacing: 1px; }
.kpi-value { font-family: "Playfair Display", Georgia, serif;
font-weight: 700; font-size: 34px;
line-height: 1; color: var(--ink);
margin-bottom: 12px; }
.pill { display: inline-flex; align-items: center; gap: 4px;
padding: 3px 9px; border-radius: 4px;
font-size: 12px; font-weight: 600; line-height: 1.4; }
.pill.down { background: var(--accent-red-soft); color: var(--accent-red); }
.pill.up { background: #ebf5ef; color: var(--accent-green); }
.pill.flat { background: #f1efea; color: var(--ink-muted); }
.prior { display: block; margin-top: 10px;
font-size: 12px; color: var(--ink-muted); }
/* === Dual risers / fallers panels === */
.two-col { display: grid; grid-template-columns: 1fr 1fr;
gap: 20px; margin-bottom: 22px; }
@media (max-width: 760px) { .two-col { grid-template-columns: 1fr; } }
/* === Sections (collapsible tables) === */
.section { background: var(--surface); border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,.04);
margin-bottom: 22px; overflow: hidden; }
/* Top-border accent on risers/fallers tiles */
.section.risers { border-top: 3px solid var(--accent-green); }
.section.fallers { border-top: 3px solid var(--accent-red); }
.section-header { padding: 18px 28px; border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between;
align-items: center; cursor: pointer; }
.section-header h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 18px; font-weight: 700; }
.section.risers .section-header h2 { color: var(--accent-green); }
.section.fallers .section-header h2 { color: var(--accent-red); }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th { background: #faf8f4; padding: 12px 22px;
text-align: left; font-weight: 600;
text-transform: uppercase; letter-spacing: .6px;
font-size: 11px; color: var(--ink-muted);
border-bottom: 1px solid var(--border); }
tbody td { padding: 12px 22px; border-bottom: 1px solid #f4f1eb; }
tbody tr:last-child td { border-bottom: none; }
.delta-up { color: var(--accent-green); font-weight: 600; }
.delta-down { color: var(--accent-red); font-weight: 600; }
.badge { display: inline-block; padding: 3px 9px; border-radius: 4px;
font-size: 11px; font-weight: 600; }
.badge.green { background: #ebf5ef; color: var(--accent-green); }
.badge.red { background: var(--accent-red-soft); color: var(--accent-red); }
.badge.yellow { background: #fef6e3; color: #b67a08; }
.badge.blue { background: #eef2f8; color: #2c4a7a; }
.badge.grey { background: #f1efea; color: var(--ink-muted); }
.back-top { position: fixed; bottom: 24px; right: 24px;
background: var(--accent-red); color: #fff;
width: 44px; height: 44px; border-radius: 50%;
border: none; font-size: 20px; cursor: pointer;
box-shadow: 0 4px 12px rgba(200,49,47,0.30); }
footer { text-align: center; padding: 32px 24px;
font-size: 12px; color: var(--ink-muted); }
/* === Print === */
@media print {
nav { display: none; position: static; }
header { padding: 36px 32px 28px; }
header h1 { font-size: 42px; }
.section-header { cursor: default; }
.kpi-row { page-break-inside: avoid; }
.kpi-tile, .section {
box-shadow: none; border: 1px solid var(--border);
}
.back-top { display: none; }
}
</style>
</head>
<body>
<header>
<div class="header-inner">
<div class="eyebrow">Top Movers Report</div>
<h1>{ORG_NAME} Top Movers</h1>
<p class="lede">Biggest gainers and decliners for {METRIC_NAME} by {DIMENSION_NAME}, {PERIOD_A_LABEL} vs {PERIOD_B_LABEL}.</p>
<div class="meta">
<span><span class="icon">📅</span> {PERIOD_A_LABEL}</span>
<span><span class="icon">📊</span> {DATA_VIEW}</span>
<span><span class="icon">🕔</span> Prepared {GENERATED_DATE}</span>
</div>
</div>
</header>
<nav>
<a href="#summary">Summary</a>
<a href="#gainers">Gainers</a>
<a href="#decliners">Decliners</a>
<a href="#newdisappeared">New & Gone</a>
<a href="#fulltable">Full Table</a>
</nav>
<div class="container">
<!-- Summary KPI Tiles -->
<div class="section-label">Watchlist Summary</div>
<div id="summary" class="kpi-row">
<div class="kpi-tile flat">
<div class="kpi-head"><div class="kpi-label">Total Items Compared</div></div>
<div class="kpi-value">{TOTAL_ITEMS}</div>
<span class="prior">Dimension values in scope</span>
</div>
<div class="kpi-tile up">
<div class="kpi-head"><div class="kpi-label">Biggest Gain</div></div>
<div class="kpi-value">{TOP_GAINER_VALUE}</div>
<span class="pill up">▲ {TOP_GAINER_NAME}</span>
</div>
<div class="kpi-tile down">
<div class="kpi-head"><div class="kpi-label">Biggest Drop</div></div>
<div class="kpi-value">{TOP_DECLINER_VALUE}</div>
<span class="pill down">▼ {TOP_DECLINER_NAME}</span>
</div>
<div class="kpi-tile flat">
<div class="kpi-head"><div class="kpi-label">New Entries</div></div>
<div class="kpi-value">{NEW_ENTRIES}</div>
<span class="prior">Appeared this period</span>
</div>
<div class="kpi-tile flat">
<div class="kpi-head"><div class="kpi-label">Disappeared</div></div>
<div class="kpi-value">{DISAPPEARED}</div>
<span class="prior">Dropped out this period</span>
</div>
</div>
<!-- Gainers + Decliners side by side -->
<div class="two-col">
<div id="gainers" class="section risers">
<div class="section-header" onclick="toggle('gain-body')">
<h2>▲ Top 10 Gainers</h2>
<span id="gain-body-icon">▾</span>
</div>
<div id="gain-body">
<table>
<thead><tr>
<th>Item</th>
<th>{PERIOD_A_LABEL}</th>
<th>{PERIOD_B_LABEL}</th>
<th>Delta</th>
<th>% Change</th>
</tr></thead>
<tbody>
<!-- Top 10 gainers, sorted by delta desc -->
<!--
<tr>
<td>{ITEM_NAME}</td>
<td>{VALUE_A}</td>
<td>{VALUE_B}</td>
<td class="delta-up">+{DELTA}</td>
<td><span class="badge green">+{PCT}%</span></td>
</tr>
-->
</tbody>
</table>
</div>
</div>
<div id="decliners" class="section fallers">
<div class="section-header" onclick="toggle('dec-body')">
<h2>▼ Top 10 Decliners</h2>
<span id="dec-body-icon">▾</span>
</div>
<div id="dec-body">
<table>
<thead><tr>
<th>Item</th>
<th>{PERIOD_A_LABEL}</th>
<th>{PERIOD_B_LABEL}</th>
<th>Delta</th>
<th>% Change</th>
</tr></thead>
<tbody>
<!-- Top 10 decliners, sorted by delta asc -->
<!--
<tr>
<td>{ITEM_NAME}</td>
<td>{VALUE_A}</td>
<td>{VALUE_B}</td>
<td class="delta-down">−{DELTA}</td>
<td><span class="badge red">−{PCT}%</span></td>
</tr>
-->
</tbody>
</table>
</div>
</div>
</div>
<!-- New Entries & Disappeared -->
<div id="newdisappeared" class="section">
<div class="section-header" onclick="toggle('nd-body')">
<h2>New Entries & Disappeared Items</h2>
<span id="nd-body-icon">▾</span>
</div>
<div id="nd-body">
<table>
<thead><tr>
<th>Item</th>
<th>Status</th>
<th>{PERIOD_A_LABEL}</th>
<th>{PERIOD_B_LABEL}</th>
<th>Notes</th>
</tr></thead>
<tbody>
<!-- New entries: badge blue "New Entry" -->
<!-- Disappeared: badge grey "Disappeared" -->
</tbody>
</table>
</div>
</div>
<!-- Full Table -->
<div id="fulltable" class="section">
<div class="section-header" onclick="toggle('full-body')">
<h2>Full Comparison Table</h2>
<span id="full-body-icon">▾</span>
</div>
<div id="full-body">
<table>
<thead><tr>
<th>Item</th>
<th>{PERIOD_A_LABEL}</th>
<th>{PERIOD_B_LABEL}</th>
<th>Delta</th>
<th>% Change</th>
<th>Status</th>
</tr></thead>
<tbody>
<!-- All items sorted by absolute delta desc -->
</tbody>
</table>
</div>
</div>
</div>
<button class="back-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">↑</button>
<footer>Top Movers — {ORG_NAME} — Generated {GENERATED_DATE}</footer>
<script>
function toggle(id) {
var el = document.getElementById(id);
var ic = document.getElementById(id + '-icon');
if (el.style.display === 'none') { el.style.display=''; ic.textContent='\u25be'; }
else { el.style.display='none'; ic.textContent='\u25b8'; }
}
</script>
</body></html>
```
---
## Workflow Summary
1. Confirm metric, dimension, and both time periods.
2. Run `runReport` for Period A with dimension breakdown (limit 50).
3. Run `runReport` for Period B with same parameters.
4. Join on dimension value; compute delta, pctChange, and status.
5. Sort into: Top 10 Gainers, Top 10 Decliners, New Entries, Disappeared.
6. Generate HTML report inline, write to `/tmp/cja_top_movers_report_<YYYY-MM-DD_HHMMSS>.html`.
7. Open with `open /tmp/cja_top_movers_report_<YYYY-MM-DD_HHMMSS>.html`.
8. Deliver a 3-line inline summary: "Top gainer: X (+Y%), Top decliner: Z (−W%).
N new items entered the top 50; M items disappeared."
---
## Important Guardrails
- **Read-only monitoring.** Never modify segments, metrics, or project definitions.
- **Confirm metric and segment scope** before running. Vague requests ("watch everything") need narrowing — ask which dimensions and metrics matter most.
- **Use consistent comparison windows.** "Top movers" must compare equal-length periods; mismatched windows produce false signals.
- **Distinguish noise from signal.** Very low-traffic dimension values (e.g., a segment with 5 visits) can show 500% swings — apply a minimum threshold (e.g., at least 100 sessions) before flagging as a mover.
- **Cap the watchlist size.** Surface the top 10–20 movers by default; overwhelming users with 100 dimension items defeats the purpose.
- **Attribute changes to context.** When possible, note known business events (campaigns, releases, outages) that could explain movements.
## Example Interaction
> "Which marketing channels moved the most last week vs the week before?"
1. Metric: Sessions (top usage default)
2. Dimension: Marketing Channel
3. Period A: last week, Period B: the week before
4. Run both reports, join results
5. Gainers: Paid Social +32%, Organic Search +8%
6. Decliners: Email −18%, Direct −5%
7. New: Affiliate (not in top 50 prior week)
8. Generate and open report
9. Inline summary: "Paid Social drove the biggest gain (+32%). Email was
the top decliner (−18%). Affiliate channel appeared for the first time
in the top rankings."
Referenced files: 1
Technical details
- First seen
- Sep 30, 2026 · 22:02 UTC
- Last seen
- Oct 1, 2026 · 12:00 UTC
- Collection status
- Collected
plugin_asdk_app_6a3ed08a777481919fe5c4b0c1bb9171
Download listing JSON