
Multi-turn survey simulations
Source:vignettes/multi-turn-survey-simulations.Rmd
multi-turn-survey-simulations.Rmdrun_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 respondentThe 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.