A tutorial and conceptual guide for building surveys with the SRP Survey DSL.

© 2026 SRP Solutions SRP LLC. All rights reserved.

The SRP Survey DSL and its parser are proprietary technology of SRP Solutions SRP LLC. This guide may be freely shared for non-commercial and educational purposes with attribution. For licensing inquiries or more information, contact SRP Surveys.

01 Introduction

1.1 What is the SRP Survey DSL?

SRP Survey DSL is a (S)imple, (R)eadable, (P)arsable language for defining surveys. You write survey structure in a .srp file using a clean, readable syntax, and the tool generates interactive HTML surveys from that description.

The DSL handles:

  • Survey structure (pages, groups)
  • Question types (34 types, from simple radio buttons to heatmap image interactions)
  • Conditional logic (show questions/pages based on previous answers)
  • Segmentation (tag respondents into groups based on responses)
  • Survey flow control (skip logic, early termination, completion)

1.2 Key Concepts

ConceptDescription
PageContainer for questions; respondents see one page at a time
QuestionAn individual question within a page
Option / Item / Row / ColumnThe answer choices within a question
SegmentA tag applied to a respondent based on their answer
ActionAn instruction that controls survey flow (skip, terminate, complete)
GroupA named collection of pages (for navigation) or segments (for AND logic)

1.3 How a Survey Runs

  1. Respondent opens the survey HTML in a browser
  2. Pages are shown one at a time in definition order (unless skip logic applies)
  3. Segments are activated in real-time as the respondent makes selections
  4. Conditional pages and questions appear/disappear based on active segments
  5. Navigation actions (skip_to, complete, terminate) route the respondent
  6. Responses are auto-saved to IndexedDB; resumable across browser sessions
  7. Survey ends when a complete or terminate page is reached

02 Building Your First Survey

2.1 Minimal Survey

Every survey needs at least one page with a title, and best practice is to end with an explicit completion page.

ruby
# my_survey.srp

page "welcome" do
  title "Welcome to Our Survey"
  "Thank you for participating. This survey takes about 5 minutes."
end

page "questions" do
  title "Your Opinion"

  SingleSelect "satisfaction" do
    text "How satisfied are you with our service?"
    required

    option "Very satisfied"
    option "Satisfied"
    option "Neutral"
    option "Dissatisfied"
  end
end

page "thank_you" do
  title "Thank You"
  action :complete

  Notification "completion_message" do
    text "Your responses have been recorded. Thank you!"
  end
end

2.2 Adding Questions

Questions go inside page blocks. Every question requires a unique name as its first argument:

ruby
page "demographics" do
  title "About You"

  SingleSelect "age_group" do
    text "What is your age group?"
    required

    option "18–24"
    option "25–34"
    option "35–44"
    option "45–54"
    option "55+"
  end

  MultiSelect "interests" do
    text "What topics interest you? (Select all that apply)"
    min_selections 1

    option "Technology"
    option "Business"
    option "Design"
    option "Science"
    option "Arts"
  end

  OpenEnded "comments" do
    text "Any other comments?"
  end
end

2.3 Running and Validating

bash
# Check for errors
ruby survey_dsl.rb --file my_survey.srp --validate

# Generate HTML and open in browser
ruby survey_dsl.rb --file my_survey.srp --outfile my_survey.html
open my_survey.html  # macOS; use xdg-open on Linux

# Debug mode with full stack traces
DEBUG=1 ruby survey_dsl.rb --file my_survey.srp

03 Pages and Structure

Page Definition Rules

  • Unique names — Each page name must be unique across the survey
  • Define once — A page appears exactly once in the .srp file
  • Order matters — Default flow follows definition order
  • Completion pages at the end — Pages with action :complete or action :terminate should appear last
ruby
# ✅ Pages defined in logical flow order
page "intro" do ... end
page "screener" do ... end
page "main_questions" do ... end
page "thank_you" do
  action :complete
  ...
end
page "disqualified" do
  action :terminate
  ...
end

Page Attributes

ruby
page "my_page" do
  title "Page Title"               # Required — shown as heading
  text "Optional description"      # Creates a Notification question
  randomized                       # Randomize question order on this page
  show_only_if "segment_name"      # Conditional display
  action :complete                 # :complete or :terminate (optional)

  # PageTimer — control how long respondents spend on this page
  min_seconds 15                   # Block Next until 15s elapsed
  max_seconds 120                  # Auto-advance after 120s
  show_timer true                  # Display countdown/stopwatch
end

PageTimer attributes are independent — use any combination:

  • min_seconds prevents rushed responses (e.g., stimulus exposure tasks)
  • max_seconds auto-advances after a fixed window (e.g., timed exercises)
  • show_timer makes the timer visible; without it, the countdown runs silently

