Experiment builder reference
Every configuration card on the story settings page, explained in depth. Each section below describes what a card does, when you'd use it, every field it exposes, and the gotchas that have surprised researchers before. If you just want to scan the platform, the landing page has the short version.
Workspace & collaborators #workspace
What it does
Every story belongs to one user — the creator. The creator can grant other registered users co-owner access so a small team can run a study together without sharing a login.
When to use it
- You're co-running a study with another researcher and they need to download data or replace HTML.
- You're handing a study over to a colleague — add them as co-owner, confirm they can access it, then remove yourself with "Leave this study".
Roles & permissions
| Action | Creator | Co-owner |
|---|---|---|
| Rename the study | Yes | Yes |
| Edit completion behaviour, conditions, behavioural & multimodal settings | Yes | Yes |
| Replace the HTML / upload assets | Yes | Yes |
| Download data | Yes | Yes |
| Add or remove collaborators | Yes | No |
| Delete the study | Yes | No |
Gotchas
Assets #assets
What it does
Hosts the static files your Twine story references — images, audio, video, custom CSS,
anything you'd load with a relative URL. Files are served at
/play/<slug>/assets/ and the platform injects a <base>
tag into your HTML so paths like img/Logo.png resolve correctly.
When to use it
- Your Twine uses any external media (images, sounds, fonts, etc.).
- You want to keep your
.htmlsmall by externalising large media.
Fields
| Field | Notes |
|---|---|
| Pick files | Multi-select individual files. Folder structure is not preserved — all files land at the root of the assets dir. |
| Pick a whole folder | Uses the browser's directory picker (webkitdirectory). Subfolder structure is preserved. Use this for stories with organised img/, audio/ trees. |
Gotchas
MAX_ASSET_FILE_MB on
the server if you self-host; the default suits Prolific-scale audio/image work.
img/logo.png replaces the existing file silently. Useful for iteration; surprising
if you forgot.
webkitRelativePath. Firefox and Safari
support this; some older Linux distros' Firefox builds don't. Use the flat picker if a
researcher reports the directory option doesn't surface a picker.
Tracker contract #tracker-contract
What it does
Every story you upload gets a small JavaScript block injected at the bottom of its HTML before it's served to participants. That block — the tracker — is what turns a stand-alone Twine export into a tracked experiment. It reads the participant's id from the URL, resumes their save if they're returning, records every passage they visit, every click and form change they make, and ships that back to the platform. The contract describes exactly what the tracker captures and what it sends — so you can audit, plan analysis, and design URLs against a fixed surface.
How it gets into your HTML
- Injected automatically when you upload a story (or add / replace a condition's HTML).
- Re-injection is idempotent: re-running it on already-tracked HTML is a no-op byte-for-byte.
- The Tracker version card on the story page can re-inject the latest tracker into existing HTML without re-uploading — your sessions are preserved.
- Two HTML elements are added, immediately before
</body>: a<style id="tracker-hide">that hides the page until resume completes, and a<script id="trip-hustlers-tracker">that contains the tracker code.
URL parameters the platform reads
The platform accepts the participant id under any of the names below. Whichever you
supply, the play route normalises the URL with a 302 redirect so the tracker always
sees a canonical User_ID (or PROLIFIC_PID).
| Parameter | What it does |
|---|---|
| PROLIFIC_PID | Prolific's participant id. Highest priority — if present, it's the canonical id. |
| Warwick_ResponseID | Qualtrics-style response id from Warwick deployments. Accepted but not normalised; tracker still reads PROLIFIC_PID / User_ID after redirect. |
| User_ID | Generic participant id. Used when PROLIFIC_PID isn't present. |
| user_id | Lowercase fallback. The platform 302-redirects to the URL with User_ID set. |
| condition | Multi-arm assignment. Optional; for multi-arm stories the platform picks one and stickies the assignment if you omit it. |
| survey_done | Platform-internal flag set when returning from a configured pre-survey. Don't set it yourself. |
| face_preview | QA-only. Append ?face_preview=1 to see a live preview of the multimodal landmarker. Don't ship it to participants. |
U_ + 16 hex chars
— and 302-redirects so the URL the participant sees and bookmarks contains it. The
tracker then treats this id as not anonymous and resume works as normal. The
word "anonymous" in the tracker code refers to a separate edge case: if a participant
somehow lands on a URL with no id parameters AND the platform's redirect doesn't fire
(e.g. JavaScript-only embed), the tracker generates its own anon_ id and
skips resume. In practice you should never see that path.
Endpoints the tracker calls
| Method · Path | When | Purpose |
|---|---|---|
| GET /resume_twine_data | Once, on page load, in parallel with SugarCube boot. | Fetches the participant's most recent save_blob for this story. Returns { save_blob, passage_index, session_created_at } or 404. |
| POST /upload_twine_data | On every :passagedisplay that advances passage_index; via navigator.sendBeacon on tab-hidden and page-unload. |
Upserts the participant's session document. Idempotent — replays of the same passage are dropped. |
| POST /upload_twine_events | Periodic flush (default every 5 s) when behavioural or multimodal capture is on; via beacon on tab-hidden and page-unload. | Append-only stream of high-volume events (mouse, hover, scroll, key timing, idle, face blendshapes). Disabled by default. |
What the tracker uploads (the payload)
Every /upload_twine_data POST carries a JSON body with these fields. The server
upserts on (story_id, user_id), so a returning participant keeps the same row.
| Field | What it carries |
|---|---|
| user_id | The id resolved from the URL (or platform-generated). |
| condition | The condition from the URL, or "" for single-arm. |
| passage_index | SugarCube's State.length at the moment of upload. The server's regression guard refuses to clobber a higher value with a lower one. |
| save_blob | Base64-encoded SugarCube save state (Save.base64.save()). Falls back to Save.serialize() on older engines. |
| click_history | Array of every passage the participant has visited so far in this session — { title, variables, t, t_wall }. |
| focus_history | Array of every tab-visibility change since page load — { t, t_wall, type, isVisible }. Server appends, so each upload only sends new entries. |
| decisions | Array of clicks on story links/buttons captured by the delegation handler — { t, t_wall, from, text, target }. Replaces on each upload (no append). |
| inputs | Array of form-field changes inside the story — { t, t_wall, passage, name, type, value }. |
| reason | Why the upload fired: passage (normal), hidden (tab backgrounded), pagehide (unload). Useful for filtering dropout analytics. |
Resume flow
- On load, the tracker fetches
/resume_twine_data?user_id=...in parallel with SugarCube booting. - It waits until SugarCube has rendered its first passage and the resume call has returned.
- If a
save_blobis present, the tracker callsSave.base64.load()(or the legacySave.deserialize()) and forces a re-render withEngine.show()— so the participant lands on the passage they left, not the start passage. - The hider
<style>is removed once resume is settled. Until then, the page is blank — so a returning participant never sees a flash of the start passage before their save loads. - If multimodal capture is enabled with Require successful capture, resume also waits for the camera + landmarker to come up. A capture-required overlay covers the page if they don't.
Engine requirement
- SugarCube v2.37+. Uploads of Harlowe or Chapbook stories are rejected at upload time
(the platform checks
<tw-storydata>+ theapplication-name="SugarCube"meta). - jQuery is required and SugarCube ships it; nothing for you to add.
- The tracker waits for
window.SugarCube.Stateandwindow.SugarCube.Savebefore attaching any handlers. Stories that load slowly or stall on assets still get full instrumentation once SugarCube finishes booting.
What the tracker does not do
- No keystroke contents. When behavioural keystroke timing is enabled, only an
inter-keystroke category (
printable,backspace,whitespace,control) is recorded. The actual key value is never sent. If you need free-text answers, use a story<input>and read it frominputs.csv. - No camera footage. Multimodal capture sends 52 numeric blendshape scores per frame. No image, no video, no facial landmark coordinates leave the participant's browser.
- No IP, no fingerprint, no third-party calls. The tracker talks only to the platform host you've deployed.
- No mutation of the story HTML beyond the injection block. Your Twine, your assets, your CSS — all untouched.
Server-side guarantees on the upload
- One row per
(story, user_id). Replays of the samepassage_indexdon't create duplicate sessions. - Regression guard. When a returning participant's first upload looks like a boot
render (lower
passage_indexthan what's already stored), the server keeps the priorsave_bloband only touchesupdated_at. completed_atis stamped exactly once. The first upload whoseclick_historycontains a configured ending-passage title sets it; further uploads never re-stamp or clear it. The per-arm quota counts off this field.focus_historyis additive. Each upload's entries are appended to the server-stored array, with areceived_atstamp.decisionsandinputssnapshot on every upload — the server stores the latest version each time, withreceived_atpreserved from the first sighting of a given index so the timestamp doesn't drift.
Behavioural + multimodal extension
When you enable behavioural or multimodal capture for a story, the same tracker installs
extra collectors and starts streaming to /upload_twine_events on a separate
cadence (default every 5 s, plus beacons on tab hide / unload). Events live in a separate
collection from the main session, so they don't bloat the session document or the
save_blob. They show up as events.jsonl / events.csv in the
downloaded data. See Behavioural measurements and
Multimodal capture for the per-channel details.
Gotchas
src/tracker/inject.html in the platform source and re-deploy.
t / t_wall = null on every entry that came from the loaded save.
The server merges new timestamps in where present; older slots stay null.
condition param is trusted unfiltered. If a participant
edits their URL to switch arms, the platform's sticky-assignment table will lock them
to the first arm they saw — but only if they previously landed without a condition.
If you hand out per-arm URLs directly, the URL is authoritative on each visit.
navigator.sendBeacon is
not guaranteed by the spec; mobile Safari has historically dropped beacons under memory
pressure. Treat the :passagedisplay uploads as authoritative for completion
signal; treat hidden / pagehide uploads as bonus dropout data.
Tracker version #tracker-status
What it does
The platform injects a small JavaScript block ("the tracker") into every uploaded story. When the platform's tracker code changes — to fix a bug or add a feature — older stories keep their old injected code until you re-inject. This card shows whether your study is on the latest tracker and gives you a one-click refresh.
When to use it
- The card shows a vermillion "outdated" message.
- You've read release notes that mention a tracker change relevant to your study.
Gotchas
<script id="trip-hustlers-tracker"> block.
Replacing the HTML #replace
What it does
Swaps the story's HTML in place. The slug, the public URL, every collected participant session and assignment are preserved — only the story content is replaced.
When to use it
- You found a typo in your Twine and re-exported.
- You're polishing dialogue mid-study but don't want to disturb in-progress participants.
Gotchas
Conditions (multi-arm) #conditions
What it does
Lets one study serve multiple HTML variants — one per experimental arm. Each condition
gets its own injected HTML, its own public play URL
(?condition=<name>), and optionally its own completion behaviour,
weight (for randomization), and quota (cap on completed participants).
When to use it
- You're running a between-subjects experiment with two or more arms.
- You want one study, one data export, but different HTMLs per arm.
- You need quota balancing across arms (e.g. exactly 50 completions each).
Converting single → multi
The first time you add a condition, the form asks for a name to give the
existing HTML (e.g. control) and lets you upload the second arm.
The existing HTML is reused as-is — no re-injection, no copy.
Fields
| Field | Type / Default | What it controls |
|---|---|---|
| Name | [a-zA-Z0-9_-]{1,32} | Used in the URL: ?condition=<name>. Pick something short and human-readable. |
| Weight | 0–1000 · default 1 | Relative weight used by weighted randomization. Ignored under uniform / sequential. |
| Quota | 0–1,000,000 · 0 = no cap | Maximum completed participants in this arm. Once reached, new arrivals to this arm are sent to the over-quota URL. |
| Replace | .html only | Swap this arm's HTML. Slug and sessions preserved. |
| Delete | — | Removes the arm and its HTML. Existing sessions and assignments are kept but the participants assigned to it will be re-randomized on next visit. |
Randomization mode
| Mode | How it picks |
|---|---|
| uniform | Each arm equally likely. Weights ignored. |
| weighted | Probability proportional to each arm's weight. Use this for unequal allocation (e.g. 3:1 treatment-to-control). |
| sequential | Round-robins arms in declaration order. Useful for tight, deterministic balance early in a study. |
Per-arm completion override
Each row in the Conditions table can expand a "Customize completion" drawer to override the study-wide completion settings (endings, action kind, debrief text, redirect URL, delay, max session, hide UI bar) for just that arm. Tick "Override study-wide completion config" to make the override active.
Gotchas
(user_id, story) pair is locked to that arm. They'll see the same arm if
they return — even if you change weights or modes.
Pre-survey #pre-survey
What it does
Sends participants to an external survey before the Twine experience starts.
When a participant arrives at the play URL, the platform forwards every query parameter
they brought (PROLIFIC_PID, condition, User_ID, …)
to the survey URL, plus an extra redirect_url that points back to the Twine
with survey_done=1 appended. The survey is responsible for redirecting to
redirect_url on submission — otherwise the participant never reaches the
Twine.
When to use it
- You collect demographics or consent on Qualtrics / SurveyMonkey before play.
- You need a screening question (age, attention check) before the participant burns time on the story.
Fields
| Field | Notes |
|---|---|
| Pre-survey URL | The anonymous link to your survey. Don't append your own query string — the platform adds the params for you. |
Qualtrics setup (most common)
- Open the survey in the editor → Survey Flow.
- Click Add a New Element → Embedded Data. Move the new block to the very top of the flow.
- In that block, add fields named exactly:
redirect_url,PROLIFIC_PID,User_ID,condition. Leave the values empty — Qualtrics auto-fills them from the URL when "Value will be set from Panel or URL" is the default. Save the flow. - Go to Survey Options → Survey Termination (or End of Survey in newer UI) → choose Redirect to a URL.
- Paste this exactly into the URL box:
${e://Field/redirect_url} - Publish the survey and copy the anonymous link into the Pre-survey URL field above.
SurveyMonkey
Use the Custom Variables feature to capture redirect_url from the
link, then set Survey Ending → Redirect to your own webpage with the
value [redirect_url].
Google Forms
Forms doesn't support outbound redirects natively. You'd need an Apps Script add-on (a
Form Submit trigger that calls HtmlService to redirect). Use
Qualtrics or SurveyMonkey instead if you can.
Custom survey / web form
Read redirect_url from window.location.search on page load,
store it, and on submit do window.location.assign(redirect_url).
Gotchas
?PROLIFIC_PID=TEST123), complete the survey, and confirm you land on
the Twine. The classic failure: the survey redirects to a literal
${e://Field/redirect_url} string — that means Qualtrics didn't capture the
embedded-data field. Re-check step 3.
redirect_url (URL-encoded). Your survey
only needs to act on redirect_url; the rest are captured for your analysis.
Completion quota #over-quota
What it does
Caps the number of completed participants. Once the cap is hit, new participants are redirected to the over-quota URL (typically a Prolific "study full" completion code) instead of seeing the Twine. Participants who already started can always come back and finish.
When to use it
- You're paying per participant and need a hard ceiling.
- Your Prolific study has a fixed sample size and you want over-recruited participants to receive a completion code instead of starting the study.
Fields
| Field | Notes |
|---|---|
| Over-quota URL | Where to send participants once the cap is met. Supports templates: {user_id}, {USER_ID}, {PROLIFIC_PID}, {condition} (case-insensitive). For backward compatibility, user_id and condition are also appended as query params; no other tracker params are forwarded. |
| Target completions | Single-arm studies only. The cap. 0 means no cap. Per-condition caps live in the Conditions table instead. |
Gotchas
Completion behaviour #completion
What it does
Defines what happens when a participant reaches one of the configured "ending" passages: stay on the page, show a debrief, or redirect to an external URL (typically the Prolific completion link).
When to use it
- You're using Prolific and need to forward participants to a completion URL.
- You want a debrief screen with the study description, links to literature, or contact info.
- You need to cap session duration (e.g. fire completion after 30 minutes regardless of progress).
Ending passages
One passage title per line. The builder offers two helpers: a "Detected endings" chip row (passages no other passage links to — likely terminal nodes) and an "All passage names" picker. Click a chip to append it to the list.
Action fields
| Field | Type / Default | What it controls |
|---|---|---|
| Kind = none | — | Do nothing. Participant stays on the ending passage. Useful for self-paced studies. |
| Kind = debrief | — | Show the debrief text on a clean page. Plain text or basic Markdown. |
| Kind = redirect | — | Redirect to the URL below. Use this for Prolific completion. |
| Debrief text | textarea | Plain text or basic Markdown. Used only when kind = debrief. |
| Redirect URL | URL | Used only when kind = redirect. Supports templates: {user_id}, {USER_ID}, {PROLIFIC_PID}, {condition}. Case-insensitive. |
| Delay (seconds) | 0–3600 · default 10 | How long to wait after the ending passage is reached before firing the action. Gives participants time to read the final passage. |
| Max session (minutes) | 0–1440 · 0 = unlimited | Hard timer on the session. Once exceeded, the completion action fires automatically — even if no ending passage was reached. Survives reload & resume. |
| Hide UI bar | checkbox | Hide SugarCube's default UI bar (history controls, restart link, etc.). Recommended for clean Prolific-style experiments. |
Gotchas
{user_id},
{USER_ID}, {User_ID} all expand to the same value. Pick whichever
matches your downstream system's convention.
Behavioural measurements #behavioral
What it does
Captures rich behavioural signals — high-precision reaction time, mouse trajectory, hover dwell on choices, window focus / idle, scroll, keystroke timing — beyond the basic passage and choice history. Off by default. When enabled, all channels turn on unless you've already picked individual ones.
When to use it
- You're running a study where reaction time matters (e.g. forced-choice paradigms).
- You want to detect distraction or off-task behaviour (idle, blur, tab switches).
- You're studying hesitation / hover patterns before choice clicks.
Channels
| Channel | What it captures |
|---|---|
| reaction_time | High-precision performance.now() timestamps for passage display and choice click. |
| mouse_trajectory | Mouse position samples while the participant moves over a passage. Sampled at the rate below. |
| hover_dwell | How long the cursor sits on each choice link before clicking (or not). |
| rich_focus | Window blur, focus, and idle detection events. |
| scroll | Scroll position events (debounced). |
| keystroke_timing | Timing of keypresses on text inputs. Timing only — never the keys typed. |
Parameters
| Parameter | Default | What it controls |
|---|---|---|
| mouse_hz | 20 | Sample rate for mouse trajectory in Hz. 20 Hz = sample every 50 ms. |
| mouse_cap_per_session | 30000 | Max number of mouse samples per session. Hard ceiling to keep payload small. |
| flush_interval_ms | 5000 | How often the client batches events to the server. sendBeacon body limit is ~64 KB; keep ≥ 1000. |
| hover_min_ms | 150 | Minimum hover duration (ms) before a hover counts. Filters out cursor pass-throughs. |
| idle_threshold_ms | 30000 | How long without input before the session is considered idle (ms). |
| scroll_debounce_ms | 200 | Debounce window for scroll events (ms). Lower = denser sampling, larger payload. |
Gotchas
Multimodal capture #multimodal
What it does
Streams MediaPipe Face Landmarker blendshape scores (52 expression dimensions per frame) from the participant's webcam during play. No video or image data leaves the browser — only the numerical blendshape vectors are uploaded.
When to use it
- You're running affect / emotion research and need per-frame expression signals.
- You want a low-bandwidth alternative to full video recording for facial analysis.
Fields
| Field | Default | What it controls |
|---|---|---|
| Enabled | off | Master switch. The browser will prompt for camera access on play. |
| Require successful capture | off | If on, participants who deny camera access see a "Try again" error page instead of being allowed to play uncaptured. |
| sample_hz | 10 | How often blendshapes are computed (Hz). 10 Hz = every 100 ms. |
| max_samples_per_session | 30000 | Hard ceiling on samples per participant session. |
| flush_interval_ms | 5000 | How often the client batches samples to the server (ms). |
Gotchas
Downloaded data #export
What it does
The Download data button on the dashboard (and the per-study page) packages
everything the platform has collected for that story into a single ZIP file named
<slug>-<timestamp>.zip. The same archive is produced for admin
exports at /admin/studies/:id/export, with the action recorded in the audit
log. The export is generated on demand from the current database state — there's no
nightly snapshot or cached copy, so you always get the latest.
What's inside the ZIP
| File | Format | Granularity | Use for |
|---|---|---|---|
| sessions.json | JSON | One object per participant, with every nested array intact. | Complete fidelity; load into a notebook for any analysis. |
| sessions.csv | CSV | One row per participant, summary counts only. | Quick spreadsheet view of who took part and how far they got. |
| clicks.csv | CSV | One row per passage visit (flattened click_history). |
Path analysis, time-on-passage, sequence mining. |
| focus.csv | CSV | One row per tab-visibility / focus / blur event. | Attention checks; detecting tab-switching during the study. |
| decisions.csv | CSV | One row per click on a story link or button (#story a/button). |
Choice analysis — what the participant clicked and when. |
| inputs.csv | CSV | One row per change on a story <input> / <textarea> / <select>. |
Free-text answers, slider values, dropdown selections. |
| events.jsonl | JSON Lines | One JSON object per line. Behavioural + multimodal events. | High-volume streams (mouse, blendshapes); easy to parse line-by-line. |
| events.csv | CSV | Same events flattened; payload kept as a JSON-encoded column. |
When you want events in a spreadsheet too. |
Every CSV uses standard quoting (RFC 4180): fields containing commas, quotes, or newlines
are wrapped in double quotes and embedded quotes are doubled. Objects (variables,
decision data, payload) are serialised as JSON inside their cell. Excel reads it
correctly; pandas reads it with the default read_csv — for the embedded-JSON
columns, parse with json.loads on the column afterwards.
Data dictionary — sessions.json / sessions.csv
One participant per row. sessions.json includes every nested array verbatim
from the database; sessions.csv replaces the nested arrays with their
lengths (click_count, focus_event_count, …) for quick scanning.
If you only need counts, use the CSV; for any deeper analysis go to the JSON or to the
per-event CSVs below.
| Field | In | What it captures |
|---|---|---|
| user_id | both | Participant identifier the tracker recorded. Source-of-truth priority: PROLIFIC_PID > Warwick_ResponseID > User_ID > user_id. The platform generates U_<16-hex> if nothing is supplied in the URL. |
| condition | both | Arm name for multi-condition studies. Empty string ("") for single-arm stories. |
| passage_index | both | Integer count of passage transitions in this session (advances on each :passagedisplay the tracker sees). Used by the server-side regression guard to prevent boot-render uploads from clobbering progress. |
| save_blob | JSON | SugarCube's Save.base64.save() output — the complete engine state needed to resume. Opaque to the platform; only the participant's browser deserialises it. |
| click_history | JSON | Array of passage visits in order. Each element has title, variables (SugarCube State.variables snapshot), and (when captured) t / t_wall. See clicks.csv for the flat version. |
| focus_history | JSON | Tab-visibility events. Each element has timestamp, type, isVisible. Appended on every payload (additive); see focus.csv for the flat version. |
| decisions | JSON | Clicks on story links/buttons captured by the tracker's delegation handler. See decisions.csv. |
| inputs | JSON | Change events on story inputs/textareas/selects. See inputs.csv. |
| click_count | CSV | Length of click_history. |
| focus_event_count | CSV | Length of focus_history. |
| decision_count | CSV | Length of decisions. |
| input_count | CSV | Length of inputs. |
| created_at | both | Server time (ISO) when the participant's record was first written. |
| updated_at | both | Server time of the most recent upload from this participant. |
| completed_at | both | Server time when an ending passage first appeared in click_history. null until then; never re-stamped. Source-of-truth for "this participant completed" and what the per-arm quota counts against. |
Data dictionary — clicks.csv
One row per element of click_history. Visit order within a session is given by click_index.
| Field | What it captures |
|---|---|
| user_id | Participant id (see above). |
| condition | Arm name; empty for single-arm. |
| click_index | 0-based position within this participant's click_history. |
| t | Client performance.now() at the time of the visit, in ms since the play page loaded. Monotonic — immune to wall-clock changes. Empty for resumed-prefix passages: when a participant returns, the tracker can't retroactively stamp visits it didn't witness. |
| t_wall | Client Date.now() at the time of the visit, in ms since the Unix epoch. Subject to the participant's clock being correct. Empty for resumed-prefix passages. |
| title | Passage name as SugarCube sees it (entity-decoded). Match against the story's ending list to know whether this passage completed the study. |
| variables | JSON snapshot of State.variables at the moment this passage was displayed. Everything the Twine author kept in $variable form is here. |
Data dictionary — focus.csv
Tab-visibility and window-focus events. The browser fires these on backgrounding the tab, switching apps, locking the screen, etc.
| Field | What it captures |
|---|---|
| user_id, condition | As above. |
| focus_index | 0-based position within this participant's focus_history. |
| timestamp | Client Date.now() at the time of the event (ms since epoch). |
| type | Event kind. Always tabVisibility from the canonical tracker; richer values (focus, blur) appear only when behavioural rich focus is enabled. |
| is_visible | Boolean. true when the tab is currently visible; false when hidden. |
| received_at | Server time the event landed in the database. Useful when the participant's local clock is suspect. |
Data dictionary — decisions.csv
One row per click on a story link or button (the tracker delegates on #story a
and #story button). Captures the choice the participant made.
| Field | What it captures |
|---|---|
| user_id, condition | As above. |
| decision_index | 0-based position within this participant's decisions. |
| t | Client performance.now() at the click (ms since page load). Monotonic. |
| t_wall | Client Date.now() at the click (ms since epoch). |
| received_at | Server time the decision landed. |
| data | JSON of the full decision: the link text the participant saw, the resolved target passage, and any SugarCube macro metadata. The platform doesn't try to flatten this — different authoring styles produce different keys. |
Data dictionary — inputs.csv
Changes on story form fields (free-text answers, sliders, dropdowns). The tracker
records every change event on #story input / textarea
/ select.
| Field | What it captures |
|---|---|
| user_id, condition | As above. |
| input_index | 0-based position within this participant's inputs. |
| t | Client performance.now() when the value changed. |
| t_wall | Client Date.now() when the value changed. |
| received_at | Server time. |
| data | JSON with the input name, type (text, radio, range, …), and value. Final answers are easy to extract by grouping by (user_id, name) and keeping the last row. |
Data dictionary — events.jsonl / events.csv
Behavioural and multimodal events live in a separate collection — they're append-only
and high-volume, so they're streamed rather than batched into the session document.
Each row is one event. The events.jsonl file is line-delimited JSON,
ideal for pandas.read_json(path, lines=True); events.csv is
the same data with payload kept as a JSON-encoded column.
| Field | What it captures |
|---|---|
| user_id, condition | Participant + arm. |
| type | Event kind. One of: mouse, key, hover, scroll, idle, rt (reaction time), focus, session_start, plus the multimodal lifecycle set: face, face_started, face_denied, face_model_load_failed, face_stream_ended, face_capped, face_unsupported. |
| t | Client performance.now() (ms since page load). Monotonic. |
| t_wall | Client Date.now() (ms since epoch). |
| received_at | Server insert time. |
| passage | Passage title at the time of the event, or null if it fired outside an active passage (e.g. session_start). |
| payload | Type-specific JSON. mouse: {x, y} in CSS pixels. key: timing only (key codes are not recorded). hover: {selector, ms}. scroll: {x, y}. idle: {ms}. rt: {passage, ms}. face: 52-element blendshapes array. Lifecycle events have {reason}. |
Working with the data
- Python (pandas):
pd.read_csv("sessions.csv"); for embedded-JSON columns (variables,data,payload) follow up withdf["data"].apply(json.loads). - Python (notebooks, full fidelity):
json.load(open("sessions.json"))["sessions"]gives you the nested list directly. - R:
readr::read_csv()for the CSVs;jsonlite::stream_in()forevents.jsonl. - Joining:
user_idis the join key everywhere. The CSVs already carryconditionfor convenience, but it's also unambiguously available fromsessions.csv.
Gotchas
t vs t_wall. Always prefer t for
within-session timings. t_wall is the participant's wall clock — fine for
ordering across the session but unreliable for durations if the device's clock changes
mid-experiment (timezone shift, NTP correction, manual edit).
t and t_wall are empty for those
rows; only the visits the tracker actually witnessed have timing data.
inputs.csv with the actual value.
Deleting a story #delete
What it does
Removes the story, its HTML (all arms if multi-condition), its assets, and every participant session and event collected against it.
When to use it
- You uploaded the wrong file and want a clean slate.
- The study is finished and you've exported your data.
twine platform