Getting Started
Create a study
From the dashboard, click New Study. Give it a title and description. A unique study ID is generated automatically and cannot be changed later.
Configure protocols
Go to Settings → Protocols to set up what the app collects. Each protocol has a name, an emoji (shown next to the name on the participant's mobile dashboard), a schedule (continuous, cron-based, or triggered by sensor conditions), and one or more tasks: sensor collection, ESM questionnaires, or cognitive tasks.
Generate participant IDs
Under Settings, enable participant IDs if you want to track which participant is on which device. Click Generate IDs to create a batch. Each ID is a unique code that participants enter when joining.
Enroll devices
Share the study's QR code with participants. They scan it with the StudyRun app, enter their participant ID (if required), and the device begins collecting data immediately.
Study Settings
How a study is run
A StudyRun study is deployed as a server-hosted configuration that participant devices download and follow. The general workflow is:
- Configure: set up your study name, description, protocols, and data sync settings on this page.
- Create protocols: a protocol defines what data the app collects, when it collects it, and who it applies to. Each protocol contains one or more tasks (sensor collection, questionnaires, or cognitive tasks) and runs on a schedule (continuous or cron-based). You can have multiple protocols active at once (for example, one for continuous accelerometer collection and another for daily ESM questionnaires). Once a protocol is configured, lock it to create an immutable snapshot. Only locked protocols are sent to participant devices. Locking ensures that every sensor reading can always be traced back to the exact settings that produced it, which is essential for scientific reproducibility.
- Enroll participants: share the QR code or study URL with participants. They scan it in the StudyRun app to join. If participant IDs are enabled, each person must enter their pre-assigned ID.
- Collect: devices run the active protocols in the background, buffering data locally and uploading it to the server on the configured interval.
- Update: if you change a setting or protocol, devices pick up the change automatically on their next upload cycle. No action is needed from participants.
- Close: when recruitment is complete, disable enrollment so no new devices can join. Existing participants continue collecting until their protocols expire or you exit the study.
General
- Study ID
- Auto-assigned at creation and cannot be changed. Used internally to identify the study in API calls, uploads, and exports. You do not need to set this; it is shown for reference only.
- Study Name
- A human-readable name shown to participants in the mobile app on the dashboard and study information screen. Keep it clear and recognisable; participants will see this name every time they open the app. Example: "Sleep & Activity Study 2026".
- Study Description / Ethics Information
- Free-text field displayed to participants in the app after they join, under the Study Information section. This is your primary channel for informed consent and ethics disclosure. Include: study purpose, ethics committee approval number, principal investigator name and contact, data handling procedures, participant rights, and how to withdraw. Write in plain language that participants can understand.
- Study Timezone
- The timezone used when evaluating protocol start and end dates and when calculating Study Day / Study Week values for relative date boundaries. Set this to the timezone where your participants are located. If your study spans multiple timezones, choose the one where the majority of participants are based, or use UTC for a neutral baseline. This does not affect the timestamps stored in sensor readings, which are always in UTC.
Client Settings
- Show "Your Contribution" section
- Activates an optional panel in the participant app's dashboard (shown between the tasks list and the sensor list) that visualises what each participant has contributed to the study so far. It displays days enrolled, total number of ESM responses submitted, the percentage of time each sensor was switched on, today's response rate, and a six-week compliance heatmap. Everything is computed locally on the device from data the participant has already generated; enabling this option does not collect anything new. Leaving it on gives participants a sense of engagement and progress, which often improves compliance; turn it off for blinded designs where you do not want participants to see summaries of their own behaviour.
- Upload Interval
- How often (in seconds) the device attempts to send buffered sensor data to the server. Default: 300 (5 minutes). Lower values give you fresher data in the dashboard but increase battery and data usage. For most studies 300 s is a good balance. Use 60–120 s if near-real-time monitoring is important. Avoid values below 30 s.
- Batch Size
- The maximum number of sensor readings sent in a single upload request. Default: 500. Larger batches are more network-efficient but use more memory during transmission. The default works well for most studies. Only reduce this if participants report upload failures on low-memory devices.
- Max Retry Attempts
- How many times the device retries a failed upload before skipping that batch. Default: 5. Failed batches are retried on the next upload cycle. The default is appropriate for most conditions. Increase it only if your study area has very unreliable connectivity.
- WiFi Only
- When enabled, data is only uploaded when the device is on WiFi. Useful for studies collecting high-frequency sensors (e.g. audio or accelerometer at fast rates) that generate large amounts of data. Participants must connect to WiFi regularly for uploads to occur, so consider including a reminder in your participant instructions.
Deployment
- QR Code
- Encodes the study configuration URL. Participants open the StudyRun app, tap "Join Study", and scan this code to enrol. You can download the QR code as a PNG and include it in consent forms, emails, or printed materials. The URL can also be shared as a plain link for online recruitment.
- Allow Enrollments
- Controls whether new devices can join the study. Toggle this off once all participants have enrolled to prevent unexpected new enrolments. Existing participants are not affected; they continue collecting data regardless of this setting. Re-enable at any time to allow additional enrolments.
- Require Participant ID
- When enabled, participants must enter a pre-assigned ID during enrolment. Only IDs you have generated are accepted. This acts as a lightweight authentication mechanism, so only people you have given an ID to can join. Recommended for studies where you need to track individual participants or control exactly who enrols. When disabled, anyone with the QR code or URL can join anonymously.
- Participant ID Label
- The label shown to participants when they are prompted to enter their ID. Default: "Participant ID". Customise this to match the terminology used in your consent materials (for example, "Student Number", "Subject Code", or "Reference Number"). Keep it consistent with what you told participants during recruitment.
- Participant IDs
- Generate IDs in batches of 20. Each ID is a short unique code. Copy unclaimed IDs and distribute them to participants before they enrol. IDs marked as Claimed have been used; they cannot be reassigned or deleted. Delete unused IDs if they were distributed to participants who are no longer taking part. Generate additional batches at any time as your study grows.
- Configuration JSON
- The raw configuration file sent to participant devices. You can view, copy, or download it for debugging or manual deployment. In normal use you do not need to interact with this directly; the dashboard manages it for you.
Team Access
- Owner
- Full access to all study settings, data, and team management. Can delete the study. There is one owner per study: the person who created it. Ownership cannot be transferred.
- Editor
- Can modify study settings, protocols, and upload settings. Can export data and send notifications. Cannot delete the study or manage team members. Assign this role to co-investigators or research assistants who need to configure the study.
- Viewer
- Read-only access. Can view the dashboard, data, and settings but cannot make any changes. Suitable for supervisors, ethics reviewers, or collaborators who need visibility but should not modify the study.
Protocols
What is a protocol?
Protocols are the heart of a study. A protocol defines what the app collects, when it collects, and who it applies to. Each study can have multiple protocols running in parallel (for example, one for continuous accelerometer collection and another for daily ESM questionnaires). Each protocol must be locked before it is sent to devices. Locking creates an immutable snapshot so that every reading can be traced back to the exact settings that produced it.
Structure
Tasks
Each protocol contains one or more tasks. A task is either a sensor collection (one or more sensors to record), an ESM questionnaire (a set of questions shown to the participant), or a cognitive task (e.g. a reaction time test). You can combine multiple tasks in a single protocol so they all run on the same schedule. For cron-based sensor tasks you also set a duration: how many seconds the sensors run each time the cron fires.
Schedule
A protocol runs on one of three schedules. Continuous means it runs at all times while the protocol is active. Sensors collect data in the background continuously. Interactive tasks (ESM questionnaires, cognitive tasks) in a continuous protocol appear in the participant's dashboard as "Run at any time", meaning they are always available and reappear after completion. Cron triggers the protocol at specific times using a cron expression. You can also configure a randomness delay (jitter): the protocol does not fire exactly at the cron time but instead waits a random number of minutes within the jitter window before triggering. This delays both data collection and any associated notification, so participants cannot predict exactly when the next prompt will arrive, which reduces anticipatory behaviour and improves ecological validity in ESM studies. Triggered fires the protocol when a set of sensor rules evaluates to true — e.g. "when the participant has been stationary for 30 minutes inside the home geofence". Rules are evaluated server-side on every upload, so the trigger reacts to real-time behaviour rather than a preset clock; this is the foundation for Just-In-Time Adaptive Interventions (JITAIs).
Cron expression syntax
A cron expression has five space-separated fields: minute hour day-of-month month day-of-week
| Position | Field | Allowed values | Special characters |
|---|---|---|---|
| 1st | Minute | 0 – 59 | * , - / |
| 2nd | Hour | 0 – 23 | * , - / |
| 3rd | Day of month | 1 – 31 | * , - / |
| 4th | Month | 1 – 12 | * , - / |
| 5th | Day of week | 0 – 7 (0 and 7 = Sunday) | * , - / |
*: every value
,: list e.g. 9,13,21
-: range e.g. 1-5
/: step e.g. */2
Common examples
| Expression | Fires at | Typical use |
|---|---|---|
| 0 9 * * * | 9:00 AM every day | Daily morning check-in |
| 0 9,13,21 * * * | 9 AM, 1 PM, 9 PM daily | 3× daily ESM questionnaires |
| 0 8,12,16,20 * * * | 8 AM, 12 PM, 4 PM, 8 PM daily | 4× daily diary prompts |
| 30 7,12,18 * * * | 7:30 AM, 12:30 PM, 6:30 PM daily | Meal-time experience sampling |
| 0 */2 * * * | Every 2 hours | High-frequency physiological snapshots |
| 0 8-22/3 * * * | Every 3 hours between 8 AM and 10 PM | Daytime-only sampling |
| 0 9 * * 1-5 | 9 AM on weekdays only | Work-day diary study |
| 0 9 * * 1 | 9 AM every Monday | Weekly well-being check-in |
| 0 20 * * 0 | 8 PM every Sunday | End-of-week retrospective questionnaire |
| 0 0 * * * | Midnight every day | Daily overnight passive sensor burst |
| 0 10 1,15 * * | 10 AM on the 1st and 15th of each month | Bi-monthly cognitive task |
| */15 8-22 * * * | Every 15 min between 8 AM and 10 PM | Intensive sampling (use sparingly) |
Triggered rules
A Triggered protocol fires when a rule tree built from sensor predicates evaluates to true. Rules combine individual conditions (e.g. "activity is WALKING", "heart rate exceeds 120", "device is inside a 100 m geofence") with AND / OR / NOT operators and nested groups. Each condition picks a sensor, a field, an aggregation (latest, count, average, changed, any, inside/outside, etc.) and, where applicable, a time window (current upload, last N minutes, today, this week, since enrollment). The server re-evaluates the rule every time the device uploads sensor data, so the end-to-end latency from "condition met" to "task on the participant's dashboard" is one upload cycle (typically under a minute).
Triggered protocols also carry a cooldown (minimum minutes between firings per device), a max firings cap (per device, over the life of the study), a duration (how long the triggered task remains actionable), and an optional jitter delay. They support the same notification options as cron protocols, including High Priority.
| Rule (plain English) | Scenario |
|---|---|
| Weekdays, 20:00–20:30, device is currently inside the 500 m home geofence | Evening at-home check-in that only fires when the participant is actually at home during a weekday evening window. |
| Location (GPS) enters the 200 m work geofence, and Day is Mon–Fri, 07:00–11:00 | Arrival-at-work EMA — prompts a short mood/stress questionnaire right when the participant arrives, weekdays only. |
| Motion Activity any = RUNNING over last 20 min, AND activity latest = STATIONARY, AND HealthKit heart_rate latest > 110 | Post-workout recovery survey: they were running recently, are now stationary, and their HR is still elevated. |
| Pedometer steps sum < 100 over last 2 hr, AND Motion Activity latest = STATIONARY, AND Time between 09:00–17:00 | Sedentary-work nudge during business hours only. |
| App Usage event_type count > 60 over last 30 min, AND Time between 08:00–23:00 | Phone-overuse intervention — more than 60 foreground switches in half an hour usually signals distracted use (Android only). |
| Screen event count = 0 over last 3 hr, AND Time between 10:00–21:00 | Low-engagement check-in — participant hasn't interacted with the phone in 3 hours during daytime (Android only). |
| iBeacon uuid latest = "fda50693-a4e2-4fb1-afcf-c6eb07647825" AND proximity latest = "IMMEDIATE", AND Day is Tue or Thu | Lab-visit protocol — fires automatically when the participant walks into the instrumented room on study lab days. |
| Media Playing state latest = "playing", AND Motion Activity latest = WALKING, AND Headphone connected latest = "1" | Commute-with-music context — likely moment to ask about listening habits, with minimal interruption. |
| HealthKit where data_type = "sleep_analysis" value sum < 360 over today, AND Time between 07:00–10:00 | Poor-sleep follow-up — less than 6 hours in bed last night, asked during a gentle morning window. |
| Calendar count = 0 over last 60 min, AND Time between 12:00–14:00, AND Day is Mon–Fri | Lunch-break availability — fires only when no calendar events are currently active, avoiding interruption during meetings. |
| Battery level latest < 15 | Low-battery fail-safe — typically used inside a NOT group to suppress other triggers when the phone is about to die. |
| Location (GPS) outside the 300 m home geofence, AND Day is Sat or Sun, AND Time between 14:00–18:00, AND Motion Activity latest = WALKING | Weekend leisure-walk experience sampling — catches them walking outdoors, not indoors moving about. |
Rules referencing a sensor that no other active protocol is collecting are flagged at lock time (but still allowed, in case you plan to enable collection later). Platform-specific sensors (e.g. App Usage / Screen on Android, HealthKit on iOS) are badged with 🤖 or 🍏 in the rule builder.
Notifications
Cron-based and triggered protocols can show a notification to the participant when the protocol fires. Configure a title and message at the protocol level. The notification fires at the same moment the tasks become available (including any jitter delay), so participants always receive the notification and the data collection burst at the same time.
High Priority notification
When enabled, the scheduled notification is delivered at the OS's time-sensitive urgency tier: iOS UNNotificationInterruptionLevel.timeSensitive (pierces Focus and Do Not Disturb), Android IMPORTANCE_HIGH channel with CATEGORY_REMINDER (heads-up popover, bypasses "Priority only" DND). Leave it off and the notification is posted at default urgency, which may be silenced by the participant's Focus, sleep schedule, or Do Not Disturb settings.
Turn this on for momentary ESM prompts that expire quickly, safety check-ins, or any questionnaire where a missed response is worse than interrupting the participant. Leave it off for routine sampling protocols where the participant can reasonably reply later. High Priority notifications consume the user's attention budget, and overuse trains participants to silence the app entirely.
See Send Notifications → High Priority notifications in the docs for the full platform-by-platform behaviour table and the OS-level overrides participants can apply.
Scheduling options
- Start and end dates
- Optionally set a start date and end date for a protocol. Dates can be absolute (a fixed calendar date) or relative (e.g. 7 days after the participant enrolled). Relative dates are evaluated per participant, so each person gets the same study timeline regardless of when they joined. Before the start date the protocol is inactive; after the end date it stops automatically. Leave both blank to run indefinitely. This is ideal for phased studies, for example a baseline week of passive sensing followed by an intervention week with ESM prompts.
- Participant filter
- By default a protocol applies to all enrolled participants. Use the participant filter to restrict it to specific participant IDs. This lets you run different protocols for different groups, for example a control group with passive sensing only and a treatment group with additional ESM prompts.
How protocols reach devices
- Which protocols are sent?
- When a device checks in, the server evaluates all protocols and sends only those that are locked, enabled, within their date range, and pass the participant filter for that device. Unlocked (draft) protocols, disabled protocols, expired protocols, and protocols filtered to other participants are never sent.
- Protocol order and sensor priority
- The order of protocols matters. If two protocols are active at the same time and both collect the same sensor, the settings of the first protocol (highest in the list) take precedence for the duration of the overlap. Reorder your protocols to ensure the desired configuration wins when schedules clash.
- Staying in sync
- Devices check for updated configurations every time they upload data, typically every few minutes. If you change a protocol (adjust a sampling rate, add a sensor, or update a cron schedule), enrolled devices will pick up the change on their next upload cycle. No action is needed from participants.
Sensors
Collection modes
Interval
Polls the sensor at a fixed rate (e.g. every 100 ms). Configure the sampling rate to balance data resolution vs. battery use.
Event-driven
Only records when a change exceeds the threshold (e.g. acceleration changes by more than 0.5 m/s²). More battery-efficient for slow-changing signals.
✓ Available · Foreground Only while app is on screen · — Not available · Privacy-sensitive Requires clear justification in your IRB application
Motion & Orientation
| Sensor | Description | Data fields | Android | iOS | Mode | Default rate |
|---|---|---|---|---|---|---|
| Accelerometer | Raw acceleration including gravity along x, y, z axes. | x, y, z (m/s²) |
✓ | Foreground | Interval | 10 s |
| Linear Accelerometer | Acceleration with gravity removed, isolating user movement. | x, y, z (m/s²) |
✓ | Foreground | Interval | 10 s |
| Gyroscope | Angular rotation rate around x, y, z axes. | x, y, z (rad/s) |
✓ | Foreground | Interval | 10 s |
| Gravity | Direction and magnitude of the gravitational acceleration vector. | x, y, z (m/s²) |
✓ | Foreground | Interval | 10 s |
| Rotation | Device orientation as a rotation vector. | x, y, z, w |
✓ | Foreground | Interval | 10 s |
| Magnetometer | Ambient magnetic field strength. Useful for indoor positioning. | x, y, z (μT) |
✓ | Foreground | Interval | 10 s |
| Significant Motion | Fires once when the device detects significant movement (e.g. user starts walking). Low power. | trigger event |
✓ | — | Event-driven | — |
| Motion Activity | Classifies user activity: walking, running, driving, cycling, or stationary. | activity type, confidence |
✓ | ✓ | Event-driven | — |
| Pedometer | Step count and estimated distance walked. | steps, distance (m) |
✓ | ✓ | Event-driven | — |
| Headphone Motion | Head tracking data from AirPods Pro or compatible headphones. | pitch, roll, yaw (rad) |
— | Foreground | Interval | 10 s |
Accelerometer
- x float (m/s²)
- Acceleration along the device's X axis (pointing right when held in portrait). Includes the gravitational component; a device lying still on a flat surface reads ~9.8 m/s² on z, not x.
- y float (m/s²)
- Acceleration along the device's Y axis (pointing up in portrait mode). Includes gravity.
- z float (m/s²)
- Acceleration along the device's Z axis (pointing out of the screen). Includes gravity. A device lying flat, face up, reads approximately −9.8 m/s² here.
Linear Accelerometer
- x float (m/s²)
- Linear acceleration along X with the gravitational component subtracted, isolating user-induced motion.
- y float (m/s²)
- Linear acceleration along Y with gravity removed.
- z float (m/s²)
- Linear acceleration along Z with gravity removed. All three axes read ~0 when the device is stationary.
Gyroscope
- x float (rad/s)
- Rotation rate around the X axis (pitch, i.e. tilting the device forward or backward).
- y float (rad/s)
- Rotation rate around the Y axis (roll, i.e. tilting the device left or right).
- z float (rad/s)
- Rotation rate around the Z axis (yaw, i.e. rotating the device clockwise or counter-clockwise when lying flat).
Gravity
- x float (m/s²)
- Gravitational acceleration projected onto the device's X axis. Reflects device tilt (left/right) relative to gravity.
- y float (m/s²)
- Gravitational acceleration projected onto the Y axis. Reflects device tilt (forward/backward) relative to gravity.
- z float (m/s²)
- Gravitational acceleration projected onto the Z axis. A device held upright (portrait) shows ~−9.8 m/s² on y; lying flat face-up shows ~−9.8 m/s² on z.
Rotation
- x float
- On Android: X component of the unit rotation quaternion. On iOS: pitch angle in radians (forward/backward tilt), mapped to this field because iOS uses Euler angles internally.
- y float
- On Android: Y component of the unit rotation quaternion. On iOS: roll angle in radians (left/right tilt).
- z float
- On Android: Z component of the rotation quaternion. On iOS: yaw angle in radians (compass rotation around the vertical axis).
- w float
- Scalar component of the quaternion (Android only). Always 0.0 on iOS, which provides Euler angles rather than a quaternion. Do not interpret w as a quaternion component when using iOS data.
Magnetometer
- x float (μT)
- Magnetic field strength along the device's X axis in microteslas.
- y float (μT)
- Magnetic field strength along the Y axis.
- z float (μT)
- Magnetic field strength along the Z axis. Total field magnitude is √(x²+y²+z²). Earth's geomagnetic field is typically 25–65 μT.
Significant Motion
- detected string
- Always the string "1". Emitted each time the OS hardware trigger fires. This is a one-shot trigger, and it automatically re-arms after each event. Android only; no iOS equivalent.
Motion Activity
- activity string
- Classified activity type. Both platforms: WALKING, RUNNING, CYCLING, AUTOMOTIVE, STATIONARY, UNKNOWN. Android additionally reports ON_FOOT (walking or running, coarser classification) and TILTING (device orientation changing significantly). iOS uses Core Motion's CMMotionActivityManager.
- confidence float (0–1)
- Confidence in the classification, expressed as a decimal. Three possible values: 0.33 (low), 0.66 (medium), 1.0 (high). Both platforms use this same three-level scheme.
Pedometer
- steps integer
- Number of steps taken since the last emitted reading (delta count, not cumulative). A reading is only emitted when at least one new step is detected. Uses the device's built-in step-counting hardware (CMPedometer on iOS, Step Counter sensor on Android).
- distance float (m)
- Total cumulative distance walked since the pedometer session started, in metres. iOS only (from CMPedometerData.distance). Android does not emit this field.
Headphone Motion
- pitch float (rad)
- Head tilt forward or backward. Positive values indicate looking down (chin toward chest). iOS only, requires AirPods Pro (1st/2nd generation), AirPods 3rd generation, or AirPods Max.
- roll float (rad)
- Head tilt left or right (ear toward shoulder). Positive values indicate tilting to the right.
- yaw float (rad)
- Head rotation left or right in the horizontal plane (turning to look sideways). Positive values indicate turning to the right. Readings are only emitted when the headphones are worn and motions are actively detected.
Environment
| Sensor | Description | Data fields | Android | iOS | Mode | Default rate |
|---|---|---|---|---|---|---|
| Barometer | Atmospheric pressure. Can indicate altitude changes or weather shifts. | pressure (hPa) |
✓ | ✓ | Interval | 90 s |
| Light | Ambient light level from the device's light sensor. | lux |
✓ | — | Event-driven | — |
| Temperature | Ambient temperature from the device sensor (not all devices have this). | °C |
✓ | — | Interval | 60 s |
| Proximity | Detects how close an object is to the screen (e.g. phone held to ear). | distance (cm), near flag |
✓ | Foreground | Event-driven | — |
| Ambient Noise | Sound pressure level from the microphone. Records dB level only, no audio content. | dB |
✓ | Foreground | Interval | 60 s |
| Weather | Current weather, air quality, and UV at the participant's location via OpenWeatherMap. | temperature, humidity, pressure, wind, condition, aqi, pollutants, uv_index |
✓ | ✓ | Interval | 1 hr |
Barometer
- pressure float (hPa)
- Atmospheric pressure in hectopascals. On iOS, converted from the native kPa value returned by CMAltimeter (multiplied by 10). On Android, read directly from the hardware TYPE_PRESSURE sensor which already returns hPa. Standard sea-level pressure is ~1013 hPa.
- altitude float (m)
- Relative altitude change in metres since the sensor was started, not absolute altitude above sea level. iOS only (from CMAltimeterData.relativeAltitude). Not emitted on Android. Useful for detecting floor changes in a building.
Light
- lux float (lx)
- Ambient illuminance from the hardware front-facing light sensor, in lux. Android only. Typical values: < 1 lx (darkness), 50–500 lx (indoor lighting), 1000–8000 lx (daylight indoors), > 10 000 lx (direct sunlight). Not available on iOS.
Temperature
- celsius float (°C)
- Ambient air temperature from the Android TYPE_AMBIENT_TEMPERATURE hardware sensor. Android only. This sensor is not present on most consumer smartphones; availability varies by manufacturer and model. Not available on iOS.
Proximity
- distance float (cm)
- Distance from the sensor to the nearest object. Both iOS and most Android devices return only binary values: 0.0 cm (near; object detected) or 5.0 cm (far; no object). A small number of Android devices return actual centimetre measurements.
- near integer ("0" or "1")
- 1 if an object is detected close to the screen (e.g. phone held to ear during a call), 0 otherwise. Derived from the distance value on both platforms (near = 1 when distance < 5 cm).
Ambient Noise
- decibels float (dB)
- Sound pressure level computed as 20 × log₁₀(RMS) from the microphone's audio buffer, rounded to 1 decimal place. A value of −160 dB indicates silence (near-zero amplitude). No audio is stored anywhere; only this numerical level is recorded. Available on both iOS and Android.
- rms float
- Root mean square amplitude of the raw audio samples before dB conversion. Android only; not emitted on iOS.
Weather
Requires an OpenWeatherMap API key in the sensor extras config. The key is used for three calls per poll cycle, all against coordinates coarsened to ~1.1 km precision:
Weather (free tier) via /data/2.5/weather
Returns temperature, humidity, pressure, wind, and a textual condition description.
Air quality (free tier) via /data/2.5/air_pollution
Returns OpenWeather's 1–5 AQI index (1 = Good, 5 = Very Poor) plus component concentrations in µg/m³ for CO, NO, NO2, O3, SO2, PM2.5, PM10, and NH3.
UV index (subscription) via /data/3.0/onecall
Returns the current UV index. One Call 3.0 is a separate OpenWeather product: activate the subscription in your OpenWeather account (first 1,000 calls/day are free). If the subscription is not active, this call returns 401 and the UV field is simply omitted from the reading; weather and AQI still flow normally.
Each poll call to each endpoint counts toward OpenWeather's rate limits. At a 1-hour interval per device (the default) you will stay well inside the free tiers even with many participants.
- temperature_c float (°C)
- Current outdoor temperature at the device's location, from OpenWeatherMap.
- humidity_pct integer (%)
- Relative humidity percentage (0–100).
- pressure_hpa float (hPa)
- Atmospheric pressure from the weather API. This is a large-scale meteorological measurement distinct from the on-device barometer sensor.
- wind_speed_ms float (m/s)
- Wind speed at the queried location.
- condition string
- Human-readable weather description from OpenWeatherMap (e.g. "clear sky", "light rain", "scattered clouds").
- aqi integer (1–5)
- OpenWeather's Air Quality Index for the location. 1 = Good, 2 = Fair, 3 = Moderate, 4 = Poor, 5 = Very Poor. Derived from the most significant pollutant at the location. Omitted if the air-pollution endpoint failed.
- aqi_pm2_5_ugm3 float (µg/m³)
- PM2.5 (fine particulate matter, diameter ≤ 2.5 µm) concentration. One of the strongest predictors of respiratory and cardiovascular health impact in ambient air.
- aqi_pm10_ugm3 float (µg/m³)
- PM10 (inhalable particulate matter, diameter ≤ 10 µm) concentration.
- aqi_no2_ugm3 float (µg/m³)
- Nitrogen dioxide concentration. Traffic-heavy areas typically see elevated NO2.
- aqi_o3_ugm3 float (µg/m³)
- Ground-level ozone concentration. Peaks on sunny days when precursor pollutants react in sunlight.
- aqi_so2_ugm3 float (µg/m³)
- Sulphur dioxide concentration, typically from industrial emissions.
- aqi_co_ugm3 float (µg/m³)
- Carbon monoxide concentration.
- aqi_no_ugm3 float (µg/m³)
- Nitric oxide concentration.
- aqi_nh3_ugm3 float (µg/m³)
- Ammonia concentration.
- uv_index float
- Current UV index (0 = no risk, 11+ = extreme). Sourced from OpenWeather's One Call 3.0 endpoint and present only when that subscription is active on the configured API key. If the subscription is not enabled, this field is absent from the reading but weather and AQI are unaffected.
- latitude float (°)
- Full-precision latitude recorded locally. This value is not sent to OpenWeatherMap; the upstream queries use coordinates coarsened to two decimal places (~1.1 km) to limit re-identification.
- longitude float (°)
- Full-precision longitude recorded locally. Same coarsening policy as latitude.
Location & Connectivity
| Sensor | Description | Data fields | Android | iOS | Mode | Default rate |
|---|---|---|---|---|---|---|
| GPS Location | Device coordinates using GPS, Wi-Fi, and cell tower triangulation. | latitude, longitude, altitude, speed, accuracy, bearing |
✓ | ✓ | Interval | 60 s |
| Wi-Fi | Scans nearby Wi-Fi access points. | SSID, BSSID, RSSI, frequency, security |
✓ | Foreground | Interval | 60 s |
| Bluetooth | Scans nearby Bluetooth Low Energy (BLE) devices. | name, address, RSSI, type |
✓ | ✓ | Interval | 60 s |
| Network | Network connectivity changes (Wi-Fi, cellular, offline). | connected, type, is_wifi, is_mobile, is_airplane |
✓ | ✓ | Event-driven | — |
| Telephony | Cell tower information and mobile network signal strength. | network type, signal, carrier, roaming, cell ID |
✓ | — | Interval | 30 s |
| iBeacon | Detects nearby iBeacon devices by scanning for BLE advertisement frames with the Apple iBeacon format. All detected beacons are reported automatically. | UUID, major, minor, RSSI, proximity, accuracy |
✓ | ✓ | Interval | 10 s |
GPS Location
- latitude float (°)
- WGS-84 latitude of the device's position, in decimal degrees.
- longitude float (°)
- WGS-84 longitude, in decimal degrees.
- altitude float (m)
- Altitude above sea level in metres. GPS altitude is generally less accurate than horizontal position.
- accuracy float (m)
- Estimated horizontal accuracy radius in metres at 68% confidence, meaning the true position is within this radius with ~68% probability.
- speed float (m/s)
- Instantaneous speed in metres per second. Returns −1 when unavailable (e.g. the device is stationary or no speed estimate is possible from the current fix).
- bearing float (°)
- Direction of travel in degrees clockwise from true north (0° = north, 90° = east). Called "course" on iOS. Returns −1 when unavailable (e.g. no movement).
- provider string
- Source of the location fix. Always "cllocation" on iOS. On Android, typically "gps" (GPS hardware) or "fused" (Google Fused Location Provider, which blends GPS, Wi-Fi, and cell data).
Wi-Fi
- ssid string
- Network name (SSID). On iOS, only the currently connected network is reported; scanning surrounding networks is not permitted. On Android, one reading is emitted per access point found in the full Wi-Fi scan.
- bssid string
- Hardware MAC address of the access point (e.g. "AA:BB:CC:DD:EE:FF").
- rssi integer (dBm)
- Received signal strength in dBm. More negative = weaker signal (e.g. −50 dBm is much stronger than −90 dBm). Android only; not available on iOS via NEHotspotNetwork.
- frequency integer (MHz)
- Wi-Fi channel frequency (e.g. 2412 for 2.4 GHz channel 1, 5180 for 5 GHz band). Used to distinguish 2.4 GHz and 5 GHz networks. Android only.
- security string
- Security protocol of the network (e.g. "WPA2", "WPA3", "Open"). Android only.
Bluetooth
- device_name string
- Advertised BLE peripheral name. "Unknown" if the peripheral broadcasts no name in its advertisement packet.
- address string
- Device identifier. On Android, the Bluetooth MAC address of the peripheral. On iOS, a CoreBluetooth-assigned UUID that is unique per app–device pair; Apple randomises this for privacy, so it does not correspond to the real hardware MAC and will differ if the app is reinstalled.
- rssi integer (dBm)
- Signal strength of the advertisement packet at the time of discovery. More negative = further away.
- device_type string
- Always "BLE" on both platforms. Only Bluetooth Low Energy advertisements are scanned; classic Bluetooth (BR/EDR) devices are not reported.
Network
- connected integer ("0" or "1")
- 1 if the device has an active, verified network path (a path that can reach the internet), 0 otherwise.
- network_type string
- Active interface type: "wifi", "mobile", or "other" (includes VPN and Ethernet on iOS). On Android, emits "none" when the network connection is lost.
- is_wifi integer ("0" or "1")
- 1 if the active path uses a Wi-Fi interface.
- is_mobile integer ("0" or "1")
- 1 if the active path uses a cellular data interface.
- is_airplane integer ("0" or "1")
- 1 if airplane mode is enabled. Read from Android’s Settings.Global.AIRPLANE_MODE_ON. Always 0 on iOS — Apple does not expose airplane mode state to third-party apps.
Telephony
- network_type string
- Mobile network generation of the serving cell: "LTE" (4G), "NR" (5G, Android API 29+), "WCDMA" (3G/UMTS), "GSM" (2G/EDGE), or "UNKNOWN". Android only.
- signal_strength integer (dBm)
- Signal strength of the serving cell in dBm. Requires Android API 29+. May be 0 on older devices where the API is unavailable.
- carrier_name string
- Name of the mobile network operator as reported by the SIM (e.g. "Verizon", "EE", "Deutsche Telekom").
- is_roaming integer ("0" or "1")
- 1 if the device is currently registered on a foreign (roaming) network.
- cell_id string
- Cell tower identifier. The plugin iterates getAllCellInfo() and emits one reading per cell in the list; in practice only the serving cell is usually returned.
iBeacon
- uuid string
- The beacon's proximity UUID. Used to identify which beacon deployment the device detected.
- major integer
- Beacon major identifier (0–65535). Typically used to group related beacons, e.g. by building or floor.
- minor integer
- Beacon minor identifier (0–65535). Typically used to identify an individual beacon within a major group.
- rssi integer (dBm)
- Received signal strength of the beacon advertisement.
- proximity string
- Estimated proximity zone based on calculated distance: "IMMEDIATE" (approx. < 0.5 m), "NEAR" (approx. < 3 m), "FAR" (approx. > 3 m), or "UNKNOWN" when the distance cannot be estimated.
- accuracy float (m)
- Estimated distance to the beacon in metres, calculated from the RSSI using the standard iBeacon path-loss formula. This is an estimate and can be noisy in reflective environments.
Device & System
| Sensor | Description | Data fields | Android | iOS | Mode | Default rate |
|---|---|---|---|---|---|---|
| Battery | Battery level and charging status. | level (%), status, plugged, health, temperature |
✓ | ✓ | Event-driven | — |
| Low Power Mode | Low Power Mode (iOS) / Power Save Mode (Android) state changes. | enabled |
✓ | ✓ | Event-driven | — |
| Do Not Disturb | Do Not Disturb / Focus mode state changes. | active, filter_level |
✓ | ✓ | Event-driven | — |
| Screen | Screen on/off and lock/unlock events. Android only — iOS does not expose screen on/off or lock/unlock events to third-party apps. | event |
✓ | — | Event-driven | — |
| Processor | CPU usage statistics. | user_load, system_load, idle_load (%) |
✓ | Foreground | Interval | 30 s |
| Timezone | Fires when the device timezone changes (e.g. travel). | timezone ID, UTC offset |
✓ | ✓ | Event-driven | — |
| App Installations | Logs when apps are installed or uninstalled. | package name, event type, app name |
✓ | — | Event-driven | — |
| Headphone Status | Detects wired and Bluetooth headphone connect/disconnect. | connected, type, device name |
✓ | ✓ | Event-driven | — |
| Ringer / Volume | Ringer mode and volume level changes. | ringer_mode, ring_volume, media_volume |
✓ | — | Event-driven | — |
| Device Info | Hardware and software snapshot collected once at enrollment. On by default and cannot be disabled (essential to the functioning of StudyRun). | model, manufacturer, os_version, platform, screen, storage, RAM, CPU, locale, timezone, available_sensors |
✓ | ✓ | Event-driven | Once at enrollment |
| Settings | Logs sensor toggle events, informed-consent agreements, and app-dormancy markers. On by default and cannot be disabled (essential to the functioning of StudyRun). | sensor, action |
✓ | ✓ | Event-driven | — |
Battery
- level float (%)
- Battery charge level from 0 to 100. iOS reports a float (from UIDevice.batteryLevel × 100); Android reports an integer percentage.
- status string
- Charging state: "charging", "discharging", "full", or "unknown". Available on both platforms.
- plugged string
- Power source when charging: "ac" (mains adapter), "usb", "wireless" (inductive), or "none". Android only.
- health string
- Battery health reported by Android: "good", "overheat", "dead", or "unknown". Not available on iOS.
- temperature float (°C)
- Battery temperature. Android only. Not available on iOS.
Low Power Mode
- enabled integer (0/1)
- 1 when Low Power Mode (iOS) / Power Save Mode (Android) is active, 0 otherwise. Android uses PowerManager.ACTION_POWER_SAVE_MODE_CHANGED; iOS uses NSProcessInfoPowerStateDidChangeNotification.
Do Not Disturb
- active integer (0/1)
- 1 when DND or any Focus mode is active, 0 otherwise.
- filter_level string
- Android only. One of "all" (no filtering), "priority" (priority senders only), "alarms" (alarms only), "none" (total silence), or "unknown". Empty string on iOS (iOS exposes only a boolean).
Screen
- event string
- Screen state transition. Emits "on" (display powered on), "off" (display powered off), or "unlocked" (device unlocked from lock screen). Android only.
Processor
- user_load float (%)
- CPU usage attributed to user-space processes. On Android, read from /proc/stat. On iOS, approximated as (activeProcessorCount / processorCount) × 100 because iOS does not expose true per-process CPU time to apps; treat iOS values as a coarse activity indicator rather than an accurate percentage.
- system_load float (%)
- CPU usage attributed to the kernel and system processes. Android only. Always 0.0 on iOS.
- idle_load float (%)
- Idle CPU percentage, computed as 100 − user_load − system_load.
Timezone
- timezone_id string
- IANA timezone identifier (e.g. "America/New_York", "Europe/Athens"). One reading is emitted at sensor start and again whenever the timezone changes. Useful for disambiguating local time in longitudinal studies.
- utc_offset integer (seconds)
- Current UTC offset in seconds, accounting for Daylight Saving Time (e.g. −18000 for UTC−5, 10800 for UTC+3). Use this alongside timestamps to reconstruct local time.
App Installations
- package_name string
- Android package name of the affected application (e.g. "com.spotify.music"). Android only.
- event_type string
- Installation event type: "INSTALLED" (new app added), "UPDATED" (existing app version changed), or "REMOVED" (app uninstalled).
- app_name string
- Human-readable application label as shown to the user (e.g. "Spotify", "Instagram").
Headphone Status
- connected integer ("0" or "1")
- 1 if a headphone or audio output device is currently connected, 0 if nothing is connected.
- type string
- Connection type: "wired" (3.5 mm, Lightning, or USB-C adapter), "bluetooth", or "none".
- device_name string
- Name of the connected audio device (e.g. "AirPods Pro", "Sony WH-1000XM5"). Empty string when nothing is connected.
Ringer / Volume
- ringer_mode string
- Ringer profile: "normal", "vibrate", "silent", or "unknown". Android only — iOS does not expose ringer mode or mute-switch state to apps. On iOS, this field is always "unavailable" and the sensor is marked as unavailable (it will not activate).
- ring_volume integer (0–100)
- Ringer and notification volume level as a percentage. Android only.
- media_volume integer (0–100)
- Media (audio output) volume level as a percentage. Available on Android. iOS emits only this field (media volume), since ringer mode is not accessible.
Device Info
A one-time snapshot of the device's hardware and software configuration, collected when the participant enrolls in the study. Not re-collected if the participant re-opens the app while already enrolled. The record is also stored in a dedicated device registry on the server (separate from sensor readings) and is used to populate the Devices list in the study dashboard. This sensor is on by default and cannot be disabled. It is essential to the functioning of StudyRun.
- platform string
- "Android" or "iOS".
- model string
- Device model identifier. Android: the marketing name from Build.MODEL (e.g. "Pixel 7"). iOS: the internal machine identifier (e.g. "iPhone14,3") — not the human-readable product name.
- manufacturer string
- Device manufacturer (e.g. "Google", "Samsung"). Always "Apple" on iOS.
- device_name string
- User-assigned device name. Android: same as model. iOS: the name set in Settings → General → About (e.g. "Nikos’s iPhone").
- os_version string
- Operating system version string (e.g. "Android 14" or "iOS 17.2").
- os_build string
- Build fingerprint / display string (e.g. "UQ1A.240105.004"). Android only.
- api_level integer
- Android SDK API level (e.g. "34" for Android 14). Android only.
- screen_width integer (px)
- Physical screen width in pixels.
- screen_height integer (px)
- Physical screen height in pixels.
- screen_density string
- Screen pixel density. Android: dots-per-inch as an integer string (e.g. "420"). iOS: scale factor as a multiplier string (e.g. "3x" for a 3× Retina display).
- total_ram_gb float (GB)
- Total physical RAM in gigabytes.
- total_storage_gb float (GB)
- Total internal storage capacity in gigabytes.
- available_storage_gb float (GB)
- Free internal storage at the time of enrollment in gigabytes.
- cpu_arch string
- CPU architecture. Android: first entry from Build.SUPPORTED_ABIS (e.g. "arm64-v8a"). iOS: the machine identifier string (same as model), since iOS does not expose the CPU architecture directly to apps.
- cpu_cores integer
- Number of available logical CPU cores.
- locale string
- Device locale at enrollment. Android: BCP 47 language tag (e.g. "en-US"). iOS: ISO 639-1 language code only (e.g. "en").
- timezone string
- IANA timezone identifier at enrollment (e.g. "America/New_York", "Europe/Athens").
- available_sensors string (comma-separated)
- Hardware sensors present on the device. Android: full list of sensor names from SensorManager (e.g. "BMI160 Accelerometer, AK09918 Magnetometer, ..."). iOS: subset probed at runtime — reports which of Accelerometer, Gyroscope, Magnetometer, DeviceMotion, Barometer, GPS, and Proximity are present.
Settings
Every row in this stream has the same two-field shape: a subject name (sensor field) and a payload (action field). The sensor field distinguishes the category of event. Rows of sensor_type = SETTINGS cover three categories: sensor toggle events (ACCELEROMETER, LOCATION, WIFI, etc.), participant consent (CONSENT), and app-dormancy markers (GAP_DETECTED). Other internal events you may see elsewhere in your exports are their own sensor_type rather than a SETTINGS sub-event, namely APP_START (every cold launch of the app), CRASH_REPORT (genuine crashes), and MICRO_RANDOMISATION (branch allocations for micro-randomised protocols). Those three are described at the bottom of this entry for reference. This sensor is on by default and cannot be disabled. It is essential to the functioning of StudyRun.
Within the SETTINGS stream, filter by the sensor field to isolate each category.
sensor = <SensorType>
A row where action is "ENABLED" or "DISABLED" marks the participant or app toggling that sensor on or off. Useful for reconstructing the exact enabled-window for any given sensor on a given device.
sensor = "CONSENT"
Informed-consent agreement. Two variants coexist:
• Mobile-asserted CONSENT. Written by the app the moment the participant taps "I agree to be part of this study" in the consent dialog (or when the app records a renewed consent on re-enrolment). Payload: {"action":"AGREED"}.
• Server-asserted CONSENT (SERVER_ENROLL). Written by the server at POST /api/v1/studies/{id}/enroll, the moment the backend receives the enrolment request. Payload: {"action":"AGREED", "source":"server_enroll", "enroll_ip":<ip>}. Filter on action->>'source' = 'server_enroll' to isolate these; they provide an independent, server-verified audit trail of when and from where each participant enrolled, and they survive even if the mobile-side row fails to upload.
sensor = "GAP_DETECTED"
A marker written when the app wakes up from background and discovers that more than one hour has passed since its last activity. The payload is a JSON string {"gap_ms":<ms>, "last_alive_ms":<epoch_ms>} giving the exact length of the dormant period and the timestamp immediately before the app went silent. Present on both iOS and Android, emitted on every wake event (BGTask, region exit, WorkManager tick, app resume, boot). Use this to diagnose aggressive OS battery optimisation or participants who force-close the app; see Troubleshooting → Long gaps (hours) for how to read them.
─────────────
The following events are their own sensor_type, not SETTINGS sub-events, but are described here because they play a similar diagnostic / metadata role.
sensor_type = "APP_START"
Written once on every cold launch of the app (including the first launch post-install and every launch after the OS has killed the process). Payload is a JSON object with message, app_version, build_number, and os_version. Pair runs of APP_START rows with GAP_DETECTED to distinguish "app crashed and was restarted by the OS" (GAP_DETECTED followed by APP_START) from "app was simply re-opened by the participant" (APP_START without a preceding GAP_DETECTED).
sensor_type = "MICRO_RANDOMISATION"
Written when a protocol containing a Micro-randomisation task selects a branch for that participant. Payload records protocol_id and branch_index (0-based, matches the branch order in the protocol editor). For cron protocols, one row per firing (each firing is an independent randomisation). For continuous protocols, one row per session: the branch is chosen when the session begins and held for its duration. These rows are what the Compliance table cross-references to colour-code per-branch tasks by branch assignment.
sensor_type = "CRASH_REPORT"
Written only for genuine, uncaught exceptions. Payload is a JSON object with exception_type (the exception class name), message (truncated to 1000 characters), and stack_trace (truncated to 5000 characters). Distinct from APP_START: a cold launch produces an APP_START row, a crash produces a CRASH_REPORT row just before the app dies.
- sensor string
- Identifies the subject of this SETTINGS record. Possible values: any SensorType name for a toggle event (e.g. "ACCELEROMETER", "LOCATION", "WIFI"); "CONSENT" for a participant-consent or server-enrolment event; "GAP_DETECTED" for an app-dormancy marker.
- action string
- The event payload. For sensor toggles: "ENABLED" or "DISABLED". For CONSENT: either the literal string "AGREED" (mobile-asserted) or a JSON object with source and enroll_ip fields (server-asserted). For GAP_DETECTED: a JSON string with gap_ms and last_alive_ms fields quantifying the dormant period.
User Behaviour
| Sensor | Description | Data fields | Android | iOS | Mode | Default rate |
|---|---|---|---|---|---|---|
| App Usage Privacy-sensitive | Records foreground and background transitions for each app on the device. No usage duration is stored on the reading itself — if you need time-in-app, compute it during analysis from the timestamps of consecutive events. | package name, event type |
✓ | — | Event-driven | — |
| Communication Privacy-sensitive | Call and SMS log metadata (duration, type). No message content is recorded. | comm_type, direction, duration |
✓ | — | Event-driven | — |
| Keyboard Privacy-sensitive | Keystroke timing and session statistics. Two modes: RAW (per-keystroke) or BATCHED (session summaries). | package name, event type, text content, keystroke count, timing |
✓ | — | Event-driven | — |
| Touch Privacy-sensitive | Touch interaction events (taps and long presses) captured via accessibility services. | action, package name, class name, content description |
✓ | — | Event-driven | — |
| Screen Text Privacy-sensitive | Captures on-screen text content using accessibility services. | package name, text content |
✓ | — | Event-driven | — |
| Screenshot Privacy-sensitive | Periodic screen captures at a configurable interval. | base64 JPEG, width, height |
✓ | — | Interval | 60 s |
| Notifications Privacy-sensitive | Logs notification metadata (app name, category). Configure detail level with contentLevel: metadata, title, or full. |
package name, app name, category, event [, title] [, text] |
✓ | — | Event-driven | — |
| Media Playing | Currently playing music or media: title, artist, and playback state. | state, app, title, artist, album, position |
✓ | Foreground | Event-driven | — |
App Usage
- package_name string
- Android package name of the app that changed foreground state (e.g. "com.instagram.android"). Android only — iOS does not permit monitoring of other apps.
- event_type string
- "MOVE_TO_FOREGROUND" when the app comes to the foreground (user opened or switched to it), or "MOVE_TO_BACKGROUND" when it is hidden behind another app. Recorded using the Android UsageEvents API. No session duration is computed on the device — derive it from the time difference between an app's foreground and subsequent background event.
Communication
- comm_type string
- Type of communication event: "CALL" or "SMS". Android only. No phone numbers, contact names, or message text is recorded.
- direction string
- For calls: "INCOMING", "OUTGOING", "MISSED", or "OTHER". For SMS: "RECEIVED" or "SENT".
- duration_seconds integer (s)
- Call duration in seconds. Only present for CALL events; not recorded for SMS messages.
Keyboard
Set keyboardMode to RAW or BATCHED.
- package_name string
- Android package name of the app where typing occurred (e.g. "com.whatsapp"). Android only — requires Accessibility Service to be enabled.
- event_type string
- "KEYSTROKE" in RAW mode (one reading per key press), or "TYPING_SESSION" in BATCHED mode (one reading per burst of typing). BATCHED mode flushes after 5 s of inactivity or after 30 s maximum session duration.
- text_content string
- The typed text delta for a single keystroke (RAW mode), or the full accumulated text of the typing session (BATCHED mode). Deletions appear as "[DEL:N]" tokens where N is the number of characters deleted.
- keystroke_count integer
- Number of keystrokes in this reading: always 1 in RAW mode, the total keystroke count for the session in BATCHED mode.
- start_time_ms integer (ms)
- Unix timestamp in milliseconds of the first keystroke in this reading.
- end_time_ms integer (ms)
- Unix timestamp of the last keystroke. Equal to start_time_ms in RAW mode.
Touch
- action string
- Touch event type: "CLICK" (single tap) or "LONG_CLICK" (long press). Android only, via Accessibility Service. Touch coordinates are not recorded; only the event type and target element metadata.
- package_name string
- Package name of the app in which the touch occurred.
- class_name string
- Accessibility class name of the touched view (e.g. "android.widget.Button", "android.widget.TextView"). Reflects the UI element type, not its position.
- content_description string
- Accessibility content description of the touched element, if set by the app (often the button label, icon alt text, or semantic label). Empty string if not set.
Screen Text
- package_name string
- Package name of the app whose screen content was captured. Android only, via Accessibility Service.
- text_content string
- All visible text from the current screen, collected by traversing the full accessibility node tree from the root window. Multiple text nodes are joined with newlines. Duplicate text nodes are de-duplicated. Capture is debounced by 500 ms after each screen change to avoid capturing partial UI states during animations or transitions.
Screenshot
The Screenshot sensor uses Android's MediaProjection API, which the operating system requires to render certain content as black pixels. This affects DRM-protected video (for example YouTube, Netflix, and other streaming apps) and any app screen marked as secure (for example banking apps, password managers, and payment or password entry fields). As a result, screenshots captured while such content is on screen will appear mostly or entirely black. This is an OS-level security guarantee that applications cannot bypass, and it means the sensor physically cannot capture passwords, payment screens, or protected video. Captured frames may also show a small correctly-rendered fragment (such as a status bar element) alongside the black area, which is normal.
- file_path string
- Base64-encoded JPEG image of the screen at the time of capture, prefixed with "base64:". Captured at JPEG quality 30 to reduce storage size. Android only.
- width integer (px)
- Screen width in pixels at the time of capture.
- height integer (px)
- Screen height in pixels at the time of capture.
Notifications
Set contentLevel to metadata, title, or full.
- package_name string
- Package name of the app that posted the notification (e.g. "com.google.android.gm"). Android only, via Notification Listener Service.
- app_name string
- Human-readable application name (e.g. "Gmail").
- category string
- Android notification category (e.g. "msg", "social", "email", "call", "reminder"). "unknown" if the posting app did not set a category.
- event string
- "posted" when a notification appears in the status bar, "removed" when it is dismissed or cleared by the user or the app.
- title string
- Notification title text. Only present when contentLevel is set to "title" or "full". Not collected at the default "metadata" level.
- text string
- Notification body text. Only present when contentLevel is set to "full". Not collected at "metadata" or "title" levels.
Media Playing
- state string
- Playback state: "playing", "paused", "stopped", "buffering", or "none" (nothing playing). Android reports precise state from the MediaSession API. iOS infers "playing" from MPNowPlayingInfoCenter having a non-empty title or artist; explicit pause/stop detection is not available on iOS.
- app_package string
- Package name of the media app (e.g. "com.spotify.music"). On iOS, always "now_playing_info" — the system media center does not expose the originating app's identity.
- title string
- Track, episode, or media title.
- artist string
- Artist, podcast creator, channel, or uploader name.
- album string
- Album name. May be empty for podcasts, live streams, or videos.
- duration_ms integer (ms)
- Total track or episode duration in milliseconds. 0 if unknown (e.g. live streams).
- position_ms integer (ms)
- Current playback position in milliseconds. On Android, from the MediaSession PlaybackState. Always 0 for the notification-fallback source (Chrome, Firefox, etc.) and on iOS.
Health & Wearable
| Sensor | Description | Data fields | Android | iOS | Mode | Default rate |
|---|---|---|---|---|---|---|
| HealthKit | Reads a broad range of health data from Apple Health: activity, body measurements, vitals, sleep, audio exposure, walking quality, and Clinical Health Records (allergies, conditions, medications, lab results, immunizations, procedures, vital signs, coverage and clinical notes). Clinical records carry the raw FHIR JSON so downstream analysis can parse whatever FHIR release the provider returned. | data_type, value, unit, end_ms; clinical rows additionally carry display_name, fhir_resource_type, fhir_resource_id, fhir_version, fhir_json |
— | ✓ | Interval | 60 s |
| Health Connect | Reads a broad range of health data from Android's Health Connect: activity, body measurements, vitals, sleep, and more. Requires the Health Connect app (built into Android 14+) and a source app (Fitbit, Samsung Health, Wear OS watch, etc.) that writes data into it. | data_type, value, unit, end_ms |
✓ | — | Interval | 60 s |
HealthKit
- source string
- Always "healthkit". Identifies the data origin for queries that combine HealthKit and other sensor types.
- data_type string
- The metric or record type in this reading. Quantity types (e.g. "steps", "heart_rate", "vo2_max", "active_energy_burned", "walking_speed"), category types (e.g. "sleep_analysis", "mindful_session", "handwashing_event"), and clinical records (all prefixed with "clinical_"): "clinical_allergy", "clinical_condition", "clinical_coverage", "clinical_immunization", "clinical_lab_result", "clinical_medication", "clinical_procedure", "clinical_vital_sign", and "clinical_note". Each reading contains exactly one type.
- value string
- Numeric value for quantity types in the unit shown by the "unit" field. For category types, an integer representing the sample value (e.g. sleep stage enum). Empty for clinical records — see fhir_json instead.
- unit string
- Unit of measurement for quantity types (e.g. "count", "count/min", "m", "kcal", "kg", "degC", "mmHg"). Literal "category" for category types. Absent for clinical records.
- end_ms integer
- End timestamp of the underlying sample in epoch-ms. For instantaneous samples equals the row timestamp; for sessions (sleep, exercise) marks the session end.
- display_name string
- (Clinical records only) Human-readable title of the record as shown in the Apple Health UI (e.g. "Penicillin" for an allergy).
- fhir_resource_type string
- (Clinical records only) FHIR resource type of the underlying record (e.g. "AllergyIntolerance", "Condition", "MedicationRequest", "Observation", "Procedure", "Immunization", "Coverage", "DocumentReference").
- fhir_resource_id string
- (Clinical records only) FHIR resource identifier from the source provider. Stable within a provider; use together with fhir_resource_type to de-duplicate across polls.
- fhir_version string
- (Clinical records only) FHIR release the record was encoded in (e.g. "DSTU2", "R4"). Varies by healthcare provider.
- fhir_json string
- (Clinical records only) The raw FHIR resource as a JSON string. Shipped verbatim so downstream analysis can interpret whatever FHIR release the provider used.
Health Connect
- source string
- Always "health_connect". Identifies the data origin.
- data_type string
- The health metric in this reading (e.g. "steps", "heart_rate", "sleep", "active_energy_burned"). Each reading contains exactly one type.
- value string
- Numeric value in the unit shown by the "unit" field. For category/session types (e.g. sleep stage), this is an integer representing the stage enum.
- unit string
- Unit of measurement: e.g. "count", "bpm", "m", "kcal", "kg", "celsius", "mmHg". For sleep stages, "stage".
- end_ms integer
- End timestamp of the underlying record in epoch-ms. For instantaneous samples, equals the row timestamp; for sessions (sleep, exercise), marks the session end.
Personal Data
| Sensor | Description | Data fields | Android | iOS | Mode | Default rate |
|---|---|---|---|---|---|---|
| Calendar Privacy-sensitive | Calendar event metadata for the next 24 hours. No event content beyond the title is recorded. | event title, start/end time, all-day flag |
✓ | ✓ | Interval | 5 min |
| Contacts Privacy-sensitive | Contact list summary: total count only. No names, numbers, or emails are recorded. | total count, groups count |
✓ | ✓ | Interval | 5 min |
Calendar
- event_title string
- Title of the calendar event (e.g. "Team meeting"). One reading is emitted per event found in the next 24-hour window at the time of the poll. On Android, all device calendars are queried. On iOS, only calendars the user granted access to via EventKit.
- start_time integer (ms)
- Event start time as a Unix timestamp in milliseconds (UTC).
- end_time integer (ms)
- Event end time as a Unix timestamp in milliseconds (UTC).
- is_all_day integer ("0" or "1")
- 1 if the event spans the full day with no specific start/end time, 0 for timed events.
Contacts
- total_count integer
- Total number of contacts in the device address book at the time of the reading. No individual contact data (names, phone numbers, email addresses) is stored anywhere.
- groups_count integer
- Number of contact groups (e.g. "Family", "Work"). Android only — not emitted on iOS.
ESM & Tasks
Experience Sampling Method (ESM)
ESM questionnaires let you prompt participants with surveys at scheduled times. Add an ESM task to any protocol with a cron schedule (e.g. 0 9,13,21 * * * for 9 AM, 1 PM, and 9 PM daily). When the protocol triggers, the participant has a configurable time window to respond before the questionnaire expires. Notifications are configured on the protocol, not on the ESM task itself.
Question types
| Name | Description |
|---|---|
| Free Text | Open-ended text response. Participant types freely. |
| Single Choice | Radio buttons. Participant picks exactly one option from a list you define. |
| Multiple Choice | Checkboxes. Participant picks one or more options. |
| Likert Scale | Agreement scale (e.g. 1–5). Configurable: 3, 5, 7, or 9 points. |
| Numeric Slider | Draggable slider between a min and max value. Set the step size for granularity. |
| Visual Analogue Scale | 0–100 continuous slider with custom endpoint labels (e.g. "Not at all" to "Extremely"). |
| Yes / No | Simple binary choice. |
| Numeric Input | Text field that only accepts numbers. |
| Scale with Labels | Discrete scale where each point has a custom label (e.g. Poor, Fair, Good, Very Good, Excellent). |
| Matrix / Grid | Table with rows (items) and columns (rating options). Useful for rating multiple items on the same scale. |
| Ranking | Drag-to-reorder list. Participant ranks options by preference. |
| Date Picker | Calendar date selection. |
| Time Picker | Hours and minutes selection. |
| Date + Time Picker | Combined date and time selection. |
Questionnaire settings
Title
Shown at the top of the questionnaire on the participant's device.
Instructions
Optional text shown to the participant before the questions begin.
Required
Per-question toggle. Required questions must be answered before the participant can submit.
Collected data: ESM
Each submitted questionnaire produces one record with the following fields.
| Field | Type | Description |
|---|---|---|
| protocol_id | string | ID of the protocol that triggered this questionnaire. |
| protocol_sequence | string | Task position within the protocol. For regular tasks this is a plain integer (e.g. "1"). For tasks inside a micro-randomisation branch it is a comma-separated triple "<mr_position>,<branch>,<task>" (all 1-based, e.g. "3,1,2" = MR at position 3, branch 1, sub-task 2). Use this to group co-occurring records and identify micro-randomisation branches. |
| trigger_type | string | "COMPLETED" if the participant submitted the questionnaire, "SKIPPED" if they dismissed it, or "EXPIRED" if the expiration window passed before submission. |
| <question_id> | varies | Each question in the questionnaire appears as its own column named after the question's ID (e.g. data.PQ1, data.mood, data.Q3). The value type depends on the question type: a string for free text, a number for sliders and numeric input, a boolean for Yes/No, an array for multiple-choice or ranking, or a nested object for matrix/grid. Skipped or expired responses have no question columns. |
Cognitive tasks
Psychomotor Vigilance Task (PVT)
A sustained-attention reaction time test. A visual stimulus appears at random intervals and the participant taps as fast as possible. Commonly used to measure alertness and sleep deprivation effects.
Test duration (default 180s) controls how long the entire test runs. Min interval (default 2000ms) and max interval (default 10000ms) define the random wait range before each stimulus appears. Lapse timeout (default 500ms) is the threshold above which a response is classified as a lapse rather than a valid response; in standard PVT research, 500ms is the accepted cutoff. False start threshold (default 100ms) is the minimum reaction time for a response to count as valid; anything faster is physiologically impossible and recorded as a premature tap. Show feedback toggles whether participants see their reaction time after each tap.
- protocol_id string
- ID of the protocol that triggered this task.
- protocol_sequence string
- Task position within the protocol. For regular tasks this is a plain integer (e.g. "1"). For tasks inside a micro-randomisation branch it is a comma-separated triple "<mr_position>,<branch>,<task>" (all 1-based, e.g. "3,1,2" = MR at position 3, branch 1, sub-task 2). Use this to group co-occurring records and identify micro-randomisation branches.
- status string
- "COMPLETED" if the participant finished the test, or "SKIPPED" if they dismissed without responding.
- totalTrials integer
- Total number of stimuli presented during the test (valid responses + lapses + false starts).
- validTrials integer
- Responses within the valid range (faster than lapse timeout, slower than false start threshold).
- meanRt integer (ms)
- Mean reaction time across all valid trials.
- medianRt integer (ms)
- Median reaction time across all valid trials.
- reciprocalRt float (1/s)
- Mean of 1/RT across valid trials. A standard PVT metric less sensitive to outlier slow responses.
- fastest10pct integer (ms)
- Mean RT of the fastest 10% of valid trials.
- slowest10pct integer (ms)
- Mean RT of the slowest 10% of valid trials.
- fastestRt integer (ms)
- Single fastest valid reaction time recorded.
- slowestRt integer (ms)
- Single slowest valid reaction time recorded.
- lapses integer
- Number of responses slower than the lapse timeout (default ≥500 ms). High lapse counts indicate drowsiness or inattention.
- falseStarts integer
- Number of taps faster than the false start threshold (default <100 ms). These are premature responses recorded before the stimulus was cognitively processed.
- trials JSON array
- Raw trial-level data as a JSON array. Each entry has rt (reaction time ms), type ("valid", "lapse", or "false_start"), and ts (timestamp ms when the trial ended).
Photo Diary
Prompts the participant to take a photo at a scheduled time. When the task fires, the app displays a full-screen sheet with the prompt and a camera button. The participant can take a photo and submit it, or skip. Skipped entries are recorded with trigger_type "SKIPPED" and an empty image. Photos are stored as base64-encoded JPEGs and uploaded with the next upload batch.
Instructions: the prompt text shown to the participant (e.g. "Take a photo of your current meal"). Participants can always skip the task.
- protocol_id string
- ID of the protocol that triggered this task.
- protocol_sequence string
- Task position within the protocol. For regular tasks this is a plain integer (e.g. "1"). For tasks inside a micro-randomisation branch it is a comma-separated triple "<mr_position>,<branch>,<task>" (all 1-based, e.g. "3,1,2" = MR at position 3, branch 1, sub-task 2). Use this to group co-occurring records and identify micro-randomisation branches.
- trigger_type string
- "COMPLETED" if the participant took and submitted a photo, or "SKIPPED" if they dismissed without taking one.
- prompt string
- The instruction text shown to the participant at capture time (from the protocol's Instructions field).
- image_data string (base64)
- Base64-encoded JPEG of the captured photo, compressed at 70% quality. Empty string when trigger_type is "SKIPPED". Images can be large; factor this into upload frequency and storage planning.
- width integer (px)
- Width of the captured image in pixels. 0 when skipped.
- height integer (px)
- Height of the captured image in pixels. 0 when skipped.
Audio Recording
Prompts the participant to record a spoken response at a scheduled time. The app displays a full-screen sheet with the prompt and a Start Recording button. The participant can stop early at any point or skip. Recordings are stored as base64-encoded M4A (AAC) audio and uploaded with the next upload batch.
Instructions: the prompt shown to the participant (e.g. "Describe how you are feeling right now"). Max duration: the maximum recording length in seconds (default 120 s). Participants can always stop early or skip.
- protocol_id string
- ID of the protocol that triggered this task.
- protocol_sequence string
- Task position within the protocol. For regular tasks this is a plain integer (e.g. "1"). For tasks inside a micro-randomisation branch it is a comma-separated triple "<mr_position>,<branch>,<task>" (all 1-based, e.g. "3,1,2" = MR at position 3, branch 1, sub-task 2). Use this to group co-occurring records and identify micro-randomisation branches.
- trigger_type string
- "COMPLETED" if the participant recorded and submitted audio, or "SKIPPED" if they dismissed without recording.
- prompt string
- The instruction text shown to the participant at recording time (from the protocol's Instructions field).
- audio_data string (base64)
- Base64-encoded M4A (AAC) audio of the recording. Empty string when trigger_type is "SKIPPED". Audio files can be large; factor this into upload frequency and storage planning.
- duration_seconds float (s)
- Actual recorded duration in seconds. May be less than the configured max if the participant stopped early. 0 when skipped.
- format string
- Audio container format. Always "m4a" on both Android and iOS.
Show Webpage
Opens a researcher-configured URL inside the app using a native webview. The participant reads or interacts with the page and taps a Close button to finish the task. Useful for delivering instructions, reading material, information sheets, or external surveys without leaving StudyRun. Only the task duration is logged; no browsing history, no page content, no final URL. URLs support a special {PARTICIPANT_ID} token that is replaced at runtime with the participant's short ID (the part before the "-" character), enabling personalised links to external survey platforms, per-participant instruction pages, or any URL that needs to identify who is viewing it.
Instructions: optional pre-amble text shown above the webview. URL: the full URL to load (must start with http:// or https://). The webview supports free navigation; researchers should pick a URL where unrestricted navigation is acceptable. Personalised URLs: place the token {PARTICIPANT_ID} anywhere in the URL and it will be replaced with the participant's short ID at runtime. Only the part of the ID before the "-" character is used (e.g. if the full ID is "P01-a7f3b2", the token resolves to "P01"). This is useful for linking to external surveys (Qualtrics, REDCap, LimeSurvey, etc.) that accept a participant identifier as a URL parameter, or for serving per-participant content from your own server. Examples: https://survey.example.com/{PARTICIPANT_ID}/start → https://survey.example.com/P01/start; https://qualtrics.com/jfe/form/SV_abc?pid={PARTICIPANT_ID} → https://qualtrics.com/jfe/form/SV_abc?pid=P01. The token is case-sensitive and must be written exactly as {PARTICIPANT_ID} including the curly braces. If no token is present, the URL is used as-is for all participants. If participant IDs are not enabled for the study, the token resolves to an empty string.
- protocol_id string
- ID of the protocol that triggered this task.
- protocol_sequence string
- Task position within the protocol. For regular tasks this is a plain integer (e.g. "1"). For tasks inside a micro-randomisation branch it is a comma-separated triple "<mr_position>,<branch>,<task>" (all 1-based, e.g. "3,1,2"). Use this to group co-occurring records.
- trigger_type string
- "COMPLETED" when the participant tapped Close, or "EXPIRED" if the session window closed before they opened or completed it.
- instructions string
- The instruction text configured by the researcher (may be empty).
- url string
- The resolved URL that was actually loaded in the participant's webview. If the configured URL contained {PARTICIPANT_ID}, this field shows the URL after replacement (e.g. "https://survey.example.com/P01/start"), not the template.
- duration_ms integer (ms)
- Time between the webview appearing on screen and the participant tapping Close. 0 for expired tasks that were never opened.
Show Markdown
Displays a researcher-authored Markdown (CommonMark) page inside the app. When the task fires, the app renders the Markdown as formatted HTML (headings, bold / italic, bullet and numbered lists, links, code blocks, and quotes are all supported), and the participant reads it and taps Close to finish. Useful for delivering short study instructions, daily reminders, safety information, or any structured text content without having to host a webpage externally. No participant input is collected beyond the view duration. The raw Markdown source is stored with each result so the content can be reviewed alongside the duration even if the protocol is later edited or deleted.
Markdown content: the CommonMark source. Researchers use a textarea in the protocol editor, with a Help me link that opens a quick reference to Markdown syntax (headings, lists, links, bold / italic, code). Any heading the participant should see lives in the Markdown itself (via # or ##); there is no separate title field. Links work: tapping one opens the URL in the system browser. Images (via ) are rendered if the URL is reachable. There is no length limit enforced by the app, but keep the content short; participants are expected to read and dismiss in under a minute.
- protocol_id string
- ID of the protocol that triggered this task.
- protocol_sequence string
- Task position within the protocol. For regular tasks this is a plain integer (e.g. "1"). For tasks inside a micro-randomisation branch it is a comma-separated triple "<mr_position>,<branch>,<task>" (all 1-based, e.g. "3,1,2"). Use this to group co-occurring records.
- trigger_type string
- "COMPLETED" when the participant tapped Close, or "EXPIRED" if the session window closed before they opened or completed it.
- content string
- The raw Markdown source the participant was shown (copy of what the researcher authored at the time the task was locked). Preserved verbatim so the rendered text can be reconstructed later.
- duration_ms integer (ms)
- Time between the rendered page appearing on screen and the participant tapping Close. 0 for expired tasks that were never opened.
Micro-randomisation
A container task for micro-randomised trials (MRTs) and just-in-time adaptive interventions (JITAIs). Each micro-randomisation has N branches, each with a probability (integer percentages summing to 100). Each branch can contain zero or more sub-tasks of any type (ESM, PVT, Photo Upload, Audio Recording, Show Webpage, Show Markdown). An empty branch acts as a control arm, so no tasks are presented. When the protocol fires, the app randomly selects one branch based on the configured probabilities and presents only that branch's sub-tasks. The branch selection happens on the participant's device and is logged as a separate sensor reading. Sub-tasks within the selected branch execute sequentially and each logs its own result under its own sensor type, just like top-level tasks. For cron-based protocols, each firing is an independent randomisation, which is ideal for within-person designs. For continuous protocols, the branch is selected once when the session is created and repeats until the study config is reloaded.
Branches: each branch has an optional label (e.g. "Treatment A", "Control"), a probability (integer 0–100, must sum to 100 across all branches), and zero or more sub-tasks. Researchers configure the branches and their probabilities in the protocol editor. Sub-tasks can be any interactive task type: ESM Questionnaire, PVT, Photo Upload, Audio Recording, or Show Webpage. Sensor Collection and nested Micro-randomisation are not allowed inside branches. An empty branch (no sub-tasks) serves as a control arm: the app logs the randomisation event but presents no task to the participant, and the protocol advances to the next top-level task.
- protocol_id string
- ID of the protocol containing this micro-randomisation.
- branch_index integer
- Zero-indexed branch that was randomly selected (e.g. 0 for the first branch, 1 for the second).
- branch_probability integer
- The configured probability (%) of the selected branch.
- total_branches integer
- Total number of branches in the micro-randomisation container.
- session_id string
- Session identifier linking this randomisation event to the sub-task results that follow. Use this to join the branch selection with the ESM responses, PVT trials, etc. that were presented as a consequence of this selection.
Data & Export
Accessing your study data
StudyRun gives you real-time access to everything your participants have collected. The Data tab lets you preview live readings as they arrive. The Visualisation tab shows coverage heatmaps so you can spot collection gaps instantly. The Export tab lets you download the full dataset in your preferred format for analysis. All timestamps are stored in UTC; no timezone conversion is applied before export.
Viewing data
- Data tab
- A live, scrollable table of sensor readings. Filter by sensor type, device, participant ID, or time range. Useful for spot-checking data quality during the study (for example, verifying that a newly enrolled participant is successfully uploading, or confirming a specific sensor is recording correctly). Each row shows the timestamp (UTC), sensor type, device ID, participant ID, and the sensor-specific data payload.
- Virtual sensor types
- The sensor-type filter also includes a couple of virtual entries that don't come from participant devices. HEARTBEAT exposes device-heartbeat events from the dedicated heartbeats table, so you can confirm when each device checked in regardless of whether any sensor was active. PROTOCOL_TRIGGER appears only in studies that have at least one JITAI (triggered) protocol with recorded firings; each row is one moment when a trigger rule evaluated TRUE for a device and the cooldown / max-firings gates allowed the protocol to run. The
data.eval_contextcolumn holds a JSON snapshot of the rule-tree evaluation — which leaves matched and with what sensor values — useful for verifying trigger cadence and debugging rules after the fact. - Visualisation tab
- Charts and heatmaps built from your collected data. The Sensor Completeness heatmap shows, per participant, which sensors have reported data over time, helping you identify individuals or sensors with collection gaps. The Client Connectivity heatmap shows device heartbeat activity over time: a green cell means the device checked in during that period, red means no heartbeats were received. Use this to distinguish a participant who simply has not uploaded yet from one whose app has stopped running.
Exporting
- CSV
- One row per sensor reading, with the sensor-specific data fields flattened into separate columns. Easiest to open in Excel or import into R and SPSS. Nested fields (e.g. accelerometer x, y, z) become separate columns. Best for smaller datasets or when you need a quick look at the data.
- JSON
- Each reading is a JSON object preserving the full nested data structure exactly as it was stored. Best for programmatic processing in Python or JavaScript where you want the complete payload without flattening decisions. Delivered as a gzip-compressed .json.gz file (roughly 10x smaller than raw JSON): pandas.read_json() reads it directly, or use gzip.open() in Python / zcat on the command line. Decompress it first if you want to open it in a text editor.
- Parquet
- Columnar binary format. Highly compressed and fast to read with pandas, Spark, or DuckDB. The best choice for large datasets (millions of readings). Parquet preserves type information (timestamps, integers, floats) without string conversion.
- Applying filters
- Before exporting, apply any combination of filters: date range, sensor types, specific devices, or specific participants. Only data matching all active filters is included. This lets you export a single sensor across all participants, or everything collected for a specific participant, without downloading the full dataset.
- Chunked downloads
- Clicking Prepare download splits your export into date-range files (UTC day-aligned, roughly 2.5 million readings each), so a months-long high-frequency study arrives as a series of modest, reliably-downloadable files instead of one enormous one. The chunks tile the full time range with no gaps and no overlaps: downloading every chunk gives you exactly the complete dataset, and each file name carries its date range so a folder of chunks stays sortable. Use Download all to fetch them one after another; if a download fails or your laptop sleeps, re-download just that chunk.
- Download size
- There is no size limit on exports. Files are streamed directly to your browser as native downloads (with a normal browser progress bar); the server does not buffer the entire file in memory, and neither does the browser. Row counts and sizes shown in the download planner are estimates; the actual files are compressed and typically smaller. For very large datasets, Parquet compresses significantly better than CSV.
Data schema
- timestamp
- Unix timestamp in milliseconds, UTC. All sensor readings from all devices share the same reference. When comparing readings across devices or merging with other datasets, no timezone conversion is required; all values are already in UTC.
- sensor_type
- String identifier for the sensor, for example ACCELEROMETER, LOCATION, HEART_RATE, or BATTERY. Matches the sensor names shown in the Sensors reference tab, which also lists all sensor-specific fields in the data object.
- device_id
- Unique identifier for the physical device. Stable for the lifetime of a study enrolment. If a participant re-enrolls on the same phone (e.g. after re-installing the app), they receive a new device ID. Multiple devices can share the same participant ID if a person has enrolled on more than one device.
- participant_id
- The participant identifier provided at enrolment. Maps directly to the ID you distributed during recruitment. If participant IDs are not required by your study, this is an auto-generated anonymous ID assigned at enrolment.
- batch_id
- Unique identifier for the upload batch this reading arrived in. Used internally for deduplication: if a device retries a failed upload, the server recognises the batch ID and discards the duplicate without storing it twice. You do not need to do anything with this field in your analysis.
- protocol_id
- The ID of the locked protocol that was active when this reading was collected. Because protocols are versioned snapshots, this field lets you trace every reading back to the exact sensor configuration, schedule, and participant filter that was in effect at collection time.
- data
- A JSON object containing all sensor-specific fields for this reading. For example, an accelerometer reading's data object contains x, y, z (in m/s²). See the Sensors tab for the complete field definitions for each sensor type. In CSV exports this object is flattened into individual columns.
Understanding gaps in your data
- Short gaps (minutes)
- Normal and expected. Gaps of a few seconds to a few minutes occur when the device restarts, switches between WiFi and mobile data, or the OS temporarily suspends the app. Data is cached on the device and uploaded as soon as connectivity resumes; no readings are lost.
- Longer gaps (hours)
- Usually indicate the app was force-closed by the participant or killed by the OS battery manager. Check the Client Connectivity heatmap to see the last heartbeat time for the affected device. If the device shows no heartbeats for an extended period, the app is likely not running. See the Troubleshooting section for steps to fix this.
- Sensor-specific gaps
- If only one sensor has gaps while others look normal, the most likely cause is a denied permission or an iOS foreground-only sensor (such as the accelerometer or microphone) recording gaps when the app is in the background. Check the Sensors tab to see which sensors are foreground-only on iOS.
- Missing participants
- If a participant appears in the Sensor Completeness chart but has no data for several sensors, they may not have granted the required permissions. Share the Participant Guide with them and ask them to check their app permissions in phone settings.
Send Notifications
Reaching participants outside the schedule
The Send Notifications panel lets you push an ad-hoc message to participant devices at any time, independent of the protocol cron schedule. Use it for study reminders, schedule changes, announcements, or whenever you need to reach participants outside their normal collection routine. Notifications are delivered through the app's own upload channel: no third-party push services (APNs or FCM) are involved, and no separate push infrastructure is required.
Composing a notification
- Title
- The headline of the notification, shown in the OS notification tray and inside the app. Defaults to "StudyRun". Customise it to give participants context at a glance (for example "Study Reminder" or your study's name). Keep it short: mobile operating systems truncate long titles.
- Message
- The body text of the notification. Participants see this both in the OS notification and inside the app's notification history. Be concise; most people read notifications as a one-line summary. For ESM reminders, include a clear call to action. For schedule changes, be specific about what has changed and when.
- Target: All devices
- Broadcasts to every currently enrolled device in the study. Use for study-wide announcements, general reminders, or messages that apply to all participants equally.
- Target: Specific devices
- Select individual device IDs from the list. The table shows each device's participant ID and the time of its last heartbeat, so you can identify the right device even without knowing the device ID in advance. Use this when you need to reach a specific device (for example, to follow up on a connectivity issue).
- Target: Specific participants
- Select by participant ID. All devices belonging to that participant receive the message. Use this when you want to reach a participant regardless of how many devices they have enrolled (for example, a participant who is enrolled on both a phone and a tablet).
High Priority notifications
The High Priority notification checkbox, available both when composing an ad-hoc notification here and when configuring a CRON-scheduled protocol, asks the operating system to treat the alert as time-sensitive. The effect differs by platform: iOS and Android expose different levels of "urgent" notifications, and the app maps High Priority to the most prominent tier each OS permits without requiring elevated entitlements.
Use it sparingly. High Priority notifications bypass some of the system-level quieting that participants rely on (Focus modes on iOS, "Priority only" Do Not Disturb on Android). Overuse erodes the signal; participants quickly learn to ignore or silence the app. Reserve High Priority for notifications that are genuinely time-critical: a momentary ESM that expires in minutes, a safety check-in, or a schedule change the participant must act on immediately.
| Platform | Normal notification | High Priority notification |
|---|---|---|
| iOS | Delivered at default urgency. Appears in Notification Centre and may show a banner if the phone is unlocked, but is silenced when the participant has Focus, Do Not Disturb, or a sleep schedule active. No sound is played while the device is locked in Focus. |
Delivered as UNNotificationInterruptionLevel.timeSensitive. Pierces Focus and Do Not Disturb (unless the participant has specifically disabled Time Sensitive alerts for the app), plays a sound even when the device is silenced by a Focus mode, and is offered the chance to break through during scheduled Downtime.
|
| Android |
Posted to the app's default notification channel at IMPORTANCE_DEFAULT. Appears silently in the shade, does not produce a heads-up popover, and is suppressed when "Priority only" or "Total silence" Do Not Disturb is enabled.
|
Posted to a separate high-priority channel at IMPORTANCE_HIGH with CATEGORY_REMINDER. Produces a heads-up popover with sound and vibration. Because CATEGORY_REMINDER is in the default allow-list for "Priority only" Do Not Disturb, it still reaches the participant when most other notifications are being suppressed.
|
Participants can always override these defaults from the OS. On iOS they can disable Time Sensitive notifications for the StudyRun app in Settings → Notifications. On Android they can lower the importance of the high-priority channel or block the app entirely in System Settings → Apps → Notifications. Respect these overrides: a participant who has silenced the app is telling you something, and repeated High Priority attempts will not reach them.
How delivery works
- Delivery model
- When you send a notification, it is queued on the server. Each device downloads pending notifications the next time it uploads sensor data, typically every few minutes on the default upload interval. This means delivery does not require an always-on network connection from the server side and works even on networks that block push notification services.
- Sent
- The device has downloaded the notification from the server. The message has reached the device and is stored locally. This status updates as soon as the device's next upload cycle completes.
- Confirmed
- The device has shown the OS notification to the participant. The participant's phone has displayed the alert in the notification tray. A notification can be Sent but not yet Confirmed if the device received it but the OS has not displayed it (e.g. Do Not Disturb is active).
- Delivery timing
- On the default upload interval of 5 minutes, most devices receive the notification within 5–10 minutes of sending. Devices with WiFi-only upload enabled may take longer if the participant is not connected to WiFi. Devices that are offline or not running the app will receive the notification as soon as they come back online and complete an upload.
Tracking delivery
- History table
- Every notification you send is recorded permanently with the time it was sent, the target audience, and overall Sent and Confirmed counters. The history persists for the life of the study, so you can always go back and verify that a particular message was sent and when.
- Per-device breakdown
- Click any row in the history table to expand it and see the per-device status: device ID, participant ID, the exact time the notification was sent to that device, and the time the device confirmed receipt. Use this to identify which specific devices have not yet received a message.
- Status badges
- Complete means all targeted devices have confirmed receipt. Pending means one or more devices are still outstanding, either waiting to complete an upload or because the OS has not yet displayed the notification. Partially delivered means some devices confirmed while others remain pending.
Permissions
- Who can send
- Only study owners and editors can compose and send notifications. Viewers can see the notification history and delivery counters but cannot send new messages. This prevents accidental notifications from team members who should have read-only access.
Examples
| Title | Message | Target | Purpose |
|---|---|---|---|
| Study Reminder | Hi! Just a reminder to keep the StudyRun app running in the background so we can collect your data. Thank you for your participation! | All devices | Sent at the start of the study to all participants. Helps ensure everyone has the app running correctly before meaningful data collection begins. |
| Permission needed | We noticed your location data has not been collected recently. Please open the StudyRun app and check that location permission is set to 'Always Allow' in your phone settings. | Specific participant | Targeted at a single participant whose location sensor shows a gap in the Sensor Completeness chart. More effective than a blanket message because it tells the participant exactly what to fix. |
| Schedule change | The study schedule has been updated. Please open the StudyRun app once today so it can download the new settings. No other action is needed. | All devices | Sent after a researcher unlocks and re-locks a protocol with revised sensor settings or a new cron schedule. Prompts participants to open the app so the new configuration is downloaded sooner rather than waiting for the next automatic upload. |
| Questionnaire reminder | You have an unanswered questionnaire waiting in the StudyRun app. Please take a moment to complete it. It only takes 2 minutes! | Specific devices | Sent manually to the subset of devices whose last ESM questionnaire was recorded as SKIPPED or EXPIRED in the export. Useful mid-study when compliance rates drop below an acceptable threshold. |
| Study ending soon | Your participation in the Sleep & Activity Study ends in 3 days. The app will continue collecting data until then. No action needed. Thank you for taking part! | All devices | Sent a few days before the study end date to keep participants informed and reduce support enquiries about why the app stops. Also gives participants time to flag any issues before the data collection window closes. |
| Upload your data | Please connect to WiFi and open the StudyRun app briefly so your latest data can be uploaded before we close the study tomorrow. | Specific devices | Sent to participants whose devices have a large backlog of unuploaded readings (identifiable via the Client Connectivity heatmap). Particularly important when the study uses WiFi-only upload and the end date is approaching. |
| Technical issue resolved | We have fixed a technical issue with the server. If your app showed an upload error in the last 24 hours, please open it briefly to retry. Your data has not been lost. | All devices | Sent after a server outage or maintenance window to reassure participants and prompt a re-upload. Reduces participant anxiety and prevents them from uninstalling the app due to persistent error messages. |
Security & Privacy
Designed for sensitive research data
StudyRun is built for academic research involving personal and sensitive participant data. All communication is encrypted, all credentials are hashed and never stored in plain text, and your data never passes through third-party services. On paid subscription servers, each deployment runs on its own EC2 virtual machine instance with its own EBS storage volumes in the AWS region you choose; your Study Data is logically isolated from every other customer's within the StudyRun application and database. The underlying AWS physical infrastructure operates under AWS's standard shared-tenancy model with hypervisor-level isolation between AWS tenants; AWS Dedicated Host or Dedicated Instance tenancy is available as a paid add-on on request. The shared demo environment is multi-tenant and is intended only for evaluation and testing, not for real-participant data collection.
Data in transit
- Transport encryption
- All traffic between participant devices and the server is encrypted with TLS 1.2/1.3, enforced by Caddy (the TLS-terminating reverse proxy in each deployment). The server does not accept plain HTTP connections. Weak cipher suites (NULL, MD5, RC4, export-grade) are disabled. The mobile app validates the server's TLS certificate before transmitting any data; a misconfigured or self-signed certificate will cause the app to refuse to connect.
- No third-party intermediaries
- Sensor data travels directly from participants' phones to your server. There are no analytics platforms, cloud relay services, or external APIs in the data path. You control the server, the database, and the entire network path. This makes it straightforward to document the data flow for ethics committee review.
Authentication
- Researcher passwords
- Account passwords are hashed with bcrypt, a slow key derivation function designed to resist brute-force attacks even if the database is compromised. Plain-text passwords are never stored or logged anywhere in the system.
- JWT sessions
- Researcher sessions use short-lived JWT access tokens (60 minutes) paired with longer refresh tokens (7 days). Tokens are signed with a server-side secret. An expired or tampered token is always rejected. This means a stolen session token has a limited window of usefulness.
- Device tokens
- Each enrolled device receives a unique 32-byte cryptographically random token (prefixed with dt_). The raw token is shown to the device once during enrollment and never stored again. The server stores only a SHA-256 hash of the token. On every upload, the device sends the raw token; the server hashes it and compares. Even if someone reads the database, they cannot extract device tokens.
- Token binding
- A device token is bound to a specific device ID and study ID at enrollment. On every upload, the server verifies that the token, device ID, and study ID all match. If an attacker obtains a valid token but uses a different device ID, the upload is rejected. This prevents token reuse across devices or studies.
- Brute-force protection
- All sensitive endpoints are rate-limited per source IP. Login: 5 attempts per minute. Registration: 3 per minute. Enrollment: 5 per minute. Uploads: 60 per minute. Configuration fetches: 30 per minute. Exceeding any limit returns a 429 response. This makes credential guessing and enrollment ID enumeration impractical.
Participant data integrity
- Upload deduplication
- Every upload batch has a unique batch ID. If a device retries after a network timeout, the server recognises the batch ID and discards the duplicate, so readings are never counted twice. This makes upload retries safe without requiring coordination from the device.
- Payload checksums
- Devices compute a SHA-256 checksum over their reading payload (count + sorted timestamps) and include it in every upload. The server independently recomputes the hash. If they diverge, the server logs a warning flagging possible data corruption in transit. This provides an early-warning signal for infrastructure issues.
- Configuration integrity
- Devices hash their current configuration on every upload. The server computes the expected hash based on the current locked protocols, participant filter, and date boundaries. If the hashes differ, the server tells the device to re-fetch its configuration, ensuring every device always runs the latest approved settings without the participant needing to take any action.
- Complete audit trail
- Every sensor reading is stored with its study ID, device ID, participant ID, sensor type, timestamp, batch ID, and the locked protocol version that was active at collection time. You can always trace any reading back to the exact device, participant, and protocol configuration that produced it, which is a requirement for scientific reproducibility and ethics compliance.
Access control
- Role-based permissions
- Studies have three roles. Owner has full control, and can delete the study, manage team members, and change any setting. Editor can modify settings, protocols, and export data, but cannot delete the study or manage the team. Viewer has read-only access and can view the dashboard, data, and settings but cannot make changes. Assign Viewer to supervisors, ethics reviewers, or collaborators who need visibility without the ability to interfere.
- Enrollment control
- You can disable enrollment at any time from Study Settings to prevent new devices from joining while existing participants continue collecting. When participant IDs are required, only devices presenting a valid, pre-generated, unclaimed ID can enroll. IDs are claimed atomically at the database level to prevent two devices claiming the same ID simultaneously.
- Team invitations
- Team members are invited by email. Each invitation generates a single-use code that expires after 7 days. The code is consumed on first use and cannot be reused. Expired or already-used codes are rejected. This prevents invite links from being forwarded or reused after the intended recipient has joined.
Operational security
- Secrets management
- Database credentials and signing secrets are stored as Docker secrets: file-based, mounted on tmpfs in RAM inside containers, never written to disk. Secrets are not passed as environment variables, so they do not appear in process listings, docker inspect output, or crash logs.
- Automated backups
- The server runs daily database backups using pg_dump in compressed custom format (-Fc). The last 7 daily backups are retained automatically; older backups are deleted.
- Input validation
- All API inputs are validated with strict constraints before processing. Upload batches are capped at 10,000 readings per request. IDs are capped at 200 characters. Notification target lists at 1,000 entries. String fields have maximum lengths enforced. Oversized or malformed payloads are rejected immediately with a 400 response.
- Protocol versioning
- Locking a protocol creates an immutable snapshot with a unique version ID. Researchers must explicitly unlock a protocol to make changes, creating a clear before/after boundary in the data. Unlocked (draft) protocols are never sent to devices; participants only ever run reviewed and approved configurations.
Participant Guide
Welcome to the study
StudyRun is a research app that collects sensor data from your phone while you go about your daily life. Once you join a study, the app runs quietly in the background. You don't need to open it for data to be collected. This guide explains how to get set up, what the app needs to work properly, and what to do day-to-day.
- Install and join: download the app, scan the QR code your researcher gave you, and you are enrolled in a few taps.
- Grant permissions: the app will ask for access to the sensors your study uses. Granting them is essential for the data to be complete.
- Keep the app installed: the most important thing you can do is not force-close or uninstall the app. It handles everything automatically.
- Respond to prompts: if your study includes questionnaires, tap the notification when it arrives and fill it in within the time window shown.
Getting started
- Install the app
- Download StudyRun from the App Store (iPhone) or Google Play (Android). The app requires iOS 16 or later, or Android 8.0 (API level 26) or later. Search for StudyRun or use the link your researcher provided. Installation is free.
- Join a study
- Your researcher will give you a QR code or a URL link. Open the StudyRun app, tap Join Study, then either tap Scan QR Code and point your camera at the code, or paste the URL into the text field and tap Join. The app will download the study configuration and you will be enrolled within a few seconds.
- Participant ID
- Some studies require a participant ID to control who can join. Your researcher will give you a code. Type it exactly as given, including any hyphens or capital letters (it is case-sensitive), then tap Join. If the ID is not accepted, double-check for spaces or typos and try again. If it still fails, contact your researcher, who may need to generate a new code for you.
- What happens after joining
- The app begins collecting data immediately according to your study's schedule. You will see a dashboard with the study name, a list of active sensors and their current status, and a count of readings waiting to be uploaded. You do not need to do anything; data is collected and uploaded automatically in the background.
Granting permissions
When the app starts a new sensor it will show a system dialog asking for permission. Always tap Allow. If you deny a permission, that sensor records nothing and your study data will be incomplete. If you accidentally denied a permission, you can re-grant it in your phone's Settings app at any time. The instructions below show exactly where to find each setting.
- Location (iPhone)
- Location must be set to Always to allow collection when the app is in the background (which is most of the time). If you only granted While Using, go to Settings > Privacy & Security > Location Services > StudyRun and change the setting to Always. Without Always permission, location readings will be missing whenever your phone screen is off.
- Location (Android)
- When the location dialog appears, tap Allow all the time. If you previously tapped Allow only while using the app, go to Settings > Apps > StudyRun > Permissions > Location and change it to Allow all the time. On some Android versions this option only appears in Settings, not in the in-app dialog.
- Bluetooth / Nearby devices
- If your study scans for nearby Bluetooth devices, grant Bluetooth access when prompted. On Android 12 and later this permission is called Nearby devices. This permission only scans for devices; it does not pair with anything or access your existing Bluetooth connections.
- Microphone
- Some studies measure ambient sound levels to estimate noise exposure. If asked for microphone access, tap Allow. The app records only the sound level in decibels; it does not record audio, capture conversations, or store any sound content. Think of it as a loudness meter, not a recorder.
- Health data (iPhone)
- If the study reads Apple Health data (steps, heart rate, sleep, etc.), a Health access screen will appear listing the specific data types requested. Toggle on everything in the list and tap Allow All, then Done. You can adjust these at any time in Settings > Health > Data Access & Devices > StudyRun.
- Health data (Android)
- If the study uses Health Connect, the app will open the Health Connect app (install it from Google Play first if needed). Tap Allow for each data type listed. You can review and change these permissions later by opening Health Connect > App permissions > StudyRun.
- Physical activity (Android)
- Required on Android 10 and later for step counting and activity recognition. Grant this permission when prompted. Without it, step counter and activity sensors will not record any data.
Keeping the app running
The most common reason for missing data is the app being stopped by the battery manager or by the participant force-closing it. Follow the steps below to make sure StudyRun can run continuously in the background.
- Don't force-close the app
- Do not swipe StudyRun away from your recent apps list to close it. On Android this stops the background service immediately. On iOS it suspends collection. The app does not need to be visible on screen to collect data; just leave it installed and running. If you accidentally close it, simply open it again and it will resume.
- Battery optimization (Android)
- Android phones from most manufacturers (Samsung, Xiaomi, OnePlus, Huawei, Oppo, Realme) aggressively limit background apps to save battery. You must disable this for StudyRun: go to Settings > Apps > StudyRun > Battery and set it to Unrestricted or No restrictions. On Samsung devices, also open Device Care > Battery > Background usage limits and make sure StudyRun is not listed under Sleeping or Deep sleeping apps. This is a one-time setup.
- Background App Refresh (iPhone)
- Go to Settings > General > Background App Refresh and make sure it is turned On both globally and for StudyRun specifically. Also check that Low Power Mode is not active (Settings > Battery); when Low Power Mode is on, iOS suspends background apps including StudyRun, which will cause gaps in your data.
- Foreground notification (Android)
- While StudyRun is running, you will see a small persistent notification in your notification tray. This is normal and required; it is how Android allows apps to run reliably in the background. Do not dismiss it. If this notification disappears, the service has been stopped and you should open the app to restart it.
- If the app seems stopped
- On iPhone: open the app briefly to resume background collection. On Android: open the app and check the dashboard. If sensors show as inactive, tap the sensor toggle to restart collection. If problems persist, try restarting your phone and opening StudyRun again. Contact your researcher if the issue continues.
Day-to-day use
- Reading your dashboard
- Open the app at any time to see the study dashboard. It shows the study name, pending tasks at the top, and the list of active sensors with status indicators below. Everything scrolls together as one list.
- Tasks waiting for you
- When a scheduled task fires (a questionnaire, photo diary, PVT test, etc.), it appears in the 'Tasks Waiting for You' section at the top of your dashboard. Each card shows the protocol name and how much time you have left to complete it. Tap a card to start the task. If a protocol has multiple tasks, they are presented one after another; complete each one before the next appears. Once all tasks in a session are done, the card disappears. If you do not complete a task before its time runs out, it expires and is removed automatically.
- Always-available tasks
- Some tasks may show 'Run at any time' instead of a countdown. These are always available and can be completed whenever you want. After you finish them, they will reappear so you can do them again later.
- Responding to notifications
- You will receive a notification when a new task is ready. Tap the notification to open the app, and the task will be visible in your dashboard. You do not need to respond immediately; the task stays in your dashboard until its time window expires.
- Uploading data manually
- Data uploads automatically every few minutes when you are connected to the internet. If your study uses WiFi-only upload, data only uploads when you are on WiFi, so make sure you connect to WiFi at least once a day. You can also force an immediate upload at any time by tapping Upload data now on the dashboard.
- Pausing a sensor
- If a specific sensor is causing problems (for example, high battery drain from GPS), you can pause it from the dashboard by tapping the toggle switch next to its name. This stops that one sensor without affecting the rest of the study. Let your researcher know so they are aware of the gap in data. You can re-enable it at any time.
- Checking your upload status
- The dashboard shows Pending readings: the number of sensor readings stored on your device that have not yet been uploaded. Under normal conditions this number stays low (under a few hundred). A very large number over several days suggests uploads are not happening; check your internet connection.
Privacy and leaving the study
- Your data and privacy
- Your data travels directly from your phone to your researcher's private server over an encrypted connection. No third-party companies or cloud services see your data. The researcher has configured exactly which sensors are active, and you can see the full list on your dashboard. For details on what data is collected and how it will be used, refer to the informed consent form your researcher provided.
- Leave the study
- You may leave the study at any time without giving a reason. Open the app, expand the Study Information section on the dashboard, and tap Exit Study. The app will upload any remaining data that has not yet been sent, then permanently delete all study data from your device. This cannot be undone.
- Uninstalling the app
- If you uninstall StudyRun, any data that has not yet been uploaded will be lost and cannot be recovered. Before uninstalling, open the app and tap Upload data now to ensure everything has been sent, then follow the Leave the study steps above. Once you have left the study and confirmed the upload, it is safe to uninstall.
Troubleshooting
Diagnosing problems
Most issues fall into one of three categories: the device is not uploading, a sensor is not collecting data, or the data looks incomplete or wrong. Start by checking the Client Connectivity heatmap to see when a device last checked in, and the Sensor Completeness heatmap to identify which sensors have gaps. These two views will usually tell you whether the issue is with the app running at all, or with a specific sensor.
Device not uploading
- Check the app is running
- On Android, the app runs as a foreground service with a persistent notification in the notification tray. If that notification has disappeared, the service has been killed; open the app to restart it. On iOS, open the app briefly; this resumes background collection. If the app crashes on launch, ask the participant to reinstall it.
- Battery optimization (Android)
- The single most common cause of missing data on Android. Manufacturers (Samsung, Xiaomi, Huawei, OnePlus, Oppo) aggressively kill background services to save battery. To fix: go to Settings → Apps → StudyRun → Battery and set it to Unrestricted. On Samsung devices, also open Device Care → Battery → Background usage limits and ensure StudyRun is not in the list of sleeping apps. These steps must be done once per device.
- Background App Refresh (iOS)
- Go to Settings → General → Background App Refresh and verify that it is enabled globally and specifically for StudyRun. If it is disabled, iOS will suspend the app before it can complete an upload. Also check that Low Power Mode is not active, because Low Power Mode disables Background App Refresh system-wide.
- WiFi-only upload
- If the study has WiFi Only upload enabled, the device will not upload while on mobile data. This is intentional for studies with large data volumes, but participants must connect to WiFi regularly. If upload gaps correlate with times the participant is away from home, consider disabling WiFi Only in the study's Data Sync Settings.
- Device storage full
- If the device's storage is nearly full, the app may fail to write new readings to the local database or fail to read the database during upload. Ask the participant to free up space. The app will resume normal operation automatically once storage is available.
Sensor not collecting
- Permission denied
- The most common cause of a sensor with zero readings. The participant must grant the required OS permission. Check: Location (must be set to Always, not just While Using, for background collection), Bluetooth / Nearby Devices (Android 12+), Microphone, Camera, HealthKit (iOS) and Health Connect (Android). Refer the participant to the Participant Guide for step-by-step instructions for each permission.
- iOS foreground-only sensors
- Several sensors on iOS only collect while the app is in the foreground: Accelerometer, Gyroscope, Magnetometer, Linear Accelerometer, and Microphone. These will show gaps whenever the app is in the background, which is most of the time for a typical participant. This is a hard iOS platform limitation and cannot be worked around. If continuous accelerometer data is required, Android devices are required.
- Sensor not in any active protocol
- Confirm in the Configure tab that the sensor is included in at least one protocol that is locked, enabled, within its date range, and whose participant filter includes the target participant. Draft (unlocked) protocols are never sent to devices; a protocol must be locked for devices to see it.
- Protocol date range
- If the protocol has a relative or absolute start or end date, confirm the participant is within the active window. Relative dates (e.g. Study Day 1–7) are evaluated from each participant's individual enrolment date. A participant who enrolled late may not be in range yet, or may have already passed the end date.
Data looks wrong or incomplete
- Short gaps are normal
- Gaps of a few seconds to a few minutes are expected during device restarts, network transitions (WiFi to mobile), or brief OS suspensions. Data is cached locally on the device and uploaded automatically when connectivity resumes. These short gaps do not represent lost data.
- Long gaps (hours)
- Gaps of several hours almost always mean the app was force-closed by the participant, killed by the OS battery manager, or the device was offline for an extended period. Check the Client Connectivity heatmap for the affected device; the heartbeat pattern will show exactly when the device was last active and when it came back online. As an additional diagnostic, the app emits a GAP_DETECTED marker whenever it wakes up and finds that more than one hour has passed since its last activity. These markers appear in the SETTINGS data stream with sensor=GAP_DETECTED and an action payload of {"gap_ms":..., "last_alive_ms":...}, giving the exact duration of the dormant period and the timestamp immediately before it. Filter the Data tab to sensor_type=SETTINGS and look for GAP_DETECTED rows to pinpoint when the app was killed and for how long. A run of GAP_DETECTED markers for the same participant over several days is a strong signal that the participant's phone (or their battery-optimisation settings) is aggressively killing the app and that they may need personalised guidance to keep StudyRun running in the background.
- Wrong participant ID
- If a participant typed their ID incorrectly at enrolment, their data will be filed under the wrong ID. You cannot change the ID on a running device. The participant must leave the study (which uploads any remaining data) and re-enrol with the correct ID, if enrollment is still open. To avoid this, always confirm the ID with the participant immediately after they join.
- Export appears empty
- Check that your export filters are not too restrictive. The most common mistake is having a date range, sensor filter, or participant filter active that excludes the data you expect to see. Open the Data tab with the same filters to verify data exists there before trying to export.
- “An export is already running”
- The server runs one export per study at a time (and a small number overall) so that large downloads cannot pile up and slow the whole dashboard. If you see this message, an earlier export for the study is still streaming, possibly one you started in another tab and navigated away from. Wait for it to finish, then try again. Large multi-week exports of high-frequency sensors can take a few minutes; the download progress in your browser confirms it is moving.
Registration issues
- Academic email required
- During the beta, account creation is restricted to email addresses at recognised academic institutions. The list is drawn from the Hipo university-domains-list dataset plus a small internal allowlist. If your institution's domain is not recognised but should be (small institute of technology, new university, etc.), email support at support@studyrun.org with your institution name and domain, and an admin can either add the domain to the allowlist or create the account directly. Consumer addresses (gmail.com, outlook.com, etc.) cannot register.
- Verification code didn't arrive
- After you click Create Account the page freezes in place and shows a code input. A 6-character code is emailed to the address you entered. Most providers deliver within 30 seconds. If nothing arrives after 2–3 minutes: (1) check your Spam / Junk folder (the message is from StudyRun and mentions a verification code), (2) confirm the email you typed is correct by using Edit details to step back, (3) click Resend code. Your institution's mail filter may block automated codes — try registering with a different academic address you have access to, or contact support.
- Verification code expired
- Each code is valid for 2 hours. If you wait longer, clicking Verify returns a 'verification session expired' message. Just click Resend code — a fresh code is emailed and a new 2-hour window starts. Your form data is preserved; you don't have to retype anything.
- Changed your mind about the email
- In the verification step, click Edit details and start over to unfreeze the form. Change the email (or anything else) and click Create Account again; a new code is sent to the new address. The old code cannot be used because the token records a hash of the submitted form — any change invalidates it.
Study configuration issues
- Protocol not reaching devices
- A protocol is only sent to a device if all of the following are true: it is locked (not in draft), it is enabled, the current date falls within its start/end range, and the participant filter includes that device's participant ID. Check each condition in the Configure tab. After making any change, devices will pick it up on their next upload cycle, typically within a few minutes.
- Notifications not arriving
- Check the Send Notifications history table for the notification in question. If Sent is 0, the target devices have not yet completed an upload since you sent it; wait for the next upload cycle. If Sent is non-zero but Confirmed is low, the OS on some devices may be blocking notification display. Ask the affected participants to check that notifications are enabled for StudyRun in their phone settings.
- Questionnaire (ESM) not appearing
- ESM tasks are triggered by their protocol's cron schedule. Check that the protocol is locked and that the cron expression is correct. You can test a cron expression at crontab.guru. Also confirm that the protocol's notification is configured; without a notification, participants may not know to open the app when a questionnaire is due.
Advice
Bluetooth vs. iBeacon sensors
They overlap partly but capture different data. Both scan Bluetooth Low Energy advertisements, but they answer different questions.
Key differences
| Aspect | Bluetooth | iBeacon |
|---|---|---|
| What gets through | Every BLE advertisement | Only advertisements with Apple's iBeacon manufacturer-data header (0x4C 0x00 0x02 0x15) |
| Fields returned | device_name, address, rssi, device_type | uuid, major, minor, rssi, proximity, accuracy |
| Identity semantics | Address identifies a physical transmitter | UUID identifies a deployment, major/minor pinpoint a specific beacon inside it |
| Distance info | Raw RSSI only | Raw RSSI plus a computed accuracy (metres) and a categorical proximity (IMMEDIATE / NEAR / FAR / UNKNOWN) |
| Packet parsing | None, raw scan hits | App parses manufacturer data to extract UUID/major/minor/txPower |
| Default rate | 60 s | 10 s |
| Android | Yes | Yes |
| iOS | Yes | Yes |
Real-world use cases
Bluetooth answers "what BLE devices are near the participant right now?"
- Social context (number of devices near the participant, as a rough crowding / sociality proxy).
- Device inventory (the participant owns a smartwatch, wireless headphones, a Tile, a car key fob, etc.).
- Cross-device proximity in multi-participant studies.
- A catch-all environmental signal.
iBeacon answers "is the participant near a beacon the researcher placed at location X?"
- Indoor positioning with deployed beacons: one in the kitchen, one in the bedroom, one in the bathroom, etc. The participant's presence in each room becomes an observable signal.
- Workplace or lab studies: beacons at desks, meeting rooms, or specific equipment.
- Clinical and hospital studies: beacons near doors, medication cabinets, waiting areas.
- Any scenario where the researcher physically installs BLE tags at known locations and wants to know when a participant visits.
How they overlap
Every iBeacon is also a Bluetooth device. If you enable Bluetooth, your scans will see your deployed beacons in the raw BLE output too, but only by their MAC address and RSSI, not by their structured UUID/major/minor. Conversely, the iBeacon sensor is strictly filtered: it ignores non-iBeacon BLE advertisements (phones, earbuds, cars).
So the rule of thumb:
- If you want every BLE device near the participant, enable Bluetooth.
- If you have beacons you have deployed at known locations and want to detect proximity to them, enable iBeacon (either alongside Bluetooth or instead, depending on what else you need).
Operational note
Both sensors use the same permissions (BLUETOOTH_SCAN on Android 12+, ACCESS_FINE_LOCATION on older Android, Bluetooth + Location-when-in-use on iOS), share the same underlying BLE scanner, and both are blocked if the participant disables Bluetooth or denies the permission. If Bluetooth is off for any reason, iBeacon is off too.