Groups

Groups bundle pages together for navigation and optional randomization.

ruby
# Sequential group — skip_to "onboarding" always goes to "welcome" first
group "onboarding", "welcome", "instructions", "practice"

# Randomized group — useful for A/B testing or reducing order bias
group "product_survey", "version_a", "version_b" do
  randomize
end

# Navigate to a group
option "Start tutorial" do
  action :skip_to, "onboarding"
end

04 Question Types

All questions share a common pattern:

ruby
QuestionType "unique_name" do
  text "Question prompt"
  required          # optional
  # type-specific attributes
end

For the complete syntax of each type, see DSL_REFERENCE.md §6.

4.1 Text Input

TypeWhen to Use
OpenEndedShort free-text responses; supports numeric mode and if_value actions
NumberNumeric input with unit display, decimal control, and explicit min/max
DiscussionLong free-text responses (multi-line textarea)
FillInTheBlankSentence completion tasks

Use OpenEnded with numeric for simple age/income questions. Use Number when you need explicit unit labels or decimal control.

ruby
OpenEnded "age" do
  text "What is your age?"
  numeric :min_value, 18, :max_value, 120
  required
  action :terminate, :if_value, "< 18"
end

Number "weight" do
  text "What is your current weight?"
  min_value 0.1
  max_value 500.0
  unit_label "lbs"
  placeholder "Enter weight"
end

FillInTheBlank "completion" do
  text "I would describe our service as ___ and ___."
  required
end

4.2 Selection

TypeWhen to Use
SingleSelectChoose one from several; default radio buttons
MultiSelectChoose multiple from several
DropdownLong lists of options (countries, states, etc.)
ButtonCheckboxMulti-select rendered as toggle buttons
ButtonRatingLabeled scale rendered as buttons
ruby
SingleSelect "preference" do
  text "Which option do you prefer?"
  randomized
  add_other_option

  option "Option A"
  option "Option B"
  option "Option C"
end

Dropdown "country" do
  text "Select your country."
  option "United States"
  option "Canada"
  option "United Kingdom"
  add_other_option
  required
end

When to use Dropdown vs SingleSelect

Use Dropdown when you have more than ~8 options or need a compact layout. Use SingleSelect (radio style) when you have fewer options and want them all visible at once.

SingleSelect Styles

SingleSelect supports three rendering styles via the style attribute: :radio (default — vertical radio buttons), :dropdown (compact select box), and :slider (continuous scale rendered as an inline slider). The slider style requires min_label and max_label to label the poles.

ruby
# Default radio style (no style declaration needed)
SingleSelect "channel" do
  text "How did you hear about us?"
  option "Social Media"
  option "Friend or Colleague"
  option "Search Engine"
  option "Advertisement"
  required
end

# Dropdown style — compact, good for long option lists
SingleSelect "country_single" do
  text "Which country are you from?"
  style :dropdown
  option "United States"
  option "Canada"
  option "United Kingdom"
  required
end

# Slider style — continuous scale rendered as inline slider
SingleSelect "agreement" do
  text "How much do you agree with this statement?"
  style :slider
  min_label "Strongly Disagree"
  max_label "Strongly Agree"
  required
end

ButtonCheckbox and ButtonRating

Both types use button — not option — to define their choices. ButtonCheckbox allows multiple selections; ButtonRating is single-select and best used for labeled scales.

ruby
ButtonCheckbox "important_features" do
  text "Which features matter most? (Select up to 3)"
  min_selections 1
  max_selections 3
  randomize_buttons

  button "Price"
  button "Quality"
  button "Speed"
  button "Support"
  required
end

ButtonRating "satisfaction" do
  text "How satisfied are you with our service?"

  button "Very Dissatisfied"
  button "Dissatisfied"
  button "Neutral"
  button "Satisfied"
  button "Very Satisfied"
  add_why_field
  required
end

4.3 Ranking and Sorting

TypeWhen to Use
RankingOrder items by preference
CardSortGroup items into categories (information architecture research)
CardRatingRate items one at a time
MaxDiffBest-worst scaling; most/least important
ruby
Ranking "priorities" do
  text "Rank these features from most to least important."
  randomize_items

  item "Performance"
  item "Ease of Use"
  item "Price"
  item "Customer Support"
  required
end

MaxDiff "feature_prefs" do
  text "For each set, select the MOST and LEAST important feature."
  items_per_set 4
  randomize_sets

  item "Price"
  item "Quality"
  item "Speed"
  item "Support"
  item "Innovation"
  item "Brand"
  required
end

CardRating

