Skip to main content

Authoring experiments programmatically

The HyperStudy v3 API lets you create, read, update, and validate experiment definitions from code. This page shows how to build experiments with the Python SDK's typed builders — but the underlying REST endpoints are the same whether you call them from Python, JavaScript, R, or curl.

For the full endpoint reference, see the interactive API Reference. For data-fetching workflows (events, recordings, ratings), see the Python Guide.

Quickstart

from hyperstudy import HyperStudy, Experiment, State, show_text

hs = HyperStudy() # reads HYPERSTUDY_API_KEY

exp = Experiment(
name="Welcome study",
required_participants=1,
states=[
State(id="intro", focus_component=show_text("Welcome to the study!")),
],
)

info = hs.create_experiment(experiment=exp)
print(info["id"]) # → exp_...

The typed builders convert snake_case Python fields to the camelCase shape the backend expects (required_participantsrequiredParticipants). Unknown top-level fields are preserved as-is, so the SDK does not block on newly added backend features.

Building blocks

Experiment

The root object. Only name is required.

exp = Experiment(
name="My Study",
description="Short description",
required_participants=2,
randomize_states=False,
completion_screen_duration_ms=5000,
)

State

A single screen/phase. Each state has an id and (usually) a focus_component that defines what the participant sees.

from hyperstudy import State, TransitionRules, show_video

s = State(
id="watch",
name="Watch the clip",
order=1,
focus_component=show_video("https://example.com/clip.mp4"),
transition_rules=TransitionRules(type="auto"),
)

FocusComponent

One component per state. Use a factory helper (next section) or construct directly:

from hyperstudy import FocusComponent, ComponentType

c = FocusComponent(
type=ComponentType.SHOW_TEXT,
config={"text": "Hello", "fontSize": 24},
)

Role

Used for multi-participant experiments.

from hyperstudy import Role

roles = {
"speaker": Role(name="Speaker", participant_count=1),
"listener": Role(name="Listener", participant_count=1),
}
exp = Experiment(name="Pair study", required_participants=2, roles=roles)

Waiting room and timeouts

from hyperstudy import WaitingRoomConfig, DisconnectTimeout

exp = Experiment(
name="Multi-participant study",
required_participants=2,
waiting_room_config=WaitingRoomConfig(
max_wait_time_ms=60_000,
countdown_time_ms=3_000,
),
disconnect_timeout=DisconnectTimeout(
enabled=True,
duration_ms=30_000,
),
)

Component factories

Each factory returns a FocusComponent with the standard config keys filled in. Pass extra keyword arguments to set any other config field the backend accepts.

All factories accept a keyword-only id= argument; if omitted, an 8-character ID is generated automatically. Directly-constructed FocusComponent(...) instances do NOT auto-generate an ID — set id= explicitly when bypassing the factories.

FactoryComponent typeRequired args
show_text(text, *, id=None, **extra)showtexttext
show_image(url, *, id=None, **extra)showimageurl
show_video(url, *, id=None, **extra)showvideourl
vas_rating(prompt, *, output_variable, id=None, **extra)vasratingprompt, output_variable
text_input(prompt, *, output_variable, id=None, **extra)textinputprompt, output_variable
multiple_choice(prompt, options, *, output_variable, id=None, **extra)multiplechoiceprompt, options, output_variable
likert_scale(prompt, *, output_variable, scale_points=7, id=None, **extra)likertscaleprompt, output_variable
ranking(prompt, options, *, output_variable, id=None, **extra)rankingprompt, options, output_variable
waiting(duration_ms, *, id=None, **extra)waitingduration_ms

For component types without a factory (code, continuousrating, videochat, textchat, rapidrate, audiorecording, sparserating, trigger, scannerpulserecorder, gazeoverlay), construct directly:

from hyperstudy import FocusComponent, ComponentType

video_chat = FocusComponent(
type=ComponentType.VIDEO_CHAT,
config={"durationMs": 120_000},
)

Keyword arguments other than id pass through to the component's config:

show_text("Hello", fontSize=32, color="navy", textAlign="center")
# → config = {"text": "Hello", "fontSize": 32, "color": "navy", "textAlign": "center"}

