Skip to contents

If you haven’t already, read the Getting Started guide for authentication basics.

1. Why Run R Inside Your Study?

Running R code inside your formr study has always been possible — calculate items, showif conditions, and inline labels can already draw on the current participant’s own data. The V1 API broadens this to data from other participants. A calculate item can now ask “what did everyone else answer?” and branch, display, or store based on the answer. Here is what becomes possible:

  • Adapt in real time. Check what other participants have answered so far and branch, skip, or compute norms on the fly.
  • Balance experimental groups automatically. Count completions per condition and route each new participant to the smaller cell, compensating for differential attrition without manual intervention.
  • Synchronise participants. Poll the database for incoming sessions and advance a whole cohort at once — enabling live dyadic tasks, focus groups, or team exercises inside an asynchronous survey framework.

All of these patterns are built on the same small set of tools: authentication, context variables, and a single data-fetching function. The walkthroughs in §4–§7 show each pattern from start to finish.

These are just starting points — since you can run arbitrary R code, any server-side logic that benefits from cross-session data or further API functions is fair game.

2. Where the API Code Goes

The V1 API is called from calculate items — hidden fields that run R on the server between surveys. The results are then displayed or acted upon by two other mechanisms that already existed in formr.

A. Calculate Items (API Entry Point)

A calculate item evaluates an R expression when the participant reaches it. The last value is stored in the session data and can be used by later units (showif conditions, labels, other calculate items). This is where formr_api_authenticate() and formr_api_fetch_results() go.

# Inside a calculate item's "value" field:
formr_api_authenticate()

past <- formr_api_fetch_results(.formr$run_name, item_names = "score", join = TRUE)
mean(past$score, na.rm = TRUE)

Use calculate items for any logic that needs cross-session data: computing norms, generating tokens, processing JSON, or counting completions per experimental cell.

B. Inline R in Labels (Display + Fetch)

Labels render text and plots to the participant. They can call API functions just like calculate items — formr_api_authenticate() and formr_api_fetch_results() work here too.

# In a note item's label — fetch and display in one step:
# ```{r, echo=FALSE, results='asis'}
# formr_api_authenticate()
# scores <- formr_api_fetch_results(.formr$run_name,
#   item_names = "engagement", join = TRUE)
# cat("The sample mean is ", mean(scores$engagement, na.rm = TRUE), ".")
# ```

Use echo=FALSE to hide the code and results='asis' to print raw output. You can also render ggplot2 charts this way (see the Group Norms walkthrough in §5).

As a style choice, you can separate fetch logic into a calculate item and keep labels minimal — this makes studies easier to debug. But nothing prevents you from doing both in one label, as the Group Norms walkthrough in §5 does.

C. Inline R in Choices (Display)

Choice labels can also display dynamic content, useful when a dropdown menu should reflect live database contents.

# Inside a mc_multiple choice option's label — fetch and display in one step:
# ```{r}
# formr_api_authenticate()
# posts <- formr_api_fetch_results(.formr$run_name, item_names = "title", join = TRUE)
# posts$title[1]
# ```

3. Your Toolkit

Every piece of server-side R code needs the same few ingredients.

Authentication

Inside a run, credentials are injected automatically. Just call:

The package detects .formr$access_token and .formr$host set by the server. The token is valid for the duration of the request and is revoked when the request finishes.

Run Context

Two hidden variables are always available:

Variable What it holds
.formr$run_name The name of the current run (e.g. "daily_diary")
survey_run_sessions$session The current participant’s session code
run_name <- .formr$run_name
user_session <- survey_run_sessions$session

Use these to fetch the right data and associate new data with the right session, making your code portable across runs.

Fetching Data from Other Surveys

The function for reading data from within a run is formr_api_fetch_results(). It differs from formr_api_results() in important ways:

formr_api_results() formr_api_fetch_results()
Auto-reverses items Yes No
Auto-computes scales Yes No
Returns processed data Yes No
item_names filter No Yes
Default run_name .formr$run_name .formr$run_name
Default join TRUE FALSE