CardRating presents items one at a time on individual cards. Respondents rate each card on a numeric scale (1 to number_of_ranks). Use randomize_items to vary card order across respondents.

ruby
CardRating "concept_ratings" do
  text "Rate each concept on the scale below."
  number_of_ranks 5
  randomize_items

  item "Convenience"
  item "Value for Money"
  item "Innovation"
  required
end

CardSort

CardSort requires both item entries (the cards respondents drag) and category entries (the buckets to sort into). Use allow_unsorted to let respondents leave cards unclassified.

ruby
CardSort "navigation_sort" do
  text "Sort each page into the category where you'd expect to find it."
  randomize_items
  allow_unsorted

  item "Our Team"
  item "Pricing"
  item "Blog"
  item "API Documentation"

  category "Company Info"
  category "Products"
  category "Resources"
  required
end

4.4 Matrix Questions

Matrix questions show multiple items (rows) rated on shared criteria (columns).

TypeWhen to Use
SingleSelectMatrixRate multiple items on the same scale
MultiSelectMatrixSelect multiple attributes for each item
OpenEndedMatrixCollect text for each row-column combination
RatingMatrixMultiple items on a shared numeric rating scale
SingleSelectBipolarRate items on opposing-pole scales
SingleSelectBipolarMatrixMultiple items, each on its own bipolar scale
ruby
SingleSelectMatrix "service_ratings" do
  text "Rate each aspect of our service."
  required
  randomize_rows

  row "Customer Support"
  row "Product Quality"
  row "Pricing"
  row "Onboarding"

  column "Excellent"
  column "Good"
  column "Fair"
  column "Poor"
end

SingleSelectBipolarMatrix

SingleSelectBipolarMatrix is a matrix where each column has its own pair of opposing poles instead of shared labels. Define columns with a block containing left_label and right_label. Use scale to control the number of points on each bipolar scale.

ruby
SingleSelectBipolarMatrix "brand_perceptions" do
  text "Rate our brand on each dimension."
  scale 7
  randomize_rows
  required

  row "Design"
  row "Reliability"
  row "Value"

  column "Quality" do
    left_label "Low Quality"
    right_label "High Quality"
  end
  column "Personality" do
    left_label "Boring"
    right_label "Exciting"
  end
end

4.5 Rating Questions

TypeWhen to Use
RatingStar or numeric rating for one item
SliderContinuous or stepped scale; supports multiple rows
NPSNet Promoter Score (fixed 0–10 scale)
ThisOrThatForced binary choice (A/B)
ruby
Rating "overall_satisfaction" do
  text "How would you rate your overall experience?"
  number_of_ranks 5   # default; adjust for a different scale size
  add_why_field
  required
end

# Single-track slider
Slider "budget_comfort" do
  text "How comfortable are you with our pricing?"
  min_value 0
  max_value 10
  step 1
  default_value 5
  min_label "Not at all comfortable"
  max_label "Very comfortable"
  add_why_field
  required
end

# Multi-row slider — one track per row
Slider "attribute_ratings" do
  text "Rate each attribute on a 1–7 scale."
  min_value 1
  max_value 7
  default_value 4

  row "Ease of Use"
  row "Performance"
  row "Value for Money"
  required
end
ruby
NPS "recommend_score" do
  text "How likely are you to recommend us?"
  min_label "Not at all likely"
  max_label "Extremely likely"
  add_why_field
  required
end

ThisOrThat "style_preference" do
  text "Which design style do you prefer?"
  this "Minimal"
  that "Feature-rich"
  add_why_field
  required
end

SingleSelectBipolar

SingleSelectBipolar is the single-item variant of a bipolar scale — one question with two opposing poles. Use scale to set the number of points, left_label / right_label for the pole labels, and add_none_option to allow a "Not applicable" opt-out.

ruby
SingleSelectBipolar "brand_impression" do
  text "Rate your overall impression of our brand."
  left_label "Very Negative"
  right_label "Very Positive"
  scale 7
  add_none_option
  required
end

4.6 Image-Based Questions

TypeWhen to Use
HeatMapCapture where respondents click on an image
TimedHeatMapHeatMap with a countdown timer
StickyNoteRespondents place labeled notes on an image
ruby
HeatMap "ui_feedback" do
  text "Click on areas of the interface that feel confusing."
  image "https://example.com/interface-screenshot.png"
  min_clicks 1
  max_clicks 5

  category "Confusing" do
    color "#e74c3c"
  end
  category "Works well" do
    color "#2ecc71"
  end
  required
end

TimedHeatMap

TimedHeatMap works like HeatMap but shows the image for a limited time before hiding it. The time_limit attribute (in seconds) is required — the image disappears after the countdown, then the respondent records their clicks.