Use camelCase keys for these pass-through fields — they go to the backend unchanged. Factory-set keys (e.g. text for show_text) take precedence over extras with the same name.

Validation

Two layers of validation:

  1. Client-side (Pydantic) at construction time — typos in field names, missing required fields, wrong types raise pydantic.ValidationError immediately.
  2. Server-side via validate_experiment() — dry-run against the same validator the backend uses for create_experiment.
from hyperstudy import HyperStudy, Experiment, State, show_text
from pydantic import ValidationError

hs = HyperStudy()

# Catches obvious errors before any HTTP traffic.
try:
exp = Experiment(name="", required_participants=0)
except ValidationError as e:
print(e)

# Catches structural issues the schema can't model.
exp = Experiment(name="Check me", states=[State(id="s1", focus_component=show_text("Hi"))])
result = hs.validate_experiment(exp)
print(result) # → {"valid": True} or {"valid": False, "errors": [...]}

Use validate_experiment in a CI pipeline to catch breaking schema changes before you publish.

Round-trip workflow

Fetch an existing experiment, modify it, and push it back:

hs = HyperStudy()

# Read. Returns the camelCase wire-format dict.
config = hs.get_experiment_config("exp_abc123")

# Mutate. Because the dict is in wire format, mutations use camelCase keys —
# writing config["randomize_states"] would *add a new field* rather than
# update the existing one.
config["name"] = "Renamed"
config["randomizeStates"] = True

# Write. You can pass the raw dict back via **config…
hs.update_experiment("exp_abc123", **config)

# …or re-parse it into Experiment for type-checked edits in snake_case.
from hyperstudy import Experiment
exp = Experiment.model_validate(config)
exp.completion_screen_duration_ms = 8000
hs.update_experiment("exp_abc123", experiment=exp)

Experiment.model_validate accepts both snake_case and camelCase keys, so an exported config from the UI imports cleanly.

Schema reference

The most-used top-level fields:

Python (snake_case)Wire (camelCase)TypeRequiredDescription
namenamestrExperiment name.
descriptiondescriptionstrFree-text description.
required_participantsrequiredParticipantsint >= 1Number of participants needed to start.
randomize_statesrandomizeStatesboolRandomize state order per participant.
statesstateslist[State]Ordered list of states.
rolesrolesdict[str, Role]Role definitions (multi-participant).
global_componentsglobalComponentsdict[str, dict]Persistent components running throughout.
waiting_room_configwaitingRoomConfigWaitingRoomConfigPre-experiment lobby behavior.
disconnect_timeoutdisconnectTimeoutDisconnectTimeoutHow long to wait on a disconnect before dropping.
completion_screen_duration_mscompletionScreenDurationMsint >= 0Final screen duration.
consent_form_enabledconsentFormEnabledboolShow consent form?
consent_form_titleconsentFormTitlestrConsent form title.
consent_form_contentconsentFormContentstrConsent form HTML.
instructions_enabledinstructionsEnabledboolShow instructions?
instructions_titleinstructionsTitlestrSingle-page instructions title.
instructions_contentinstructionsContentstrSingle-page instructions HTML body.
instructions_pagesinstructionsPageslist[InstructionsPage]Multi-page instructions (alternative to title/content).
post_experiment_questionnairepostExperimentQuestionnairePostExperimentQuestionnairePost-task survey.
variablesvariablesdictExperiment variables for dynamic content.

For the authoritative schema (including per-component config shapes), see the interactive API Reference and search for Experiment.

Troubleshooting

ValidationError: 1 validation error for Experiment / name / String should have at least 1 character

You called Experiment(name="") or omitted name entirely. name must be a non-empty string.

ValidationError: ... type / Input should be 'showtext', 'showimage', ...

You passed an unrecognized component type. Check ComponentType for the supported values, or construct with FocusComponent(type=ComponentType.X, config=...).

{"valid": false, "errors": [...]} from validate_experiment

The structure passed Pydantic but failed the backend's per-component validation — most often a required config key missing for a specific component type. The error list names the offending path; cross-reference the API Reference for that component's config schema.

TypeError: create_experiment requires a 'name' ...

Either pass name="..." directly or include it on the Experiment you pass via experiment=.

Next steps