PsyCloud

R SDK (psycloudr)

psycloudr lets you build a PsyCloud study from R. It is designed for the way many psychology projects already work: stimulus tables in data frames, design checks in scripts, and analysis planned before data collection begins.

The useful mental model is: make the trial table, describe the screen for each row, and name the columns you want saved.

Pre-release install

psycloudr is under active development and not yet on CRAN. Install it from the monorepo:

# install.packages("remotes")
remotes::install_github("bbuchsbaum/psycloud", subdir = "packages/psycloudr")

Core authoring and write_bundle() are pure R. Some preview/workbench helpers need extra local tools such as the V8 package or the psycloud CLI.

Start With The Data You Want

Before building the study, decide what columns should appear in your final data:

ColumnWhat it means
conditioncongruent or incongruent
colorthe ink color on this trial
wordthe printed word
correct_keythe response key that counts as correct
response fieldskey press, RT, correctness, timeout

The example below builds those columns, shows one trial per row, and writes a bundle that Studio and the runtime can open.

stroop.R
library(psycloudr)
 
instructions_md <- paste(
  "# Color Stroop",
  "",
  "Name the INK COLOR, not the word.",
  "",
  "Keys:",
  "- r = red",
  "- g = green",
  "- b = blue",
  "",
  "Press any key to begin.",
  sep = "\n"
)
 
bundle <- experiment(id = "demo.stroop", name = "Color Stroop") |>
  instructions(instructions_md) |>
  phase("trials", label = "Stroop trials") |>
    factors(
      condition = c("congruent", "incongruent"),
      color = c("red", "green", "blue")
    ) |>
    cross() |>
    derive(
      "word",
      expr_if(
        row$condition == "congruent",
        uppercase(row$color),
        case_when(
          row$color == "red" ~ "GREEN",
          row$color == "green" ~ "BLUE",
          .default = "RED"
        )
      )
    ) |>
    derive(
      "correct_key",
      case_when(
        row$color == "red" ~ "r",
        row$color == "green" ~ "g",
        .default = "b"
      )
    ) |>
    repeat_rows(2) |>
    shuffle(seed = seed_per_participant("stroop-demo")) |>
  screen("trial") |>
    present(fixation_cross(x = "center", y = "center", size = 36), 500) |>
    ask(
      text(trial$word, fill = trial$color, font_size = 72, x = "center", y = "center"),
      keypress(keys = c("r", "g", "b"), correct = trial$correct_key, timeout_ms = 2000)
    ) |>
    record(condition, color, word, correct_key) |>
  end_screen("# Done\n\nThanks for participating.") |>
  bundle(with_auto_ids = TRUE)
 
write_bundle(bundle, dir = "stroop_bundle", overwrite = TRUE)

Run it with:

source("stroop.R")

The stroop_bundle/ directory is the portable study. You can import it into Studio, validate it with the CLI, or keep it with the R script that generated it.

Participant-paced pages

If write_bundle() warns that the instructions or end screen can wait forever, that means those pages wait for a key press. That is usually intentional for instructions. Timed trials should still have explicit limits such as timeout_ms = 2000.

Read The Pipeline

The R SDK uses base R's pipe, |>, so the study reads from top to bottom:

  1. Start the study

    experiment(id, name) gives the study a stable identity. instructions(...) adds a one-screen introduction that waits for a key.

  2. Build the trial table

    phase("trials") opens the block. factors(...) names independent variables, cross() makes all combinations, derive(...) computes new columns, repeat_rows(...) adds repetitions, and shuffle(...) randomizes.

  3. Draw one row

    screen("trial") describes the display for one row. trial$word and trial$color mean "use the value from the current trial."

  4. Collect the response

    keypress(..., correct = trial$correct_key, timeout_ms = 2000) stores the response and lets PsyCloud score correctness.

  5. Save analysis columns

    record(condition, color, word, correct_key) copies these trial columns into the output data beside the response fields.

row$ And trial$

Two pronouns do most of the work:

Use thisWhereMeaning
row$colorinside derive(), filter_rows(), constraintsa row while the trial table is being built
trial$colorinside screen() stimuli and responsesthe participant's current trial at runtime