ruby
TimedHeatMap "first_impression" do
  text "Click on the first thing that catches your eye. You have 5 seconds."
  image "https://example.com/landing-page.png"
  time_limit 5       # seconds — required
  min_clicks 1
  max_clicks 3
  required
end

StickyNote

StickyNote lets respondents place labeled sticky notes on an image. Each category block defines a note type; the color attribute sets the note's background color.

ruby
StickyNote "feedback_notes" do
  text "Place notes on the areas of the page you'd like to comment on."
  image "https://example.com/interface.png"
  min_clicks 1
  max_clicks 5

  category "I like this" do
    color "#2ecc71"
  end
  category "Needs improvement" do
    color "#e74c3c"
  end
  required
end

4.7 Specialized Questions

TypeWhen to Use
ConstantSumBudget allocation / point distribution
DatePickerDate input with range constraints
AutosuggestTypeahead text input with pre-defined suggestions
TextHighlighterRespondent highlights phrases in a passage
USAddressUS address collection
InternationalAddressInternational address collection
AgeDate-of-birth (month/day/year)
PhoneNumberPhone number input
MediaUploadFile upload (image, video, document)
ruby
DatePicker "event_date" do
  text "When did the issue occur?"
  min_date "2020-01-01"   # ISO 8601 format: YYYY-MM-DD
  max_date "2026-12-31"
  required
end
ruby
# Static suggestion list
Autosuggest "brand" do
  text "Which brand comes to mind first?"
  suggestions ["Nike", "Adidas", "Puma", "Reebok", "New Balance"]
  allow_custom false   # restrict to listed suggestions only
  placeholder "Start typing a brand..."
  required
end

# Server-side suggestions (pass a URL path for live lookup)
Autosuggest "city" do
  text "Which city do you live in?"
  suggestions "/api/cities"
  placeholder "Type your city..."
  required
end
ruby
TextHighlighter "ad_reactions" do
  text "Highlight phrases you like or dislike in this ad copy."
  passage "Experience the future of coffee with our revolutionary BrewMaster Pro. Every morning deserves a perfect cup crafted just for you. Our patented SmartBrew technology learns your preferences."
  categories ["Like", "Dislike", "Confusing"]   # colors auto-assigned; up to 6 categories
  min_highlights 1
  max_highlights 5
  required
end
ruby
ConstantSum "budget_allocation" do
  text "Allocate 100 points across these priorities."
  sum_to 100   # default is 100
  randomize_rows
  required

  row "Quality"
  row "Speed"
  row "Price"
  row "Support"
end

Address and demographic types have minimal syntax — they render structured, validated input fields automatically:

ruby
USAddress "home_address" do
  text "Please enter your home address."
  required
end

InternationalAddress "billing_address" do
  text "Please enter your billing address."
  required
end

Age "date_of_birth" do
  text "Please enter your date of birth."
  required
end

PhoneNumber "contact_phone" do
  text "Please enter your phone number."
  required
end

MediaUpload "supporting_docs" do
  text "Upload any supporting documents (optional)."
  accept ".pdf,.docx,.png,image/*"   # MIME types or file extensions
  max_files 3
  multiple true                       # default; set false for single-file only
end

4.8 Choice Experiments (ConjointChoice)

ConjointChoice presents concept cards side-by-side; each card shows the same set of attributes with different values (levels). The respondent picks their preferred option.

ruby
ConjointChoice "car_pref" do
  text "Which car would you be most likely to purchase?"
  required

  attribute "Brand"
  attribute "Price"
  attribute "Fuel Type"
  attribute "Warranty"

  profile "Option A" do
    level "Brand", "Toyota"
    level "Price", "$28,000"
    level "Fuel Type", "Hybrid"
    level "Warranty", "3 years"
  end

  profile "Option B" do
    level "Brand", "Ford"
    level "Price", "$24,000"
    level "Fuel Type", "Gasoline"
    level "Warranty", "5 years"
  end

  add_none_option      # adds "None of these" card
  randomize_profiles   # shuffle card order to reduce position bias
end

Tips:

  • Define at least 2 profiles; 2–5 is typical per task
  • Use the same attributes across all profiles so the table layout is consistent
  • randomize_profiles reduces left/right position bias across respondents
  • Chain multiple ConjointChoice questions on separate pages to vary the trade-off presented

4.9 Display Elements

Notification displays text without requiring any input. It cannot be marked required.

ruby
Notification "instructions" do
  text "Please read each question carefully before answering."
end

At the page level, a bare string is a shorthand for Notification:

ruby
page "welcome" do
  title "Welcome"
  "Thank you for participating in this survey."
end

