# Measure bounded session paths

> Ask your agent to measure which events follow or led to one anchor event, walk the exact instances behind an edge, read their replay evidence, and open the result as a proof page.

Canonical page: https://anectico.com/docs/investigate/product-paths/


Paths measure what resolved people did **around one event**, inside the same session: which nodes
followed it (forward) or led to it (backward), up to a bounded number of steps and a bounded time
horizon. It answers a narrower, more precise question than a funnel — "what happens after
`checkout_started`?" rather than "did people complete an ordered sequence?" — by walking every
qualifying session once and reporting exactly what it found.

Your agent runs the measurement through MCP or the CLI. You open the stored result as a read-only
proof page.

## Ask your agent

> "What do people do in the 30 minutes after `checkout_started`, in the same session? Measure the
> last 7 days and send me the proof link."

> "Take the busiest edge in that result and list the exact sessions behind it, with their replay
> evidence."

| Job | MCP tool or action | CLI command |
| --- | --- | --- |
| Check which path shapes and bounds the server accepts | `get_product_query_capabilities` | `anectico analytics capabilities` |
| Measure paths around one anchor event | `query_product_analytics` | `anectico analytics query` |
| Read a stored paths result again | `get_analytics_result` | `anectico analytics result get` |
| Page the exact walked instances behind an edge | `list_analytics_path_instances` | `anectico analytics result path-instances` |
| Page the resolved people behind an edge | `list_analytics_participants` | `anectico analytics result participants` |

