Skip to content

stimFormat

Experiments Surveys & forms On form rows it controls whether images preload next to the question; it does not make a row a survey question (that is the type column).

stimFormat declares what kind of stimuli the trial’s stim columns hold. It determines how the trial’s stimuli are displayed (mapping) and, for media trials, doubles as the literal file extension appended to stimulus basenames. The special value list switches the row into stimulus-list expansion mode.

Put the file extension of your stimuli in the cell, leading dot included: .png, .mp3, .mp4.

For stimuli that are not files, use a keyword instead: word for text shown on screen, survey, html for a custom HTML app, or list to draw the trial’s stimuli from an uploaded stimulus list.

The cell holds a single value and letter case does not matter.

Stimulus type mapping:

ValueTrial typeNotes
.bmp .gif .jpeg .jpg .pdf .png .webpimage.tiff also works, but is deprecated
wordwordStim cells rendered as text/HTML
.aac .flac .m4a .mp3 .ogg .opus .wavaudio.aiff .wma deprecated
.mp4 .webmvideo.ogv .wmv .avi .flv .mkv .mov deprecated
surveysurvey
htmlappLoads stim/stim1; see “Custom HTML trials” below for the in-app testableSDK API
listderivedType computed from the first picked stimulus’s extension; extensionless or unknown-extension first stim becomes word unless type is instructions
anything else, incl. an empty cell, image, audio, video, forminstructionsDefault case

Note there is no form stimFormat; form trials are driven by the type column.

Leave the cell empty, or leave the column out, and the row becomes an instructions trial.

  • stim, stimList and stimN: stimFormat decides whether the values in those cells are filenames (media), literal text (word), stimulus-list names (list), or an app reference (html).
  • On image, audio and video trials you write the filename without its extension and stimFormat supplies it. If a cell already ends in exactly that extension, it is not appended a second time.
  • On list rows the trial’s type and extension come from the first stimulus picked from the list, and the media that gets preloaded comes from the picked rows (stim_actual) rather than the stim columns.
  • A single row with stimFormat: list (any case, surrounding spaces ignored) switches the whole trial file into list expansion, and every generated row keeps stimFormat: list. Two things about the column itself can stop that happening for the whole file: see Tips & gotchas.
  • In multiplayer arenas, stimFormat is part of the trial information shared between players.
  • stimScreens (screen grouping) is not supported on audio or video trials and is rejected when you save. On rows made dynamic by stimFormat: list, or by #list# markers in stim or stimList, stimScreens accepts only a single broadcast count.

Image trial, basenames in stim columns, extension from stimFormat:

typestimFormatstim1stim2presTimekeyboardkey
test.pngcirclesquare5001 21

Word trial:

typestimFormatstim1keyboardkey
testwordHELLOh jh

List trial (type derived from picked stimuli’s extensions):

typestimFormatstimlistOptions
testlistsoundsrandom

Custom HTML trials (stimFormat=html): loading and the testableSDK

Section titled “Custom HTML trials (stimFormat=html): loading and the testableSDK”

stimFormat: html turns the trial into an “app” trial: the value of stim (preferred) or stim1 (fallback) is loaded into an iframe after the trial’s ITI. The trial then waits indefinitely for the app to save a response; keyboard/button response columns do not apply.

When to resort to a custom HTML trial. Reach for stimFormat: html only when no built-in trial expresses the interaction: drag-and-drop, canvas/games, gesture or free-drawing input, multi-step logic inside a single trial, rich third-party widgets, or embedding an externally hosted web app. Prefer built-ins first: keyboard/button/click responses for RT tasks, responseType widgets (slider, box, dropdown, likert, rank, stars) and type=form for surveys, stimFormat media types for images/audio/video. A custom HTML trial trades away Testable’s automatic handling: its RT is coarser (see below), key scoring, responseWindow, presTime and feedback columns do not apply, and the app itself must call saveHtmlTrialResponse or the trial never advances. The app’s files (HTML plus any CSS/JS/media) must be uploaded to the project’s Stimuli section beforehand: over MCP, use put_stimulus to upload them (text content for html/js/css, base64 for binaries), list_stimuli to check what exists, and delete_stimulus to remove files.

  • The stim cell first goes through variable replacement, so %response%, %rt%, %correct%, %responseCode% and the full variable syntax are substituted. Pattern: index.html?a=%response% passes the previous trial’s response as a query parameter, readable in the app via new URLSearchParams(window.location.search).get('a').
  • Predefined app names cubefield and langfuse map to built-in apps.
  • Values starting with http are loaded as-is (externally hosted apps are allowed).
  • Anything else is a relative path into the project’s stimuli folder: .html is AUTO-APPENDED when the value does not already contain .html, and leading ../ sequences are stripped. So stim: my_trial and stim: my_trial.html both work when the experiment runs (the editor’s validation is stricter; see the stim page).
  • A cache-busting ts=<timestamp> query parameter is always appended, including for external URLs.
  • Uploaded HTML/CSS/JS files and media stimuli all live in the same stimuli folder, so they can reference each other with bare relative paths (styles.css, script.js, image1.png).