In the Stroop example, row$color helps compute word and correct_key. Later, trial$word, trial$color, and trial$correct_key are filled in on each participant trial.

If you use dplyr too

psycloudr has its own expression case_when(). If another package masks it, call psycloudr::case_when(...) inside derive(...).

If You Have A Fixed Item Table

Not every study is a full factorial crossing. For a lexical decision, recognition memory, or sentence task, you may already know the exact items. Define those rows directly in an R data frame and pass them to trials(stimuli).

lexical-decision.R
library(psycloudr)
 
stimuli <- data.frame(
  item_id = c("cat_01", "blit_01"),
  letter_string = c("CAT", "BLIT"),
  condition = c("word", "nonword"),
  correct_key = c("f", "j")
)
 
bundle <- experiment(id = "demo.lexical", name = "Lexical Decision") |>
  instructions("Press F for WORD and J for NONWORD.") |>
  phase("main", label = "Trials") |>
    trials(stimuli) |>
  screen("trial") |>
    ask(
      text(trial$letter_string, font_size = 64, x = "center", y = "center"),
      keypress(keys = c("f", "j"), correct = trial$correct_key, timeout_ms = 2500)
    ) |>
    record(item_id, condition, correct_key) |>
  end_screen("# Done\n\nThank you.") |>
  bundle(with_auto_ids = TRUE)
 
write_bundle(bundle, dir = "lexical_bundle", overwrite = TRUE)

For small and medium item sets, this is usually clearer than maintaining a separate spreadsheet. If you already have a CSV from another source, import it only after checking the rows and column names explicitly.

Check Before You Run Participants

These are the checks to run before recruitment:

summary(bundle)            # phases, screens, bindings
preview_trials(bundle)     # materialized trial table
validate_bundle(bundle)    # schema validation
lint_bundle(bundle)$counts # warnings/errors by type
 
write_bundle(bundle, dir = "stroop_bundle", overwrite = TRUE)

preview_trials(bundle) is especially important. It lets you confirm that the condition balance, repetitions, randomization seed, derived correct keys, and analysis columns match your design before anyone participates.

You can also preview interactively in RStudio:

psycloud(bundle, width = 800, height = 600)

Common Recipes

Study needUse
factorial design`factors(...)
fixed item tabletrials(stimuli)
fixed delaypresent(stimulus, duration_ms)
response-gated screenask(stimulus, response)
keyboard responsekeypress(keys = c("f", "j"), correct = ..., timeout_ms = ...)
"press any key" pageinstructions(...), end_screen(...), or anykey()
survey pagesurvey_page(...), form_response()
output condition columnsrecord(condition, item_id, ...)

For a questionnaire, keep the items in a data frame and render them on one page:

items <- data.frame(
  name = c("calm", "alert", "nervous"),
  label = c("Calm", "Alert", "Nervous")
)
 
bundle <- experiment(id = "demo.mood", name = "Mood Survey") |>
  instructions("Please rate how you feel right now.") |>
  phase("survey", label = "Mood ratings") |>
    factors() |>
  screen("survey") |>
    ask(
      survey_page(
        items,
        response_type = "likert",
        labels = c("Not at all", "A little", "Moderately", "Very much"),
        required = TRUE,
        submit_label = "Continue"
      ),
      form_response()
    ) |>
    bind(role = "trial") |>
  end_screen("# Thanks") |>
  bundle(with_auto_ids = TRUE)

What To Learn First

Start with experiment(), phase(), factors() or trials(), screen(), and record(). That is enough to build many simple reaction-time, categorization, memory, and survey studies. Add derived columns, feedback, counterbalancing, and adaptive logic after the basic design table feels natural.

Low-level pc_* constructors are available for advanced package work and migration edge cases, but most study authors should stay with the fluent verbs shown here.

Worked Examples

Runnable examples live under packages/psycloudr/inst/examples/high-level/, including Stroop, simple reaction time, recognition memory, PANAS, Luck and Vogel change detection, and design-first memory. The vignettes also include side-by-side jsPsych-to-psycloudr versions of common paradigms.

Next