4.10 EmotionSelector

EmotionSelector presents an emoji-based emotional response question. By default it shows five preset emotions (😄 Delighted, 🙂 Happy, 😐 Neutral, 🙁 Unhappy, 😠 Angry) — no configuration needed. Adding any emotion declaration replaces the entire default set with your custom emotions.

ruby
# Default emotions — works with no additional attributes
EmotionSelector "feel_about_product" do
  text "How do you feel about this product?"
  required
end

# Custom emotions — replaces all five defaults
EmotionSelector "brand_reaction" do
  text "How does this brand make you feel?"
  emotion "excited",   label: "Excited",   emoji: "🤩"
  emotion "curious",   label: "Curious",   emoji: "🤔"
  emotion "satisfied", label: "Satisfied", emoji: "😌"
  emotion "bored",     label: "Bored",     emoji: "😑"
  emotion "annoyed",   label: "Annoyed",   emoji: "😤"
  add_why_field
  required
end

Custom vs. default: Defining even one emotion replaces the full default set. If you want to keep the standard five emotions, omit all emotion declarations entirely.

05 Rich Text Formatting

The DSL processes markdown automatically in all text values.

MarkdownResult
**bold**bold
*italic* or _italic_italic
__underline__underlined text
`code`inline code
~~strikethrough~~strikethrough

Works in: question text, option labels, row/column labels, page titles, notification text.

ruby
SingleSelect "app_rating" do
  text "How **satisfied** are you with our _new_ app?"

  option "**Very** satisfied"
  option "*Somewhat* satisfied"
  option "~~Dissatisfied~~ Not satisfied"
end

06 Collecting Specific Answer Types

Special Options

ruby
SingleSelect "experience" do
  text "Rate your experience."

  option "Excellent"
  option "Good"
  add_none_option         # Appends "None" at the bottom
  add_dont_know_option    # Appends "Don't Know"
  add_other_option        # Appends "Other (please specify)"
  add_why_field           # Appends a free-text "Why?" field
end

Anchoring Options During Randomization

Use anchored inside an option block to keep it at the bottom when randomized is active:

ruby
SingleSelect "programming_language" do
  text "What is your favorite programming language?"
  randomized

  option "Python"
  option "JavaScript"
  option "Ruby"
  option "Java"
  option "None of the above" do
    anchored
  end
end

Numeric Constraints

ruby
MultiSelect "team_tools" do
  text "Which tools does your team use? (Select up to 3)"
  max_selections 3

  option "Slack"
  option "Jira"
  option "Confluence"
  option "GitHub"
  option "Figma"
end

MultiSelectMatrix "feature_usage" do
  text "For each product, select all features you use."
  min_selections_per_row 1
  max_selections_per_row 3

  row "Product A"
  row "Product B"

  column "Analytics"
  column "Reporting"
  column "Integration"
  column "Automation"
end

07 Conditional Logic and Segmentation

7.1 The Concept: Segments

A segment is a tag that gets attached to a respondent when they make a specific selection. Once a segment is active, it stays active for the rest of the survey.

Segments are the foundation of conditional logic: pages and questions can be shown or hidden based on which segments are active.

7.2 Creating Segments

Segments are created inside option, row, or column blocks:

ruby
# Option segment — activates when this option is selected
SingleSelect "role" do
  text "What is your role?"

  option "Developer" do
    segment "developer"
  end
  option "Manager" do
    segment "manager"
    segment "decision_maker"    # A respondent can have multiple segments
  end
end

# Matrix row segment — activates based on column selection
SingleSelectMatrix "ratings" do
  text "Rate our services."

  row "Customer Support" do
    segment "support_issues", :if_column, "Poor"
    segment "support_satisfied", :if_column, "Excellent", "Good"  # OR logic
  end

  column "Excellent"
  column "Good"
  column "Fair"
  column "Poor"
end

# Matrix column segment — activates based on row selection
SingleSelectMatrix "ratings" do
  text "Rate our services."

  row "Customer Support"
  row "Product Quality"

  column "Poor" do
    segment "cs_issues", :if_row, "Customer Support"
    segment "quality_issues", :if_row, "Product Quality"
  end
end

7.3 Using Segments with show_only_if

Once segments exist, use show_only_if to make pages and questions conditional:

ruby
# Single segment condition
page "developer_questions" do
  title "Developer Feedback"
  show_only_if "developer"

  SingleSelect "ide_preference" do
    text "Which IDE do you prefer?"
    option "VS Code"
    option "JetBrains"
    option "Vim/Neovim"
  end
end

