Stagebook Syntax Reference

July 9, 2026 · View on GitHub

A concise, precise reference for the Stagebook experiment description language. For detailed explanations, see the individual guides: Treatment Files, Elements, Prompts, Conditions, Discussions, Templates.

1. Top-Level Structure

templates: # optional: array of template definitions
consent: # optional: array of consent arms — the host shows one, selected by name
introSequences: # required: array of intro sequence objects
treatments: # required: array of treatment objects

2. Primitives

  • Names: 1-64 chars; [a-zA-Z0-9 _-] plus ${field} placeholders.
  • Durations: positive integer (seconds).
  • Positions: zero-based nonnegative integers.
  • Visibility: showToPositions / hideFromPositions — nonempty int arrays.
  • Time gates: displayTime (nonnegative int), hideTime (positive int) — seconds into stage.

3. Templates

templates:
  - name: <name>
    contentType: <element|stage|treatment|...>  # required
    content: <any structure>

# Usage:
- template: <name>
  fields: { key: value }       # ${key} substitution
  broadcast: { d0: [...] }     # cartesian expansion

Content types: introSequence, introSequences, elements, element, stage, stages, treatment, treatments, reference, condition, conditions, player, groupComposition, introExitStep, introSteps, exitSteps, consentArm, consent, discussion, broadcastAxisValues.

4. References

