Skip to content

Studio Scripts

Han Elias edited this page Oct 21, 2026 · 1 revision

Scripts

Builder → Scripts adds behavior beyond asking questions. It has two halves: a library of five ready-made behaviors (timers, shuffles, matching fields, page randomization, experimental assignment), which work on every plan, and Custom JavaScript (Plus) for anything else. This page describes each library script exactly, how custom code runs, and where scripts take effect.

The Scripts tab is under More in the Builder's tab strip.


The Scripts tab

3 scripts. Library scripts are stored by what they do …           [+ Add script]

Script                            Kind         Trigger
Timer on q_task · 30s             library      onQuestionShow    open  edit  remove
Shuffle options of q_brand        library      onQuestionShow    open  edit  remove
Flag speeders                     custom JS    onSubmit          open  edit  remove
  • The note reads: "N scripts. Library scripts are stored by what they do and regenerated by the engine; Custom JavaScript runs verbatim in the survey page, with the page's own access."
  • Add script opens a menu of the five library scripts, each with a short description, and Custom JavaScript ("Your own code on a trigger — page, question, answer, submit.").
  • Choosing one opens an editor card titled "New · <script>" (or "Edit · <script>"). **Add** (or **Apply**) stays disabled until the required fields are filled. **Cancel** closes the card.
  • The table lists every script with a one-line summary, its Kind (library or custom JS) and its Trigger. The row actions are:
    • open: selects the targeted question or page in Structure;
    • edit;
    • remove: deletes the script at once; use Undo if needed.
  • With no scripts: "No scripts. Add one from the library — a timer, option shuffle, matching-field check — or write Custom JavaScript."
  • A question that a script targets carries a script pill on its card in Structure (tooltip "A script targets this question (Scripts tab)").

Scripts are part of the questionnaire: versioned with each Save and included in questionnaire.py. Library scripts are stored by what they do (for example "timer, 30 seconds on q_task"), so they read clearly in the generated Python and keep working when the engine is updated.


The library

Library scripts pick their questions by Id. They work whether or not a question's Id matches its variable name: Studio builds their code for the name the answer is stored under.

Timer on question

"Auto-advance after N seconds on one question — for timed tasks and attention checks."

Field Notes
Question pick from "— pick a question —"
Seconds whole number, at least 1; default 30

Summary: "Timer on q_task · 30s".

  • The countdown starts when the question first becomes visible. After N seconds the survey presses Next for the respondent.
  • The automatic Next is an ordinary one. If the page has an unanswered required question (or any other message blocking the page), it is refused: the respondent sees the message and stays on the page. For a timed task, leave the timed question optional, or put it alone on its page.
  • Leaving the page cancels the timer, whichever way the respondent leaves: Next, ← Previous, a page dot, or submitting the survey. A timer only ever presses Next on its own page; a respondent who waits on the page is moved on when the time runs out.
  • The timer runs once per question in a session: going back to the page does not start it again.

A survey published before this fix did not cancel the timer, which then pressed Next on whatever page was showing, submitting the survey on the last page. Publish it again.

Shuffle options

"Random option order on one question (the same as the question's Randomize switch, but seedable)."

Field Notes
Question a question with answer options
Seed ("optional — with a seed each respondent keeps one order, after a reload too; respondents still differ") any text; leave empty for an unseeded shuffle

