Experiment surfaces
An experiment surface is the channel an experiment runs on — web, email, push, in-app, mobile, and more. Apex unifies every surface under one type, one hub, and one query. You run a subject-line test, a DOM test, and a mobile feature-flag test against the same data model and the same analytics.
The ten surfaces
Apex reserves ten surfaces in its type system. Seven are live today; three are reserved for upcoming work so their types are stable from day one.
| Surface | Status | What it tests | Default allocation | Stats engine |
|---|---|---|---|---|
web | Live | Apex changes the page (snippet) or your code does (useApexVariant + server assign) | Hash 50/50 | Frequentist (z-test) |
email | Live | Subject lines, body content, CTA copy in email | Thompson sampling | Bayesian Monte Carlo + χ² |
push | Live | Title, body, deep links, rich media in push notifications | Thompson sampling | Bayesian Monte Carlo + χ² |
in_app | Live | In-app banners, modals, toasts | Thompson sampling | Bayesian Monte Carlo + χ² |
mobile | Live | JSON-payload feature flags read via Apex.getVariant() | Hash 50/50 | Frequentist (z-test) |
journey_arm | Live | One step of an Adaptive Journey, arm vs arm | Thompson sampling | Bayesian Monte Carlo + χ² |
paid_channel | Live | Ad holdout: a share of your known people is held back from one network's ad sets; installs are counted in both lanes | Fixed hold-back share (5–50%) | Frequentist (z-test), counted forward-only from the confirmed exclusion |
creative | Reserved | Ad creative A/B (headline + hero) | — | — |
landing_page | Reserved | Destination URL A/B tied to Apex Links | — | — |
feature_flag | Reserved | Rollout + ramp management | — | — |
Info
Reserved surfaces can be named on existing experiments and flow through the type system, but the API rejects POST /api/experiments with a reserved surface until Apex lights them up.
Info
paid_channel is not a head-to-head channel test. It answers one question: did these ads cause installs, or would those people have installed anyway? Apex draws the held-back list from people it can hash (email or phone), puts a do-not-show Custom Audience on the ad sets (Meta; you attach it by hand elsewhere), and counts nothing until it has confirmed the list is on every ad set. Creating one needs a workspace admin and the Ad holdouts switch on for the workspace. See POST /api/experiments with a paidChannel block, apex.experiments.createAdHoldout, or the MCP create_experiment with surface: "paid_channel".
Why one data model, not five
Before Apex unified the surface taxonomy, a website experiment and an email subject-line experiment were different entities with different fields. You could not query "all experiments that tested CTA copy across any channel" because "CTA" meant something different in each type.
The unified Experiment type fixes that. Every experiment carries surface: ExperimentSurface at the top level and a variants: ExperimentVariant[] array whose inner type is narrowed by surface:
WebVariant— DOM changes, redirect URL, screenshotEmailVariant— subject, body overrides, CTA copyPushVariant— title, body, deep-link URL, imageInAppVariant— title, body, CTA, layout (banner/modal/toast)MobileVariant— arbitrary JSON payload read by your app
Shared fields sit at the top level: primary/secondary/guardrail metrics, audience rules, allocation strategy, attribution window, hypothesis, prediction, learnings. Every surface gets the same scientific-method treatment and feeds the same belief graph.
How surfaces route in the hub
The unified hub at /dashboard/experiments shows every experiment, every surface, in one list. The segmented pill filter at the top of the hub narrows the view to a single surface when you want it.
Clicking into an experiment opens the unified detail page at /dashboard/experiments/[id], which renders surface-appropriate previews:
- Web shows who applies the variant (Apex / your code), DOM-change count, and any redirect URL.
- Email shows subject + CTA.
- Push / In-app show title + body.
- Mobile renders the JSON payload.
- Ad holdout (
paid_channel) shows whether the ads are really off for the held-back people, then two lanes: the install rate of people who saw the ads and of people held back. Before the network confirms the exclusion it shows no number at all.
Every surface that has a legacy detail editor (web, email, push, in-app) includes an "Edit in full [surface] editor" link (where the label reflects the actual surface) that jumps to the richer surface-specific editor for deeper work. Mobile and all reserved surfaces live entirely on the unified detail page because there's no legacy to link to.
One experiment contract across surfaces
Apex stores each surface in the shape its runtime needs, then returns one uniform Experiment type from the API. Existing experiments do not need a migration when a new surface joins the unified view, and callers do not have to understand the storage split.
If you're building integrations directly against the API, you never see the split — it's a pure implementation detail of the store.
Next
- Run your first cross-surface test by opening the unified hub.
- Browse the experiments concept doc for who applies the variant, sticky assignment, and first-paint flicker.
- See the API reference for the
GET /api/experiments?unified=trueand related endpoints.