Skip to content

QML Syntax

QML (Questionnaire Markup Language) is a YAML-based language for creating formally verified questionnaires. This page provides a brief technical overview.

Overview

QML enables:

  • Formal verification of questionnaire logic using SMT solvers
  • Conditional branching with preconditions and postconditions
  • Integer-based outcomes for all responses, enabling mathematical analysis
  • Embedded Python code for computations (restricted subset for verification)

Key Components

Structure

qmlVersion: "1.0"
questionnaire:
  title: "Survey Title"
  codeInit: |
    # Python initialization code
  blocks:
    - id: b_section
      kind: Sequence            # Mandatory. Sequence | Roster | Sample | Random
      precondition:             # Optional: applies to all items in block
        - predicate: condition
      items:
        - id: q_item
          kind: Question
          precondition:
            - predicate: condition
          postcondition:
            - predicate: validation
          input:
            control: ControlType

Item Types

  • Comment: Display text without collecting responses
  • Question: Single integer outcome
  • QuestionGroup: Vector of integer outcomes
  • MatrixQuestion: Matrix of integer outcomes

Input Controls

  • Switch: Binary (0/1)
  • Radio: Single selection from labeled options
  • Checkbox: Multiple selection (bit mask encoding)
  • Dropdown: Single selection with prefix/suffix
  • Editbox: Free-form integer within [min, max]
  • Slider: Visual integer selection
  • Range: Interval selection (two integers encoded via Szudzik pairing)

Conditional Logic

Preconditions determine visibility:

precondition:
  - predicate: q_age.outcome >= 18

Postconditions enforce constraints:

postcondition:
  - predicate: q_children.outcome < q_household.outcome
    hint: "Children must be fewer than household size"

Code Blocks

Restricted Python subset for formal verification:

Supported: - Arithmetic: +, -, *, // - Comparisons: <, <=, >, >=, ==, != - Boolean: and, or, not - Control: if/elif/else, for (limited) - Ternary: x = 1 if condition else 0 - Tuple unpacking: a, b = 1, 0 - Built-ins: range(), abs(), int(), bool() - Outcome access: item.outcome

Not supported: - Functions, classes, imports - While loops, break, continue - Dictionaries, strings methods

Complete Examples

This section demonstrates all item kinds and control types with working examples.

Item Kind: Comment

Display informational text without collecting responses:

- id: intro_comment
  kind: Comment
  title: "Welcome to our demographic survey. Your responses help us understand our community better."

Comments can have conditional display:

- id: parent_notice
  kind: Comment
  title: "The following questions are about your children."
  precondition:
    - predicate: q_has_children.outcome == 1

Item Kind: Question

Single-outcome questions with various control types.

Control: Switch

Binary choice (0 = off, 1 = on):

- id: q_has_children
  kind: Question
  title: "Do you have children?"
  input:
    control: Switch
    off: "No"
    on: "Yes"
    default: 0

Control: Radio

Single selection from labeled options:

- id: q_education
  kind: Question
  title: "What is your highest level of education?"
  input:
    control: Radio
    labels:
      1: "High School"
      2: "Bachelor's Degree"
      3: "Master's Degree"
      4: "Doctorate"
    default: 1

Control: Dropdown

Single selection with optional prefix/suffix text:

- id: q_country
  kind: Question
  title: "Country of residence"
  input:
    control: Dropdown
    left: "I live in"
    right: ""
    labels:
      1: "United States"
      2: "Canada"
      3: "United Kingdom"
      4: "Germany"
      5: "Other"

Control: Checkbox

Multiple selection (bit mask encoding):

- id: q_interests
  kind: Question
  title: "Select all that apply:"
  input:
    control: Checkbox
    labels:
      1: "Reading"       # Bit 0: value 2^0 = 1
      2: "Sports"        # Bit 1: value 2^1 = 2
      4: "Music"         # Bit 2: value 2^2 = 4
      8: "Travel"        # Bit 3: value 2^3 = 8
      16: "Cooking"      # Bit 4: value 2^4 = 16

Selecting "Reading" and "Music" produces outcome: 1 | 4 = 5

Control: Editbox