# Multiple segments — shown if respondent is in ANY (OR logic)
page "feedback_collection" do
  title "Help Us Improve"
  show_only_if "support_issues", "quality_issues"

  OpenEnded "improvement_suggestions" do
    text "What specific improvements would you like to see?"
    required
  end
end

Page-level vs. question-level show_only_if

The DSL syntax is identical at both levels, but the scope and consequences differ:

  • Page-level — the entire page (title and all questions) is hidden as a unit. The respondent never visits the page; required questions on it are automatically bypassed.
  • Question-level — only that one question is hidden. The page is still visited and its other questions render normally. The hidden question is excluded from required validation.

You can combine both: a page can have its own show_only_if and also contain questions with their own show_only_if. The page condition is the outer gate — a question's condition is only relevant if its page is already visible.

7.4 AND Logic: Group-Segments

Use group to require ALL listed segments to be active simultaneously (AND logic):

ruby
# Create segments
MultiSelect "pets" do
  text "Which pets do you own?"

  option "Dog" do
    segment "dog_owner"
  end
  option "Cat" do
    segment "cat_owner"
  end
end

# Group requires BOTH dog_owner AND cat_owner
group "multi_pet_owner", "dog_owner", "cat_owner"

# Only shown to respondents who own BOTH a dog AND a cat
page "multi_pet_care" do
  title "Caring for Multiple Pets"
  show_only_if "multi_pet_owner"
  ...
end

How the parser tells page groups from segment groups: After the full survey file is parsed, the validator checks every group's members against all defined segment names. If every member is a known segment name, the group becomes a segment group (AND logic). If no member is a segment name, the group is treated as a page group and its members must all be valid page names. If a group mixes segment names and page names, a validation error is raised immediately.

7.5 Mixed AND/OR Logic

Combine groups and segments in show_only_if for complex targeting:

ruby
group "premium_candidate", "power_user", "high_satisfaction"

# Show to: (power_user AND high_satisfaction) OR bird_owner
page "upgrade_offer" do
  show_only_if "premium_candidate", "bird_owner"
  ...
end

7.6 Practical Segmentation Recipe

Here is a complete, realistic example of segmentation for a customer satisfaction survey:

ruby
page "satisfaction" do
  title "Your Experience"

  SingleSelectMatrix "service_ratings" do
    text "Rate each aspect of your experience."
    required

    row "Customer Support" do
      segment "support_poor", :if_column, "Poor", "Fair"
      segment "support_great", :if_column, "Excellent"
    end
    row "Product Quality" do
      segment "quality_poor", :if_column, "Poor"
    end

    column "Excellent"
    column "Good"
    column "Fair"
    column "Poor"
  end
end

# Only shown to respondents with support concerns
page "support_improvement" do
  title "Help Us Improve Support"
  show_only_if "support_poor"

  MultiSelect "support_improvements" do
    text "What would improve your support experience?"
    min_selections 1

    option "Faster response times"
    option "More knowledgeable staff"
    option "Better documentation"
    option "24/7 availability"
  end
end

# Testimonial request for satisfied respondents
page "testimonial_request" do
  title "Share Your Story"
  show_only_if "support_great"

  SingleSelect "testimonial_consent" do
    text "Would you be willing to share your positive experience?"
    option "Yes"
    option "Maybe later"
    option "No"
  end
end

08 Survey Navigation and Actions

8.1 Default Flow

Without any actions, respondents move through pages in definition order. Pages with show_only_if conditions that are not met are automatically skipped.

8.2 Page Actions (:complete, :terminate)

Pages support action :complete (survey finished successfully) or action :terminate (disqualified or early exit). Pages with these actions should be defined at the end of the .srp file.

ruby
page "thank_you" do
  title "Thank You"
  action :complete

  Notification "completion_message" do
    text "Your responses have been recorded."
  end
end

page "disqualified" do
  title "Thank You for Your Time"
  action :terminate

  Notification "disqualify_message" do
    text "Unfortunately, you do not meet the criteria for this survey."
  end
end

8.3 Option Actions

Options can trigger navigation when selected:

ruby
SingleSelect "age_check" do
  text "Are you 18 or older?"
  required

  option "Yes" do
    action :skip_to, "main_survey"
  end
  option "No" do
    action :skip_to, "disqualified"
  end
end

8.4 Matrix Conditional Actions (:if_column, :if_row)

Matrix rows and columns can trigger actions based on their specific selections:

ruby
SingleSelectMatrix "feature_ratings" do
  text "Rate these features."

  row "Critical Feature" do
    # Skip to detailed feedback if this row is rated Poor
    action :skip_to, "detailed_feedback", :if_column, "Poor"
  end

  column "Poor" do
    # Skip to improvement page if Customer Support is rated Poor
    action :skip_to, "improvement_page", :if_row, "Customer Support"
  end

  column "Excellent"
  column "Good"
  column "Fair"
  column "Poor"
