On this page:
study?
make-study
run-study
current-participant-id
current-participant-owner?
current-participant-identity-user?
6.2.1.1 Study variables
defvar
defvar*
defvar/  instance
defvar*/  instance
undefined
undefined?
if-undefined
with-namespace
with-root
call-with-study-transaction
with-study-transaction
get/  linked/  instance
6.2.1.2 Low-level data access
put
get
6.2.1.3 Participant groups
get-current-group-name
put-current-group-name
6.2.1.4 Steps
step?
make-step
make-step/  study
map-step
map-study
next
next?
done
done?
6.2.1.5 Step Widgets
button
form
skip
when-bot
6.2.1.6 Step Timings
current-step-timings
6.2.1.7 Study loops
for/  study

6.2.1 Studies and Steps🔗

 (require congame/components/study) package: congame-core

procedure

(study? v)  boolean?

  v : any/c
Returns #t when v is a study.

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
Creates a study that can be run by run-study. The name argument is used in debugging and in the admin interface. Any step that is part of the study has to be listed in steps.

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

(run-study s [req #:bindings bindings])  any/c

  s : study?
  req : request? = (current-request)
  bindings : (hash/c symbol? any/c) = (hasheq)
Runs the study s under req with bindings.

Returns the database ID of the current participant in the current study instance.

Returns #t if the current participant is also the owner of the current study instance, #f otherwise.

This is useful for conditionally displaying admin-only content or enabling special controls for study owners.

Returns the identity server URL if the current participant enrolled in the study through an identity server, #f otherwise.

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)

Defines a study variable with participant scope bound to id. The study variable can be accessed inside the study steps using id and updated with (set! id expr).

When set, the value of the study variable will be stored in the Congame server database under the current study, instance, and participant.

syntax

(defvar* id)

(defvar* id global-id)
Like defvar, but creates a variable that is additionally visible to any child studies (see make-step/study).

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)

Like defvar, but creates a variable with instance scope that is, the stored value is shared by all participants in the study instance.

syntax

(defvar*/instance id)

(defvar*/instance id global-id)
Like defvar*, but creates a variable with instance scope that is, the stored value is shared by all participants in the study instance, and is also visible to child studies.

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.

A special value used internally to represent study variables that have been created with defvar but not yet assigned a 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
Returns #t if v is a study variable which has been created but not yet given any value, #f otherwise.

syntax

(if-undefined study-var alt)

Returns the current value of study-var if it has been given a value, or alt if study-var is undefined?.

syntax

(with-namespace namespace body ...+)

Wraps one or more defvar* or defvar*/instance definitions so that each variable automatically receives a globally unique identifier based on namespace.

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 ...+)

Wraps defvar and defvar/instance definitions to store them under a custom root identifier root-id instead of the default *root*.

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 ...)

Calls proc (or evaluates expr ... in such a way as to prevent any study variables from being updated by other participants until completed. The result of proc (or expr ...) becomes the result of the expression.

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*
Retrieves an instance-scoped study variable from a linked study instance identified by pseudonym.

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)
Stores value under the symbol key in the current step scope.

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)
Retrieves the value stored under the symbol key from the current step scope.

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

(get-current-group-name)  string?

procedure

(put-current-group-name group-name 
  [#:participant-id participant-id]) 
  void?
  group-name : string?
  participant-id : integer? = (current-participant-id)
get-current-group-name returns the current participant’s group name. If the participant has not been assigned to a group, returns an empty string "".

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

(step? v)  boolean?

  v : any/c
Returns #t when v is a step.

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
Creates a step for use in a study.

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
Creates a step that executes the child study s when reached.

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.

procedure

(map-step s proc)  step?

  s : step?
  proc : (-> handler/c handler/c)
Transforms the step s by applying proc to its handler.

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.

procedure

(map-study s proc)  study?

  s : study?
  proc : (-> handler/c handler/c)
Transforms every step in the study s by applying proc to each step’s handler.

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.

canary

next : next?

procedure

(next? v)  boolean?

  v : any/c
A special value that can be used as a transition result to cause a study to transition to the next step, whatever step that may be.

The predicate next? returns #t if v is identical to next, #f otherwise.

canary

done : done?

procedure

(done? v)  boolean?

  v : any/c
A special value that can be used as a transition result to cause a transition to the end of the study.

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
Renders a button with the given label that executes action when pressed. After the action is executed, moves the participant to the step named by to-step-id or the next step if to-step-id is #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)
Renders the form represented by f using render and executes action on successful submission, then continues to the next step in the study.

The #:id argument is useful for identifying the button within bot handlers.

procedure

(skip [to-step-id])  void?

  to-step-id : symbol? = #f
Skips to the step named by to-step-id or the next step in the study if to-step-id is #f.

syntax

(when-bot expr)

When the value of current-user-bot? is not #f, returns the result of expr converted to a string (in display mode), otherwise returns "".

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

(current-step-timings)

  (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?))
Returns a pair (cons total-time focus-time) containing timing information for the current step, where both values are measured in milliseconds.

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?)
Iterates like for but each result of the last body accumulated into a list of steps, which are passed to make-study to produce a study.

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"))))