Summary: "Shuffle options of brand_list (seed 42)". The shuffle happens when the question is first shown (the table's Trigger column says onQuestionShow).

  • Without a seed, each load of the survey draws a new order.
  • With a seed, the order comes from the seed and the respondent's id: each respondent sees one stable order, kept after a reload or a resume, and orders still differ between respondents. Two questions with the same seed and the same number of options are shown in the same order to a given respondent; give them different seeds for independent orders.
  • "None of the above", exclusive options such as "None of these" and a choice that is the question's Other keep their positions, as with Randomize option order; the other options are shuffled among the remaining positions. To keep an ordinary last option last, see Shuffle but keep the last option last.

A survey published before seeds worked gave every respondent an independent order and shuffled "None of the above" and exclusive options too. Publish it again. See also Seeded option order.

Validate fields match

"Two answers must be equal (e-mail twice, code confirmation); shows a message on the second field."

Field Notes
Field A the first question
Field B the second question; must be a different one
Message ("shown on field B") the message; left empty, respondents see "Fields do not match." (the placeholder "The two entries do not match." is only an example)

Summary: "email must match email_confirm".

  • The check runs whenever an answer changes. If both answers exist and differ, the message appears under Field B and Next is refused until they match.
  • As soon as either field is corrected, the message disappears and Next works again, whether the respondent fixes Field B or Field A. (In a survey published before this fix, correcting Field A left the message and blocked Next; publish it again.)
  • The comparison is exact: Ann@x.org and ann@x.org differ.

Randomize pages

"Content pages are served in random order for each respondent." The form says "No settings. Terminal pages (final, screen-out, redirect) keep their place."

  • The first page, the last page and every end page (Final, Screen-out, Redirect) keep their positions. The other pages are shuffled into the remaining positions.
  • Nothing is shuffled unless at least two pages are free to move.
  • Each load of the survey deals a new order. A respondent who resumes a saved interview continues in the order they were dealt, on the page they left.

Put your introduction or screener on the first page and your end pages after the last content page. See Page order for why. Summary: "Randomize page order".

Assign to a condition

"Draw each respondent into one experimental arm and store it in a variable — split-ballot, vignettes, A/B stimuli. Branch on it in Logic."

Field Notes
Variable (hint "the arm is stored here — branch on it in Logic") default condition. Errors: "Letters, digits and _ only, not starting with a digit." and ""x" is already a collected variable — pick another name."
Seed (hint "optional — with a seed a respondent always gets the same arm"; while balancing, "off while balancing — the arm depends on who answered first") any text, or empty
Balance Keep the arms level against their quotas
Arms Code, Label, Weight, Share; Add arm; at least two ("An assignment needs at least two arms")

The draw happens once, before the first page. On Add/Apply the variable is written to the codebook with the arms as its value labels. As the hint says, you can then branch on it in Logic: a show if, hide if or branch rule that reads the arm variable works anywhere in the survey, the first page included. Summary: "Assign condition · 2 arms · balanced".

With a Seed, the arm is worked out from the seed and the respondent's id: respondents are spread over the arms by their weights, and the same respondent always lands in the same arm. (A survey published before this fix sent every respondent of a seeded assignment to the same arm; publish it again.) The full description, with balancing against quotas, quota cells on the arms and branching on the arm, is in Experimental assignment.

The example study draws its A/B message this way: Variable message_arm, arms 1 Sleep message and 2 Time message, with a seed. Written as code, its questionnaire has no codebook entry for the arm, and needs none: the Builder's checks count a variable a script writes as known, and a flow's Responses node, Data → Export in SPSS or Stata and a research bundle's codebook label it with the arms.


Custom JavaScript (Plus)

The one place in Studio where you write code: a snippet that runs in the respondent's browser at a moment you choose.

Field Notes
Name (placeholder "Flag speeders") optional; shown in the table
Trigger when it runs (see below); default onPageEnter
Target ("optional — every one when empty") for page and question triggers only: — any page — / — any question —, or one page or question. Questions are listed by Id; a question whose Id differs from its variable name is listed as id → variable (the script targets the Id, and its answer is stored under the variable). Changing the Trigger clears the target
Code ("JavaScript · n/2000") up to 2,000 characters. A new script starts with a commented template (below)

The template a new custom script starts with:

// answers: the live answers — what a script writes is submitted too, except keys starting with __
// utils: shuffle, sample, shuffleOptions, clamp, debounce, now, formatDate · api: get(url), post(url, data)
// context: trigger, startedAt, respondentId, surveyId, page, pageEnteredAt, question, and this script's own keys
// Example: flag speeders on submit (add a variable "speeder" in the Codebook so the data labels it)
// if (utils.now() - context.startedAt < 60000) answers.speeder = 1;

Triggers

Trigger Hint in the form When it runs Target
onInit "once, when the survey loads" once, before the first page appears none
onRandomize "after the declared randomization ran" right after onInit none
onPageEnter "when a page is shown (target: page name)" each time a page is shown page
onPageExit "when the respondent leaves a page (target: page name)" when leaving a page with Next, Previous or a page dot (not when the last page is submitted) page
onQuestionShow "when a question becomes visible (target: question id; its answer is keyed by the variable)" the first time a question becomes visible in the session question
onAnswer "when an answer changes (target: question id; its answer is keyed by the variable)" after each change to an answer question
onSubmit "right before the answers are sent" on the last page's submit, and when an end page is reached none

With a target, the script runs only for that page or question. Without one, it runs for every page or question.

What your code receives

The code runs as the body of async function(answers, utils, api, context), so await is allowed. The form reminds you: "Runs as function(answers, utils, api, context) inside the survey page, with the page's own access — it is not sandboxed. Writes to answers flow back into the store and are submitted with the response, except keys starting with __; its keys are variable names, not question ids (answers.errors[variable] shows a validation message). Declare a variable a script writes in the Codebook, so the data labels it. context holds trigger, startedAt, respondentId, surveyId, page, pageEnteredAt and, for question triggers, question."

Argument What it gives you
answers the respondent's live answers, keyed by variable name (the name in the codebook), not by question Id. What you write into it flows back into the survey, and it is submitted with the response, except keys starting with __, which are the runtime's own
answers.__errors__ write answers.__errors__["<variable>"] = "message" to show a message under a question; it also blocks Next until cleared. Changing that question's answer clears it
answers.__options__ answers.__options__["<variable>"] is the question's option list in display order; replace it to reorder the options
answers.__pages__ the list of pages in the order they will be served
utils shuffle(list, seed?) (a shuffled copy), sample(list, n, seed?), shuffleOptions(options, seed?) (a shuffled copy of an option list that keeps "None of the above", exclusive options and the question's Other in place), clamp(value, min, max), debounce(fn, ms), now() (the time in milliseconds), formatDate(date) (YYYY-MM-DD). With a seed (any text), the same seed and list give the same order in every browser
api get(url) and post(url, data): JSON requests that return the parsed reply, or null if the request fails
context what the survey knows when the script runs (below), plus any keys set on the script itself, which win

context holds:

Key Value
trigger the trigger that ran the script, such as "onSubmit"
startedAt when the interview started, in milliseconds. For a respondent who resumed a saved interview, the start of the interview they resumed
respondentId the interview's respondent id, the one seeded shuffles and seeded assignments are drawn with
surveyId the published survey's id
page the name of the page on screen; for onPageEnter / onPageExit, the page entered or left
pageEnteredAt when that page was entered, in milliseconds
question for onQuestionShow and onAnswer only: the question the script runs for, by the name its answer is stored under

Every variable a question writes is its own key in answers: one per Matrix row (for a matrix trust, answers.trust_police), one per choice of a multiple-choice question in the wide layout (answers.brands_1 is 1 when chosen and 0 when offered and not chosen), one per MaxDiff pick and per Conjoint task. With Add “Other (please specify)”, the answer holds the Other code (the Inspector's hint says which, "stored as -66; the text goes to brand_other" by default) and the typed text is its own key (answers.brand_other). For a question that writes several variables, the __errors__ and __options__ entries are keyed by the question's Id. (See Multiple choice.)

An error inside your code is written to the browser's console, and the survey carries on.

Note. The runtime was updated. Before, context was always empty, a question that writes several variables kept one combined answer under its Id, and everything a script wrote was submitted, __ keys included. A survey published before the update runs your scripts the old way until you publish it again; check scripts written for the old layout (for example one that reads answers["trust"]["trust_police"]) before you do. A script that stored flags in answers.__flags__ (as the template once suggested) stops recording them once republished: write answers.speeder = 1 instead. Responses collected earlier keep their __flags__, and Data, exports and flows show its keys as columns (speeder).

Question Ids in your code

For most questions the Id and the variable name are the same, and nothing below matters. Where they differ (a preset gives Id q5 and variable nps_5, for example), write the variable name in your code.

Scripts written with the Id keep working in the common forms: when Studio builds the survey (previews and published surveys alike), it rewrites answers["q5"], answers.q5 (with ?. too, and in a spread such as [...answers.q5]) and the same forms on __errors__, __options__ and __timers__ to the variable name. It does not rewrite an Id used any other way, for example stored in a constant (const id = "q5") or used as an object key. The engine's check then warns SCRIPT_STALE_QUESTION_ID ("Script '' still names question 'q5' as a string or a bare identifier; its answer is stored under 'nps_5' … Where the script means the question, write 'nps_5'."). Comments, regular expressions and longer strings that merely mention the Id are not reported.

Because of that translation, a question whose Id differs from its variable cannot have an Id that your scripts use for something else: a codebook variable no question collects that a script writes (answers.panel = 1 next to a question with Id panel, or any other write — a destructuring, delete, answers.panel.push(…); see What counts as a script writing a name), the variable an Assign to a condition writes, another question's Other text or Matrix row, or a name starting with __. The script would reach the question's answer instead, so the Builder flags such an Id and the Save is marked errors (see Names an Id may not take). A script that writes the question's own Id (answers.q5 = 7, to prefill it) is fine as long as the codebook has no entry named q5: it writes the question's answer. If the codebook still has one, left behind when an earlier Builder renamed the question's variable, delete that entry in Builder → Codebook rather than renaming the Id; a new Id would leave the script writing the entry instead of the question.

Examples

These examples were checked against how the survey runs scripts. Replace the variable names (q_age, q_brand) with your own, and pick the question as Target.

Flag fast completions

The template's own example, as one script:

// Name: Flag speeders · Trigger: onSubmit
if (utils.now() - context.startedAt < 60000) answers.speeder = 1;

speeder is submitted with the response. context.startedAt is the start of the interview, so a respondent who resumes later keeps their original start time. Declare speeder in the codebook (under variables in the Source tab; the Codebook tab has no button to add a variable) so the data labels it; a codebook variable that a custom script writes is not reported as unused. Only a write in the code itself counts: while the template's example is still a comment (// if (…) answers.speeder = 1;), nothing writes speeder, and the Save warns UNUSED_VARIABLE for it. Once the code itself writes answers.speeder, Used by in the Codebook tab reads "referred to by the custom script name" for speeder, and Delete asks before removing the entry; while the write is still inside a comment it reads unused. Keep the entry either way. Studio already records each response's duration for you (see Data Quality); this example shows the mechanics.

Time spent on one page

// Name: Video time · Trigger: onPageExit · Target: video
answers.video_seconds = Math.round((utils.now() - context.pageEnteredAt) / 1000);

The script runs whenever the respondent leaves the page video, with Next or going back, so a respondent who returns to the page is timed on their last visit.

A custom check on one question

// Trigger: onAnswer · Target: q_age
const v = answers.q_age;
if (v !== undefined && v !== null && v !== "" && Number(v) < 16) {
  answers.__errors__["q_age"] = "You must be 16 or older to take part.";
}

Shuffle but keep the last option last

Randomize option order and Shuffle options already keep "None of the above", exclusive options and the question's Other in place. For an ordinary option that should stay last (a "Don't know" that is not exclusive, say), switch the question's Randomize option order off and use:

// Trigger: onQuestionShow · Target: q_brand
const key = "q_brand"; // the question's variable name, not its Id
const opts = answers.__options__[key];
if (Array.isArray(opts) && opts.length > 2) {
  const last = opts[opts.length - 1];
  answers.__options__[key] = utils.shuffle(opts.slice(0, -1)).concat([last]);
}

The last option of the list stays last: the one you put last in the Builder, or, on a question with Add “None of the above”, the None option, which is always last in the list. To keep the order stable for each respondent across reloads, pass a seed built from their id: utils.shuffle(opts.slice(0, -1), "brands:" + context.respondentId).

Plans

  • Custom JavaScript is included from Plus. On Free, the Custom JavaScript menu item is disabled with a Plus pill (tooltip "Included from Plus").
  • If a Save on Free carries a custom script (for example from an import), the tab shows "Custom JavaScript is included from Plus: this Save will not deploy until the custom script is removed or the plan upgraded." Publishing it is refused with "Custom JavaScript in the questionnaire is included from Plus — remove the script or upgrade to deploy this Save".
  • Library scripts work on every plan.

Where scripts run

Where Scripts run?
Structure → Preview, Test → Walkthrough, share preview links yes
the header Preview build and published surveys yes
Test → Simulate only Assign to a condition (each simulated respondent gets an arm, drawn by the weights) and Randomize pages (each gets a page order). Timers, Shuffle options, Validate fields match and Custom JavaScript do not run: simulation generates data from the questionnaire, not in a browser

In previews, a balanced assignment always uses the weighted draw, quotas are not checked, and api requests to other sites may be blocked by those sites. Structure → Preview, the Walkthrough and share links get a new respondent id each time they load (Restart included), so seeded shuffles and seeded assignments draw again there.


Checks and errors

When you Save, the engine checks scripts:

  • A script whose target is no longer a page name, a question Id or a question's variable name (a page or a question deleted, a question's Id changed) makes the Save errors, so it cannot be published: "Script '' targets '' which is not a known question ID or page name." (an unnamed script shows as 'None'). Renaming a page updates the target of Custom JavaScript that runs on it, as it updates branch rules and Skip to, and renaming a variable updates the scripts that name it. Changing a question's Id (Inspector → Advanced) does not: it breaks a script that targets the old Id unless the old Id is also the question's variable name. Edit the script and pick the new target.
  • For Assign to a condition: fewer than two arms, duplicate codes, an empty label or a seed combined with balancing are refused. The form prevents all four.

The engine also returns three script warnings, shown in Validation → Engine and with the Save (whose state becomes warnings). They do not block publishing, but the script they name does not do what you meant:

Code Meaning
SCRIPT_STALE_QUESTION_ID a custom script names a question by an Id that differs from its variable name, in a form Studio does not rewrite (see Question Ids in your code)
SCRIPT_TARGET_IS_A_PAGE an onQuestionShow or onAnswer script targets a page, so it never runs. Target a question, or use onPageEnter / onPageExit
SCRIPT_TARGET_IS_A_QUESTION an onPageEnter or onPageExit script targets a question, so it never runs. Target the page that holds the question, or use onQuestionShow / onAnswer

The form cannot produce the last two, because changing the trigger clears the target; they come from the Source tab or an import.


Security and privacy

  • Custom JavaScript runs as part of the survey page, with the same access to the page as the survey itself. It is not isolated from the page. A sandbox setting that a script may carry (visible in the Source tab) is kept with the script but not applied.
  • Everything you write into answers, except keys starting with __, is stored with the response and reaches your data like any answer.
  • The code is sent to every respondent's browser and is readable in questionnaire.py. Never put passwords, API keys or other secrets in a script.
  • api.get / api.post send requests from the respondent's browser. Tell respondents in your consent text if you contact other services.
  • Custom JavaScript never runs on Studio's servers or in your analysis flows.

See also


← Theme and Branding · Studio contents · Importing Questionnaires →

Start here: Studio · Library


📚 Library

Getting started

Survey design

Validate & simulate

Data & analysis

Reporting

Frontend & deploy

Tooling

More


🎛️ Siamang Studio

Get started

Account & workspace

Build the questionnaire

Fieldwork

Data

Analysis & output

Integrations & admin

Reference

Clone this wiki locally