Skip to Content
ReferenceRuntime API

Runtime API

This page covers the parts of a funnel that aren’t React components: the window.appfunnel client bus, the events your funnel emits, @variant experiment files, and the project files that are generated for you.

The window.appfunnel bus

Every event your funnel emits is available on window.appfunnel, a client-side object present on every funnel page. A script on your page subscribes to it to react to funnel events, and can also read values and drive navigation.

interface AppfunnelBusApi { on(event: string, cb: (data: unknown) => void): () => void // subscribe; '*' for all. Returns unsubscribe. off(event: string, cb: (data: unknown) => void): void readonly events: BusEvent[] // bounded replay history getVariable(key: string): unknown getVariables(): Record<string, unknown> getCurrentPageId(): string | null getCustomerId(): string | null getVisitorId(): string | null goTo(pageKey: string): boolean // false for an unknown key or before the flow mounts goNext(): void goPrevious(): void navigation(): NavSnapshot | null // null before a <FunnelView> mounts }
// A late-loading pixel: replay past events, then subscribe to future ones. const af = window.appfunnel for (const e of af.events) handle(e) const unsub = af.on('purchase.complete', (data) => { fbq('track', 'Purchase', { value: data.amount, currency: data.currency }) })
MemberBehavior
on(event, cb)Subscribe to one event, or '*' for all (the wildcard callback receives { event, data }). Returns an unsubscribe function.
off(event, cb)Unsubscribe.
eventsA bounded replay history (last 250 events). A pixel that loads after funnel.start / page.view can read the events it missed here, then on() for the rest.
getVariable(key)Read a single store variable.
getVariables()Read all variables as a map.
getCurrentPageId()The current page id, or null.
getCustomerId() / getVisitorId()The durable identity ids, or null.
goTo(pageKey)Jump the flow to a page key. Returns false for an unknown key or before the flow mounts.
goNext()Advance via the current page’s routing, else the linear next page.
goPrevious()Step back to the previous page in flow history.
navigation()The current flow snapshot (NavSnapshot), or null before the flow mounts.

NavSnapshot is { key, index, total, progressPercentage, canGoBack, nextCandidates }.

The bus is read-only by design: it has no emit and no setVariable, so a page script can observe events and navigate, but can’t fire events or change funnel state. goTo / goNext / goPrevious do nothing until the funnel has mounted (they warn once so an early call is easy to spot).

The events your funnel emits

Your funnel emits a fixed set of events automatically — as pages change, emails are captured, and charges settle. You don’t fire these; you subscribe to them (on the bus, or an installed pixel does). The one thing you can emit is a custom event, via useTracker.

Automatic events

EventFired whenPayload highlights
funnel.startThe funnel view mounts.{}
page.viewEach page change.{ pageId, pageKey?, isInitial?, eventId? }
page.exitLeaving a page.{ pageId, durationMs, activeMs } (active = visible time)
user.registeredThe first user.email write (identify()).{ email, eventId? }
experiment.exposureFirst landing on an experiment slot.{ experimentId, variant, version? }
checkout.startA checkout begins.{ productId?, amount?, currency?, surface?, eventId? }
checkout.payment_addedAn inline driver reports a payment method added.{ productId?, amount?, currency?, eventId? }
checkout.failedA charge fails.{ category, declineCode?, productId?, intent? }
purchase.completeA charge settles.{ amount?, currency?, productId?, eventId? }
subscription.createdA subscription settles.{ amount?, currency?, productId?, eventId? }

Writing a response (useResponse / funnel.responses.set(…)) does not emit a discrete event — so if you need to track an answer, send a custom event with useTracker.

Custom events

import { useTracker } from '@appfunnel-dev/sdk' function Quiz() { const { track } = useTracker() return <button onClick={() => track('quiz_skipped', { step: 3 })}>Skip</button> }

useTracker().track(name, data?) sends your own custom event — the ones the automatic events above don’t cover. Those fire on their own; don’t re-emit them.

@variant experiment files

Page-level A/B/n on a single page inside one funnel. The only source-side marker you write is the @ in a variant file’s name: pages/welcome@b.tsx is a variant of the welcome page. The flow keeps the stable page key (welcome); only the rendered component swaps per visitor.

pages/ welcome.tsx # the control (slot 'welcome') welcome@b.tsx # a variant of the same slot

What’s code vs what’s configured:

  • You write (code): the variant page fileswelcome@b.tsx. That’s the whole author surface.
  • You configure (in the dashboard, not in funnel source): the experiment — which variants are in the test, their weights, the status, and the winning metric. Changed anytime without a republish.

There is no experiments block in funnel.ts. Weights and status live in the dashboard, not in code. Create the pages/<slot>@<label>.tsx sibling, then configure the experiment in the dashboard. A returning visitor always keeps the same variant.

To branch within a page on the assigned variant, read it with useExperiment(id). Platform guide: Experiments. Editor workflow: Connected agents.

package.json

The funnel’s package.json is where you add any npm packages your pages import. Two rules:

  • The @appfunnel-dev/sdk version is pinned for you — leave it as it is (no ^ or ~).
  • Runtime dependencies only. Add packages under dependencies; devDependencies and install scripts aren’t included when your funnel is built.

Guide: Project Structure.

Generated files

One file, mount.tsx, is generated from your funnel.ts — it wires your pages and checkout together for you. Don’t hand-edit it; it’s regenerated on every publish, and in the editor it’s hidden from the file tree. You shape everything it does by editing funnel.ts and your page files.

Last updated on