Free-form integer input with bounds:

- id: q_age
  kind: Question
  title: "What is your age?"
  input:
    control: Editbox
    min: 0
    max: 120
    left: ""
    right: "years old"
    default: 25
  postcondition:
    - predicate: q_age.outcome >= 18
      hint: "You must be 18 or older to participate"

Control: Text

Open-ended free-text input. The outcome is a string (not an integer), so it is ignored by Z3 analysis. Use for qualitative feedback, suggestions, or comments that need textual answers.

- id: q_feedback
  kind: Question
  title: "Describe your experience with our service."
  input:
    control: Text
    placeholder: "Type your answer here..."
    maxLength: 500

Optional properties:

  • placeholder: Hint text shown when the textarea is empty
  • maxLength: Maximum number of characters allowed

Control: Slider

Visual integer selection:

- id: q_satisfaction
  kind: Question
  title: "How satisfied are you with our service?"
  input:
    control: Slider
    min: 0
    max: 10
    step: 1
    left: "Not satisfied"
    right: "Very satisfied"
    labels:
      0: "0"
      5: "5"
      10: "10"
    default: 5

Control: Range

Interval selection (two integers encoded as one):

- id: q_price_range
  kind: Question
  title: "What is your acceptable price range?"
  input:
    control: Range
    min: 0
    max: 1000
    step: 50
    left: "$"
    right: ""

The outcome is encoded via Szudzik pairing. Selecting \([100, 500]\) produces a single integer.

Item Kind: QuestionGroup

Vector of outcomes with identical control for each sub-question:

- id: qg_family_ages
  kind: QuestionGroup
  title: "Please enter the age of each family member:"
  questions:
    - "Yourself"
    - "Spouse"
    - "First child"
    - "Second child"
  precondition:
    - predicate: q_household_size.outcome >= 2
  input:
    control: Editbox
    min: 0
    max: 120
    right: "years"
  postcondition:
    - predicate: qg_family_ages.outcome[0] >= 18
      hint: "Primary respondent must be 18 or older"

Access outcomes via indexing: qg_family_ages.outcome[0], qg_family_ages.outcome[1], etc.

Item Kind: MatrixQuestion

Matrix of outcomes (rows × columns):

- id: mq_language_proficiency
  kind: MatrixQuestion
  title: "Please indicate the language proficiency level for each family member:"
  rows:
    - "English"
    - "Spanish"
    - "French"
    - "German"
    - "Mandarin"
  columns:
    - "Yourself"
    - "Spouse"
    - "First Child"
    - "Second Child"
  input:
    control: Dropdown
    labels:
      0: "None"
      1: "Beginner"
      2: "Intermediate"
      3: "Advanced"
      4: "Native"

Access outcomes via row/column indexing: mq_language_proficiency.outcome[0][1] for first language (English), second family member (Spouse).

The matrix creates a grid where each cell represents one family member's proficiency in one language. This example produces a 5×4 matrix (5 languages × 4 family members) = 20 individual outcomes.

Complex Example with Code Blocks

Demonstrating preconditions, postconditions, and code blocks:

qmlVersion: "1.0"
questionnaire:
  title: "Income Survey"
  codeInit: |
    total_income = 0
  blocks:
    - id: b_demographics
      items:
        - id: q_employment
          kind: Question
          title: "Are you currently employed?"
          input:
            control: Switch
            off: "No"
            on: "Yes"

        - id: q_income
          kind: Question
          title: "What is your annual income?"
          precondition:
            - predicate: q_employment.outcome == 1
          input:
            control: Editbox
            min: 0
            max: 1000000
            left: "$"
            right: "per year"
          codeBlock: |
            total_income = q_income.outcome

        - id: q_spouse_employed
          kind: Question
          title: "Is your spouse employed?"
          precondition:
            - predicate: q_employment.outcome == 1
          input:
            control: Switch
            off: "No"
            on: "Yes"

        - id: q_spouse_income
          kind: Question
          title: "What is your spouse's annual income?"
          precondition:
            - predicate: q_spouse_employed.outcome == 1
          input:
            control: Editbox
            min: 0
            max: 1000000
            left: "$"
            right: "per year"
          codeBlock: |
            total_income = total_income + q_spouse_income.outcome

        - id: q_household_income
          kind: Question
          title: "What is your total household income?"
          input:
            control: Editbox
            min: 0
            max: 2000000
            left: "$"
            right: "per year"
          postcondition:
            - predicate: q_household_income.outcome >= total_income
              hint: "Household income cannot be less than reported individual incomes"