Inside a run, you almost always want formr_api_fetch_results() — raw data without transformations is safer when you process it yourself.

data <- formr_api_fetch_results(.formr$run_name,
  item_names = c("name", "age", "score"), join = TRUE)

Always specify item_names to keep requests fast. The result is a tibble with one row per session and a column per requested item (plus a session column). If the same item name appears in multiple surveys, the survey name is prefixed.

The current() Shorthand

In showif conditions and value expressions, formr repeats items within a session. The helper current(x) returns the most recent submission of an item — the last element of the vector, which is always the current session’s value:

# In a showif condition — check the current selection
current(menu_survey$choice) == "option_a"

# In a calculate item — capture the latest input
current(my_survey$text_input)

This is cleaner than the equivalent base-R x[length(x)] pattern and makes your intent explicit. See ?current for details.


4. Walkthrough: Participant Counter

The simplest complete example: greet each participant by their number in the study, using cross-session data.

Run Structure

Position Type Name What it does
10 Survey register Collects participant’s name
20 Calculate participant_count Counts all registrations so far
30 Survey welcome Shows “You are participant #N”

Calculate: participant_count

formr_api_authenticate()

past <- formr_api_fetch_results(.formr$run_name, item_names = "name", join = TRUE)

if (nrow(past) > 0) nrow(past) + 1 else 1

Survey: welcome

A note item with this label:

# ```{r, echo=FALSE, results='asis'}
# cat("## Welcome, Participant #", participant_count, "\n\n", sep = "")
# cat("Please proceed with the study.")
# ```

5. Walkthrough: Real-Time Group Norms

Problem: A single score tells a participant nothing. Showing how they compare to the current sample (descriptive norms) increases engagement and provides real value.

Research context example: Occupational burnout surveys where participants see their score plotted against the organisational distribution in real time.

Run Structure

Position Type Name What it does
10 Survey burnout Contains a regular item called engagement
20 Survey feedback Note item whose label fetches, plots, and displays

The burnout survey has a regular item called engagement where the participant enters their score. The feedback label does everything in one step — no separate calculate item needed, no data stored between units.

Survey: feedback (label)

A note item whose label fetches all engagement scores via the API and renders a ggplot comparing the current participant against the sample distribution:

# ```{r, echo=FALSE, results='asis', fig.width=6, fig.height=3}
# library(ggplot2)
# formr_api_authenticate()
#
# # Fetch all participants' engagement scores
# all_scores <- formr_api_fetch_results(.formr$run_name,
#   item_names = "engagement", join = TRUE)
#
# # Current participant's own score — local, no API needed
# my_engagement <- current(burnout$engagement)
#
# ggplot(all_scores, aes(x = engagement)) +
#   geom_density(fill = "grey70") +
#   geom_vline(xintercept = my_engagement, colour = "red", linewidth = 1) +
#   labs(
#     title = "Your engagement score vs. the organisation",
#     subtitle = paste0("Sample: ", nrow(all_scores), " colleagues"),
#     x = "Engagement", y = ""
#   ) +
#   theme_minimal()
# ```

Note two patterns worth reusing:

  1. The current participant’s own score (current(burnout$engagement)) is available locally from the session — no API roundtrip needed.
  2. The API call (formr_api_fetch_results) only fetches what the label cannot already see: other participants’ data. This is one roundtrip, one OpenCPU session, and nothing stored in the database beyond what the burnout survey already saves.

6. Walkthrough: Dynamic Group Balancing

Problem: In field experiments, attrition often differs between conditions. Static random assignment at the start produces unequal cell sizes by the end. Manually monitoring and rebalancing is tedious and error-prone.

Solution: Count completed sessions per condition on every new entry and route the participant to the currently smaller group.

Run Structure

Position Type Name What it does
10 Survey intake Baseline demographics
20 Calculate pick_condition Fetches prior completions, picks smaller group
30 Survey intervention_a Treatment module A (shown if condition == “A”)
40 Survey intervention_b Treatment module B (shown if condition == “B”)