end

Multi-Condition AND Logic (Matrix)

Combine :if_column and :if_row in a single action to require both conditions:

ruby
row "Technical Support" do
  # Skip only if THIS row is rated Poor AND Customer Support is ALSO Poor
  action :skip_to, "escalation", :if_column, "Poor", :if_row, "Customer Support", "Poor"
end

Count-Based Actions

Trigger an action based on how many rows have a particular column value:

ruby
row "Service Quality" do
  # Skip if more than 2 rows are rated "Poor" overall
  action :skip_to, "improvement_survey", :if_column_count, "Poor", :greater_than, 2
end

Count operators: :greater_than, :greater_than_or_equal, :equals, :less_than, :less_than_or_equal

Segment-Based Conditions (:if_segment, :if_group)

Matrix row blocks also support two segment-awareness conditions. :if_segment fires only when a specific named segment is active for the current respondent. :if_group fires only when all segments belonging to a named group are simultaneously active, enabling AND-logic across multiple matrix rows.

ruby
row "Critical Issue" do
  # Skip to escalation only if the respondent is also in the "enterprise" segment
  action :skip_to, "escalation_page", :if_segment, "enterprise_customer"

  # Skip to priority page only if ALL segments in the group are active
  action :skip_to, "priority_review", :if_group, "high_value_at_risk"
end

8.5 Value-Based Actions (:if_value on OpenEnded)

OpenEnded questions can trigger actions based on what the respondent types:

ruby
OpenEnded "age" do
  text "What is your age?"
  numeric :min_value, 0, :max_value, 120
  required

  action :terminate, :if_value, "< 18"
  action :terminate, :if_value, "> 69"
  action :skip_to, "senior_questions", :if_value, ">= 65"
end

# Text matching with lists
OpenEnded "programming_language" do
  text "What is your primary programming language?"

  action :skip_to, "web_dev", :if_value, "== JavaScript, TypeScript, HTML"
  action :skip_to, "systems", :if_value, "== C, C++, Rust, Go"
end

Comparison operators: <, <=, >, >=, ==, !=

For == and !=, comma-separated lists use OR logic: "== val1, val2" means "equals val1 OR val2".

8.6 Multiple Conditional Actions on OpenEnded

OpenEnded questions support multiple action calls. Actions are evaluated in declaration order; the first matching condition fires and stops evaluation.

ruby
# ✓ All three actions are evaluated in order
OpenEnded "income" do
  text "What is your annual household income?"
  numeric :min_value, 0

  action :terminate, :if_value, "< 10000"
  action :skip_to, "high_income", :if_value, "> 150000"
  action :skip_to, "standard", :if_value, ">= 10000"
end

Note: option, row, and column blocks each support exactly one action — a second action call overwrites the first.

8.7 Groups for Navigation

Navigate to a group to jump to a collection of pages:

ruby
group "product_section", "product_a", "product_b", "product_c" do
  randomize  # Random page from the group
end

option "Tell us about your product" do
  action :skip_to, "product_section"
end

09 Dynamic Content

9.1 Text Piping ({{question_name}})

Use {{question_name}} in any text field to insert the respondent's previous answer at runtime:

ruby
SingleSelect "favorite_fruit" do
  text "What is your favorite fruit?"
  option "Apple"
  option "Banana"
  option "Cherry"
end

SingleSelect "fruit_rating" do
  text "You selected {{favorite_fruit}}. How would you rate it?"
  option "Excellent"
  option "Good"
  option "Poor"
end

Notification "summary" do
  text "Thank you for your feedback about {{favorite_fruit}}!"
end

Rules:

  • The referenced question must appear before the piping question in survey flow
  • Before the respondent answers the source question, a placeholder ___ is shown
  • Piped text updates reactively if the respondent goes back and changes their answer
  • Works with markdown: "You chose **{{favorite_fruit}}**"

Display formats by source type:

Source TypeDisplay
SingleSelect, DropdownSelected option text
MultiSelect, ButtonCheckboxComma-separated with "and" before last item
OpenEnded, DiscussionThe typed text

9.2 Dynamic Options (options_from, rows_from, columns_from)

Populate a question's options or rows from a previous question's selections:

ruby
MultiSelect "owned_products" do
  text "Which products do you own?"
  option "Laptop"
  option "Phone"
  option "Tablet"
  option "Smartwatch"
end

# Only the selected products become rows
SingleSelectMatrix "product_satisfaction" do
  text "Rate your satisfaction with each product you own."
  rows_from "owned_products"
  required

  column "Very Satisfied"
  column "Satisfied"
  column "Neutral"
  column "Dissatisfied"
