Flows

Analytics & experiments

How Flows measures a funnel without ever handling PHI, and how presentation-plane A/B tests optimize conversion without touching the clinical questions.

A published funnel raises two questions you'll want answered continuously: how well is it converting, and can a different version of the marketing convert better? Flows answers both from data that is PHI-free by construction — the analytics count events, not people, and every experiment is confined to the presentation plane, so no measurement or test ever reaches a patient's medical answers. This is the promise: analytics that cannot leak PHI — by schema, not by policy.

Reading either surface requires the FLOWS_ANALYTICS_VIEW permission. See Roles & permissions for how the Flows roles split.

Where the numbers come from#

Every session emits a small set of PHI-free events as the patient moves through the funnel — the same event log described in The PHI boundary, scoped to Flows:

Event Emitted when
flow.viewed The hosted page or embed renders and a session is created.
step.completed A patient finishes a step (partial save). Carries the stepId, never the answers.
flow.qualified The submission evaluated to an eligible verdict.
flow.disqualified The submission hit a disqualifying answer or rule guard.
experiment.exposed A session was assigned to an experiment arm. Carries the experimentId and the variant key.

None of these carries a name, an email, or a clinical answer — only ids, a step id, and a verdict. The patient's answers are encrypted at rest and never reach the analytics tables. That's the difference between analytics that shouldn't leak PHI and analytics that can't: there is no field on these events to leak.

Funnel analytics#

GET /flows/:id/stats

Returns the aggregate performance of a single flow — top-of-funnel views, sessions started, qualified vs. disqualified outcomes, and per-step completion counts so you can see exactly where patients drop off.

curl https://api.neolife.health/v1/flows/flow_7h2k/stats \
  -H "Authorization: Bearer $NEOLIFE_API_KEY"
{
  "views": 4210,
  "sessions": 3180,
  "qualified": 1120,
  "disqualified": 640,
  "qualifyRate": 0.266,
  "byStep": [
    { "stepId": "landing", "completes": 3180 },
    { "stepId": "goals", "completes": 2740 },
    { "stepId": "medical-history", "completes": 1980 },
    { "stepId": "consent", "completes": 1760 }
  ]
}
Field Type Description
views number Times the funnel rendered (flow.viewed). The denominator for qualifyRate.
sessions number Distinct sessions started on this flow.
qualified number Submissions that reached an eligible verdict (flow.qualified).
disqualified number Submissions stopped by a disqualifying answer or rule guard (flow.disqualified).
qualifyRate number qualified / views, or 0 when there are no views.
byStep array Per-step completion counts, one entry per stepId. Read top-to-bottom, the falling counts are your drop-off curve.

The byStep array is the workhorse here: a large fall between two adjacent steps is a presentation problem you can fix and — with an experiment — prove you fixed. A fall at a clinical step is usually a real disqualification, not a copy problem; those are patients the funnel is supposed to stop. Don't optimize a clinical step's drop-off away. If the medical questions are turning away patients you believe should qualify, that's a questionnaire question, handled through the certified corpus — see Intake & provider approval.

The qualify in qualifyRate is the deterministic verdict, not a sale. A qualified session cleared the intake gate and produced a certificate; it has not yet checked out or been approved by a provider. Checkout is a handoff to the clinic's own store, and provider approval is a separate licensure act. qualifyRate measures the funnel, not revenue.

A/B experiments#

An experiment runs two versions of a funnel against each other — a control (the published flow as-is) and a variant (the same flow with a set of presentation-plane overrides) — and reports which converts better. The rule that makes this safe is absolute:

An experiment may only patch the presentation plane. Landing copy, outcome copy, theme tokens, step order — anything the AI and the clinic are free to author. It may not change a medical question, a disqualifier, or a consent block. A variant that targets a clinical step is rejected at creation time.

Because the variant only changes presentation, both arms produce the identical clinical-plane hash — the certificate a patient earns is exactly the same whether they saw control or variant. You are testing the marketing, never the medicine. AI drafts, a licensed provider signs; an experiment doesn't get to touch either.

Create an experiment#

POST /flows/:id/experiments

Creates and starts an experiment on a flow's active (or draft) version. overrides is a list of step patches; each is validated against the source definition, and any patch that targets a clinical step is rejected.