This example demonstrates:

  • Conditional visibility: Income questions only appear if employed
  • Code blocks: Track cumulative income across questions
  • Postcondition validation: Ensure household income is consistent with individual incomes
  • Chained dependencies: Each question depends on previous answers

Block Kinds

Every block declares a mandatory kind. Recognized values:

Kind Status Semantics
Sequence Implemented Visit inner items once in declared order. Default block behavior.
Roster Implemented Repeat inner items per set bit in an iterateOver bitmask (see below).
Sample Reserved (not yet implemented) Future flow mode — selects N items in declared order.
Random Reserved (not yet implemented) Future flow mode — selects N random items (does NOT randomize order).

Blocks of any kind can also carry optional precondition and postcondition lists that propagate to every inner item (block-level rules fire before item-level rules). Static validation (Z3) treats every kind the same — all inner items must be reachable and well-typed; the kind only changes runtime traversal.

Roster

A Roster block (kind: Roster) repeats its inner items once per active label-key. The author declares:

  • iterateOver: a Python expression that must evaluate to a non-negative integer treated as a bitmask.
  • labels: a map of power-of-2 integer keys (1, 2, 4, 8, 16, …) to display strings — declares the universe of possible iterations.

The engine walks the set bits in iterateOver from low to high. For each set bit whose key appears in labels, the inner items run once with that bit value as the iteration's intrinsic identity.

Two authoring shapes ship in v1:

1. Multiselect-driven (Checkbox feeds directly into the roster)

A Checkbox outcome IS the bitmask integer (sum of selected power-of-2 keys). It flows directly into iterateOver with no intermediate code:

qmlVersion: "1.0"
questionnaire:
  title: "Daily Meal Tracker"
  blocks:
    - id: meal_selection
      kind: Sequence
      items:
        - id: q_meals_eaten
          kind: Question
          title: "Which meals did you eat today?"
          input:
            control: Checkbox
            labels:
              1: "Breakfast"
              2: "Lunch"
              4: "Dinner"
              8: "Snack"

    - id: per_meal
      kind: Roster
      title: "Per-meal details"
      iterateOver: "q_meals_eaten.outcome"
      labels:
        1: "Breakfast"
        2: "Lunch"
        4: "Dinner"
        8: "Snack"
      items:
        - id: q_satisfaction
          kind: Question
          title: "How satisfied were you?"
          input:
            control: Slider
            min: 1
            max: 5
        - id: q_notes
          kind: Question
          title: "Any notes?"
          input:
            control: Textarea

Respondent ticks Breakfast + Dinner → q_meals_eaten.outcome = 5 (bit 1 + bit 4) → engine walks two iterations: bit 1 (Breakfast), then bit 4 (Dinner).

2. Numeric "How many?" (plain math builds the mask)

For a numeric count, an upstream codeBlock builds a "first N bits" mask via plain math (2 ** n - 1). No bit-shift operator (<<) needed:

qmlVersion: "1.0"
questionnaire:
  title: "Family Roster"
  blocks:
    - id: count_block
      kind: Sequence
      items:
        - id: q_family_count
          kind: Question
          title: "How many family members do you have?"
          input:
            control: Editbox
            min: 1
            max: 4
          codeBlock: |
            family_mask = 2 ** q_family_count.outcome - 1

    - id: per_member
      kind: Roster
      title: "Family member details"
      iterateOver: "family_mask"
      labels:
        1: "Member 1"
        2: "Member 2"
        4: "Member 3"
        8: "Member 4"
      items:
        - id: q_member_name
          kind: Question
          title: "Name?"
          input:
            control: Editbox
            min: 0
            max: 100
        - id: q_member_age
          kind: Question
          title: "Age?"
          input:
            control: Editbox
            min: 0
            max: 120

