
Description
Run the Journey Report — how far into the site a visit gets and where it ends. Use when the user asks to run the journey report, asks how deep visitors go, where the site loses people, what route visitors take after landing, which pages or sections have the worst loss or exit rate, engagement depth, or how a section of the site performs. Also use for follow-up questions about visit paths, entry points, campaigns or engaged time once a report has been run.
SKILL.md
Run the Journey Report.
The staircase report asks how often a visitor comes back. This asks how much of the site a single visit gets through. They answer different questions and both are meant to exist.
It is built for content sites where signing someone up is not the KPI — the job is to bring people in, have them consume something, and have them leave with a better impression. So the report measures how far into the site a visit gets and where the site loses it. Conversions are counted but are never the headline.
Nothing in it is specific to one customer. Sections are derived from the URL path, so the report runs against any site or business unit without being told what that site contains first.
Output ONLY the report markdown, then a single file:// link to the HTML version. No preamble, no postscript, no re-narration. The report is the analysis; you are the pipe. This applies to conversational invocations exactly as it does to the slash command.
The one exception is the follow-up questions in the last section below. Those are answered in chat, in prose, because the report deliberately does not try to answer everything.
Steps
- Resolve the plugin root and the venv python. Re-run this whenever you start a new shell:
plugin_root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-<plugin-root>}}" plugin_python="${AGENTIC_ANALYTICS_VENV:-${AGENTIC_ANALYTICS_DATA_DIR:-$HOME/.local/share/agentic-analytics}/venv}/bin/python" [ -x "$plugin_python" ] || plugin_python="python3"<plugin-root>is the directory two levels above this SKILL.md. - Load the config. Read
${XDG_CONFIG_HOME:-~/.config}/agentic-analytics/bucket.jsonforbucket,profileandcache_dir. If it is missing, tell the user to run the init flow first (/agentic-analytics:init) and stop. Do not guess values.
That is the only config file. Journey settings, if there are any, live under ajourneykey in the same file — the patternidentify-employeesalready uses foremployee_filter. If the key is absent the report runs on defaults, which is the intended way to point it at a new site or business unit. There is nothing to create and nothing to copy.
What the report works out for itself: sections come from the URL path, so everything under/products/belongs to sectionproducts, and so does/productsitself. No list of sections, nothing read from page metadata. A leading locale is detected from the data and removed first, so/de-de/productsand/productsare one section and the report does not name six languages as its six biggest sections; it says so when it does this, andlocale_prefixesoverrides it. Two things a URL cannot tell you are the only entries worth adding — how the site tags paid traffic (source_rules) and which traffic blocs cannot be measured (traffic_quality_exclusions) — plus tuning such as the engaged-time bar. - Resolve the site.
--sitewins; thenjourney.site; then, if the lake holds exactly one apikey, that one. With several and no answer the engine refuses and lists them, rather than picking. - Top up the cache. Follow the
update-cacheskill to pull the latest data into the local lake. The pull is incremental, so an already-synced customer pays milliseconds. If the pull fails (auth lapse, S3 hiccup), fall through to whatever is in the lake — the report states its own coverage.
The Journey Report offers a 90-day window and each window compares against a prior window of equal length, so a full 90-day comparison needs 180 days on disk. The canonical 60-day pull keeps the cache current for the default 60-day window; the 90-day comparison accumulates over subsequent runs as the lake grows. Until it has enough data the engine withholds the change figure and says why, which is correct but less useful. - Run the report. Two ways, and the difference matters.
Serve it when the user is at their machine and might want to ask anything beyond what the page shows. This is the default; prefer it."$plugin_python" "$plugin_root/skills/journey-report/scripts/serve.py" \ [--site <site-id>] --windows 7,30,60,90 --default-window 60
It builds the report, opens the browser, and stays running so the question box on the page can reach the data. Tell the user in one line that it is running and that Ctrl-C stops it. Do not wait for it to exit.
Write files when the user wants something to keep, attach or send. A file has no server behind it, so the question box on that copy explains that rather than pretending.slug="<cache_dir>$([ -n "<site-id>" ] && echo "-<site-id>" || echo "-all")" "$plugin_python" "$plugin_root/skills/journey-report/scripts/journey.py" \ [--site <site-id>] \ --windows 7,30,60,90 --default-window 60 \ --output-md /tmp/journey-$slug.md \ --output-html /tmp/journey-$slug.html \ --output-json /tmp/journey-$slug.json
Neither takes--config: both read the plugin's ownbucket.json. Pass--siteonly when the user names one, or when the lake holds more than one site and the config does not say which.
If either exits non-zero, do not improvise. The engine refuses to render when a consistency check fails, by design: a plausible-looking wrong number is worse than no report, because nobody audits a number that looks fine. Show the user the error verbatim and stop.--no-strictexists for debugging and must never be used for a customer-facing run. - Output. When you served it, give the URL the server printed and stop —
the report is in the browser and the user reads it there. When you wrote
files, read
/tmp/journey-$slug.mdand emit its contents verbatim, then the HTML link on the last line, one blank line after the body:[Open the HTML version in browser](file:///tmp/journey-$slug.html)
Link the.html, never the.md— the markdown is what you just emitted. Thefile://protocol matters: a bare path gets intercepted by an editor.
The HTML carries more than the markdown, deliberately: the three-level route tree, the entry/campaign/source filters, the window switcher and the PDF export. Do not try to mirror it into chat.
What the report contains
Engagement depth is the metric everything else hangs off. A visit is a run of events from one visitor with no gap over the configured session gap. A qualifying action is a pageview of a URL not already seen in that visit that cleared the engaged-time bar, or a download, or a video play. Depth is the count of those, minimum 1. The headline is the share of visits at depth 2 or more.
Alongside it the report always shows the same share computed without the engaged-time rule. That contrast is not decoration: it is what demonstrates the metric is not ordinary pages per session, and the engaged-time signal comes from Parse.ly heartbeats arriving as events in the DPL, which is the commercial differentiator.
The headline will be low. Never inflate it by excluding single-action visits from the denominator, and never count non-engaged pageviews toward it. It is a baseline to improve.
Route tree, anchored on a selectable entry point and going forward three levels. Branches are sections, so it reads as "of the visits that landed on /products, how many went on to /news, to /support, or to another page inside /products". Exit is a first-class branch at every level, listed first, with its own share and volume, because it is the largest outcome. No conversion gating: every path is traced whether or not it ends in a conversion. That gating is the specific limitation of Parse.ly's own Journeys view that this section exists to fix.
Loss map, pages ranked by the share of visits that end on them, with the most common next step for those who did continue.
Section scoreboard, with size in one column and pull in the other. The finding lives in the contrast, not in either column alone. Sections are derived, so this table needs no list of what the site contains.
Site shape, measured rather than assumed. Sections read correctly where the site has a section layer and degenerate on a flat site, where every article sits at the root and becomes a section of one page. Both are real shapes; the report states which one it is looking at instead of applying a threshold that would make a flat site look structured.
Composition tiles, so the headline is auditable: engaged against loaded pageviews, downloads, video plays, visits, unique visitors, median engaged time.
Traffic set aside. Blocs configured as traffic-quality exclusions are reported with their own volume and headline rather than dropped. Where a signal is genuinely absent, say which signal is missing and what it would unlock. Never render a silent zero.
Filter scope — decided, do not change
| Filter | Scope |
|---|---|
| Time window (7 / 30 / 60 / 90 days) | Global. Every section recomputes. A header saying "last 7 days" while any number on the page still reflects 60 is a correctness bug, not a gap. |
| Entry point | Local to the route tree |
| Campaign | Local to the route tree |
| Entry source | Local to the route tree |
Every window compares against the immediately prior window of equal length, anchored on the latest complete day in the data, never on today.
Follow-up questions
The report is a starting point, not the product. There are two places a follow-up can be answered, and they are good at different things.
The question box on the served page is you. It hands the question to
claude verbatim and renders the reply, so the user gets the command line
without leaving the report. Nothing is added to the question and nothing is
pre-answered; whatever you would say in a terminal is what appears in the box.
Point the user at it rather than asking them to switch windows.
It runs headless with an explicit read-only allow-list, so a question can read
the data but cannot change anything. If the box reports that tools were denied,
the machine's permission settings are stricter than the default; starting the
server with JOURNEY_ASK_PERMISSION_MODE=bypassPermissions is the way out.
You, here in the conversation, for anything it cannot do: an odd cut, a combination of filters, a question that needs judgement rather than a table. Query the same derived tables rather than guessing or re-reading raw parquet. Build them once, then use SQL:
"$plugin_python" - <<'PY'
import sys, json
sys.path.insert(0, "PLUGIN_ROOT/scripts/db/lib")
import catalog, sessions
cfg = json.load(open("CONFIG_PATH"))
con = catalog.connect(); catalog.ensure_view(con, "dpl", "events")
lo, hi = sessions.available_days(con, cfg["site"])
sessions.build(con, cfg, site=cfg["site"], day_start=lo, day_end=hi)
# ... then query j_visit / j_step / j_pv / j_sess
PY
The tables, and what one row means in each:
| Table | One row is | Key columns |
|---|---|---|
j_pv | a pageview | vsid, ts, url_path, title, engaged_seconds, section, is_section_landing, source, campaign, exclusion |
j_act | anything that can earn a point | kind in pageview / download / videostart, conv_type, conv_label |
j_sess | j_act plus a visit id | sid |
j_step | a distinct URL within one visit, in order | step_idx (0 is the entry), qualifies, engaged_seconds |
j_visit | a visit | entry_path, entry_title, entry_section, source, campaign, depth_qualified, depth_raw, loaded_pageviews, downloads, video_plays, max_step_idx, exclusion |
Filter exclusion IS NULL to match the report's clean base. Three worked examples:
-- which pages send the most traffic onward into a different section
SELECT s.title, count(*) AS onward
FROM j_step s JOIN j_step n
ON n.vsid = s.vsid AND n.sid = s.sid AND n.step_idx = s.step_idx + 1
JOIN j_visit v ON v.vsid = s.vsid AND v.sid = s.sid
WHERE v.exclusion IS NULL AND s.section <> n.section
GROUP BY 1 ORDER BY onward DESC LIMIT 20;
-- entry pages with the worst loss rate, this month against last
SELECT v.entry_title,
date_trunc('month', v.started_at) AS month,
count(*) AS visits,
round(100.0 * avg(CASE WHEN v.max_step_idx = 0 THEN 1 ELSE 0 END), 1) AS loss_pct
FROM j_visit v WHERE v.exclusion IS NULL
GROUP BY 1, 2 HAVING count(*) > 200 ORDER BY 1, 2;
-- for campaign traffic, the most common three-page path
SELECT s0.url_path AS p1, s1.url_path AS p2, s2.url_path AS p3, count(*) AS visits
FROM j_visit v
JOIN j_step s0 ON s0.vsid = v.vsid AND s0.sid = v.sid AND s0.step_idx = 0
JOIN j_step s1 ON s1.vsid = v.vsid AND s1.sid = v.sid AND s1.step_idx = 1
JOIN j_step s2 ON s2.vsid = v.vsid AND s2.sid = v.sid AND s2.step_idx = 2
WHERE v.exclusion IS NULL AND v.campaign = 'PaidCampaign'
GROUP BY 1, 2, 3 ORDER BY visits DESC LIMIT 20;
Answer follow-ups in prose with the numbers inline. Two rules carry over from the report itself: state the volume behind any rate you quote, and if the answer is that the data cannot support the question, say that instead of producing a number.
Constraints (re-stated for emphasis)
- No "Running the journey report…" preamble, no "Two things stand out…" postscript.
- If a consistency check fails, surface the error and stop. Do not render partial output and do not reach for
--no-strict. - If the lake has no events for the configured site, say so in one line and stop.
- The trailing link points at the
.htmlfrom step 4, never the.md. - Sections are derived from the URL and must stay that way. If you find yourself wanting to write a list of the site's sections, or to read one from page metadata, stop: that is how this became a bespoke report for one customer the first time.
- There is one config file and the plugin already owns it. Do not add a second.
More skills from the agentic-analytics repository
View all 8 skillsclear-configs
remove local agentic analytics configuration files
Jul 24AWSConfigurationOperationsconversions-report
generate conversion attribution reports
May 23AnalyticsReportingSalesgetting-started
set up agentic-analytics plugin
Aug 13AnalyticsDocumentationReferenceidentify-employees
filter internal employee traffic from reports
Jul 24AnalyticsData CleaningOperationsrefresh-cache
clear and rebuild local data cache
Jul 24CachingDebuggingPerformancestaircase-report
generate staircase audience relationship reports
Aug 13AnalyticsCustomer SuccessReporting
More from Automattic
View publisherannotate
collect visual feedback with browser annotation tools
studio
May 6FrontendProductivityUX CopyUX Designblock-content
write editable WordPress block markup
studio
May 27Block EditorCSSHTMLWordPresshosting-plans-helper
provide WordPress.com hosting plan information
studio
Jul 2PricingReferenceWordPressliberate
migrate websites to WordPress
studio
Jul 9CMSMigrationWeb DevelopmentWordPressneed-for-speed
run frontend performance audits for WordPress sites
studio
May 6AuditFrontendPerformanceWordPressplugin-recommendations
recommend WordPress plugins for site features
studio
May 27Content CreationPlugin DevelopmentWordPress