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 })
})| Member | Behavior |
|---|---|
on(event, cb) | Subscribe to one event, or '*' for all (the wildcard callback receives { event, data }). Returns an unsubscribe function. |
off(event, cb) | Unsubscribe. |
events | A 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
| Event | Fired when | Payload highlights |
|---|---|---|
funnel.start | The funnel view mounts. | {} |
page.view | Each page change. | { pageId, pageKey?, isInitial?, eventId? } |
page.exit | Leaving a page. | { pageId, durationMs, activeMs } (active = visible time) |
user.registered | The first user.email write (identify()). | { email, eventId? } |
experiment.exposure | First landing on an experiment slot. | { experimentId, variant, version? } |
checkout.start | A checkout begins. | { productId?, amount?, currency?, surface?, eventId? } |
checkout.payment_added | An inline driver reports a payment method added. | { productId?, amount?, currency?, eventId? } |
checkout.failed | A charge fails. | { category, declineCode?, productId?, intent? } |
purchase.complete | A charge settles. | { amount?, currency?, productId?, eventId? } |
subscription.created | A 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 slotWhat’s code vs what’s configured:
- You write (code): the variant page files —
welcome@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/sdkversion is pinned for you — leave it as it is (no^or~). - Runtime dependencies only. Add packages under
dependencies;devDependenciesand 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.