end

Rules:

  • Source question must appear before the dependent question in survey flow
  • If the source has no selections, an informational message is shown
  • options_from, rows_from, and columns_from each accept one source question name

10 Hidden Questions

A question marked hidden exists in the survey's logic but is not rendered to the respondent. Useful for tracking variables, session metadata, or validation placeholders.

ruby
page "survey_start" do
  title "Your Feedback"

  # Visible question
  SingleSelect "satisfaction" do
    text "How satisfied are you overall?"
    required

    option "Satisfied" do
      segment "satisfied"
    end
    option "Unsatisfied" do
      segment "unsatisfied"
    end
  end

  # Hidden — not shown to respondent, used for internal tracking
  OpenEnded "session_timestamp" do
    text "Session start timestamp"
    hidden
  end
end

Hidden questions:

  • Do not appear in rendered HTML
  • Still participate in segment evaluation and conditional logic
  • Are excluded from required-field validation
  • Do not count against recommended question limits per page

11 Best Practices

Survey Design

  • Keep question text under 500 characters
  • Limit radio button options to 10 or fewer for readability
  • Use descriptive page titles that reflect what the page is about
  • Set appropriate min_selections / max_selections for multi-select questions
  • Always end surveys with an explicit completion page containing a Notification question

Question Naming

  • Use snake_case: "satisfaction_level" not "SatisfactionLevel"
  • Use descriptive names: "demo_age" not "q1"
  • Prefix by section for easier maintenance: "demo_*", "product_*", "satisfaction_*"
  • Names must be unique across the entire survey

Page Organization

  • Define pages in the order respondents will encounter them
  • Pages with action :complete or action :terminate go at the end
  • Use show_only_if to skip pages that don't apply — don't use skip_to for everything
  • Group related pages together for easier maintenance

Segmentation

  • Only define segments that are referenced elsewhere in show_only_if or group conditions
  • Keep segment names descriptive: "power_user" not "s1"
  • Use group-segments (AND logic) sparingly; OR logic is often sufficient
  • Test segment activation by validating your .srp file

12 Common Patterns and Recipes

Screener Survey (Early Termination)

ruby
page "screener" do
  title "Quick Qualification Check"

  SingleSelect "age_check" do
    text "Are you 18 or older?"
    required

    option "Yes"
    option "No" do
      action :skip_to, "disqualified"
    end
  end

  SingleSelect "customer_check" do
    text "Are you a current customer?"
    required

    option "Yes"
    option "No" do
      action :skip_to, "disqualified"
    end
  end
end

page "main_survey" do
  title "Your Feedback"
  # ... main questions
end

page "complete" do
  title "Thank You"
  action :complete

  Notification "completion_message" do
    text "Thank you for completing this survey!"
  end
end

page "disqualified" do
  title "Thank You for Your Interest"
  action :terminate

  Notification "disqualify_message" do
    text "Thank you, but you do not qualify for this survey."
  end
end

Matrix with Targeted Follow-Up

ruby
page "ratings" do
  title "Product Ratings"

  SingleSelectMatrix "product_ratings" do
    text "Rate each product."
    required

    row "Product A" do
      segment "product_a_poor", :if_column, "Poor", "Fair"
    end
    row "Product B" do
      segment "product_b_poor", :if_column, "Poor", "Fair"
    end

    column "Excellent"
    column "Good"
    column "Fair"
    column "Poor"
  end
end

page "product_a_followup" do
  title "Product A Feedback"
  show_only_if "product_a_poor"

  OpenEnded "product_a_issues" do
    text "What issues did you have with Product A?"
    required
  end
end

page "product_b_followup" do
  title "Product B Feedback"
  show_only_if "product_b_poor"

  OpenEnded "product_b_issues" do
    text "What issues did you have with Product B?"
    required
  end
end

Dynamic Follow-Up (options_from + show_only_if)

ruby
page "selection" do
  title "Your Preferences"

  MultiSelect "selected_features" do
    text "Which features have you used?"
    required

    option "Analytics" do
      segment "analytics_user"
    end
    option "Reporting" do
      segment "reporting_user"
    end
    option "Integration" do
      segment "integration_user"
    end
  end
end

page "feature_ratings" do
  title "Rate Your Experiences"
  show_only_if "analytics_user", "reporting_user", "integration_user"

  SingleSelectMatrix "feature_satisfaction" do
    text "Rate each feature you have used."
    rows_from "selected_features"  # Only rows for features they selected
    required

    column "Very Satisfied"
    column "Satisfied"
    column "Neutral"
    column "Dissatisfied"
  end
end