Skip to main content

Reading flow analytics

Every published flow has an Analytics tab that answers two questions: how many people move through the flow (the conversion funnel), and which variant of each A/B experiment is winning. The tab reads two endpoints — GET /flows/:id/analytics for the funnel and GET /flows/:id/ab-results for experiments — against the flow’s recorded execution history.

Where to find it

The Analytics tab lives inside the Flows page, not on a standalone route:
  1. Open Flows in the dashboard and click the Analytics tab.
  2. Pick a flow from the picker. The selected flow lives in the URL (/flows?tab=analytics&flow=<flow-id>), so you can bookmark or share a link that lands on a specific flow’s numbers.
A bare /flows/analytics visit redirects to /flows?tab=analytics — the directory exists only to host the tab’s component, not as a page in its own right.

Reading conversion analytics

GET /flows/:id/analytics returns the flow-level rollup plus the per-node funnel:
  • entered — executions that started within the entry window (the trailing window by default, or between since and until).
  • completed — of those, how many ran to a successful finish.
  • converted — how many contacts then hit the conversion goal (an event or a trait update) within the attribution window.
  • completion_ratecompleted / entered, or null when nobody entered.
  • conversion_rateconverted / entered, or null when entered is zero.
  • funnel — one step per node, ordered by traversal (see below).
The goal is read from the flow’s saved definition unless you override it per request. Override params: An override wins over the stored goal — use it to compare funnels against different definitions of “converted.”

Per-node drop-off

The funnel lists one step per node, ordered breadth-first from the trigger — the same order contacts traverse the graph. Nodes unreachable from a trigger still appear, appended in definition order. Each step carries:
  • reached — executions that touched this node.
  • dropped_off — how many of the previous step’s traffic did not reach this step (0 on the entry node).
  • step_conversion_rate — share of the previous step’s traffic that reached this one.
  • overall_conversion_rate — share of total entries that reached this node.
Skipped branches — the “no” arm of a condition, the unused arm of an experiment — show up as a large drop on one branch and a healthy rate on the other. That is the funnel doing its job: compare rates against the previous step, not against the flow entry, to find where traffic actually leaks.

A/B experiment results

GET /flows/:id/ab-results rolls up every abTest node in the flow, accepting the same override params as /analytics:
  • experiments — one entry per experiment node: its variants plus winner_variant_id.
  • One row per variant — variant_id, variant_label, assigned (executions routed into it), completed, converted, and both rates.
  • winner_variant_id — the leading variant by goal-conversion rate (or by completion rate when no goal is configured). It is null on a tie or when no variant has assignments yet — the UI never declares a false winner.

Define a conversion goal

The goal lives on the flow definition under conversionGoal — either an event name or a contact trait (optionally pinned to a value), with an optional attribution windowDays (1–365). Save it on the definition like any other node config:
Trait-based goals use { "type": "trait", "trait": "plan", "value": "paid" } — only contacts whose plan was set to paid within the window count as converted. Without a goal, converted stays zero and experiments rank variants by completion rate instead.

curl examples

Fetch the funnel for a flow, overriding the goal to an ad-hoc event:
Fetch experiment winners:

Common gotchas

  • Empty experiments array — the flow has no abTest nodes, or none produced a run in the window. Add an experiment node and republish to populate it.
  • winner_variant_id is null — a dead tie, or no variant has any assignments yet. Wait for more traffic; the tab does not crown a winner on zero data.
  • All rates are null — nothing entered in the window, so division has no denominator. Widen since/until or check the trigger still fires.
  • The goal never fires — without a stored goal or a query override, converted stays zero and every conversion_rate is null. Save conversionGoal on the definition, or pass goal_event / goal_trait per request.
  • Override surprises — a query-supplied goal wins over the stored one, so two tabs looking at “conversion” can disagree. The response echoes the resolved goal field; always check it.
  • Rates are fractions, not percents — multiply by 100 (and guard the null case of an empty denominator) before rendering.
Both endpoints are tenant-scoped reads: they aggregate only your own flow’s recorded executions. Rates and counts can never leak across tenants.