Body field Type Description
name string A label for the experiment.
splitPct number The share of sessions assigned to the variant arm (the rest are control).
overrides array Presentation-plane step patches applied to build the variant.
curl -X POST https://api.neolife.health/v1/flows/flow_7h2k/experiments \
  -H "Authorization: Bearer $NEOLIFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Shorter landing headline",
    "splitPct": 50,
    "overrides": [
      { "stepId": "landing", "blocks": { "heading": "Start in two minutes" } }
    ]
  }'
{
  "id": "fexp_9m1x",
  "name": "Shorter landing headline",
  "status": "running",
  "splitPct": 50
}

If an override names a step that doesn't exist, or names a clinical step, the request fails with a 400 and a message that says which step — the guard runs before the experiment is created, so a bad experiment never goes live.

How assignment works#

Assignment is deterministic and sticky, decided server-side. When a session starts on a flow with a running experiment, the arm is derived from the session id and the experiment's split — so the same session always sees the same arm across partial saves and reloads, and a patient never flips between control and variant mid-funnel. There is no client-side coin flip and nothing to tamper with. Each assignment emits an experiment.exposed event carrying the experimentId and the variant key, and the session is stamped with its variantKey (control or variant) for later attribution.

Read results#

GET /flows/:id/experiments

Lists the flow's experiments, newest first, each with its live results — sessions and qualified counts per arm, and the qualify rate for a direct control-vs-variant comparison.

curl https://api.neolife.health/v1/flows/flow_7h2k/experiments \
  -H "Authorization: Bearer $NEOLIFE_API_KEY"
[
  {
    "id": "fexp_9m1x",
    "name": "Shorter landing headline",
    "status": "running",
    "splitPct": 50,
    "winnerVariant": null,
    "startedAt": "2026-07-08T15:00:00Z",
    "results": {
      "control": { "sessions": 812, "qualified": 214, "qualifyRate": 0.263 },
      "variant": { "sessions": 798, "qualified": 251, "qualifyRate": 0.314 }
    }
  }
]
Field Type Description
status string running or stopped.
splitPct number The variant's assignment share.
winnerVariant string | null The arm declared the winner when the experiment was stopped; null while running.
startedAt string (date-time) When the experiment began.
results.control / results.variant object Per-arm sessions, qualified, and qualifyRate (qualified / sessions, or 0 with no sessions).

Both arms measure the same thing the funnel-level qualifyRate measures — the eligible verdict — so the comparison is apples-to-apples. A variant that qualifies more sessions is better presentation for the same clinical bar, which is exactly what you want to optimize.

Stop an experiment#

POST /flows/experiments/:expId/stop

Ends an experiment. Optionally record which arm won; the flag is stored on the experiment for the record and does not itself promote the variant.

Body field Type Description
winnerVariant string Optional. The arm to record as the winner (for example variant).
curl -X POST https://api.neolife.health/v1/flows/experiments/fexp_9m1x/stop \
  -H "Authorization: Bearer $NEOLIFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "winnerVariant": "variant" }'
{ "ok": true }

To make the winning variant permanent, fold its overrides into the flow itself in the builder and re-publish. Re-publishing runs the same licensure gate every publish does — even a change that only touches presentation copy re-records the clinical-plane hash and its provider approver, so the published flow is always a signed one.

Feeding analytics downstream#

The stats and experiment endpoints are for reading in the console. To move funnel signal into your own tools, use the two PHI-safe rails:

  • The event log and webhooks. flow.viewed, flow.qualified, flow.disqualified, and the other flow events flow through the same durable, signed Events API and webhooks as the rest of the platform. Ad-pixel and CRM destinations are wired off these — see Integrations for what each tier of destination is allowed to receive.
  • Hydrate on demand. These events carry ids and a verdict, never a person. When you need to attribute a qualified session to a contact, read it over the authenticated API at the moment you use it, the way The PHI boundary describes.

Recap#

  • Funnel analytics (GET /flows/:id/stats) count views, sessions, qualified, disqualified, and per-step drop-off — from events that carry no PHI, so the numbers are safe to expose anywhere.
  • qualifyRate measures the deterministic intake verdict, not a sale or a provider approval; a clinical step's drop-off is usually a real disqualification, not a copy bug.
  • A/B experiments patch only the presentation plane, so both arms share an identical certificate — you optimize the marketing, never the medicine.
  • Assignment is deterministic, sticky, and server-side; a variant that targets a clinical step is rejected before it goes live.
  • Promote a winner by folding its overrides into the flow and re-publishing through the same provider-signed publish gate.

Related#

  • Flows API — the full endpoint reference for stats, experiments, and the public runtime.
  • Intake & provider approval — the deterministic verdict qualifyRate counts, and why clinical drop-off is by design.
  • The PHI boundary — why flow events and analytics carry only ids and status.