Calculate: pick_condition

formr_api_authenticate()

# Fetch the condition assignments from all completed sessions
past <- formr_api_fetch_results(.formr$run_name,
  item_names = "assigned_condition", join = TRUE)

count_a <- sum(past$assigned_condition == "A", na.rm = TRUE)
count_b <- sum(past$assigned_condition == "B", na.rm = TRUE)

# Assign to the smaller group; break ties randomly
if (count_a <= count_b) "A" else "B"

Showif conditions

Position 30 (intervention_a) showif:

current(pick_condition) == "A"

Position 40 (intervention_b) showif:

current(pick_condition) == "B"

7. Walkthrough: Synchronising with a Waiting Room

Problem: Live dyadic tasks, focus groups, and team exercises require multiple participants to start a module simultaneously. Asynchronous survey frameworks let everyone progress at their own pace.

Solution: Trap early arrivals in a refresh loop, then advance the whole cohort at once via the API.

Run Structure

Position Type Name What it does
10 Survey lobby Intake survey
20 Survey waiting_room Auto-refreshing hold page (submit: 2000 ms)
30 SkipBackward loop_back Returns to position 20
40 Survey dyadic_task The live interaction — only visible after release

The waiting room survey has a single hidden submit button configured with submit: 2000 in its survey settings, causing it to re-submit every 2 seconds. A SkipBackward unit (position 30) immediately sends the session back to position 20, creating a continuous loop.

An administrator trigger (run manually or on a cron schedule) polls for queued sessions and releases them:

# Admin trigger — run outside the study, on your local machine or a cron job
formr_api_authenticate(host = "https://api.rforms.org", account = "admin")

# Find sessions currently at the waiting room (position 20)
queued <- formr_api_sessions("my-run-name", active = TRUE)
waiting <- queued$session[queued$position == 20]

if (length(waiting) >= 2) {
  formr_api_session_action("my-run-name",
    session_codes = waiting, action = "move_to_position", position = 40)
  message("Released ", length(waiting), " participants to the dyadic task.")
}

After release, sessions land directly on position 40 (the dyadic_task survey), bypassing the SkipBackward loop.

Participant experience

  1. Complete the lobby survey.
  2. Land on the waiting room — the page auto-re-submits every 2 s, keeping them in the loop.
  3. When the admin trigger finds enough participants, it moves them past the SkipBackward to the live task.

8. Patterns for Robust Code

The walkthroughs above keep code minimal for clarity. When you adapt them to a real study, a few defensive habits will save you debugging time — code running inside formr runs on the server and a single unhandled error can break a participant’s flow.

Handling JSON data. formr handles JSON in three ways:

  1. Automatic: When a calculate item returns a named list (like list(my_score = ..., sample_n = ...)), formr serialises it to JSON and stores it. You never write toJSON() for this — it just works.
  2. Persistence: For complex state that must survive across OpenCPU requests (e.g., a balancing matrix or network adjacency list), use jsonlite::toJSON(x, auto_unbox = TRUE) to store it explicitly as a session variable, and jsonlite::fromJSON() to read it back in a later calculate item.
  3. Parsing responses: API columns sometimes contain nested JSON (e.g., multi-select choices). Always wrap parsing in a tryCatch:
safe_parse_json <- function(x) {
  tryCatch(
    jsonlite::fromJSON(x),
    error = function(e) list()
  )
}

Guard against empty results. Before processing fetched data, check that it has rows:

data <- formr_api_fetch_results(.formr$run_name, item_names = "score", join = TRUE)

if (nrow(data) == 0 || all(is.na(data$score))) {
  result <- 0
} else {
  result <- max(data$score, na.rm = TRUE)
}

Use item_names. Always specify the exact columns you need when calling formr_api_fetch_results() inside a run. This keeps requests fast and avoids pulling unnecessary data.


Next Steps