Repository navigation
Studio 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.
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.
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.
"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.
"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.
"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.organdann@x.orgdiffer.
"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".
"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.
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;| 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.
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,
contextwas 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 readsanswers["trust"]["trust_police"]) before you do. A script that stored flags inanswers.__flags__(as the template once suggested) stops recording them once republished: writeanswers.speeder = 1instead. Responses collected earlier keep their__flags__, and Data, exports and flows show its keys as columns (speeder).
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.
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.
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.
// 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.
// 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.";
}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).
- 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? |
|---|---|
| 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.
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.
- 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
sandboxsetting 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.postsend 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.
- Quotas and Randomization
- Logic and Branching
- Theme and Branding
- Testing Your Survey
- Plans, Trial and Billing
← Theme and Branding · Studio contents · Importing Questionnaires →
siamang · Siamang Studio · Free for noncommercial use · Commercial licensing · Wiki source: wiki/
Getting started
Survey design
- Variables and Measurement
- Question Types
- Pages Blocks and Structure
- Visibility and Branching
- Quotas
- Scripts
Validate & simulate
Data & analysis
Reporting
Frontend & deploy
Tooling
More
Get started
Account & workspace
- Sign Up and Sign In
- Account and Profile
- Organizations and Team
- Projects
- Project Settings
- Plans, Trial and Billing
Build the questionnaire
- The Builder
- Question Types
- Codebook and Variables
- Logic and Branching
- Quotas and Randomization
- MaxDiff and Conjoint
- Theme and Branding
- Scripts
- Importing Questionnaires
- Question Bank and Library
- Testing Your Survey
- AI Assistant
Fieldwork
- Publishing and Environments
- Links, QR, Embeds, Access
- Panel Providers
- Email Invitations
- What Respondents See
- Live Monitoring
Data
Analysis & output
- Analysis Flows
- Node Reference
- Cleaning and Weighting
- Coding Open Answers
- Reports
- History and Versions
- Reproducibility
Integrations & admin
Reference