These are MCP read actions. Your agent finds them with `list_read_actions` and runs them with
`execute_read_action`. Measuring needs `analytics:query`, `analytics:read` and `persons:read`;
reading the instances and the people needs `analytics:read` and `persons:read`; over MCP the key
also needs `mcp:read`. Reading page-path node keys needs the content read permission (see
[Define a measurement](#define-a-measurement)).

## What your agent gets back

The response carries `paths.manifest`, `state`, `summary`, `steps`, `edges`, `other`, and the two
selection registries described below. Every count keeps occurrences, instances and people apart
(see [Read the counts](#read-the-counts-occurrences-instances-and-people-are-never-the-same-number)).
Every displayed edge declares an `instances_selection_id` and a `people_selection_id`, which your
agent pages with the last two tools in the table.

Over MCP, the result carries a link to the proof page in its `links`, in the entry titled
"Analytics result" (the address is its `href`). Every tool in the table that reads this stored
result carries the same link. The CLI prints the result and no link.

## Open the proof

Open the link from your agent. It opens the stored result in the read-only result viewer, with the
project named in the page header. See [Proof pages](/docs/agents/proof-pages) and
[Result viewer](/docs/agents/result-viewer).

The page never measures again. It shows what was stored, when it was measured (in UTC) and when the
stored result expires. Its one action is **Copy link**. To change the question, ask your agent to
measure again with a new definition and a new execution key.

You see:

- a state banner, then the summary, including the terminal breakdown and `excluded_no_session`;
- one bar per step, showing its node breakdown with `other` folded in;
- the edge table described below. An edge's instances count opens the walked paths behind it —
  timestamps, the highlighted transition step, the terminal and the replay reference — and its
  people count opens the resolved people behind it.

## What one path instance is

For each **(subject, session)**, a path instance starts at the **first** anchor occurrence inside
the measurement window. Anchor occurrences that carry no session ID are never stitched to
anything; they are counted in the summary's `excluded_no_session` and never become an instance.

From the anchor, the walk follows the same subject's events of the **same session only** — never
another session, even the same person's — for up to `transitions` steps, within `horizon_minutes`
of the anchor. Forward reads follow-up after the anchor; backward reads history before it, and that
history must be inside your retained, readable window or the measurement fails
`HISTORY_UNAVAILABLE`.

## Define a measurement

```json
{
  "version": 2,
  "timezone": "UTC",
  "window": {"relative": {"lookback_seconds": 604800}},
  "paths": {
    "anchor": {"name": "checkout_started"},
    "direction": "forward",
    "transitions": 2,
    "horizon_minutes": 30,
    "node_key": "event"
  }
}
```

Run it the same way as any other kind: `anectico --project PROJECT_UUID analytics query --file
paths.json --execution-key EXECUTION_UUID`, MCP's `query_product_analytics`, or `POST
/api/v1/analytics/query`.

- **`anchor`** is exactly one event selector, with its own event-property filters if you need them
  — a path's nodes are every event of the anchor's own session, so an event predicate belongs on
  the anchor, never at the top level. Person-property filters, cohorts and environment restrict
  which **subjects** can anchor a path, the same way they do for any other kind.
- **`direction`** is `forward` (what happened after the anchor) or `backward` (what led to it).
- **`transitions`** is 1 to 4 steps.
- **`horizon_minutes`** is 1 to 30; omitted or `0` normalizes to 30.
- **`node_key`** is `event` (the default) or `event_and_path`, which additionally keys a `$pageview`
  node by its sanitized route — an invalid route keeps an empty page for that node, it never falls
  back to a different key. Reading page-path node keys needs the content read permission; without
  it, a request naming `event_and_path` is refused the same way a missing permission refuses
  anything else.

Paths are **person-unit only** (`QUERY_UNIT_UNSUPPORTED` for the account unit) and accept event
filters on the anchor only: a breakdown, a comparison window, a top-level event filter, a
`filter_group`, or an account filter is refused with `QUERY_PATHS_INVALID`, as is a missing or
unknown direction/node key, a missing anchor, or zero transitions. More than 4 transitions or 30
horizon minutes is `QUERY_PATHS_LIMIT`. `anectico analytics capabilities` (or the equivalent
MCP/REST discovery call) reports the kind with `path_directions`, `path_node_keys`, and every
`max_path_*` bound — a shape it does not list is refused before anything runs.

## How the walk decides what happened

A repeat of the node you are already on is **absorbed** — it is examined, but it is never a
transition, so three page views of the same route in a row are one node, not three. Events that
land at the **exact same millisecond** and name more than one distinct node cannot be ordered: no
guess is made, and the walk stops there with the terminal `ambiguous_order`. A tie of otherwise
identical events at the same millisecond — the same node reached twice at once — is not ambiguous;
it is a repeat, absorbed the same way any other repeat is.

Every instance ends in exactly one of six terminals, and the six always sum to the total instance
count:

| Terminal | Meaning |
| --- | --- |
| `continues_beyond_limit` | The walk used every allowed transition, and the session shows a further transition inside the horizon. |
| `session_end` | No further event of the session inside the horizon after the last transition. |
| `horizon` | The session continued inside the horizon, but only repeating the current node — the horizon, not the session, ended the walk. |
| `ambiguous_order` | A same-millisecond tie of different nodes stopped the walk. |
| `truncated` | The instance examined the platform's bounded event budget without a decided terminal. |
| `immature` | A **forward** instance whose horizon had not closed at the moment the result was measured — never a fabricated `session_end`. Backward paths, which only read history, are never immature. |

`state` on the result reflects what could be established: `measured` (every terminal decided),
`partial` (at least one instance is `immature` — its path may still grow when you measure again),
`insufficient_evidence` (coverage of the scanned span was not sufficient — counts are disclosed as
observed), or `unavailable` (platform coverage could not be established at all — nothing is
disclosed: no summary, no steps, no edges, no selections).

## Read the counts: occurrences, instances, and people are never the same number

Every count in a paths result keeps three things apart, on purpose:

- **occurrences** — every time an instance made a transition. A loop (A→B→A→B) makes the A→B
  transition twice, so its occurrences are 2.
- **instances** — distinct walked paths. The same loop is still one instance.
- **people / unresolved** — distinct resolved people, and distinct unresolved identities, among
  those instances, kept separate and never merged.

`steps[k]` lists the nodes instances reached at walk distance `k` (`k=0` is the anchor itself),
ranked by instances, then event and page. Only the most frequent nodes are shown per step; the rest
are folded into that step's `other`, which discloses how many distinct nodes were omitted and how
many instances they account for — the displayed nodes plus `other` always partition the step's
reach exactly.

`edges` lists the displayed transitions, time-ordered `from` → `to` whatever the walk direction,
with occurrences, instances, and people/unresolved kept separate as above. `other` is the
**deduplicated union** of every transition not displayed: its `occurrences` is a sum (loops still
add), but its `instances` and `people` count each contributing instance and person **once**, even
if that instance made two different omitted transitions. The proof page labels this row
"Other · union, deduplicated" for exactly that reason — it is not the sum of the omitted edges'
individual counts.

## Page the exact walked instances, with replay evidence

Every displayed edge (and `other`, when it has omitted edges) declares an `instances_selection_id`.
Page it — 1 to 50 at a time — with `anectico analytics result path-instances RESULT_UUID
--selection-id SELECTION_UUID`, MCP's `list_analytics_path_instances`, or `GET
/api/v1/analytics/results/{id}/path-instances`. Each instance is one frozen walked path:

- `path_instance_id` — stable across reads of the same instance, and never a person or participant
  ID.
- `contributor`, `session_id`, `anchor_time`, and `terminal`.
- `events` — the walked nodes in walk order (the anchor at step 0, then the first event of each
  transition), each with its own timestamp.
- `edge_steps` — the transition steps at which **this selection's** edge (or, for `other`, any
  omitted edge) occurs in this instance; a loop lists more than one.
- `replay` — always present, because every instance has a session ID: `available` opens the
  recording, `none_recorded` is an honest absence (a session that was never recorded), and
  `withheld` means your current credential cannot read session replay. Treat `withheld` as a
  permissions gap, never as proof nothing was recorded — the proof page's instance panel never
  collapses the two.

An instance selection is a page of **walked paths**, not a population of people: it cannot become
an audience and does not page through the participants endpoint.

## People and audiences

Every displayed edge (and `other`) also declares a `people_selection_id`: the distinct resolved
people who made that transition, an ordinary resolved-people population. Page it with `anectico
analytics result participants`, the equivalent MCP call (`list_analytics_participants`), or the REST
participants route, and create an audience from it exactly the way you would from any other kind's
selection (`create_analytics_audience`, or `anectico analytics audiences create`).

## What paths cannot do

A paths result is a graph of selections, not the one scalar or row stream some other operations
need:

- A **metric watch** refuses a paths definition outright (`UNSUPPORTED_RESULT_SELECTOR`).
- A **Live panel** refuses to render a paths result the same way.
- An **export** of a paths result is refused: its evidence is the instance pages above, which an
  export does not carry. Page the walked instances to review that evidence.
- `analytics result contribution` has no drill-down chain for paths either — the instance pages
  above are the evidence, not a separate contribution read.

A saved insight of a paths definition, and a dashboard widget bound to one, render normally: the
widget shows the same state, summary, and step/edge shapes described above.
