HTML DSL
A survey is authored as HTML with survey-specific tags and attributes. The parser builds the model, validates canonical rules, and preserves only supported compatibility paths.
Minimal question
<question type="single" name="Q1" required>
<description>Which option do you prefer?</description>
<options>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</options>
</question>
Naming
| Object | Canonical attribute | Notes |
|---|---|---|
| Question | name | Must be globally unique and cannot collide with system names. |
| Section | name | Section names can be jump targets. |
| Loop | name | Loop items can use {{item}} placeholders. |
| Option or attribute | value | Signed integer code. Negative values are valid. |
Type boundaries
| Type | Purpose |
|---|---|
single | One option answer, optionally repeated by attributes. |
multi | Multiple option answers, optionally repeated by attributes. |
ranking | Click-to-rank answers stored in flat rank slots such as Q1_rk1. |
rating | Scale, star, or semantic rating semantics. |
nps | Net Promoter Score scale. |
text | Short or long open text. |
number | Numeric answer with semantic mode and visual display style separated. |
date | Date or date range. |
video | Uploaded video media gate. Optional require-full-watch="true" blocks progression until playback ends. |
glb | Uploaded GLB/3D media gate rendered through model-viewer. |
dropdown, multi-dropdown | Dropdown style selection. Menus overlay the page instead of pushing later questions; mobile uses a full-screen dismissible menu. |
info | Information-only block. |
run-code | Flow/code execution block; no normal answer field. |
Rendered class contract
Rendered DSL elements receive deterministic classes such as question, question-desc, question-options, option-1, option--2, option-input, question-attributes, and attr-1. Authored class attributes append presentation classes; they do not change logic or answer schema.
Template Expressions
Survey HTML DSL text and non-identity attributes can use JavaScript-style Template Expressions inside {{ ... }}. They are evaluated at Survey Link render time and in Run generate with the same restricted logic context as if expressions.
<script name="helpers">
function answerConvert(value) {
return String(value || "").trim().toUpperCase();
}
</script>
<question type="number" name="BUDGET"
min="{{ SEGMENT.val() === 1 ? 10 : 5 }}"
max="{{ SEGMENT.val() === 1 ? 60 : 45 }}"
step="{{ SEGMENT.val() === 1 ? 5 : 1 }}"
pre-text="{{ answerConvert(SEGMENT.val()) }}">
<description>Hello {{ answerConvert(NAME.val()) || "SHOPPER" }}</description>
</question>
Rules:
- Empty
{{ }}is a hard authoring error and the exact expression token is highlighted red in Survey HTML Mode and supported GUI fields. - Non-empty expression tokens receive a lightweight violet authoring highlight. If a fixed enum/dropdown attribute is authored as a Template Expression in HTML Mode, GUI Mode shows the control as disabled
EXPand points authors back to HTML Mode. - Question/option/attribute/group identity attributes such as
name,type, andvaluecannot be dynamic because response storage and export names must stay stable. <survey-config>attributes are static-only and cannot use Template Expressions.- Non-identity attributes can be dynamic, including
min,max,step,pre-text,post-text,option-columns,option-columns-min,expanded,hide-group,required, and display attributes. - Script helper calls are allowed only when the helper is declared in the same Survey HTML DSL
<script>as a restricted top-level function orregisterSurveyHelper(...). - Runtime expression errors fail open where possible, are deduplicated, and appear in the TEST console and Run generate diagnostics.
Dynamic numeric/date attributes are validated after evaluation. If a dynamic min / max pair becomes impossible, Survey Link falls back to a responseable state and logs the fallback instead of blocking the respondent.
Groups
<group> can be used inside <options>, <attributes>, <share-list>, and loop <items> to keep nested option/attribute/item structure.
<options order="random" group-order="random">
<group name="CORE" if="Q1.o1" order="asc" group-order="rotation" fix>
<label>Core brands</label>
<option value="1">Brand A</option>
<option value="2">Brand B</option>
</group>
</options>
Group attributes:
| Attribute | Meaning |
|---|---|
if | Expression-only visibility condition for the group and every descendant. Authoring syntax errors are hard errors; runtime errors fail open. |
order | Local leaf order inside this group. Options, attributes, and share-list groups support random, rotation, asc, and desc; loop item groups support random and rotation. |
group-order | Local ordering for this group’s direct child groups. |
fix | Keeps the group fixed when the parent group-order is applied. |
Container-level order remains the default. Group-level order / group-order override only inside that group. Hidden groups are removed before required validation and auto-punch/skip logic. Inserted share-list groups keep the authored <insert-list> source while runtime expands the reusable group tree.
Quota DSL
Quota source HTML is wrapped by one <quota-source> root and one or more <quota-sheet> children. The sheet id is the canonical quota name used by Workbench tabs, Excel sheet names, import validation, and runtime quota checkpoints.
<quota-source>
<quota-sheet id="gender_age" title="Gender × Age" flat-view="false">
<columns>
<leaf id="male" label="Male" if="Q1.o1" />
<leaf id="female" label="Female" if="Q1.o2" />
</columns>
<rows>
<leaf id="age_18_34" label="18-34" if="AGE.value >= 18 && AGE.value <= 34" />
<leaf id="age_35_plus" label="35+" if="AGE.value >= 35" />
</rows>
</quota-sheet>
<quota-sheet id="concept_quota" title="Concept quota" priority="true" priority-count="1">
<rows>
<leaf id="r1" label="Concept A" if="true" />
<leaf id="r2" label="Concept B" if="true" />
<leaf id="r3" label="Concept C" if="true" />
</rows>
</quota-sheet>
</quota-source>
Quota rules:
| Attribute or tag | Meaning |
|---|---|
<quota-source> | Required root for the full quota HTML DSL. |
<quota-sheet> | A single quota table. id must be unique, programming-style, and 31 characters or fewer because it is used as the Excel sheet name; duplicate sheet ids are hard errors. |
flat-view="true" | Renders that sheet as a stacked flat table. Omit or set false for pivot mode. |
priority="true" | Enables underfilled-cell assignment when one respondent can match multiple non-full cells. Omit or set false for the default count-all behavior. |
priority-count="1" | Required when priority="true". Must be an integer of 1 or greater and cannot exceed the total cell count in the sheet. |
priority-type="count" / priority-type="ratio" | Optional priority mode. Default is count, which chooses cells with the lowest current complete sample count. ratio chooses cells with the lowest current/limit fill ratio and hard-errors when any cell limit is blank/null. |
<columns> / <rows> | Authored dimensions. Flat mode stacks column dimensions first, then row dimensions. |
<leaf> | Countable quota leaf. Canonical leaf attributes are id, label, and if. |
question, code, type, and allocation are not canonical quota leaf attributes. A leaf is counted when its if expression matches, whether the underlying answer came from single or multi selection. if="true" is valid and means every respondent who reaches the quota checkpoint can match that leaf; combine it with priority assignment to auto-balance concepts or cells without asking a visible question. GUI-created manual row leaves use r{num} ids and manual column leaves use c{num} ids; existing imported or picker-derived ids are preserved. Excel import templates are visible-structure only: users may edit Limit values, but label/header/schema edits are rejected instead of changing the HTML DSL.
Quota runtime and export behavior
When Survey Link reaches a <quota> checkpoint, the runtime evaluates every configured cell for that source from the persisted quota snapshot, not by querying the database from authored JavaScript. With the default priority="false", every matched non-full cell is assigned and counted; matched full cells are excluded from counting. With priority="true", only the priority-count most underfilled matched non-full cells are assigned. priority-type="count" compares current complete sample counts. priority-type="ratio" compares current/limit fill ratios and is a hard error when any cell limit is blank/null. Ties among equally underfilled eligible cells may be selected randomly. If all matched required cells are already full, the response ends as quotafull. If the checkpoint is active but no cell can be assigned or matched, the response ends as screenout with a quota-generated screenout_status value. These generated statuses start at 901 in first-seen quota source order and use labels like NOT_QUOTA:gender_quota.
Response export adds trusted quota label columns after system columns and before question columns:
| Column | Value |
|---|---|
QUOTA_SHEET_{quota_name} | JSON array text containing assigned/counted quota cell labels from the saved quota source definition, for example ["Concept A"] or ["Concept A","Concept B"]. Blank/null means no assigned cell or no trusted quota definition was available. Raw matched-but-full cells and spoofed QUOTA_SHEET_* answers are not exported here. |
QUOTA_SHEET_ is reserved for quota export metadata and cannot be used as an authored question, screen-out, share-list, or raw-control response name. The array is stored as text in tabular exports so CSV, XLSX, and JSON projections keep the same value contract.
Survey config question-name badges
<survey-config /> may control whether Survey Link renders a compact question badge.
<survey-config show-name="none" page-animation="fade" test-back-button="true" />
| Attribute | Values | Default | Meaning |
|---|---|---|---|
show-name | none, name, index | none | none hides the badge. name renders the question variable name. index renders the visible runtime order starting at 1. |
show-name="index" is evaluated from the currently rendered visible question sequence. Hidden questions do not reserve numbers, so branching paths can show different indices for the same authored question. In TEST Audit Mode, show-name="name" avoids duplicating the question name; none and index still allow Audit Mode to expose the authored question name for QA.
Terminal redirect DSL
<redirect> is an optional Survey Link terminal-routing block. Omit it to keep the built-in end page and save redirectGroup as null.
<redirect>
<group title="Default Group" default />
<group name="v2" title="Partner list">
<route url="https://example.com/complete" if="$status.complete" />
</group>
</redirect>
Key rules:
- Exactly one group is
defaultwhen<redirect>exists; a single saved group is automatically serialized withdefault. - Group names are optional, but named groups must be unique and programming-style. Only one unnamed group is allowed.
- Route
urlandifare required; duplicate route conditions inside one group are hard errors. - Path selection wins over query selection:
/s/{surveyId}/v1beats?redirectGroup=v2. - Named default groups canonicalize the bare Survey Link URL to the named path and save that group name.
- Invalid redirect group selection or unsafe final URLs block progression before response persistence.
See Terminal Redirects for the full authoring contract.
Option columns and group display
Supported question types may request a responsive option/attribute grid with option-columns and option-columns-min.
<question type="single" name="REGION" option-columns="3" option-columns-min="2">
<description>Where do you shop most often?</description>
<options>
<option value="1">North</option>
<option value="2">South</option>
<option value="3">East</option>
</options>
</question>
| Attribute | Supported on | Meaning |
|---|---|---|
option-columns | single, multi, ranking, number, text, date questions and supported <attr> rows | Desktop column count. Blank or omitted means the default one-column layout. |
option-columns-min | Same as above | Mobile/minimum column count. If option-columns is omitted, this value becomes the effective column count for all breakpoints. |
Rules:
- Values must be integers greater than or equal to 1.
option-columns-mincannot be greater thanoption-columnswhen both are present.- The effective column count cannot exceed the rendered item count.
- For
single,multi, andranking, columns affect option cards; attr-level values override question-level values per row. - For
number,text, anddate, columns affect repeated attribute input rows. <group>panels always span the full row; the grid is applied inside each group body.
single, multi, and ranking also support grouped option/attribute accordion display:
<question type="ranking" name="FEATURE_RANK" group-display="accordion" expanded="true" option-columns="2">
<description>Rank the most useful features.</description>
<options>
<group name="DISCOVERY" expanded="true">
<label>Discovery</label>
<option value="1">Search</option>
<option value="2">Recommendations</option>
</group>
<group name="CHECKOUT">
<label>Checkout</label>
<option value="3">Payment speed</option>
<option value="4">Delivery clarity</option>
</group>
</options>
</question>
| Attribute | Values | Default | Meaning |
|---|---|---|---|
group-display | default, accordion | default | Renders <group> panels normally or as collapsed accordion panels. Hard error on unsupported question types. |
expanded on <question> | true, false | false | Default open state for accordion groups. Ignored when group-display="default". |
expanded on <group> | true, false | false | Overrides the question-level accordion open state for that group. |
hide-group on <question> | true, false | false | Keeps grouping semantics, order, shuffle, and export behavior, but suppresses Survey Link group chrome for every group in the question. |
hide-group on <group> | true, false | question value | Overrides question-level hide-group; explicit false restores the group chrome under a hidden question default. |
Group accordion status icons indicate that the group contains a response; they do not change page-level navigation or submit behavior.
Question type attribute matrix
| Type | Key attributes |
|---|---|
single | required, if, `display-style=“carousel |
multi | required, min, max, display-style, option-columns, option-columns-min, group-display, expanded, option sa, option oe. |
ranking | required, min, max, rank-all, display-style, option-columns, option-columns-min, group-display, expanded; stores rank slots such as Q1_rk1. |
rating | `mode=“scale |
nps | Optional <left>, <center>, <right>, display-style where attributes exist. |
text | `mode=“short |
number | `mode=“int |
date | minDate, maxDate, range-answer="true", option-columns for repeated attributes. |
video | source, optional require-full-watch="true"; uploaded video asset gate only, no response/export column. |
glb | source; uploaded GLB asset gate only, no response/export column. |
dropdown, multi-dropdown | Selection through dropdown UI; option columns and group accordion are not supported. |
info | Description/comment only; no response value. |
run-code | Restricted flow code block; no response value and not a jump target when hidden. |
Custom HTML controls
Raw HTML can be used with or without framework <question> blocks. Bindable native controls are input, select, and textarea with a unique name. Unsupported named controls such as output, meter, progress, hidden/file inputs, and unnamed response inputs remain hard errors.
<style>
.custom-card { border: 1px solid #dcd7d3; padding: 1rem; }
</style>
<script name="helpers">
function normalizeLabel(value) { return String(value || '').trim(); }
</script>
<div class="custom-card">
<label>Email <input type="email" name="RAW_EMAIL" required /></label>
<label>Channel
<select name="RAW_CHANNEL"><option value="app">App</option></select>
</label>
<label>Feedback <textarea name="RAW_FEEDBACK" minlength="10"></textarea></label>
</div>
Raw <form> tags are allowed for custom HTML survey surfaces, but Vibe Survey still owns final response submission and persistence. Keep response names globally unique across framework questions and raw controls.