Skip to contents

run_survey_simulation() is for simulated respondents who answer a sequence of survey screens. It differs from run_prompt_grid(): prompt-grid variants are independent conversations, while a survey task is participant × model × completion and may preserve conversation state across screens.

participants <- tibble::tibble(
  participant_id = c("p001", "p002"),
  condition = c("control", "message"),
  age = c(29, 54),
  stimulus = c("A short neutral description.", "A persuasive description.")
)

flow <- tibble::tibble(
  screen_id = c("attention", "outcome"),
  block = c("pre", "post"),
  prompt = c(
    "You are {age} years old. Read: {stimulus}\nWhat phrase stood out?",
    "You are {age} years old in the {condition} condition. Rate your support."
  ),
  response_type = list(
    ellmer::type_object(phrase = ellmer::type_string()),
    ellmer::type_object(support = ellmer::type_number())
  )
)

plan_survey_simulation(participants, flow, model_config = c(flash = "gemini-2.5-flash-lite"))
out <- run_survey_simulation(participants, flow,
  model_config = c(flash = "gemini-2.5-flash-lite"),
  memory = "profile", output_dir = "survey-checkpoints"
)
out$results # one row per answered screen
out$wide    # one row per simulated respondent

The participant table is deliberately row-wise. Each row is already an assigned profile and condition, so it is not expanded into all possible profile-condition combinations. Any participant column may appear in a prompt as {column}.

Memory is an experimental design choice

With memory = "conversation", a respondent has one continuing chat. This is convenient for a genuine interview, but it can make answers unusually consistent: the model can read its earlier answer in a way a human completing separate survey screens often cannot. memory = "profile" starts a fresh chat for each screen, while still interpolating the participant profile and stimulus into each prompt. This is usually the closest approximation to independent survey items.

memory = "block" preserves context within a named block and resets between blocks. It is useful for a short vignette followed by a separate outcome module. For each policy, prompts should state the profile/stimulus needed on that screen; only conversation memory should vary.

Item-per-turn prompting maximizes control over screen wording and per-item schemas, but costs one call per item. Block prompting reduces calls while keeping some sections distinct. Whole-survey prompting is cheapest but makes ordering, missingness, and item-level parsing less transparent. Large simulated survey benchmarks can therefore be very expensive; always inspect the plan, use smoke_n, and checkpoint before scaling up.

Branching and resume

An optional display_if list-column can contain TRUE, FALSE, or a function of (participant, answers). This allows simple deterministic display logic; the function receives the current participant row and named prior responses. Random assignment and item randomization should be performed reproducibly while building participants and survey_flow and saved with the design. They are not yet generated by the runner itself, which keeps task identities and resumption auditable.

Checkpoints are one completed turn at a time. A stopped run resumes without calling completed screens again, provided the participant, flow, model settings, completion, and memory policy are unchanged. on_error = "continue" records failed turns for inspection; a later run retries them.