Saving the response: parent.testableSDK.saveHtmlTrialResponse(response[, data])

Section titled “Saving the response: parent.testableSDK.saveHtmlTrialResponse(response[, data])”

Calling it records response as the trial’s response, tears down the iframe, and advances to the next trial. The response string is trimmed before it is saved.

The optional data object saves custom variables:

  • Each key becomes its OWN COLUMN in the exported results CSV, with the value placed on the row of the trial that saved it and blank on all other rows.
  • Keep values primitive (string/number). Nothing checks that the object is flat: a nested object is stored and serialized into the CSV as [object Object].
  • The same object is also reported to the attrition/consort tracking while the trial saves; an oversized object (longer than 50000 characters once serialized) is silently left out of that report.

RT: the recorded RT is measured from the moment the app trial is SCHEDULED, before the ITI elapses and before the iframe loads. So the saved RT includes the ITI wait plus the app’s load time, not just time-in-app.

Externally hosted apps cannot call parent.testableSDK directly (cross-origin). Instead they post a message to the parent window:

window.parent.postMessage({
testable_app_result: { response: "Button clicked", custom_variables: { score: 85 } }
}, "*");

The runner listens for message events and forwards testable_app_result.response / .custom_variables to saveHtmlTrialResponse.

  • parent.testableSDK.variables.name — latest saved custom variable, or undefined if never saved. parent.testableSDK.variables.getValue(name, ref) reads at a history point: ref −1 (default) = previous trial, −2 = one before that, a number ≥ 2 = absolute spreadsheet row (data rows start at 2; header is row 1), or a label column value. The lookup is CARRY-FORWARD: it returns the latest value saved at or before the referenced trial, so a trial that did not itself save the variable still yields the preceding value; undefined only when no earlier trial ever saved it.
  • parent.testableSDK.allocatedSubjectGroups — array of the participant’s picked subject groups as label:pick strings; picks made under the auto-added default label appear as the bare pick, with no label prefix.
  • parent.testableSDK.columnForTrial(columnName, trialIndex) — standard-column value from any trial row; trialIndex is the 0-based data-row index, and an empty cell comes back as undefined. Unknown or custom column names throw a TypeError.
  • parent.testableSDK.trial.columnName — current-trial proxy. It does NOT work for standard columns: trial.stim1 and its siblings always return undefined, and only CUSTOM columns come back. Use parent.testableSDK.columnForTrial(name, window.parent.trialRow) or a custom column as the workaround.
typestimFormatstim
testhtmlmy_trial

my_trial.html (uploaded to Stimuli alongside any css/js/images it references):

<button id="go">Click me</button>
<script>
document.getElementById('go').addEventListener('click', function () {
parent.testableSDK.saveHtmlTrialResponse('Button clicked', { time_taken: 15, confidence: 'high' });
});
</script>
  • HTML trials: the testableSDK.trial proxy returns undefined for every standard column, so read standard columns with columnForTrial instead. Recorded RT for app trials includes the ITI and the app’s load time. Nested objects in saveHtmlTrialResponse’s data parameter export as [object Object].
  • Only an extension identical to stimFormat is stripped from a stim cell, so a cell that carries a DIFFERENT extension ends up with both (a.png on a .jpg trial becomes a.png.jpg).
  • Stimulus lists recognise a narrower set of extensions than ordinary trials do. If the first stimulus picked from a list ends in something outside .jpg, .jpeg, .png, .gif, .bmp, .pdf, .mp3 or .mp4 (for example .webp, .wav or .webm), the trial is treated as a word trial, and only .mp3 and .mp4 files from a list are preloaded. Stick to that narrower set inside stimulus lists.

stimFormat appears in these worked recipes:

  • Replayed random order: Shuffle a block once, then replay that exact order in a later block so both passes share identical order effects.
  • Ambient audio across trials: Start a looping background track on one trial and cut it at a later trial, leaving the rest of the block silent.
  • Between-subject conditions: Give each participant one of two stimulus sets, while shared screens run for everyone.
  • Nested block shuffle: Randomise the order of whole task sections and of the mini-blocks inside each section, keeping every block’s trials together.
  • Respond after the clip ends: Play an audio clip and only accept the keypress once playback has finished, with RT measured from the end of the clip.