6.2.1 Studies and Steps
| (require congame/components/study) | package: congame-core |
procedure
(make-study name steps [ #:requires requires #:provides provides #:transitions transitions #:view-handler view-handler #:failure-handler failure-handler]) → study? name : string? steps : (listof step?) requires : (listof symbol?) = null provides : (listof symbol?) = null transitions : (or/c #f (hash/c symbol? any/c)) = #f view-handler : (or/c #f (-> request? response?)) = #f failure-handler : (or/c #f (-> step? any/c step-id/c)) = #f
The #:transitions argument has to be a transition-graph that provides all the transitions between the steps. See transition-graph for details.
The #:requires and #:provides arguments are deprecated and included for compatibility. Use defvar* and defvar*/instance to share study variables between parent and child studies.
procedure
s : study? req : request? = (current-request) bindings : (hash/c symbol? any/c) = (hasheq)
procedure
procedure
This is useful for conditionally displaying admin-only content or enabling special controls for study owners.
procedure
Identity servers are used to decouple participant identity from their study responses, allowing researchers to pay participants without knowing their specific answers.
6.2.1.1 Study variables
A study variable is a variable whose value is recorded in the study server database.
A variable with participant scope will store/reference a separate value for each participant in each separate instance.
A variable with instance scope will store/reference a separate value for each study instance, but the value is shared by all participants in a given study instance.
syntax
(defvar id)
When set, the value of the study variable will be stored in the Congame server database under the current study, instance, and participant.
The single-argument form (defvar* id) must be used inside a with-namespace block, which automatically generates a unique global identifier. This is the recommended usage:
(with-namespace my-study.variables (defvar* score))
The two-argument form (defvar* id global-id) can be used outside of with-namespace, but you must manually ensure global-id is distinct from any identifiers that may be used in child studies.
syntax
(defvar/instance id)
syntax
(defvar*/instance id)
(defvar*/instance id global-id)
The single-argument form (defvar*/instance id) must be used inside a with-namespace block. The two-argument form can be used outside of with-namespace but requires manually ensuring the global identifier is unique.
value
You typically don’t need to use undefined directly. Instead, use undefined? to check if a variable has been set, or if-undefined to provide a fallback value.
procedure
(undefined? v) → boolean?
v : any/c
syntax
(if-undefined study-var alt)
syntax
(with-namespace namespace body ...+)
When using defvar* (which creates variables visible to child studies), there is a risk that a child study could accidentally use the same variable name and overwrite the parent’s data. The with-namespace form prevents this by prefixing each variable’s internal identifier with the namespace.
(with-namespace my-study.variables (defvar* score) (defvar*/instance group-results))
In the example above, score will internally be stored as my-study.variables:score, preventing collisions with a child study that might also define a score variable.
syntax
(with-root root-id body ...+)
This is primarily used internally by Congame (for example, by the matchmaking system) to create isolated variable storage namespaces. Most study authors do not need this form.
procedure
(call-with-study-transaction proc) → any/c
proc : (-> any/c)
syntax
(with-study-transaction expr ...)
Important: You should use one of these forms when you’re doing multiple operations that depend on each other and which involve instance-scoped variables (that is, variables with instance scope, created with defvar/instance or defvar*/instance). Variables with instance scope can be updated by other participants at any time, so it is possible that the variable could change between the time when its value is read and when it is updated.
Example:
(defvar/instance group-score) ;; ... (define (bad-update) (when (> score 100) ;; Bad: another participant could change the score before the next line runs! (set! score (+ score 50)))) (define (good-update) (with-study-transaction ; Prevents changes by anyone else during this code (when (> score 100) (set! score (+ score 50)))))
Note that each highlighted expression above is a separate reference to the score variable; without the use of a transaction, other study participants could change its value at any point between those expressions.
You don’t need with-study-transaction for single operations; a single read, or a single write of a literal value, is safe. But the moment you have a sequence of operations where later steps depend on earlier ones, and those steps touch instance variables, use with-study-transaction to keep everything atomic and consistent.
In concrete technical terms, call-with-study-transaction enters a database transaction with an isolation level of 'serializable.
procedure
(get/linked/instance pseudonym key [ default #:root root-id]) → any/c pseudonym : symbol? key : symbol? default : any/c = (lambda () (error ...)) root-id : symbol? = '*root*
This is an advanced feature used when multiple study instances are linked together (for example, when one study needs to access aggregate data from another). The pseudonym identifies the linked instance, and key specifies which variable to retrieve.
If the variable is not found and default is a procedure, it is called and its result returned. Otherwise, default is returned directly.
6.2.1.2 Low-level data access
A step scope represents the region of the database where data for a study step is stored and retrieved. Step scope is determined by the combination of the current participant, the study stack, and optional round and group information. Instance scope (used by defvar/instance) is shared between all participants in a study instance.
The functions below provide direct access to the underlying data storage. In most cases, you should use defvar and related forms instead, which provide a more convenient interface.
procedure
(put key value [ #:root root-id #:round round-stack #:group group-stack #:participant-id participant-id]) → void? key : symbol? value : any/c root-id : symbol? = '*root* round-stack : (listof string?) = (list "") group-stack : (listof string?) = (list "") participant-id : integer? = (current-participant-id)
The optional keyword arguments allow storing data in different scopes:
#:root specifies the root namespace for storage (default '*root*)
#:round and #:group specify round and group context for the data
#:participant-id allows storing data for a different participant (must be in the same study instance)
procedure
(get key [ default #:root root-id #:round round-stack #:group group-stack #:participant-id participant-id]) → any/c key : symbol?
default : (or/c any/c (-> any/c)) = (lambda () (error 'get "value not found for key ~.s" key)) root-id : symbol? = '*root* round-stack : (listof string?) = (list "") group-stack : (listof string?) = (list "") participant-id : integer? = (current-participant-id)
If no value exists for key, default is called if it is a procedure, or returned directly otherwise.
The optional keyword arguments mirror those of put and allow retrieving data from different scopes.
6.2.1.3 Participant groups
Congame supports organizing participants into named groups within a study. This is useful for implementing group-based activities such as games or collaborative tasks. Group membership is tracked per participant.
procedure
procedure
(put-current-group-name group-name [ #:participant-id participant-id]) → void? group-name : string? participant-id : integer? = (current-participant-id)
put-current-group-name assigns the current participant (or the participant specified by participant-id) to the group named group-name.
These functions are typically used with matchmaking logic to pair participants. For example:
(with-study-transaction (when (string=? (get-current-group-name) "") ; Participant not yet in a group, assign them (put-current-group-name "group-1")))
6.2.1.4 Steps
procedure
(make-step id handler [ transition #:view-handler view-handler #:for-bot bot-handler]) → step? id : symbol? handler : (-> xexpr?) transition : transition/c = (lambda () next) view-handler : (or/c #f (-> request? response?)) = #f bot-handler : (or/c #f procedure?) = #f
The id argument is a symbol that uniquely identifies this step within the study.
The handler argument is a procedure that takes no arguments and returns an X-expression representing the step’s page content.
The transition argument is a procedure that returns the next step to transition to after this step completes. It can return next to proceed to the next step in the list, done to end the study, or a symbol naming a specific step.
The #:view-handler argument, if provided, specifies a view handler for this step.
The #:for-bot argument, if provided, specifies a bot handler that determines what a bot should do when it reaches this step. If not provided and the step has a custom transition, an error will be raised when a bot reaches the step.
(make-step 'greeting (lambda () (haml (:div (:h1 "Welcome!") (button void "Continue")))))
procedure
(make-step/study id s [ transition #:require-bindings require-bindings #:provide-bindings provide-bindings]) → step? id : symbol? s : (or/c study? (-> study?)) transition : transition/c = (lambda () next) require-bindings : (listof binding/c) = null provide-bindings : (listof binding/c) = null
The s argument can be either a study value or a procedure that returns a study. When s is a procedure, it is called when the step is reached, allowing the study structure to depend on runtime values (such as participant responses from earlier steps). This is essential for dynamically generated studies using for/study.
The #:require-bindings and #:provide-bindings arguments are deprecated and included for compatibility. Use defvar* and defvar*/instance to share study variables between parent and child studies.
Embedding a dynamically generated study:
(define (make-substudy) (for/study ([i (in-range n)]) (question-step i))) (make-step/study 'questions make-substudy)
Here, make-substudy is called when the step is reached, after n has been set by a previous step.
The proc argument receives the step’s current handler (a procedure that returns an X-expression) and should return a new handler. This allows wrapping or modifying step behavior without changing the original step definition.
If s is a make-step/study step containing a child study, map-step recursively applies the transformation to all steps in that child study.
See also map-study.
This is useful for adding consistent behavior across all steps in a study, such as wrapping each page with common styling, adding logging, or injecting validation.
(define (add-border handler) (lambda () (haml (:div ([:style "border: 1px solid black;"]) (handler))))) (define bordered-study (map-study my-study add-border))
The transformation is applied recursively to any child studies contained within make-step/study steps.
The predicate next? returns #t if v is identical to next, #f otherwise.
The predicate done? returns #t if v is identical to done, #f otherwise.
6.2.1.5 Step Widgets
procedure
(button action label [ #:id id #:to-step-id to-step-id]) → xexpr? action : (-> void?) label : xexpr? id : string? = "" to-step-id : (or/c #f symbol?) = #f
The #:id argument is useful for identifying the button within bot handlers.
procedure
(form f action render [ #:id id #:enctype enctype #:combine combine-proc #:defaults defaults]) → xexpr? f : form? action : (-> void?) render : (-> (widget-renderer/c) xexpr?) id : string? = "" enctype : string? = "multipart/form-data" combine-proc : (-> any/c any/c any/c any/c) = (λ (k v1 v2) v2) defaults : hash? = (hash)
The #:id argument is useful for identifying the button within bot handlers.
syntax
(when-bot expr)
6.2.1.6 Step Timings
Congame automatically tracks timing information for each step in a study. This timing data measures how long participants spend on each page, including both total elapsed time and the time the page was actively in focus (visible to the participant).
parameter
→ (cons/c (or/c #f number?) (or/c #f number?)) (current-step-timings timings) → void? timings : (cons/c (or/c #f number?) (or/c #f number?))
The total-time is the total elapsed time since the participant first loaded the page, including time spent with the page in the background (for example, if they switched to another browser tab).
The focus-time is the total time the page was actually visible and in focus. This excludes time when the page was in a background tab or the browser window was minimized.
Both values will be #f if no timing data is available (for example, on the very first page load of a study).
In #lang conscript, this function is available as get-step-timings.
6.2.1.7 Study loops
| (require congame/components/for-study) | |
| package: congame-core | |
syntax
(for/study [#:substudies] [#:requires requires] [#:provides provides] (for-clause ...) body-or-break ... body)
requires : (listof symbol?)
provides : (listof symbol?)
Use for/study for quickly building studies with many steps that differ in only a few places.
(for/study ([phase (in-list '("Setup" "Activation" "Tear-down"))]) (page (haml (:h1 phase " Phase") (:p "...") (button void "Next"))))