The personalization slots model
On-site personalization in Devotel Orbit runs on three objects — slots, variants, and a lift report — plus one rule that decides which variant a visitor sees. The workspace how-to (defining slots, rendering with the web SDK, promoting a winner) is Personalize website content; this page is the concept underneath it: why the model is scoped the way it is, what the segment-label enum actually is, and why mis-naming a variant silently converts a clean experiment into noise. Read it once before you hand a list of variant names to measurement, and the numbers will mean what you think they mean.What a slot is — scope and windows of resolution
A slot is a named, scoped placement — a location a page declares and the SDK fills in. The scope is yours to decide:- Page-scoped:
hero-bannerexists only on the homepage;cart-emptyonly on an empty cart. Page-scoped slots are the common case — one placement, one purpose. - Pattern-scoped:
product-promocan appear on every product detail page. Pattern-scoped slots are reused placements. - Site-wide:
nav-promoorannouncement-barrender on every page. Site-wide slots are the tenant-wide overlay.
properties.slot equals that name count. Mix site-wide and page-scoped placements under the same slot name and the report pool’s arms no longer answer the same question.
The window of resolution is the moment the SDK resolves the variant — once, when the page’s script asks for the slot, after the visitor has been identified. Every later render uses the resolved payload (content, cta_url, cta_text, variant) directly; there is no server round-trip on re-renders the page performs itself. This is a deliberate choice: personalization should not lay a second fetch on the render-critical path.
Variants for a slot are managed under the slot-management endpoints; the model’s service surface is the Personalization API reference. Create one variant per slot + segment + variant triple, and keep one slot-default variant (see below) as the fallback — without it, browsers render nothing.
Variant types — the sentinel vs. named variants
Every variant row carries an optionalvariant string. The picker distinguishes exactly two shapes:
- A named variant (
'b','control', …) is assigned an identity. Any group of rows with the same name forms one named arm in the lift report. - The untagged variant (
variant IS NULL) is “the default content” — the fallback every visitor may see when no named variant matches, and the arm the lift report calls the holdout.
__default__, to select “the visitor saw no variant-specific treatment” as the baseline arm. Inside the report query, __default__ is an exact predicate: variant IS NULL. Pass a named variant as the baseline and the report measures that variant; pass __default__ and it measures visitors the picker resolved to the untagged row.
Holdout seeding is the act of dedicating an untagged variant as the experiment’s control: create the slot’s fallback, leave its variant unset, keep it active, and never tag it. The moment you tag the fallback row with a name like 'b' or 'a', its impressions stop being “untreated” in the measurement sense — they become just another named arm, indistinguishable from treatment. Keep the fallback untagged throughout an experiment so __default__ keeps meaning what it promises: “the content every visitor would have seen had there been no experiment.”
This is also the reason the picker is exact and not a prefix match: a slot name passed to the picker and a slot name stored on the row must match exactly. A wrong-selection handshake (asking for slot hero-banner when the row says hero-main) resolves to no variant at all — the response carries an empty payload and the page keeps its own markup. Within the slot the fallback ladder has exactly one rung — segment-specific beats untagged, untagged beats nothing — and there is no fuzzy cross-slot guess past that ladder.
The segment-label enum — a fixed vocabulary, add-and-read
Variants target the contact’s lifecycle segment using a closed enum of eight labels:
The enum lives in the contact scorer — the daily pass that blends engagement signals into per-contact scores and derives the label (see the contact scoring pipeline for the signals and cadence). Personalization accepts only these eight values; add-and-read semantics means you read the scorer’s vocabulary at slot-write time rather than add a new free-form label per slot. That is a deliberate contrast with the CDP segments API, which composes arbitrary audience definitions from attributes and events — a segments-object of your own. Slots target lifecycle truth, not ad-hoc audiences, so the picker can resolve with one indexed join against the contact’s score row instead of evaluating per-request segment filters.
Canonicalization matters for three surfaces:
- Writing a variant: the API validates
segment_labelagainst the enum, so a misspelled label ('at-risk','atRisk') is rejected at write time, not silently never-matching at serve time. - Reading variants: a stale label, dragged out of an old dashboard session, degrades to no filter on list/read queries — you see every variant rather than a 422. The picker treats an unknown label the same way:
segment_labelfilters apply only when the label is recognized. - Resolving visitors: the runtime endpoint picks the highest-priority active variant whose label matches the visitor’s segment; the untagged slot-default is the fallback. Because only the eight literals above are acceptable, one indexed join settles the ranking.
Lift measurement — Wald CI, two-proportion z-test, and the exact-not-default rule
The lift report answers one question: does the named variant beat the fallback (or another named variant) on the event you count as a conversion? Each render already logged an impression event with the slot and the variant it served, and each conversion event (clicked_cta, purchase, any name track() uses) is already in the same event sink. Measurement is a join, not new instrumentation.
The report computes per arm:
- Impressions — distinct visitors whose impression event carried this variant (or, for
__default__, visitors whose impression event had no variant tag). - Conversions — of those visitors, how many also fired the conversion event in the same lookback window.
- Conversion rate — conversions ÷ impressions.
is_significant flag that is true exactly when the interval excludes zero. state is no_traffic while neither arm has impressions, collecting while only one does — the statistical fields stay null rather than produce NaN-grade output until both arms have traffic.
The exact-not-default rule is what keeps that report honest. Because the report’s baseline arm is whichever predicate you pass, a baseline of 'b' measures variant b and a baseline of __default__ measures the untagged fallback — but nothing else. Mis-attributed holdout treatment degrades the measurement in a specific way: if your fallback row got tagged with a name during a rename or an import, and you still pass __default__, the report’s baseline arm contains only other untagged traffic — potentially none — while the treatment arm contains the variant you actually meant to compare against the fallback. The report then compares treatment against visitors who saw neither — a missing-baseline confound, not an artifact a significance flag can rescue. Seed holdouts by leaving the fallback untagged, and pass __default__ only while that is true.
The same filter that lets unknown segment labels degrade to no-filter applies here too: a stale segment_label on the report request widens the window rather than failing it, so a renamed segment does not hard-fail the measurement — it just widens the population.
Preview mode — the picker’s deterministic-enough semantics
Previewing a slot runs the same selection as the live runtime endpoint, with nothing published and no live page — pass a segment label and you get back the variant that would win for that segment; omit it and you get back the slot-default; pass an unknown label and you again get the slot-default because the label widens to no-filter.resolved is null when no active variant matches, which is the SDK’s “leave your own markup alone” signal.
“Deterministic-enough” names the two leniencies the picker deliberately ships: (1) an unknown segment label degrades to no-filter rather than erroring (lists, previews, and the lift report all share this), and (2) priority is a tie-breaker, not a routing rule — when a segment-specific variant and the untagged fallback both match, the picker prefers the segment-specific row, and priority ranks ties within the two halves. Neither leniency changes the one-way door: exact slot-name match decides whether anything is served at all. Use the preview to confirm a variant binds before you activate it; use the runtime behavior for what visitors actually see.
Further reading
- Personalize website content — the how-to: create variants, render on the site, and read the lift report
- Personalization API reference — the slot CRUD and lift endpoints in full
- The contact scoring pipeline — where the segment-label enum comes from
- CDP segments — the segments API slots deliberately do not target