Real scripting.
Real routing.
Python-like - script blocks for the logic a radio
button can't model — quota gates, screenouts, computed values,
cross-question validation. YAML-authored. Version-controlled.
Greppable.
Where it lives
- script is a flow-node type. It sits alongside
- qid nodes in the same YAML file, runs in the
engine's evaluation order, and reads and writes the same
session state every other node sees.
- qid: QAge
type: number
min: 18
max: 99
- script: |
if QAge.value >= 65:
hAgeBucket.set("65+")
elif QAge.value >= 45:
hAgeBucket.set("45-64")
else:
hAgeBucket.set("18-44")
- qid: Q5
text: Which brands have you used?
type: multi
options: brandList
The same accessors work in scripts, in if:
expressions, in piped display text, and in option labels.
Accessors
| Call | Returns |
|---|---|
Qxxx.value | The answer, or None if unanswered |
Qxxx.values | List of selected codes (multi, grid, rank) |
Qxxx.labels | List of selected labels |
Qxxx.selected(1) | True if code 1 is selected |
Qxxx.rank(1) | Position of code 1 in a rank question |
Qxxx.r1.value | Grid row / loop iteration accessor |
hVar.set(expr) | Write a hidden variable (becomes a column) |
quota("name", code) | Quota cell for the named quota |
.isFull() / .fill() | Quota state. fill() is called automatically on complete |
endSurvey("status") | Terminate — complete, screenout, quotafull, or custom |
goto("qid") | Jump to a later node (forward only) |
lookup("file", key) | Resolve a value from a project lookup file |
Routing
Every node accepts an if: expression. Evaluated
against session state before the step renders — if it returns
falsy, the node is skipped.
- qid: Q5
text: What draws you back to the brand?
type: open
if: QNps.value >= 9
- qid: Q6
text: What would've made it a 10?
type: open
if: QNps.value < 10 and QNps.value >= 7
For routing a script decides, call endSurvey() or
goto() inside a - script block.
Backward jumps aren't allowed — the engine runs a strict
forward graph.
Validation
Per-question validation lives on validate:.
Raise a string to show it to the respondent; return silently
to pass.
- qid: Q3
type: gridNumber
rows: brandList
min: 0
max: 100
validate: |
if sum(Q3.values) != 100:
raise "Allocations must total 100%."
- qid: Q4
type: rank
options: brandList
validate: |
if Q4.rank(1) == Q2.value:
raise "You already picked that brand at Q2 as your current one." Piping
Any accessor pipes into display text with
{{ }}. Resolved at render time
against current session state — including looped questions,
hidden variables, and lookup results.
- qid: Q7
text: You picked {{Q6.label}}. Why?
type: open
- qid: Q8
text: Here are your top three — {{Q4.labels[0]}},
{{Q4.labels[1]}}, and
{{Q4.labels[2]}}. Which one would you
actually buy next?
type: single
options: Q4.values Full example
Quota gate, derived index, and a conditional flag — one
- script block, inline with the survey:
- script: |
# Interlock quota check — fail the respondent if their
# age × gender cell is already full.
if quota("AgeGender", hAgeGenderCell.value).isFull():
endSurvey("quotafull")
# Rolling satisfaction index (used later for screenouts
# and for a derived segment column in exports).
hSatisfIndex.set(
(Q7_1.value + Q7_2.value + Q7_3.value + Q7_4.value) / 4
)
if hSatisfIndex.value < 3:
hLowSatisf.set(1)