This page covers core concepts that power Pairit experiments: flow logic, user state, randomization, matchmaking, and events.
Flow Logic
Experiments are structured as pages connected by actions. Participants navigate through pages based on button actions and conditional routing.
Page Transitions
Buttons define how participants move between pages. Each button has an action that determines the target page:
Conditional Navigation
Use branches to route participants dynamically based on their state or responses:
buttons:
- id: submit
text: "Submit"
action:
type: go_to
branches:
- when: "session_state.age < 18"
target: ineligible_page
- when: "session_state.consent == true"
target: main_study
- target: declined_page # default fallback
Branches are evaluated in order; the first matching condition determines the target. A branch without when serves as the default fallback.
Conditional Rendering
Use when on any component to show or hide it based on session_state. This lets a single page display different content per condition without duplicating pages:
components:
- type: text
when: "session_state.treatment == 'A'"
props: { text: "You are in group A." }
- type: text
when: "session_state.treatment == 'B'"
props: { text: "You are in group B." }
Components without when always render. The expression syntax is the same as branch conditions: session_state.{key} {op} {value}.
Action Types
go_to: Navigate to a specific page by idnext: Advance to the next page in linear flowsend: Mark the session as ended
User State
The session_state object stores participant data throughout a session. Components automatically write to user state, and expressions can read from it.
State Schema
Define expected fields in your config's session_state section:
session_state:
age: int
consent: bool
treatment:
type: string
default: "control"
survey_responses:
type: object
additionalProperties: true
stateKey Pattern
Components that assign values use stateKey to specify where data is stored:
components:
- type: randomization
props:
stateKey: treatment # writes to session_state.treatment
conditions: [control, A, B]
Common stateKey patterns:
- treatment - treatment group assignment
- group_id - matchmaking group identifier
- chat_group_id - chat room identifier
State Persistence
User state is persisted to MongoDB with each update. If a participant closes and reopens their session, their state is restored automatically.
Accessing State in Expressions
Use session_state.{fieldName} in when conditions and branch expressions:
Randomization
Assign participants to experimental conditions using onEnter page hooks. Randomization runs invisibly during navigation — no extra page or UI flash:
pages:
- id: chat
onEnter:
- type: randomize
conditions: [control, treatment_A, treatment_B]
stateKey: treatment
components:
- type: chat
props: { ... }
The assigned condition is stored at session_state.{stateKey} and persists across page refreshes.
Group-level randomization
Use scope: group to assign the same condition to all members of a matchmaking group:
This requires that matchmaking has already set session_state.group_id.
See Randomization for full documentation.
Match-making
Events
All component interactions in the lab runtime generate events that are stored in MongoDB (the events collection). This event-based architecture provides a component-agnostic way to track user interactions, collect research data, and enable analytics.
Event Structure
Events have a standardized structure with the following fields:
type(string): The type of event (e.g.,"survey_submission","button_click")timestamp(string): ISO 8601 timestamp when the event occurredsessionId(string): The session ID where the event occurredconfigId(string): The experiment configuration IDpageId(string): The page ID where the event occurredcomponentType(string): The type of component that generated the event (e.g.,"survey","button")componentId(string): The ID of the specific component instancedata(object): Component-specific event data (flexible structure)
Event Configuration
Components can be configured with custom event metadata in the YAML config:
components:
- type: button
id: my_button
props:
text: "Click me"
events:
onClick:
type: "custom_button_click"
data:
experiment_phase: "testing"
button_category: "primary"
Event Hooks by Component
- Buttons:
onClick- emitted when button is clicked - Survey:
onSubmit- emitted automatically when survey is submitted - Media:
onPlay,onPause,onSeek,onComplete,onError- various media interactions - Matchmaking:
onRequestStart,onMatchFound,onTimeout,onCancel- matchmaking lifecycle - Chat:
onMessageSend,onMessageReceive,onTypingStart,onTypingStop- chat interactions - Form:
onSubmit,onFieldChange- form submissions and field updates
Event Data Field
The data field is flexible and component-specific:
- Survey submissions: Contains all survey answers as key-value pairs
- Button clicks: Contains
button_id,label, and custom metadata - Media interactions: Contains
media_id,current_time,duration, etc. - Matchmaking events: Contains
pool_id,group_size,wait_duration_seconds - Chat events: Contains
chat_group_id,message_id,sender_type - Form events: Contains
form_id,field_values,field_id(on change)
Storage
Events are stored in a top-level MongoDB collection called events, with auto-generated document IDs. This allows efficient querying by session, config, page, or component type.
Extensibility
The event system is designed to be extensible. New component types can emit events by following the same structure, making it easy to add tracking for new interaction types without changing the core architecture.