Research Domains / Brand Tracker Q3 / New Study
Brief
2
Review
3
Logic
4
Quota
5
Publish

Author a new study

Upload an existing questionnaire or describe your research brief — AI will author the survey for you.

📄

Upload questionnaire

Upload a Word doc, PDF, or Excel file. The AI will interpret your existing questionnaire, map card types, and preserve all your logic.

☁️

Click to upload or drag & drop

Word · PDF · Excel · up to 50 MB

Converts logic Preserves wording Maps card types
or

Describe your brief

Tell the AI what you need. It will generate a full survey with the right card types, probes, and flow.

AI-moderated Static / scripted Mixed
Scale / Rating Single choice Multi-select AI probe Open text Image / Video Voice note NPS
5 min 10 min 15 min 20+ min
Authoring your study…
This usually takes 15–30 seconds
Parsing research brief & objectives
2
Selecting card types & question flow
3
Configuring AI probe objectives
4
Building conditional logic
5
Running compliance pre-check
6
Compiling survey program
Cards (14)
Screening
S1
Date of birth
DOB Capture · Required
S2
Gender identity
Single choice
Brand Awareness
Q1
Brands you know
Multi-select
Q1p
Brand associations
AI Probe
Q2
Luxury definition
Single choice
Brand Perception
Q3
Overall impression
NPS
Q4
Key driver attributes
Rating grid
Q4p
Attribute deep-dive
AI Probe
Purchase Intent
Q5
Purchase likelihood
Scale 1–10
Q6
Barriers to purchase
Multi-select
Q7
Open feedback
Open text + AI probe
Q8
NPS — overall brand
NPS
Code Escape Hatch
‹/›
Intelligence Gate
⚡ Wave memory + sentiment
‹/›
Orchestration Card
⚙ Reward · pipe · quota · flow
Luxury Skincare Brand Tracker — Q3 2026
✦ AI-authored 🎙 AI-moderated n=500 ~10 min · 12 cards
S1
DOB Capture
What is your date of birth?
Captures age gate. Respondents under 25 are screened out.
→ If age < 25: screen out · If age ≥ 25: continue to S2
S2
Single Choice
Which of the following best describes your gender identity?
Woman
Man
Non-binary / gender non-conforming
Prefer to self-describe
Prefer not to sayanchor
Q1
Multi-select
Which of the following luxury skincare brands are you aware of? Select all that apply.
La Mer
SK-II
Sisley Paris
Tatcha
Augustinus Bader
None of theseanchor
Q1p
🤖 AI Probe
Brand association deep-dive
Objective
Understand which specific attributes (ingredients, packaging, heritage, efficacy claims) drive awareness and consideration for each brand the respondent selected.
Max depth: 3 turns Coverage threshold: 0.80 Tone: Curious, warm
→ Skip if Q1 = "None of these"
Q3
NPS
How likely are you to recommend La Mer to a friend or colleague?
0–10 scale · Detractors (0–6) · Passives (7–8) · Promoters (9–10)
→ If NPS ≤ 6: route to barriers question · If NPS ≥ 9: route to advocacy probe
‹/›
⚡ Intelligence Gate
Code escape hatch
Cross-wave memory injection + real-time sentiment routing
↩ Prior wave recall 🧠 Sentiment scoring ⎇ Adaptive routing 📡 External API call
🔒
@r2 Sandbox — V8 isolate · no filesystem · no arbitrary network
Only @r2/* packages are resolvable. All other imports fail at compile time. Each execution runs inside its own Durable Object — zero lateral access to other sessions or studies.
@r2/runtime— interview · session · quotaCore · always on
@r2/ai-utils— SentimentClassifier · ai.injectContext · TopicExtractor (curated utilities — no raw LLM access)Core · always on
@r2/wave-store— cross-wave respondent memory · read-only prior wavesCore · always on
@r2/analytics— real-time ClickHouse queries · scoped to this studyCore · always on
@r2/integrations/rewards— RewardEngine · points dispatchEnabled by org admin
@r2/integrations/crm— CRMClient · upsert respondent fieldsEnabled by org admin
@r2/integrations/webhooks— outbound HTTP to pre-approved domains onlyNot enabled
@r2/integrations/slack— real-time alerts to workspace channelsNot enabled
@r2/experimental/personas— synthetic respondent persona builderRival Labs · opt-in
@r2/experimental/conjoint— adaptive conjoint design engineRival Labs · opt-in
🚫  Blocked at compile time:  node:* · npm imports · fetch() to non-allowlisted URLs · eval() · Function() · dynamic import() · process · globalThis mutation
intelligence-gate.ts
types.ts
output
// ── Intelligence Gate — Card IG-01 // Runs silently between Q3 (NPS probe) and Q4 (key drivers) // No UI shown to respondent — pure server-side logic import { interview, session, ai } from '@r2/runtime'; import { WaveStore } from '@r2/wave-store'; import { SentimentClassifier } from '@r2/ai-utils'; // ── Step 1: Pull this respondent's prior wave answers ───────────────────── const priorWave = await WaveStore.get({ studyId: 'lux-skin-q2-2026', // previous wave panelId: session.panelId, // same respondent, matched by panel ID fields: ['Q3_nps', 'Q2_luxury_def', 'Q1p_verbatim'], }); // ── Step 2: Score sentiment on the verbatim probe transcript just collected const probeTranscript = session.getVerbatim('Q1p'); // AI probe turns → raw text const sentiment = await SentimentClassifier.score(probeTranscript, { dimensions: ['frustration', 'excitement', 'trust', 'aspiration'], model: 'haiku', // fast — respondent doesn't wait }); // ── Step 3: Compute NPS delta vs prior wave ─────────────────────────────── const npsDelta = session.get('Q3_nps') - (priorWave?.Q3_nps ?? null); const isLapsed = priorWave?.Q3_nps >= 9 && session.get('Q3_nps') <= 6; // ── Step 4: Inject memory into AI interviewer context ──────────────────── if (priorWave) { ai.injectContext(`This respondent scored La Mer ${priorWave.Q3_nps}/10 last quarter and described luxury as "${priorWave.Q2_luxury_def}". Their verbatim included: "${priorWave.Q1p_verbatim}". Reference this naturally if relevant — do not make them feel tracked.`); } // ── Step 5: Route based on combined signals ─────────────────────────────── if (isLapsed && sentiment.frustration > 0.65) { // High frustration + was a promoter → deep churn risk module session.set('segment', 'lapsed_promoter_frustrated'); await interview.skipTo('Q_CHURN_MODULE'); } else if (sentiment.aspiration > 0.7 && npsDelta > 2) { // Growing excitement + NPS improving → advocacy & referral module session.set('segment', 'rising_advocate'); await interview.skipTo('Q_ADVOCACY_MODULE'); } else { // Default path — continue to key driver attributes session.set('segment', 'standard'); }
// Types exported by @r2/wave-store and @r2/ai-utils interface WaveRecord { panelId: string; studyId: string; completedAt: Date; [field: string]: unknown; } interface SentimentResult { frustration: number; // 0–1 excitement: number; trust: number; aspiration: number; dominant: string; // highest-scoring dimension rawScore: number; // –1 (negative) → +1 (positive) } interface AIContext { // Injected into the AI interviewer's system prompt for subsequent cards injectContext(text: string): void; setPersona(tone: 'empathetic' | 'curious' | 'celebratory'): void; }
// What this card writes to session state (available to all subsequent cards) session.segment = 'lapsed_promoter_frustrated' // or 'rising_advocate' | 'standard' session.npsDelta = -3 // vs prior wave session.sentiment = { frustration: 0.71, trust: 0.28, ... } session.priorWaveLoaded = true session.aiContextInjected = true // AI interviewer for all subsequent cards now knows: // — Respondent's prior NPS, verbatim, luxury definition // — Current emotional state (frustrated, not aspirational) // — Can reference prior wave naturally without exposing tracking // Flow jumped to: → Q_CHURN_MODULE (skipped Q4 key drivers — not relevant for lapsed promoters)
Produces
session.segment · session.npsDelta · session.sentiment · ai context injected · adaptive route decided
‹/›
⚙ Orchestration Card
Code escape hatch
Reward dispatch + piping + quota rebalancing + flow mutation
🎁 Reward API ⎄ Variable piping ⚖ Quota rebalance 🔀 Flow mutation
🔒
@r2 Sandbox — same isolate model applies
Packages active in this study: @r2/runtime · @r2/ai-utils · @r2/wave-store · @r2/integrations/rewards · @r2/integrations/crm
Show all ↓
orchestration.ts
quota-rebalance.ts
output
// ── Orchestration Card — OC-01 // Runs after Q7 (open feedback) — final logic pass before closing // Touches 4 external/internal systems in a single atomic block import { interview, session, quota } from '@r2/runtime'; import { RewardEngine } from '@r2/integrations/rewards'; import { CRMClient } from '@r2/integrations/crm'; // ── Step 1: Pipe answers into derived variables ─────────────────────────── const age = session.get('S1_age'); const nps = session.get('Q3_nps'); const segment = session.get('segment'); // set by Intelligence Gate const topBrand = session.get('Q1_selected')[0]; // first brand they selected const spendBand = age < 30 ? 'emerging' : age < 40 ? 'core' : 'established'; // Derived variables — available to remaining cards and analysis layer session.set('derived_spendBand', spendBand); session.set('derived_topBrand', topBrand); session.set('derived_npsSegment', nps >= 9 ? 'promoter' : nps >= 7 ? 'passive' : 'detractor'); session.set('derived_fullSegment', `${spendBand}_${segment}`); // ── Step 2: Dispatch reward — amount varies by segment ──────────────────── const rewardAmount = { lapsed_promoter_frustrated: 750, // extra points — retain at-risk rising_advocate: 500, standard: 350, }[segment] ?? 350; const reward = await RewardEngine.dispatch({ panelId: session.panelId, points: rewardAmount, reason: 'survey_complete', studyId: session.studyId, idempotencyKey: `${session.sessionId}-reward`, // safe to retry }); // ── Step 3: Write enriched segment back to CRM ─────────────────────────── await CRMClient.upsert({ panelId: session.panelId, fields: { luxurySegment: session.get('derived_fullSegment'), lastNPS: nps, topBrand: topBrand, surveyDate: new Date().toISOString(), }, }); // ── Step 4: Mutate remaining flow based on everything we now know ───────── if (segment === 'lapsed_promoter_frustrated' && nps <= 5) { // Replace final NPS card with a softer brand relationship question // — don't ask for NPS twice, they're already telling us they're unhappy interview.replaceCard('Q8', { type: 'single_choice', content: { text: `What would bring you back to ${topBrand}?` }, options: [ { value: 'price', label: 'A more accessible price point' }, { value: 'results', label: 'Seeing better results' }, { value: 'trust', label: 'Rebuilding trust in the brand' }, { value: 'nothing', label: "I've moved on", anchor: true }, ], }); } // ── Step 5: Quota cell self-correction ─────────────────────────────────── // (see quota-rebalance.ts tab) await quota.rebalance(session);
// quota-rebalance.ts — called by Orchestration Card at survey end // Corrects cell assignment based on answers, not just screening data export async function rebalance(session: Session) { const screenedAge = session.get('S1_age'); const selfReported = session.get('Q_spend_frequency'); // 'daily'|'weekly'|'rarely' const currentCell = session.get('quotaCellId'); // Respondent was assigned Gen Z cell at entry (age 26) // but their spend behaviour matches the "core Millennial" profile const behavioural = selfReported === 'daily' && screenedAge < 29; if (behavioural && currentCell === 'gen_z_25_28') { const coreCell = await quota.getCell('millennial_core_spender'); if (coreCell.current < coreCell.target) { // Cell has room — move them await quota.reassign(session.sessionId, 'millennial_core_spender'); session.set('quotaReassigned', true); session.set('quotaReassignReason', 'behavioural_override'); } else { // Cell full — flag as over-quota behavioural match (still completes) session.set('quotaFlag', 'over_quota_behavioural'); } } // Write final quota snapshot to ClickHouse via Queues await quota.snapshot(session); }
// What this card does — all atomic, all in a single DO execution turn 1. Piped variables written to session: session.derived_spendBand = 'core' session.derived_topBrand = 'La Mer' session.derived_npsSegment = 'detractor' session.derived_fullSegment = 'core_lapsed_promoter_frustrated' 2. Reward dispatched: RewardEngine → 750 pts → panel member #panelId idempotency key prevents double-dispatch on retry 3. CRM updated: luxurySegment, lastNPS, topBrand, surveyDate upserted 4. Card Q8 replaced in-flight: NPS card swapped for "What would bring you back to La Mer?" Respondent never sees the replaced card — flow just continues 5. Quota rebalanced: Session reassigned: gen_z_25_28 → millennial_core_spender Reason: behavioural_override (daily spend + age 26) Snapshot written to ClickHouse via CF Queues
Produces
750 pts reward dispatched · CRM upserted · Q8 replaced in-flight · quota cell corrected · 5 derived variables piped
Edit Card — S1
Content
Logic
Settings
Card type
Question text
Options
Card JSON — advanced
// Card S1 — DOB Capture const S1: Card = { id: 'S1', type: 'dob_capture', content: { text: 'What is your date of birth?', }, metadata: { required: true, executionMode: 'exact', }, }; // Skip logic if (getAge(S1.response) < 25) { interview.screenOut(); }
Conditional Flow
Screening · S1
Date of birth
DOB Capture · Required
age < 25
Screen out
Thank you page
age ≥ 25
Screening · S2
Gender identity
Single choice
Awareness · Q1
Brand awareness
Multi-select
selected ≥1 brand
AI Probe · Q1p
Brand associations
Max 3 turns · 0.80 threshold
"None of these"
Skip to
Q2 Luxury definition
Perception · Q3
NPS — La Mer
0–10 scale
NPS ≤ 6 (detractor)
Purchase · Q6
Barriers to purchase
NPS ≥ 9 (promoter)
AI Probe
Advocacy deep-dive
Quota cells
Soft quota — over-quota respondents complete but are flagged. Hard stop can be enabled per cell.
Gen Z — 25–28
Condition: age >= 25 AND age <= 28
Target:
0 / 100 collected0%
Millennial — 29–40
Condition: age >= 29 AND age <= 40
Target:
0 / 250 collected0%
Gen X — 41–45
Condition: age >= 41 AND age <= 45
Target:
0 / 150 collected0%
Global settings
Hard stop when all cells filled
Allow synthetic respondents for gap fill
Flag over-quota completions in data
Total target: 500 completes
500
Total target
3
Quota cells
Distribution
SegmentTarget%
Gen Z 25–2810020%
Millennial 29–4025050%
Gen X 41–4515030%
87
Compliance score
2 warnings · 0 errors · Study can be published after review
Issues to review
⚠️
Potentially leading phrasing — Q3
Warning
"How likely are you to recommend La Mer" presupposes familiarity. Consider "How likely would you be to recommend La Mer to someone who asked for skincare advice?" for a more neutral framing.
⚠️
GDPR — no explicit consent card
Warning
No consent / privacy notice card detected. Respondents in the EU must be shown a GDPR consent card before data collection begins. A consent card has been prepared and can be inserted automatically.
No PII collection detected
Pass
No questions requesting name, email, phone, or address were found. DOB is collected as age gate only and is not stored beyond qualification.
No double-barrelled questions
Pass
All questions ask about a single topic. No compound questions detected.
Study is live 🎉
Luxury Skincare Brand Tracker — Q3 2026 has been published and is accepting responses. Share the link or distribute via Rival Panel.
0
Responses
500
Target
3
Quota cells