Respondent answers count = 3 → codeBlock computes family_mask = 2 ** 3 - 1 = 7 → engine walks bits 1, 2, 4.

Reading roster outcomes from outside the roster

Inside a roster iteration, q_satisfaction.outcome resolves to the current iteration's value (snapshot/restore). Outside the roster (post-roster code blocks, later items' preconditions), use the dict-shaped q_satisfaction.outcomes[<bit>]:

- id: q_summary
  kind: Question
  title: "..."
  precondition:
    # Show only if Dinner satisfaction was high.
    - predicate: "q_satisfaction.outcomes.get(4, 0) >= 4"

Or aggregate across iterations:

total_satisfaction = sum(q_satisfaction.outcomes.values())

Constraints (v1)

  • iterateOver is strictly an integer expression — list/sequence types are not accepted.
  • labels keys must be positive powers of 2 (1, 2, 4, 8, 16, …); the loader rejects anything else with a clear error.
  • Checkbox controls also enforce power-of-2 label keys — aligns Checkbox semantics with Roster so a Checkbox outcome flows directly into iterateOver.
  • No string interpolation in titles. Item titles render verbatim; per-iteration display chrome (e.g., "2 of 4 · Lunch") is rendered by the survey UI from the labels[<bit>] mapping.
  • No << / >> bit-shift operators in author code blocks — use plain math (2 ** n - 1 for "first N bits"). The ** operator is allowed.
  • No list comprehensions in author code blocks (Z3 translation cost).
  • as (iterator-variable binding) and maxEntries are not supported in v1 — the bitmask design eliminates the need for both.
  • Nested rosters (Roster inside Roster) are not supported in v1.
  • iterateOver self-reference (referencing an item that lives inside the same Roster) is rejected at load time.

Bronze export shape

Each Roster produces a fixed number of columns: len(labels) × len(inner items). Column naming: {block_id}_{bit_key}_{item_id}. Cells for label-keys not active in a survey are NULL. The schema is fully deterministic from the QML declaration alone.

Example (per_meal with 4 labels × 2 inner items = 8 columns):

per_meal_1_q_satisfaction, per_meal_2_q_satisfaction, per_meal_4_q_satisfaction, per_meal_8_q_satisfaction,
per_meal_1_q_notes,        per_meal_2_q_notes,        per_meal_4_q_notes,        per_meal_8_q_notes

Plus the outer q_meals_eaten column.

Long-format Gold export is available as an opt-in transform that reshapes the wide Bronze into one parent dataset (without roster columns) plus one child dataset per Roster with columns survey_id, iteration_key, <inner_item>... — natural for analyst workflows that group by iteration.

When to use QuestionGroup vs Roster

Use QuestionGroup for multiple questions on the same page (single attribute repeated, fixed count, no per-iteration title chrome).

Use Roster for per-thing questions across one page each — runtime-counted (numeric or multiselect), one inner item per page, with iteration-major depth-first traversal.

Mathematical Semantics

Each item has associated outcome variable(s):

  • Question: \(S_i \in [\text{min}, \text{max}]\)
  • QuestionGroup: \(\mathbf{S}_i \in \mathbb{Z}^k\)
  • MatrixQuestion: \(\mathbf{S}_i \in \mathbb{Z}^{m \times n}\)

Special encodings:

  • Checkbox: Multiple selection encoded as bit mask (OR of powers of 2)
  • Range: Interval \((a, b)\) encoded via Szudzik pairing:
\[ \text{pair}(a, b) = \begin{cases} a^2 + a + b & \text{if } a \geq b \\ a + b^2 & \text{if } a < b \end{cases} \]

This bijection \(\mathbb{Z} \times \mathbb{Z} \to \mathbb{Z}\) represents two integers as one while remaining decodable.

Next Steps

  • Creating Surveys


    Learn QML syntax and best practices

    Guide

  • Manage Campaigns


    Manage campaigns with demographic targeting and monitoring

    Guide

  • Execute Surveys


    Execute surveys with dynamic flow control and lazy evaluation

    Guide

  • Analyze Results


    Analyze survey results with statistical correction and export

    Guide