> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Reading flow analytics: funnel drop-off and A/B results

> Read the Flow Analytics tab — entered / completed / converted counts, per-node drop-off funnel, and per-variant A/B experiment winners — for any published flow, plus the analytics and ab-results API endpoints.

# 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\_rate** — `completed / entered`, or `null` when nobody entered.
* **conversion\_rate** — `converted / 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:

| Param                      | Effect                                                                             |
| -------------------------- | ---------------------------------------------------------------------------------- |
| `since`, `until`           | ISO datetimes bounding the entry window (executions started within).               |
| `window_days`              | Attribution window, 1–365 days after a contact exits the flow.                     |
| `goal_event`               | Count this event as the conversion.                                                |
| `goal_trait`, `goal_value` | Count an identify setting this trait (optionally to this value) as the conversion. |

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:

```json theme={null}
{
  "id": "flow_welcome_1",
  "definition": {
    "nodes": [],
    "edges": [],
    "conversionGoal": {
      "type": "event",
      "event": "Order Completed",
      "windowDays": 7
    }
  }
}
```

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:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/flows/flow_welcome_1/analytics?window_days=14&goal_event=Signed%20Up" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

```json theme={null}
{
  "data": {
    "flow_id": "flow_welcome_1",
    "since": "2026-08-25T00:00:00.000Z",
    "until": "2026-09-08T00:00:00.000Z",
    "window_days": 14,
    "goal": { "type": "event", "event": "Signed Up" },
    "entered": 1200,
    "completed": 1080,
    "converted": 402,
    "completion_rate": 0.9,
    "conversion_rate": 0.335,
    "funnel": [
      {
        "node_id": "trigger",
        "node_type": "trigger",
        "label": "Inbound webhook",
        "reached": 1200,
        "dropped_off": 0,
        "step_conversion_rate": null,
        "overall_conversion_rate": 1
      },
      {
        "node_id": "email_1",
        "node_type": "email",
        "label": "Welcome email",
        "reached": 1196,
        "dropped_off": 4,
        "step_conversion_rate": 0.9967,
        "overall_conversion_rate": 0.9967
      }
    ]
  },
  "meta": { "request_id": "req_...", "timestamp": "2026-09-08T00:00:00.000Z" }
}
```

Fetch experiment winners:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/flows/flow_welcome_1/ab-results" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

```json theme={null}
{
  "data": {
    "flow_id": "flow_welcome_1",
    "since": "2026-08-25T00:00:00.000Z",
    "until": "2026-09-08T00:00:00.000Z",
    "window_days": 14,
    "goal": { "type": "event", "event": "Signed Up" },
    "experiments": [
      {
        "node_id": "ab_1",
        "variants": [
          {
            "variant_id": "a",
            "variant_label": "Short subject",
            "assigned": 500,
            "completed": 470,
            "converted": 190,
            "completion_rate": 0.94,
            "conversion_rate": 0.38
          },
          {
            "variant_id": "b",
            "variant_label": "Long subject",
            "assigned": 500,
            "completed": 450,
            "converted": 135,
            "completion_rate": 0.9,
            "conversion_rate": 0.27
          }
        ],
        "winner_variant_id": "a"
      }
    ]
  },
  "meta": { "request_id": "req_...", "timestamp": "2026-09-08T00:00:00.000Z" }
}
```

## 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.

<Note>
  Both endpoints are tenant-scoped reads: they aggregate only your own flow's recorded executions. Rates and counts can never leak across tenants.
</Note>