A reference identifies a value somewhere in the study state. Two forms (#240): the dotted-string sugar and the structured object form. Both are accepted at every reference site (conditions, display.reference, trackedLink/qualtrics urlParams[].reference); both parse to the same internal shape.

Every reference begins with a position selector (#298). The first segment is required and is one of:

  • self — the current participant's value
  • shared — group-shared state
  • all — addresses every participant's value as a list (for aggregating across the group)
  • A non-negative integer (0, 1, 2, …) — a specific slot index

Un-prefixed references like prompt.topicVote are rejected at parse time. The error message includes a migration hint suggesting self.prompt.topicVote for the common case.

String shorthand (the common form):

PatternExample
<position>.prompt.<name>self.prompt.topicVote
<position>.survey.<name>.<path...>self.survey.TIPI.responses.q1
<position>.submitButton.<name>.<path>self.submitButton.confirm.time
<position>.qualtrics.<name>.<path>self.qualtrics.exit.sessionId
<position>.trackedLink.<name>.<path>self.trackedLink.signup.events
<position>.timeline.<name>(.<path>)self.timeline.story.0.start
<position>.discussion.<name>(.<path>)shared.discussion.lobby.messageCount
<position>.entryUrl.params.<key>self.entryUrl.params.PROLIFIC_PID
<position>.attributes.<field>self.attributes.stableParticipantId

Structured form (#240 — preferred in new code):

reference:
  source:
    prompt | survey | submitButton | qualtrics | timeline | trackedLink | discussion |
    entryUrl | attributes
  name: <element name> # required for named sources, forbidden for external sources
  path: [<segments>...] # optional for named sources, required for external sources

For named sources, prompt references default to path: [value] when omitted (the participant's saved answer). Other named sources read the whole stored record by default. The structured form lets you override the implicit default — e.g. path: [debugMessages] to address other fields on a prompt's saved record.

For external sources, path is required. Additionally, entryUrl references must currently start the path with params (e.g. path: [params, condition] — equivalent to the dotted entryUrl.params.condition). The entryUrl.* namespace is reserved so future additions like entryUrl.path / entryUrl.host / entryUrl.href can land non-breakingly.

attributes.* is the host-supplied bag of participant metadata (#473), replacing the former connectionInfo / browserInfo / participantInfo sources — references to those are now rejected. Common fields: stableParticipantId (the anonymized id used to link exported data — always available), sampleId (the per-assignment data-row id — only from the game phase onward), name, country, timezone, language, screenWidth. The recruitment-platform id is intentionally not exposed here.

5. Conditions

conditions:
  - reference: <reference> # string sugar OR { source, name?, path? }
    comparator: <comparator>
    value: <expected value> # omit for exists/doesNotExist
    position: <position> # optional

Comparators: exists, doesNotExist, equals, doesNotEqual, isAbove, isBelow, isAtLeast, isAtMost, hasLengthAtLeast, hasLengthAtMost, includes, doesNotInclude, matches, doesNotMatch, isOneOf, isNotOneOf.

Position values: player (default), shared, or integer slot index. (After #238, position is a pure read selector. Cross-player aggregation lives in the all: / any: / none: boolean-tree operators — see Conditions.)

6. Elements

All elements accept: name?, notes?, displayTime?, hideTime?, showToPositions?, hideFromPositions?, conditions?, tags?. (file? is per-type — only prompt, audio, image, mediaPlayer accept it; see #249.)

TypeKey Fields
promptfile (required), shared?
displayreference (required), position? (default: player)
submitButtonbuttonText? (default: "Next")
timerstartTime?, endTime?, warnTimeRemaining?
separatorstyle? (thin, regular, thick)
audiofile (required)
imagefile (required), width?
mediaPlayerfile (required), name, controls?, syncToStageTime?, submitOnComplete?, startAt?, stopAt?, stepDuration?, playVideo?, playAudio?, captionsFile?, allowScrubOutsideBounds?
timelinesource (required, name of a sibling mediaPlayer), name (required), selectionType (required, range or point), selectionScope? (default all), multiSelect? (default false), showWaveform? (default true), trackLabels?
surveysurveyName (required) — deprecated; pending removal once a module-reuse pattern lands. Prefer prompt-based patterns.
qualtricsurl (required), urlParams?
trackedLinkname (required), url (required), displayText (required), helperText?, urlParams?

Media hosting requirements

The <video> element rendered by mediaPlayer always sets crossOrigin="anonymous". This is required for the Web Audio API to read the audio stream when a timeline element with showWaveform: true is attached — without it, the analyser is silently CORS-tainted and the waveform tracks render as flat lines.

This means all media URLs must be served with proper CORS headers (Access-Control-Allow-Origin: * or matching the experiment origin), regardless of whether you use the timeline. Same-origin media (e.g., served from the same host as the experiment) is unaffected.

If you see flat waveforms in the timeline despite audio playing, check the browser console — Stagebook logs a warning after 5 seconds of playback if the AnalyserNode is producing only silence:

[MediaPlayer] Waveform capture is producing all-zero data after 5s of playback...

7. Stages

gameStages:
  - name: <name>
    duration: <seconds>
    discussion: <discussion object> # optional
    elements: [...] # required, nonempty

Time bounds on elements (displayTime, hideTime, startTime, endTime) must not exceed stage duration.

8. Discussions

discussion:
  chatType: text | audio | video
  showNickname: true
  showTitle: false
  # text-only: reactionEmojisAvailable?, reactToSelf?, numReactionsPerMessage?
  # video-only: showSelfView?, showReportMissing?, showAudioMute?, showVideoMute?
  # video-only: rooms? or layout?
  showToPositions: [0, 1] # optional
  hideFromPositions: [2] # optional
  conditions: [...] # optional

9. Intro/Exit/Consent Steps

introSequences:
  - name: <name>
    introSteps:
      - name: <name>
        elements: [...] # no duration, no position-based visibility

treatments:
  - name: <name>
    exitSequence:
      - name: <name>
        elements: [...] # no shared prompts

Constraints: no shared prompts, no position/showToPositions/hideFromPositions on elements in intro steps. Exit steps disallow shared prompts.

consent: # top-level; sibling of introSequences/treatments
  - name: <name> # unique within consent: only — the host selects an arm by name
    locale: <locale> # optional — arms declare their OWN locale (pre-assignment)
    steps:
      - name: <name>
        elements: [...] # intro-step constraints apply

Consent steps take the intro-step constraints (advancement element required, no shared prompts, no position fields). Consent keys are audit-only: referencing one from intro/game/exit/groupComposition is an error; within-arm references (the gated-submit pattern) are legal; consent steps can't reference later-phase data. Collision-checked against every intro sequence and treatment; arm × arm key reuse is legal.

Debrief (#481)

Debrief content is authored as the trailing steps of exitSequence — there is no separate debrief: field. The host renders the exit sequence before the completion code, so the trailing exit steps are the debrief and the code stays gated behind them. They are ordinary exit steps (exit-step constraints apply) and may reference any earlier phase except consent (audit-only).

10. Treatments

treatments:
  - name: <name>
    playerCount: <integer>
    compatibleIntroSequences: [<names>] # required; [] = runs without an intro sequence
    groupComposition: # optional
      - position: 0
        title: "Role A"
        conditions: [...]
    gameStages: [...] # required, nonempty
    exitSequence: [...] # optional; trailing steps are the debrief (#481)

Position indices in showToPositions, hideFromPositions, groupComposition, and discussion rooms must be < playerCount.

compatibleIntroSequences (#499) names the intro sequences the treatment may follow; names resolve against the top-level introSequences: collection. Dangling names error; duplicates warn; every game/exit/groupComposition reference to intro-provided data must resolve in every listed sequence. ${field} placeholders allowed, whole-field or per-item (like groupComposition).

11. Prompt Files

Three sections separated by ---:

---
type: multipleChoice | dropdown | openResponse | noResponse | listSorter | slider
name: My Prompt # optional — human-readable identifier
---

## Markdown body text

- Response option 1
- Response option 2

name is optional. Can be any string — use it as a human-readable identifier. Prompt files must use the .prompt.md extension.

Slider requires min, max, interval in metadata. Slider initializes without a visible thumb (anti-anchoring).