formR documentation

chain simple forms into longer runs, use the power of R to generate pretty feedback and complex designs

Most documentation is inside formr – you can just get going and it will be waiting for you where you need it.
If something just doesn't make sense or if you run into errors, please let us know.

Getting Started


Creating Studies

To begin creating studies using formr, you need to sign-up with your email and obtain an administrator account. An administrator account is obtained by sending a request via email to provide@email.in. Studies in formr are created using spreadsheets. As a good starting point, you can clone the following Google spreadsheet and study it to get versed with the definitions of the item types formr supports.

1. Upload the items: With your spreadsheet ready, login to formr admin and go to Surveys > Create new Surveys.
You can either upload your spreadsheet if it was stored locally on your computer using the form Upload an item table or you could import a Google spreadsheet by enabling link sharing and using the form Import a Googlesheet. When importing a Googlesheet, you will need to manually specify the name of your survey whereas if uploading a spreadsheet, the name of your survey is obtained from the filename of the spreadsheet.

2. Manage your survey: If your spreadsheet was well formed (as described here) and the items were successfully uploaded, your survey will be added to the Surveys menu. To manage your created survey, go to Surveys > YourSurveyName.
In the survey admin area you can test your survey, change survey settings, view and download results, upload and delete survey items etc. The survey menu to the left in the survey admin area contains hints that you can trigger by hovering over the link.

3.Create a Run/Study: A formr "run" contains your study's complete design. All the things that a single participant is going to do in your study should take place in one run. Designs can range from the simple (a single survey or a randomized experiment) to the complex (like a diary study with daily reminders by email and text message or a longitudinal study tracking social network changes). If you want to go beyond simply surveys, we recommended you read more about runs before you begin. To create a run go to Runs > Create a new Run. Enter a meaningful run name which should contain only of alphanumeric characters and the dash. The name will be shown in the URL of the study, so unlike survey names your participants will see it. If the name was valid and not taken, the run will be added to the Runs menu and you will be redirected to the run admin area. Here you can add Run Units, design the complexity of your study and test your study. To modify your study definition later, you can go to Runs > YourRunName. Your run is the entry point of your study. By default, it is accessible only to you and test users, that you can create. For participants to access your study, you need to set your run as public in the admin area and it will be accessible under the URL https://study.researchmixtape.com/YourRunName/

Setting up your own formr instance

If you wish to set up your own instance of formr please follow the guidelines in our installation guide.

There is always help if you need assistance.

formr Runs/Studies


A formr "run" contains your study's complete design. All the things that a single participant is going to do in your study should take place in one run. Designs can range from the simple (a single survey or a randomised experiment) to the complex (like a diary study with daily reminders by email and text message or a longitudinal study tracking social network changes).

Inside a run, participants' data is connected, so you can track how many times a participant filled out her diary or whether her social network grew in size since the first measurement time point.

So, why "run"? In formr, runs consist of simple modules that are chained together linearly. Because most modules are boombox-themed, it may help to think of a tape running. Using controls such as the skip backward button, the pause button and the stop button, you control the participant's progression along the run. Surveys can be thought of as the record button: whenever you place a survey in your run, the participant can input data.

Data for the control units are supplied on-the-fly to the statistics programming language R. Therefore, you can dynamically generate feedback graphics for your participants with minimal programming knowledge. With more programming knowledge, nothing keeps you from making full use of R. You could for example conduct complex sentiment analyses on participants' tweets and invite them to follow-up surveys only if they express anger.

Since runs contain your study's complete design, it makes sense that runs' administration side is where every user management-related action takes place. There is a user overview, where you can see at which position in the run each participant is and when they were last active. Here, you can send people custom reminders (if they are running late), shove them to a different position in the run (if they get lost somewhere due to an unforeseen complication) or see what the study looks like for them (if they report problems).

Runs are also where you customise your study's look, upload files (such as images, videos, Javascript files), control access and enrollment. In addition, there are logs of every email sent, every position a participant has visited and of automatic progressions.

Participant links

Your run lives at https://study.researchmixtape.com/yourRunName/. Once you set its access to People who have the link can access, anyone who opens that address is given a session and enrolled — for most studies this plain link is the only one you need to share.

Each participant is identified by a session code. It travels in the link as ?code=… and is also kept in a cookie, so a participant can close the tab and pick up where they left off. To send someone their own link, use {{login_link}} in an Email, or {{login_code}} to build a custom link yourself — for instance to invite informants to rate a participant.

Advanced: starting a fresh session with ?_new_session=1

Add ?_new_session=1 to your run link and every visit starts a brand-new session: formr generates a fresh session code and forwards the visitor to their own ?code=… link. This is meant for links that cannot carry a personal code — a printed QR code or poster, a link handed to many raters at once, or one rater working through several targets in turn.

  • Other parameters are carried across, so ?_new_session=1&rated=P1 lands on ?code=…&rated=P1 and rated is still available to your survey's items.
  • It works only on the run's own address, not on a link to a particular page inside the run.
  • It grants no extra access. The run still has to be set to People who have the link can access, exactly as for anyone else arriving without a code.
  • An existing cookie is deliberately ignored: someone who already has a session gets a new, separate one instead of resuming the old one.
  • Participants land on an ordinary ?code=… link, so reloading or bookmarking that page keeps them in the session they just started.

Because every visit enrols someone new, don't use it for a link people might open twice — a reminder email, say. Use {{login_link}} there instead.

Module explanations

Surveys are series of questions (or other items) that are created using simple spreadsheets/item tables (e.g. Excel).

Survey item tables are just spreadsheets and they can just as easily be shared, reused, recycled and collaboratively edited using e.g. Google Sheets.

Surveys can remain fairly simple: a bunch of items that belong together and that a participant can respond to in one sitting. For some people, simple surveys are all they need, but in formr a survey always has to be part of a (simple) run/study.

Surveys can feature various items, allowing e.g. numeric, textual input, agreement on a Likert scale, geolocation and so on.

Items can be optionally shown depending on the participant's responses in the same survey, in previous surveys and entirely different data sources (e.g. data gleaned from Facebook activity). Item labels and choice labels can also be customised using knitr, so you can e.g. refer to a participant's pet or last holiday location by name or use people's preferred term of address.

If you use R, formr includes a few nice timesavers in its R package on Github. Data import can be automated without any funny format business. Items will be correctly typed according to the item table, not according to flawed heuristics. You will also have item and value labels available in R.
If you name your items according to the schema BFI_extra_2R, items with an R at the end can be automatically reversed and items ending on consecutive numbers with the same prefix will be aggregated to a mean score (with the name of the prefix). Using our R package codebook you can then document the data, as well as automatically generate internal consistency analyses and item frequency plots. Hence, some tedious manual data wrangling can be avoided, especially if you start giving your items meaningful and memorable names early on. The formr:: functions are also always available, whenever you use R inside formr runs and surveys.

A Form unit does what a Survey unit does — it asks the questions defined in a study's item table — but on the new v2 engine: pages change without reloading, showifs react instantly in the browser, answers are saved as participants go, and a page submitted without a connection is kept on the device and sent once the participant is back online. It also offers a one-item-per-screen "solo" layout and localized buttons.

There is no separate button for it: add the survey with Add Survey, and if the survey is on v2 (marked (v2) in the list) the unit is a Form. To put a survey on v2, upload it on v2 or move it on its settings page. In R (run conditions, other surveys' labels) a Form's data is reached exactly like a survey's: study_name$item.

The one authoring rule that changes: in a Form, showif is JavaScript, evaluated in the participant's browser; R belongs in the value column, and a hidden item's value is how an R result gets into a showif. Everything else — spreadsheet format, item types (except calculate, which a Form does not run), R elsewhere in the run — stays the same. Read the full v2 guide before switching a study that is already collecting data.

These are external links - use them to send participants to other, specialised data collection modules, such as a social network generator, a reaction time task, another survey software (we won't be too sad), anything really. However, you can also simply call upon external functionality without sending the participant anywhere – one popular application of this is sending text messages via an R API call.

If you insert the placeholder {{login_code}}, it will be replaced by the participant's run session code, allowing you to link data later (but only if your external module picks this variable up!).

Sometimes, you may find yourself wanting to do more complicated stuff like (a) sending along more data, the participant's age or sex for example, (b) calling an API to do some operations before the participant is sent off (e.g. making sure the other end is ready to receive, this is useful if you plan to integrate formr tightly with some other software) (c) redirecting the participant to a large number of custom links (e.g. you want to redirect participants to the profile of the person who last commented on their Facebook wall to assess closeness) (d) you want to optionally redirect participants back to the run (e.g. as a fallback or to do complicated stuff in formr).

You can either choose to "finish/wrap up" this component before the participant is redirected (the simple way) or enable your external module to call our API to close it only once the external component is finished (the proper way). If you do the latter, the participant will always be redirected to the external page until that page makes the call that the required input has been made.

Push notifications are a way to send notifications to participants. To be able to use them, you need to generate an application manifest in the run settings.

The manifest is a JSON file that describes your app. It is used to generate a PWA and a shortcut icon on the participant's device.

Once your study has a manifest, you can add special items (see App section of available items) like add_to_homescreen to your surveys. This will add a button to the participant's device that allows them to add your study to their homescreen. request_phone will guide the user to switch to a mobile device. And push_notification will ask the user to allow push notifications for your study. You can also insert the request_cookie item to explicitly ask participants to allow functional cookies which won't expire when the app is closed.

Sending push notifications to the user can be configured in the run. Be aware that notifications of "normal" priority can be delayed or supressed by the participants phone.You can define when a notification should expire, what message should be shown, whether the user needs to interact with the notification, whether it is silent or whether it should vibrate. Push notifications are automatically dismissed once the user opens the app. As long as push notifications exist, a badge counter (1) will be shown on the app on the operating systems that support it.

Skip backward allows you to jump back in the run, if a specific condition is fulfilled.

This way, you can create a loop. Loops, especially in combination with reminder emails are useful for diary, training, and experience sampling studies.

The condition is specified in R and all necessary survey data is automatically available. The simplest condition would be TRUE – always skip back, no matter what. A slightly more complex one is nrow(diary) < 14, this means that the diary must have been filled out at least fourteen times. Even more complex: nrow(diary) < 14 | !time_passed(days = 20, time = first(diary$created)), this means that at least 20 days must have passed since the first diary was done and that at least 14 diaries must have been filled out. But any complexity is possible, as shown in Example 2.

Example 1:

A simple diary. Let's say your run contains

  • Pos. 10. a survey in which you find out the participant's email address
  • Pos. 20. a pause which always waits until 6PM on the next day
  • Pos. 30. an email invitation
  • Pos. 40. a survey called diary containing your diary questions
  • Pos. 50. You would now add a Skip Backward with the following condition: nrow(diary) < 14 and the instructions to jump back to position 20, the pause, if that is true.
  • Pos. 60. At this position you could then use a Stop point, marking the end of your diary study.
What would happen?

Starting at 20, participants would receive their first invitation to the diary at 6PM the next day after enrolling. After completion, the Skip Backward would send them back to the pause, where you could thank them for completing today's diary and instruct them to close their web browser. Automatically, once it is 6PM the next day, they would receive another invitation, complete another diary etc. Once this cycle repeated 14 times, the condition would no longer be true and they would progress to position 60, where they might receive feedback on their mood fluctuation in the diary.

Example 2:

But you can also make a loop that doesn't involve user action, to periodically check for external events:

  • Pos. 10. a short survey called location that mostly just asks for the participants' GPS coordinates and contact info
  • Pos. 20. a pause which always waits one day
  • Pos. 30. A Skip Backward checks which checks the weather at the participant's GPS coordinates. If no thunderstorm occurred there, it jumps back to the pause at position 20. If a storm occurred, however, it progresses.
  • Pos. 40. an email invitation
  • Pos. 50. a survey called storm_mood containing your questions regarding the participant's experience of the storm.
  • Pos. 60. A stop button, ending the study.
What would happen?

In this scenario, the participant takes part in the short survey first. We obtain the geolocation, which can be used to retrieve the local weather using API calls to weather information services in the Skip Backward at position 30. The weather gets checked once each day (pause at 20) and if there ever is a thunderstorm in the area, the participant is invited via email (40) to take a survey (50) detailing their experience of the thunderstorm. This way, the participants only get invited when necessary, we don't have to ask them to report weather events on a daily basis and risk driving them away.

Advanced: computed jumps (return a position number)

Instead of a TRUE/FALSE condition, your R code can return a position number of 2 or higher — the participant then jumps straight to that position, ignoring the "skip backward to" field. This lets a single unit send different participants to different positions: e.g. c(30, 50, 70)[screener$arm] routes arm 1 to 30, arm 2 to 50, arm 3 to 70. The same works in a Skip Forward.

How formr reads the value your code returns:

  • TRUE / FALSE — the ordinary skip: jump to the position in the field below, or carry on to the next unit.
  • 0 and 1 always mean FALSE and TRUE, never positions 0 and 1. Conditions that return 1/0 rather than TRUE/FALSE — screener$consent on a 1/0 coded item, sum(x), nrow(df) — therefore keep working exactly as they always have. If you genuinely need to send someone to position 1, renumber the run.
  • 2 or higher — an absolute position to jump to.
  • Decimals are rounded to the nearest whole number, with halves rounding up: 29.5 and 30.4 both mean position 30. Rounding happens first, so 1.9 becomes 2 and does jump.
  • Several values (a vector) — only the first one is used, and a warning is recorded in the unit's log. Return a single value.
  • Anything else — a negative number, text, a date — falls back to a best-effort TRUE/FALSE reading and records a warning. Don't rely on it.
  • NA or an empty result (logical(0), NULL — typically a comparison against a column that does not exist for this participant) halts the unit and emails you; the message names the fix, e.g. isTRUE(survey$consent == "yes"). Before formr v1.10.3 an empty result silently meant FALSE.
  • Skip units created before formr v1.10.3 have their condition wrapped in as.logical({ … }). Before computed jumps existed, any non-zero number meant TRUE, and the wrapper keeps that reading (as.logical(25) is TRUE) so an older study is not rerouted. A wrapped condition that returns an R Date or POSIXct value also keeps its old reading (a past moment is TRUE, a future one FALSE); a date string is NA and halts. To use a position number in such a unit, remove the wrapper.

A few more things worth knowing:

  • The number must match a unit's position exactly; there is no nearest-match. If no unit sits at that position, the participant simply carries on with the next unit after the skip and the study's admin is emailed — nobody is ever stranded.
  • Direction is up to your number, not the unit type: a Skip Backward whose code returns a position ahead of it moves the participant forward.
  • The number is a position, not a unit — re-check your computed jumps after reordering a run.

The Routes to column in the unit's Test view shows the destination formr would actually pick for each participant — use it to check your code before you send anyone through.

This simple component allows you to delay the continuation of the run, be it
until a certain date (01.01.2014 for research on new year's hangovers),
time of day (asking participants to sum up their day in their diary after 7PM)
or to wait relative to a date that a participant specified (such as her graduation date or the last time he cut his nails).

  • Pos. 10. a survey collecting personality and contact info + the graduation data of university students
  • Pos. 20. a pause which waits until 4 months after graduation
  • Pos. 30. an email invitation
  • Pos. 40. another personality survey
  • Pos. 50. A stop button, ending the study. On this last page, the students get feedback on how their personality has changed after graduation.

See the Knitr & Markdown section to find out how to personalise the text shown while waiting.

Skip forward allows you to jump forward in the run, if a specific condition is fulfilled.

This way, you can create filters, and parallel paths or branches in a study. Filters are useful to screen participants. You may need parallel paths in a study to randomise people to one experimental branch out of many, or to make sure a certain part of your study is only completed by those for whom it is relevant.

Example 1: a filter/screening

Let's say your run contains

  • Pos. 10. a survey (depression) which has an item about suicidality
  • Pos. 20. a Skip Forward which checks depression$suicidal != 1. If the person is not suicidal, it skips forward to pos 40.
  • Pos. 30. At this position you would use a Stop point. Here you could give the participant the numbers for suicide hotlines and tell them they're not eligible to participate.
  • Pos. 40. Here you could do your real survey.
  • Pos. 50. A stop button, ending the study.
What would happen?

Starting at 10, participants would complete a survey on depression. If they indicated suicidal tendencies, they would receive the numbers for suicide hotlines at which point the run would end for them. If they did not indicate suicidal tendencies, they would be eligible to participate in the main survey.

Example 2: different paths

Let's say your run contains

  • Pos. 10. a survey on optimism (optimism)
  • Pos. 20. a Skip Forward which checks optimism$pessimist == 1. If the person is a pessimist, it skips forward to pos 50.
  • Pos. 30. a survey tailored to optimists
  • Pos. 40. a Skip Forward which checks TRUE, so it always skips forward to pos 60.
  • Pos. 50. a survey tailored to pessimists
  • Pos. 60. At this position you would thank both optimists and pessimists for their participation.
What would happen?

Starting at 10, participants would complete a survey on optimism. If they indicated that they are pessimists, they fill out a different survey than if they are optimists. Both groups receive the same feedback at the end. It is important to note that we have to let the optimists jump over the survey tailored to pessimists at position 40, so that they do not have to take both surveys.

Advanced: computed jumps (return a position number)

Instead of a TRUE/FALSE condition, your R code can return a position number of 2 or higher — the participant then jumps straight to that position, ignoring the "skip forward to" field. This lets a single unit send different participants to different arms: e.g. c(30, 50, 70)[screener$arm] routes arm 1 to 30, arm 2 to 50, arm 3 to 70. The same works in a Skip Backward.

How formr reads the value your code returns:

  • TRUE / FALSE — the ordinary skip: jump to the position in the field below, or carry on to the next unit.
  • 0 and 1 always mean FALSE and TRUE, never positions 0 and 1. Conditions that return 1/0 rather than TRUE/FALSE — screener$consent on a 1/0 coded item, sum(x), nrow(df) — therefore keep working exactly as they always have. If you genuinely need to send someone to position 1, renumber the run.
  • 2 or higher — an absolute position to jump to.
  • Decimals are rounded to the nearest whole number, with halves rounding up: 29.5 and 30.4 both mean position 30. Rounding happens first, so 1.9 becomes 2 and does jump.
  • Several values (a vector) — only the first one is used, and a warning is recorded in the unit's log. Return a single value.
  • Anything else — a negative number, text, a date — falls back to a best-effort TRUE/FALSE reading and records a warning. Don't rely on it.
  • NA or an empty result (logical(0), NULL — typically a comparison against a column that does not exist for this participant) halts the unit and emails you; the message names the fix, e.g. isTRUE(survey$consent == "yes"). Before formr v1.10.3 an empty result silently meant FALSE.
  • Skip units created before formr v1.10.3 have their condition wrapped in as.logical({ … }). Before computed jumps existed, any non-zero number meant TRUE, and the wrapper keeps that reading (as.logical(25) is TRUE) so an older study is not rerouted. A wrapped condition that returns an R Date or POSIXct value also keeps its old reading (a past moment is TRUE, a future one FALSE); a date string is NA and halts. To use a position number in such a unit, remove the wrapper.

A few more things worth knowing:

  • The number must match a unit's position exactly; there is no nearest-match. If no unit sits at that position, the participant simply carries on with the next unit after the skip and the study's admin is emailed — nobody is ever stranded.
  • Direction is up to your number, not the unit type: a Skip Forward whose code returns a position behind it moves the participant backward.
  • The number is a position, not a unit — re-check your computed jumps after reordering a run.

The Routes to column in the unit's Test view shows the destination formr would actually pick for each participant — use it to check your code before you send anyone through.

Waiting Time are like Pauses, but instead of making the participant wait, we wait for the participant.

By waiting for the participant for a certain amount of time, we can make sure that people are reminded to participate in our diary study after one hour—but only if they need a reminder. We can also make sure that a part of a study is only accessible at certain times of day.

Example 1: reminder

Let's say your run contains

  • Pos. 10. a pause (e.g. let's say we know when exchange students will arrive in their host country, and they cannot answer questions before they've been there one week)
  • Pos. 20. Now we have to send our exchange students an email to invite them to do the survey.
  • Pos. 30. a Waiting Time for 7 days. If the user clicks the link to answer questions, the study jumps to position 50, the survey. If two weeks go by without a reaction, the study moves on to the next position, the reminder.
  • Pos. 40. This is our email reminder for the students who did not react after 7 days.
  • Pos. 50. the survey we want the exchange students to fill out. We set an access window of 7 weeks for this survey (in the survey settings), so we wait at most 7 weeks for students to fill the survey out.
  • Pos. 60. Because this is a longitudinal study, we now wait for our exchange students to return home. The rest is left out.
What would happen?

The pause would simply lead to all exchange students being invited once they've been in their host country for a week (we left out the part where we obtained or entered the necessary information). After the invitation, however, we don't just give up, if they don't react. After another week has passed (one week in the host country), we remind them.
How is this done? We set a waiting time for the participant of 7 days.
Now if he doesn't answer for one week, the run will automatically go on to 40, to our email reminder (tentatively titled "Oh lover boy..."). We hope the participant clicks on the link in our invitation email before then though.
If he does, he will jump to the survey at position 60.
If he still doesn't answer, we will patiently wait for another seven weeks. This time, we set an expiry time in the survey settings to achieve this. Until seven weeks have passed he can do the survey. Once the seven weeks are over without him finishing the survey, the run moves on to the next position, which stands for waiting for return home, i.e. we gave up on getting a reaction in the first wave (but we still have "Baby, oh baby, My sweet baby, you're the one" up our sleeve).

You will always need at least one. These are stop points in your run, where you can give short or complex feedback, ranging from "You're not eligible to participate." to "This is the scatter plot of your mood and your alcohol consumption across the last two weeks".

If you combine these end points with Skip Forward, you can have several in your run: You would use the Skip Forward to check whether participants are eligible, and if so, skip over the stop point between the Skip Forward and the survey that they are eligible for. This way, ineligible participants end up in a dead end before the survey. In the edit run interface, you can see green counts of the number of people on this position on the left, so you can see easily how many people are ineligible by checking the count.
See the Knitr & Markdown section to find out how to generate personalised feedback, including plots.

This is a very simple component. You simply choose how many groups you want to randomly assign your participants to. We start counting at one (1), so if you have two groups you will check shuffle$group == 1 and shuffle$group == 2. You can read a person's group using shuffle$group. If you generate random groups at more than one point in a run, you might have to use the last one tail(shuffle$group,1) or check the unit id shuffle$unit_id, but usually you needn't do this.

If you combine a Shuffle with Skip Forward, you could send one group to an entirely different arm/path of the study. But maybe you just want to randomly switch on a specific item in a survey - then you would use a "showif" in the survey item table containing e.g. shuffle$group == 2. The randomisation always has to occur before you try to use the number, but the participants won't notice it unless you tell them somehow (for example by switching on a note telling them which group they've been assigned to).

Survey Spreadsheet


Survey spreadsheets contain the questions on a first sheet called "survey". They can optionally have a second sheet called "choices" where you define choices for multiple choice items in a long format. They can also optionally have a third sheet called "settings" where you define settings such as pagination and validation (most set these settings after uploading the survey though).

You can clone a Google spreadsheet to get started or start with an empty spreadsheet.

Some helpful tips:

  • You may want to make linebreaks in Excel to format your text. In Microsoft Excel on Macs, you need to press Command ⌘+Option ⌥+Enter ↩, on Windows it is Alt+Enter ↩. We suggest you start working from the provided sample sheet, because it already has the proper formatting and settings. In Google Spreadsheets, the combination is Option ⌥+Enter ↩.
  • Make text bold using __bold__, make it italic using *italic*.
type name label optional showif
text name Please enter your name *
number 1,130,1 age How old are you?
mc agreement emotional_stability1R I worry a lot. age >= 18
mc agreement emotional_stability2R I easily get nervous and unsure of myself. age >= 18
mc agreement emotional_stability3 I am relaxed and not easily stressed. age >= 18

Available columns

You can use more columns than the ones shown above. Unknown column types are simply ignored, so you can use them for other information.

The following column types exist:

type
set the item type (see item types tab)
name
this is simply the name of the item. You'll use it to refer to the item in your data analysis and when making complex conditions, so adhere to a systematic naming scheme (we recommend scale1, scale2, scale3R for Likert-type items).
label
This column is the text that will be shown on the left-hand side of the answer choices (for most items). You can use Markdown formatting here.
showif
If you leave this empty, the item will always be shown. If it contains a condition, such as sex == 1, it will only be shown if that condition is true. Conditions are written in R and can be arbitrarily complex. You should always test them well. It is also possible to refer to data in other surveys using e.g. other_survey$item_name != 2. If you refer to data on the same page, items will also be shown dynamically using Javascript.
optional
Nearly all items are mandatory by default. By using * in this column, you can turn items optional instead. Using ! requires a response to items that are optional by default (check, check_button).
value
Sometimes you may want a value to be pre-set when users first fill out a form. This can be especially handy in longitudinal studies, where you want to check that e.g. contact information is still up-to-date or when you want to highlight changes across days. You can, again, use arbitrarily complex R code (e.g. a_different_survey$item1 + a_different_survey$item2) to pre-set a value, but you can also simply use 1 to choose the option with the value 1 (remember that choices for mc-family items are saved as numbers, not as the choice labels per default). There is one special word, sticky, which always pre-sets the items value to the most recently chosen value. You also have to keep in mind when pre-setting strings, that they have to be marked up in R, like this "I am text" (preferably do not use single quotes because Excel will mess them up).
class
This column can optionally be added to visually style items. Find the available classes below.
choice1 - choice12
For multiple choice items, you can define the labels of the different choices here. In the database, the corresponding number will be stored (e.g., if choice1 is "honey", then the DB will record "1"). You can define at most 12 choices this way and you have no control over the value stored in the database. To define more than 12 choices or to reuse choice lists, use the choices sheet.
item_order
By default (and if you leave this column empty), items in formr are simply displayed in the order they are defined in the spreadsheet. If you assign numbers here, those will define the order instead. If several items share the same number, their order will be randomized when the survey is loaded.
block_order
You can use 1-4 letters to define blocks. For consecutively labelled blocks (without gaps), formr will then randomize block-wise (e.g., an entire block of items comes either first or second). If it helps you think about it, you can imagine, the final order of items as being determined by sorting on block_number.item_number.random_number. If that didn't help enough, how about this guide.

Randomising items and blocks

The item_order and block_order columns together cover the common randomisation schemes. Items that share an item_order value are shuffled among themselves; block_order assigns items to blocks, and consecutively labelled blocks are shuffled as wholes. Four examples:

1. Simple randomisation of items

An instruction is shown first, followed by items 1–4 in random order.

nameblock_orderitem_order
instr1
item_12
item_22
item_32
item_42

2. Random block order, fixed order within blocks

Blocks A and B are presented in random order; within each block the items keep their order.

nameblock_orderitem_order
item_1A1
item_2A2
item_3B1
item_4B2

3. Fixed block order, random order within blocks

Blocks are only shuffled against the blocks they sit next to, so an item without a block_order between two blocks pins their order. Here each submit button is left unblocked: block A always comes first, its two items are shuffled, and each block sits on its own page. (Giving the submit buttons a block_order too would make A and B a consecutive pair and randomise which comes first.)

nameblock_orderitem_order
item_1A1
item_2A1
submit12
item_3B3
item_4B3
submit24

4. Random blocks, random items within them

Blocks are shuffled and so are the items inside each block; the submit button stays at the bottom of its block's page. The same trick places an instruction item at the top of every page.

nameblock_orderitem_order
item_1A1
item_2A1
submit1A2
item_3B1
item_4B1
submit2B2

Optional classes for visual styling

Add a class column with space-separated class names to change how an item looks. The available classes are documented in the CSS classes section.

Choices Spreadsheet


Choices sheets are fairly simple. The list_name column defines the choice set. By using that name in the type column of a multiple-choice item, you assign that choice set. As you'd expect, the name column defines the value recorded in the database, the label defines what participants see by default. It is not currently possible to randomly order choice sets. You can use Javascript to achieve this.

You can clone a Google spreadsheet to get started.

list_name name label
agreement 1 disagree completely
agreement 2 rather disagree
agreement 3 neither agree nor disagree
agreement 4 rather agree
agreement 5 agree completely

Survey Item Types


There are a lot of item types, in the beginning you will probably only need a few though. To see them in action, try using the following Google spreadsheet or fill it out yourself. It contains example uses of nearly every item there is.

Plain display types

note
display text. Notes are only displayed once, you can think of them as being "answered" simple by submitting.
note_iframe
If you want to render complex rmarkdown htmlwidgets, use this.
submit timeout | auto
display a submit button. No items are displayed after the submit button, until all of the ones preceding it have been answered. This is useful for pagination and to ensure that answers required for showif or for dynamically generating item text have been given.

You can specify an optional timeout/delay (in milliseconds).
Negative values mean that the user has to wait that long until they can click submit.
Positive values mean the submit button will automatically submit after that time has passed. However, if not all items are answered or optional, the user will end up on the same page and the timer will restart. To avoid that, you have to use it together with optional items. Then, it's a way to use timed submissions. The data in the item display table can be used to check how long an item was displayed and whether this matches with the server's time for when it sent the item and received the response.

You can also set the option to auto, which will cause the page to be submitted automatically as soon as all visible items on the page have been answered. Please note that optional items also count as visible items. This can be used for e.g. creating menu-like pages.

Simple input family

text max_length
allows you to enter a text in a single-line input field. Adding a number text 100 defines the maximum number of characters that may be entered.
textarea max_length
displays a multi-line input field
tel
a single-line input for telephone numbers. Phones show the numeric keypad; no format is enforced.
url
a single-line input for a web address; the browser and the server both check that it is a URL.
cc
a single-line input for a card-style number; validated with the Luhn checksum.
blank
renders the label as plain content with no input and no note styling. Useful for markup that should not be wrapped like a note.
number min, max, step
for numbers. step defaults to 1, using any will allow any decimals.
letters max_length
like text, allows only letters (A-Za-züäöß.;,!: ), no numbers.
email
for email addresses. They will be validated for syntax, but they won't be verified unless you say so in the run.

Sliders

range min,max,step
these are sliders. The numeric value chosen is not displayed. The text shown to the left and right of the slider is defined using the choice1 and choice2 fields — every slider needs those two labels, an item without them is rejected when you upload the sheet. Defaults are 1,100,1.
range_ticks min,max,step
like range but the individual steps are visually indicated using ticks and the chosen number is shown to the right.
visual_analog_scale min,max,step
like range but with no default — the slider thumb is hidden until the participant interacts with it, and required items stay invalid until the participant actively chooses a position. The value submitted is empty (rather than the midpoint) if the participant never moves the slider; once it is moved, a whole number between min and max is stored. The end labels come from choice1 and choice2, exactly as for range (and are just as mandatory). Defaults are 0,100,1. Works in both survey and Form (v2) units.

Datetime family

date min,max
for dates (displays a date picker). Input can be constrained using the min,max parameters. Allowed values would e.g. be 2013-01-01,2014-01-01 or -2years,now.
time min,max
for times (displays an input with hours and minutes). Input can also be constrained using min,max, e.g. 12:00,17:00. Stored as a time (HH:MM:SS), so in R you get a string, not a number.
datetime_local min,max
date and time in one input (the browser's datetime-local control). Stored as a DATETIME.
datetime min,max
like datetime_local; kept for older sheets. Browsers have dropped the plain datetime control, so prefer datetime_local.
yearmonth min,max
a year and month, entered as yyyy-mm. Stored as a DATE on the first of that month.
month min,max
like yearmonth, but uses the browser's native month picker where one exists.
week min,max
an ISO week, entered as yyyy-Www. Stored as text.
year min,max
a four-digit year. Stored as a YEAR.

Fancy family

geopoint
displays a button next to a text field. If you press the button (which has the location icon on it) and agree to share your location, the GPS coordinates will be saved. If you deny access or if GPS positioning fails, you can enter a location manually.
color
allows you to pick a color, using the operating system color picker (or one polyfilled by Webshims)

Multiple choice family

The, by far, biggest family of items. Please note, that there is some variability in how the answers are stored. You need to know about this, if you (a) intend to analyse the data in a certain way, for example you will want to store numbers for Likert scale choices, but text for timezones and cities (b) if you plan to use conditions in the run or in showif or somewhere else where R is executed. (b) is especially important, because you might not notice if demographics$sex == 'male' never turns true because sex is stored as 0/1 and you're testing as female.

mc choice_list
multiple choice (radio buttons), you can choose only one.
mc_button choice_list
like mc but instead of the text appearing next to a small button, a big button contains each choice label
mc_multiple choice_list
multiple multiple choice (check boxes), you can choose several. Choices defined as above.
mc_multiple_button
like mc_multiple and mc_button
check
a single check box for confirmation of a statement.
check_button
a bigger button to check.
rating_button
min, max, step
This shows the choice1 label to the left, the choice2 label to the right and a series of numbered buttons as defined by min,max,step in between. Defaults to 1,5,1.
sex
shorthand for mc_button with the ♂, ♀ symbols as choices
select_one choice_list
a dropdown, you can choose only one
timezone
a dropdown listing every IANA time zone (e.g. Europe/Berlin). No choice list needed; the chosen zone name is stored as text.
select_multiple choice_list
a list in which, you can choose several options
select_or_add_one
choice_list, maxType
like select_one, but it allows users to choose an option not given. Uses Select2. maxType can be used to set an upper limit on the length of the user-added option. Defaults to 255.
select_or_add_multiple
choice_list, maxType,
maxChoose
like select_multiple and select_or_add_one, allows users to add options not given. maxChoose can be used to place an upper limit on the number of chooseable options. In a v2 survey, the answers are stored joined by a semicolon (Berlin, Germany;Paris, strsplit(x, ";") in R), so a choice must not contain one; the classic engine joins them by commas. Both select_or_add types also take their whole choice list in one cell, separated by semicolons in v2 (red;green;blue) and by commas in the classic engine.
rank
choice_list, random, touch
rank order by drag and drop (as in Qualtrics): the choices are shown as a list that participants reorder by dragging (mouse or touch), with the ▲/▼ buttons on each row, or with the keyboard (focus a row, then use Arrow Up/Down, Home/End). Stored as the choice names in their final order, joined by a comma, e.g. 3, 1, 2 for "3 ranked first" — strsplit(rank, ", ") in R recovers the ranking.
Options after the choice list: random (or shuffle) shuffles the starting order on every page load (so an untouched submit does not silently store your authored order); touch (or require_change) counts the item as answered only once the participant has actually interacted with it (dragged, tapped a row, or used the buttons/keyboard) — combine with a required item to force an explicit choice. Without touch, submitting stores the order as displayed, like Qualtrics. E.g. rank fruits random touch.
Participants always rank all the choices: a partial or repeated order is rejected. An answer given earlier (or a value you set in the sheet) becomes the starting order when the item is shown again, so going back does not scramble what someone already did. Only an optional item may be left unranked.
Form (v2) units only. In a v1 survey unit the drag-and-drop widget is not loaded.
mc_heading choice_list
This type permits you to show the labels for mc or mc_multiple choices only once.
To get the necessary tabular look, assign a constant width to the choices (using e.g. mc-width100), give the heading the same choices as the mcs, and give the following mcs (or mc_multiples) the same classes + hide_label.
On small screens the mc_heading will be hidden and labels will automatically be displayed again, because the tabular layout would otherwise break down.

Bot check

A gate you can put in front of (or at the end of) a form to raise the cost of automated submissions. It is a proof-of-work puzzle that is created and checked on your own formr server — nothing about the participant is sent anywhere else, so it adds no third party to your data processing agreement, unlike a hosted captcha. It makes scripted mass responding expensive, but it is friction, not identity: a person filling out your survey by hand — including a paid one — will pass it.

bot_check
shows a checkbox the participant ticks ("Verify you are human" by default). Ticking it makes the browser solve a small puzzle, which usually takes about a second. The page cannot be submitted until that has succeeded, so put it where a blocked submit is what you want. If you mark the item optional, participants may leave the box unticked and continue (the variable then stays empty); a puzzle that was attempted but failed still blocks. bot_check 3 makes the puzzle harder (difficulty 1–3, default from the server settings).
Only the word verified is stored, never the token itself, so there is nothing participant-identifying in your results table.
The wording is yours: the label is the prompt above the box, choice1 replaces the text next to the checkbox, choice2 the confirmation shown afterwards, and choice3 the text shown while it is working. That makes it usable as an "I am answering this myself" affirmation, e.g. choice1 = I confirm I am completing this survey myself, without automation. All three are optional; leave them empty for the English defaults.
Form (v2) units only.

Hidden family

These items don't require the user to do anything, so including them simply means that the relevant value will be stored. If you have exclusively hidden items in a form, things will wrap up immediately and move to the next element in the run. This can be useful for hooking up with other software which sends data over the query string i.e. https://study.researchmixtape.com/run_name/?param1=10&user_id=29
calculate
in the value column you can specify an R expression, the result of which will be saved into this variable. Useful to pull in external data or to forestall recalculating something repeatedly that you want to refer to later. If the calculation is based on values from the same module, you can insert the calculate item in the last line of the sheet behind the last submit button and its result will be stored in the database for use in further modules.
Classic Survey units only. A Form (v2) does no server-side computation, so a study with a calculate item cannot be put on v2: use a hidden item with the same value when the form needs the result, or compute it in the R of the unit that uses it.
ip
saves your IP address. You should probably not do this covertly but explicitly announce it.
referrer
saves the last outside referrer (if any), ie. from which website you came to formr
server var
saves the $_SERVER value with the index given by var. Can be used to store one of 'HTTP_USER_AGENT', 'HTTP_ACCEPT', 'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_CONNECTION', 'HTTP_HOST', 'QUERY_STRING', 'REQUEST_TIME', 'REQUEST_TIME_FLOAT'. In English: the browser, some stuff about browser language information, some server stuff, and access time.
browser
saves the participant's user-agent string. Same as server HTTP_USER_AGENT.
get var
saves the var from the query string, so in the example above get param1 would lead to 10 being stored.
random min,max
generates a random number for later use (e.g. randomisation in experiments). Minimum and maximum default to 0 and 1 respectively. If you specify them, you have to specify both.
hidden
you can use this item with a pre-set value, if you need to use data from previous pages together with data on the same page for a showif
block
Blocks progress. You can give this item a showif such as (item1 + item2) > 100 to add further requirements.

File uploads

You can ask study participants to upload image, audio, video, text, and PDF files in formr. Server-side limits for the maximal file sizes apply, you can also set lower limits per item.
file max_size_in_bytes
Permits all of the file types allowed by audio, video, image plus PDFs and some text files.
audio max_size_in_bytes
Upload audio files. If you assign the class "record_audio" in the class column, a little recording interface will be shown. It will use the device microphone.
video max_size_in_bytes
Upload video files.
image max_size_in_bytes
Upload image files. On smartphones, this can trigger the camera app.

Progressive web app items

Formr studies can be installed as a PWA (Progressive Web App). This allows you to send push notifications to users to invite them to return to the study, e.g. for experience sampling studies. To make this work, you need to generate a manifest.json file in the study/run settings.
request_phone
Helps transition desktop users to continue the study on their mobile device. On mobile devices, it automatically confirms mobile usage. On desktop, it displays a QR code for users to scan with their phone. Returns 'is_phone' for mobile users, 'is_desktop' for desktop users, 'qr_scanned' when successfully scanned, or 'not_checked' before verification.
add_to_home_screen
Displays a button that prompts users to add the study to their home screen as a PWA. The button's text can be customized using the choice field. Returns one of these statuses: 'added', 'ios_not_prompted', 'not_requested', 'not_prompted', 'already_added', 'no_support', or 'not_added'.
push_notification
Adds a button to request permission for sending push notifications. The button text can be customized using the choice field. When enabled, stores the push notification subscription data needed to send notifications to the user. For optional items, accepts 'not_requested', 'not_supported', or 'permission_denied' as valid states.
request_cookie
Prompts participants to enable functional cookies so their device can be recognised in later visits. Displays nothing on devices where this permission has already been granted. Returns 'functional_cookie' for users who had already consented, 'consent_given' after the button is used, and remains 'not_checked' until consent is provided. If the item is marked as required, the survey page cannot be submitted until functional cookie consent is recorded. In apps, we usually need functional cookies to be enabled to track users across sessions, so it makes sense to include this item after an app has been added to the home screen.

CSS classes


You might want to tinker with the look of certain form items. To do so you can use a variety of pre-set CSS classes. This is a fancy way of saying that if you make a new column in your survey sheet, call it "class" and add space-separated magic words, stuff will look different.

The classes are defined in custom_item_classes.css on GitHub, which is the authoritative list if a class you need is not documented below. Every item's wrapper also carries an item-<type> class (for example item-mc), so you can target all items of one type. If the pre-set classes are not enough, a run's settings let you add your own CSS, which is loaded on every page of that run; use the class column to attach your own class names and style them there.

These are the available styling classes:

left100, (200, …, 900)
controls the width of the left-hand column (labels). The default is left300, you have 100 pixel increments to choose from.
right100, (200, …, 900)
controls the width of the right-hand column (answers). There is no default here, usually the right-hand column will extend in accordance with the display width.
right_offset0, (100, …, 900)
controls the offset (distance) of the right-hand column to the left (not the label column, just the left). This is 300 pixels (+20 extra) by default. Analogously with left_offset100 etc. (defaults to 0).
label_align_left
label_align_center
label_align_right
controls the text alignment of the left-hand column (labels), by default it is aligned to the right.
answer_align_left
answer_align_center
answer_align_right
controls the text alignment of the right-hand column (answers), by default it is aligned to the left.
answer_below_label
This leads to answers stacking below labels, instead of them being side-by-side (the default). It entails zero offsets and left alignment. Can be overridden with offsets and the alignment classes.
hide_label
This hides the labels for mc and mc_multiple replies. Useful in combination with a fixed width for mc, mc_multiple labels and mc_heading – this way you can achieve a tabular layout. On small screens labels will automatically be displayed again, because the tabular layout cannot be maintained then.
show_value_instead_of_label
This hides the labels for mc_button and mc_multiple_button, instead it shows their values (useful numbers from 1 to x). Useful in combination with mc_heading – this way you can achieve a tabular layout. On small screens labels will automatically be displayed again, because the tabular layout cannot be maintained then.
rotate_label45, rotate_label30,
rotate_label90
This rotates the labels for mc and mc_multiple replies. Useful if some have long words, that would lead to exaggerated widths for one answer column.
mc_block
This turns answer labels for mc-family items into blocks, so that lines break before and after the label.
mc_vertical
This makes answer labels for mc-family items stack up. Useful if you have so many options, that they jut out of the viewport. If you have very long list, consider using select-type items instead (they come with a search function).
mc_horizontal
This makes answer labels for mc-family items stay horizontal on small screens.
mc_equal_widths
This makes answer labels for mc-family items have equal widths, even though their contents would lead them to have different widths. This won't work in combination with every other option for mc-styling and if your widest elements are very wide, the choices might jut out of the viewport.
mc_width50 (60, … ,
100, 150, 200)
This makes choice labels and choice buttons for mc-family items have fixed widths. If one choice has text wider than that width, it might jut out or ignore the fixed width, depending on the browser.
rating_button_label_width50
(60, … , 100, 150, 200)
This makes the labels for rating_button items have fixed widths. This can be useful to align rating_buttons buttons with each other even though the end points are labelled differently. A more flexible solution would be to horizontally center the choices using answer_align_center.
space_bottom_10
(10, 20, … , 60)
Controls the space after an item. Default value is 15.
space_label_answer_vertical_10
(10, 20, … , 60)
Controls the vertical space between label and choices, if you've set answer_below_label. Default value is 15.
clickable_map
If you use this class for a text type item, with one image in the label, this image will become a clickable image, with the four outer corners selectable (the selection will be stored in the text field). Will probably require customisation for your purposes.

v2, the new survey engine


A Form unit shows a survey as one JavaScript-rendered document: pages are submitted in the background, showifs react instantly, answers are autosaved, and a page that cannot reach the server is queued and replayed later. It is opt-in per study and lives next to the classic Survey unit, which keeps working unchanged.

The one authoring rule that changes: in v2, showif is evaluated in the participant's browser as JavaScript. Server-side R goes into the value column, and a hidden item's value is how you feed an R result into a showif. Everything else — the spreadsheet format, item types (except calculate, which a Form does not run), R elsewhere in the run — stays as it was. See showif and value.

What Form (v2) is, and when to use it

Classic Survey units render one page per request: every submit is a full page load, and every showif that cannot be decided in the browser goes back to the server. A Form unit instead renders all pages of the survey into a single document at the first load. From then on:

  • Pages are submitted without a reload. The page boundaries are still your submit items; each page is posted in the background and the next page appears in place.
  • showifs are live. They are compiled to JavaScript and re-evaluated on every input, with no round-trip.
  • Answers are autosaved. Each committed answer is saved in the background (at most one request per 20 seconds per page, plus a final flush when the tab is hidden or closed), so a participant who breaks off mid-page still leaves partial data.
  • Offline queue. If a page submit fails because there is no network (or the server errors), the page is stored in the browser and replayed once the participant is back online — see Autosave & offline.
  • Optional "solo" layout — one item per screen, auto-advance on single-choice items — and localized engine strings (buttons, banners) in the participant's language. See Layouts.
  • PWA items (add_to_home_screen, push_notification, request_phone, request_cookie) work as in v1.

For participants the form feels like an app: no flicker between pages, instant follow-up questions, and resilience on flaky connections. For your data the item table, item names and results are the same, with a few differences covered in Data & results.

When to use it: for any new study. For a study that is already collecting data, keep it on v1 until you have run the compatibility scan, fixed what it flags, and walked through the upgraded study yourself — see the upgrade checklist. Participants who are mid-form when you switch continue on the Form from where they are.

Putting a survey on v2

Every survey runs on one of two engines: v1, the classic one, or v2. A new survey is on v1 unless you choose otherwise. You add a survey to a run the same way on either engine, with Add Survey; its list of surveys marks v2 surveys with (v2), and the unit shows the survey on the survey's engine. There are three ways to put a survey on v2; none of them changes your items:

Upload it on v2
On Add a new survey, choose Engine: v2 (offered when the administrator of your formr server has enabled v2). A survey exported from formr remembers its engine, so an exported v2 survey comes back on v2. Items v2 cannot run (calculate, see below) are refused at upload.
Move an existing survey
On the survey's settings page (Surveys → your survey), the box Survey engine: v1 or v2 has Move to v2 and a compatibility check. Moving is refused while a run uses the survey as a classic Survey (to try v2, upload a copy), while the survey has items v2 cannot run, and while the check flags showifs. If you are sure the check is wrong about a showif, Move to v2 anyway on the check's page moves the survey regardless.
Import the example bundle
The run editor's Import button accepts documentation/run_components/Form_v2_Basic.json from the formr repository: a v2 survey plus an end page. The bundle contains the small form_v2_basic survey (a multiple-choice item with a JavaScript showif on a follow-up, a rank random touch, a visual_analog_scale, a bot_check and one submit), so one import creates the survey on v2 under your account and adds it to the run. Your own runs export the same way: tick Include survey items and each survey travels with the bundle, engine included.

Switch back to v1 is possible at any time. It changes only the survey's engine; every run that shows the survey then shows it on v1. You can move it to v2 again later.

Once a study is on v2, its settings page gains a Form_v2 settings block:

SettingDefaultWhat it does
Enable offline queueonA page submission that fails (no network, server error) is stored in the participant's browser and replayed when connectivity returns. Turn it off for studies whose answers must not sit on the device — a submission then fails visibly when offline. See Autosave & offline.
Show "Previous" buttonoffLets participants move back to an earlier page of the form. Going back shows the page as it was already rendered; dynamic values and labels on it are not re-computed.
LayoutDefaultDefault — multiple items per page or Solo — one item per screen. See Layouts.
Participant languageenA BCP 47 tag (en, de, de-AT, …). Sets the page's lang/dir attributes and translates the engine's own strings — buttons, save indicator, offline banner, validation fallbacks. English and German are built in; other tags keep the attribute but fall back to English strings. Your item labels are always shown as you wrote them.
Button labelsemptyYour own wording for the Next and Submit buttons (plain text, up to 100 characters). Empty keeps the built-in label in the participant language. The Next label also labels a submit item that has no label of its own.

Three classic settings are ignored on v2 and say so on the page: the maximum number of items per page and the "use paging" option (pages always come from your submit items) and "instant validation" (the browser always validates on page advance).

showif and value in v2

Two columns of the item table decide what a participant sees and what is pre-filled. In v2 they follow two rules:

  1. showif is JavaScript. It runs in the participant's browser, as written, and is re-evaluated whenever an input changes. A v1 showif is R, so the compatibility scan lists every one that still contains R; the fix is a JavaScript rewrite or the hidden-item bridge below.
  2. value is R. Anything that is not empty or a plain number is sent to R on the server, as written.
Writing a showif

Every item of the form is available under its name as a live variable, so mood == 3 or age >= 18 && consent == 1 work as they are. The R idioms of a v1 showif have to be rewritten:

v1 (R)v2 (JavaScript)
current(x), tail(x, 1)x — the current answer
single & / |, TRUE / FALSE&& / ||, true / false
x %contains% "s"contains(x, "s")
x %contains_word% "s"containsWord(x, "s")
x %begins_with% "s", x %starts_with% "s"startsWith(x, "s")
x %ends_with% "s"endsWith(x, "s")
stringr::str_length(x)x.length
is.na(x)isNA(x)

On top of the item variables, these helpers are in scope (their names are reserved — you cannot name an item after them):

HelperMeaning
isNA(x)true for an unanswered item: null, undefined, the empty string, an empty selection, or NaN. Note that 0 is an answer, not NA.
answered(x)!isNA(x)
contains(x, needle)for a multiple-selection item: was needle ticked; for text: does it contain the substring. False when x is NA.
containsWord(x, word)whole-word match in the text of x
startsWith(x, s), endsWith(x, s)prefix / suffix test on the text of x
last(x)the last element of a multiple selection; a single value is returned unchanged

How answers look inside a showif: a number-like answer is a number (so age > 17 works without quotes), anything else is a string; an unanswered mc is null; a mc_multiple / select_multiple is an array of the ticked choice values (use contains(pets, 2), not pets == 2); an unticked check is 0, matching what R sees. When a showif hides an item, that item's value is retracted from the live variables (as it will not be submitted), so showifs chained on it react too.

showif: mood == 3
showif: trigger == "yes" && answered(plain)
showif: !contains(animals, 1)
showif: last(daily_mood) > 5

Only the browser evaluates a showif. An item with a showif starts out hidden and appears as soon as its showif is true; the server never runs it. The browser knows every item of this form — including answers given on earlier pages, when the participant reloads or comes back later. Anything else, such as a run variable like ran_group or an item from another survey, is an unknown name: a showif that reads one is not true, so its item stays hidden. Bring such a value onto the form with a hidden item (below). The compatibility scanner flags these names.

Writing a value

A number is a literal default. Anything else is R, evaluated on the server with the participant's current answers overlaid on their data — so sticky keeps its meaning, and so do expressions like these:

value: 5
value: sticky
value: paste(answered_items, collapse=", ")
value: sample(c(1,1,1,2,2,2,2), 7)
value: complex_score(current(q1), current(q2))

The R source never reaches the browser. When the form is rendered, every dynamic value (and every label or choice label with inline R) is recorded on the server as an allowlisted call for that item; the browser only ever refers to the recorded call and can only supply answers to the study's own items. A participant cannot run R of their own, cannot reach other studies' data, and a call is revoked the moment the item is deleted or its expression changes in the sheet. Expressions that error in R resolve to NA, which arrives in the item as an empty value; a logical arrives as TRUE or FALSE. Results are cached for a few minutes per participant and answer set (see Instance settings).

Page-scoped resolution. Dynamic values, labels (containing `r …`), choice labels and note_iframe documents are resolved when the participant reaches their page, in one batched request after the previous page has been stored — so a value on page 2 can depend on answers from page 1. The first page works the same way, so its dynamic content paints a moment after the rest of the page. Keep heavy dynamic content off the first page if that flicker matters.

calculate items are not supported. v2 runs R only for what a page shows; it does no computation of its own. A study with a calculate item — or any other item that takes no input but carries R in value, such as a server item with a value — cannot be uploaded on v2, switched to v2 or linked to a Form unit; the compatibility scan lists these items. Replace each one by what it was for:

  • the form needs the value (a showif, a pre-filled answer, a random assignment the next page branches on): a hidden item with the same value — see below;
  • feedback or a later unit needs it (a score on the end page, eligibility for a Skip unit, a payment code): compute it there, in that unit's R, from the stored answers.
Bridging R into a showif

To gate visibility on something only R can compute, put the R into a hidden item's value and reference that item's name from the showif:

nametypeshowifvalue
treathiddenifelse(demographics$age > 30, 1, 2)
older_blocknotetreat == 1
younger_blocknotetreat == 2

The hidden item is resolved on the server, its result is written into the form, and the showifs react to it like to any other answer. You can read the resolved value while testing, it is stored with the results, and several showifs can reuse it without paying for the R call again.

Three things to keep in mind. A hidden item is resolved when its page is shown, against the answers of earlier pages — its own page's answers are still empty at that moment. Its value is posted by the browser with autosave and the page submit, so a participant (or a bot) can change it; anything you analyse or act on is safer computed in a later unit's R from the stored answers. And if the page's dynamic content cannot be loaded (network, rate limit, offline), the hidden item is submitted empty.

If the hidden item's R names another item of this survey, it is computed again whenever its page is resolved again — after the participant went back and changed that answer, say — and the new result replaces the old one. Any other value is computed once and kept: sticky and earlier entries, another survey's data, a value that names only itself, a sample() — so a random assignment is not drawn again on every revisit.

A page of nothing but hidden items is never shown. Its values are computed against the answers given so far and stored with the submit of the last shown page before it, so hidden items after the final submit button are computed from every answer and stored with the final page. Hidden items before the first shown page are computed when the form opens (their values can decide which pages show) and stored with the first page's submit.

Nothing on the form is secret: every answer this form stores is readable in the participant's browser, because showifs can use it. A value participants must not be able to see (their experimental condition, say) belongs outside the form, for example in an earlier survey of the run, and must not be referenced from a showif.

Reserved item names

These names are rejected when you upload a sheet, on every engine: session_id, created, modified, ended, id, study_id, iteration (columns of the results), and isNA, answered, contains, containsWord, startsWith, endsWith, last (the showif helpers).

The compatibility scan

Before you move an existing survey to v2, check it. The check reads every showif and value of the survey, sorts them into the groups below, and lists the items v2 cannot run. It changes nothing in your survey. Two ways to run it:

  • In formr: the survey's settings page has a Check v2 compatibility button, in the Survey engine: v1 or v2 box while the survey is on v1 and under v2 settings once it is on v2. The page lists each item to change, what is wrong and how to fix it.
  • Command line (instance administrators): php bin/form_v2_compat_scan.php <study_id|study_name> prints the same report and exits with status 0 if the study is clean and 2 if anything is flagged, so it can gate a deployment.
ColumnBucketMeaningWhat to do
showifemptyno expression—
JS-OKno R in itnothing; test it in the browser anyway
needs JS rewrite (flagged, fix: rewrite in JS)the showif contains R: function calls such as ifelse, c, paste, grepl, current, tail, …; is.na(); an infix operator such as %in% or %contains%; a single & or |; TRUE/FALSE; a namespace such as stringr::; the assignment arrow <-; a bare NA; or $ member access like survey$itemRewrite it with JavaScript operators and the helpers: x %in% c(1,2) becomes [1,2].includes(x) or x == 1 || x == 2; ifelse(a, b, c) becomes a ? b : c; is.na(x) becomes isNA(x); survey$item becomes just item if it is on the form. If the logic genuinely needs R (data from another survey, a function from your R package), move it into a hidden item's value and test that item's name in the showif — see Bridging R into a showif.
not on the form (flagged)the showif reads a name that isn't an item of this survey — a run variable such as ran_group, or another survey's item. The browser only knows this form's items, so the item would stay hiddencompute the value in a hidden item's value on this form and reference that item — see Bridging R into a showif
typenot supported in v2 (flagged, blocks the switch)a calculate item, or another item that takes no input but carries R in valuereplace it as described under calculate items are not supported
valueemptyno default—
literala number, used as-is—
Reverything else, including sticky; evaluated on the servernothing — values are never flagged

String literals are ignored when looking for R tokens, so a label such as "paste here" does not trigger a false alarm. The scan is a heuristic, but take its showif flags seriously: a showif the browser cannot evaluate keeps its item hidden for every participant, which is why flagged showifs stop the upgrade unless you choose Upgrade anyway. Always walk through the study yourself after switching.

Layout modes

The Layout setting on the survey's settings page (visible once the study is on v2) chooses how a page is presented. Both layouts use the same items, the same pages and the same data; only the presentation differs. The layout a participant actually got is recorded on their session, so you can change it mid-study without corrupting anything (it is not exported, though).

Default — multiple items per page
  • All items of an authored page (everything up to the next submit item) are shown together, as in the classic engine. A progress line reads Page 2 of 5.
  • The Next button sticks to the bottom of the viewport. On phones (screens narrower than 768 px) it becomes a full-width floating bar so it is always reachable without scrolling to the end of a long page. A page that has its own submit item uses that button's label instead.
  • Previous appears only if Allow "Previous" button is switched on for the study. It is off by default: with it off, the browser's back button is also neutralised, because every page of the form is already in the document and a participant could otherwise edit a page that has been submitted.
  • A small save indicator (a pill next to the navigation) shows Saving…, Saved, Synced or Saved offline — see Offline and autosave.
Solo — one item per screen

Each item gets a screen of its own; the participant works through the page item by item, the screen scrolls to the next item as soon as one is answered, and the page is submitted after the last item of the page. Items hidden by a showif are skipped and appear as soon as their showif becomes true. Design for it: keep labels short, and put a note before a block of questions rather than lengthy help text on every item.

  • Auto-advance. Single-choice items (mc, mc_button, rating_button, select_one, sliders such as range and visual_analog_scale) advance when a choice is made. For a plain radio item the OK button is hidden until an option is picked; selects and sliders keep OK visible because they may carry a default the participant simply accepts. Checkboxes, multiple selects, text fields and textareas need an explicit OK (the hint under a multiple-choice item says Choose all that apply, then OK).
  • Keyboard. Enter continues on text inputs, choice items and notes (hint: press Enter ↵). In a textarea Enter inserts a newline; Ctrl+Enter (⌘+Enter on a Mac) continues. ↓ and ↑ move to the next and previous item when the focus is not inside a text field or a radio/checkbox group (there the arrows move between options) and the current screen fits the viewport (otherwise they scroll). Widgets with their own keyboard handling — the rank list, date pickers, searchable selects — keep it.
  • Back goes to the previous item of the current page. Going back to a previous page is only possible with Allow "Previous" button on.
  • The progress bar never moves backwards, even when a showif reveals additional items mid-page; the 3 of 7 counter stays exact.
  • On phones the layout accounts for the on-screen keyboard: a screen that fits the visible area is locked in place, longer screens scroll normally.

Everything else — autosave, the offline queue, validation, R-computed values — behaves identically in both layouts. The two example spreadsheets pwa_items_default.xlsx and pwa_items_solo.xlsx are byte-identical; they exist only to be uploaded twice with the setting flipped.

Offline and autosave

What is saved when
MomentWhat happensIndicator
An answer changesAutosave. The changed answers are sent in the background, at most once every 20 seconds (the last change always lands). Only answers that pass the item's own validation are stored; invalid ones are silently skipped until the page is submitted. Two item types are never autosaved: bot_check (its token is single-use) and file uploads (bytes travel only with the page submission).Saving… → Saved
The tab is hidden or closedPending changes are flushed with a beacon so a participant who closes the app mid-page loses at most what they typed in the last few seconds.—
Next / SubmitPage submission. The page is validated in the browser, then posted. The server validates again and stores the answers; on validation errors the page stays with the errors highlighted, exactly as in the classic engine. On success the next page is shown — the rest of the form is already in the browser, so this takes no round-trip except for R-computed values and labels on the new page.Saved
Last page submittedA short Thanks — sending you to the next step… overlay, then the participant moves on in the run.—

Autosaved answers are what a participant sees again when they reopen the form on the same page. Autosave never advances the participant and never completes the form — only page submissions do.

The offline queue

With Offline queue on for the study (the default), a page submission that fails because the network is down or the server answers with a 5xx error is stored in the browser (IndexedDB) instead of being lost, and the participant continues to the next page as if it had gone through. A rejection with a 4xx status — validation errors, an expired session — is not queued; it is shown right away. What the participant sees:

  • The save indicator shows Saved offline; a banner says You're offline. This submission is queued and will be sent when you reconnect.
  • While offline, the banner reads Offline — submissions will sync automatically when you reconnect.
  • When the connection returns, or the form is opened again, the queue is sent in order: Syncing 2 queued submission(s)…, then All queued submissions have been sent. and the indicator shows Synced. Sending happens from an open page of the form; a closed tab does not sync in the background.

Each queued submission carries a unique id, so a page that reaches the server twice (the response was lost, the participant reloaded) is stored once. R-computed content needs the server: a page shown while offline keeps its placeholders — dynamic labels stay unresolved and R-computed values (including hidden items) stay empty, and are submitted empty. Pages shown after the connection is back are resolved as usual.

When a queued page is rejected

Answers are validated when they arrive, not when they were queued. If a queued page fails server-side validation — a required item that a client-side check let through, a choice that no longer exists — the participant is brought back to fix it: a banner says Your answers for page 3 could not be saved: please fix the highlighted answers and submit the page again with a Go to page 3 button; the page opens with the errors marked; submitting it again clears the entry from the queue and sending resumes. Nothing from a rejected page is stored until it is resubmitted, and later queued pages wait behind it. Two other outcomes empty the queue without asking: the participant has meanwhile moved on to a different form (the stale entries are dropped), or the session has expired — then the banner asks them to open their personal study link to sign in again and retry. Submissions that keep failing for other reasons are kept on the device and retried automatically; the banner offers Retry now.

Limits and lifetime
  • Uploaded files are queued too, but a single file larger than the instance limit (form_v2_offline_blob_max_mb, 10 MB by default) is refused with Submission too large to queue offline…; the participant has to retry that page online.
  • Queued answers are plain text on the participant's device. Following a logout link in the run wipes that run's queue, including unsent entries; other runs' queues on the same device are left alone.
  • The queue is per browser: a participant who switches devices before syncing takes the unsent pages with the old device.
  • With Offline queue off, a failed submission shows an error and the participant stays on the page.

New item types

  • rank — the participant orders the choices by drag and drop, buttons or keyboard. v2 only.
  • bot_check — a proof-of-work human check that runs on your own formr server, with no third party involved. v2 only.
  • visual_analog_scale — a slider without a visible default; works in classic surveys too.

All three are described in the item types reference. calculate is the one item type v2 does not run (see above).

Data & results

A Form stores answers where a classic survey does, under the same item names: the results table, the exports and R elsewhere in the run (study_name$item) work unchanged. The differences:

  • Partial data. Autosave stores each answer as it is given, so a participant who breaks off mid-page leaves what they answered so far.
  • Clearing an answer withdraws it. When a participant empties a field or deselects a choice, the next autosave removes the answer stored earlier, so it neither comes back after a reload nor stays in the data.
  • A hidden item is missing. When a showif hides an item, an answer autosaved while it was shown is removed at the next page submit — as in a classic survey, where a hidden item is always missing.
  • A choice value 0 is stored in mc_multiple, mc_multiple_button, select_multiple and select_or_add_multiple answers (the classic engine drops it: ticking 0 and 2 stores 2 there, 0, 2 in a Form). Switching a study to v2 widens those results columns where needed.
  • select_or_add_multiple separates its answers with semicolons (Berlin, Germany;Paris), so a choice may contain a comma. A choice containing a semicolon is refused when the survey is uploaded to v2 or switched to it. A select_or_add_one or select_or_add_multiple choice list written in one cell is split at semicolons too (red;green;blue). The classic engine keeps separating both with commas; the compatibility check flags a list written in one cell with commas, which would be a single choice in a Form.
  • Layout per response. The layout each response was collected under is recorded on its session (not in the exports), so changing the layout mid-study does not rewrite history.

Instance settings

These are set by the instance administrator in config/settings.php; the defaults apply when a key is absent. The operator's guide, documentation/form_v2_upgrade.md in the formr repository, has the details.

SettingDefaultWhat it does
form_v2_enabledfalseOffers v2: the Engine choice on Add a new survey and Move to v2 on a survey's settings page. Surveys already on v2 keep working without it.
form_v2_rate_limits30 / 90 / 10 / 20 per minutePer participant session: page resolutions (they run R), writes (page submits, autosaves, queue replays), the unauthenticated PWA beacon (per IP), and bot_check challenges.
form_v2_rcall_ttl300 sHow long a resolved value, label or choice label is reused for the same participant and the same answers.
form_v2_offline_blob_max_mb10Largest file a page may carry into the offline queue.
bot_check_*see the operator's guideDifficulty, memory and lifetime of the bot_check puzzle, and its signing secret (derived from the instance's encryption key by default).

Upgrade checklist for a running study

  1. Run the compatibility check.
  2. Fix what it flags: rewrite each flagged showif in JavaScript or bridge it through a hidden item, and replace each calculate item (how). Upload the corrected sheet.
  3. Try the study on v2 before switching it: export it as xlsx, upload the file under a new name with Engine: v2, and walk through every page with Test Survey. Check the flagged items and the R-computed values.
  4. If the study uses the install or push items, configure the run as an installable app and check that its manifest loads.
  5. Add the copy to your run and set its v2 settings (offline queue, Previous button, layout, language, button labels). Move to v2 on the settings page works only for a survey that no run uses as a classic Survey; Switch back to v1 undoes it at any time.

Knit R & Markdown


This section gives some guidance on how to format and customise text in formr. In many cases you'll do it right by default. You'll also see how to access the data you just collected in formr in R — for example to score an assessment, give feedback, or customise the study in other ways.

Markdown

You can format text/feedback everywhere (i.e. item labels, choice labels, the feedback shown in pauses, stops, in emails) in a natural fashion using Github-flavoured Markdown.
The philosophy is that you write like you would in a plain-text email and Markdown turns it nice.
In most cases, characters with special meaning won't entail unintended side effects if you use them normally, but if you ever need to specify that they shouldn't have side effects, escape it with a backslash: \*10\* doesn't turn italic.

* list item 1
* list item 2

will turn into a nice bulleted list.

  • list item 1
  • list item 2

# at the beginning of a line turns it into a large headline, ## up to ###### turn it into smaller ones.

*italics* and __bold__ are also easy to do.

[Named links](https://yihui.name/knitr/) and embedded images ![image description](https://imgur.com/imagelink) are easy. If you simply paste a link, it will be clickable automatically too, even easier. Email addresses are a bit special, you need the "mailto:" prefix: [Contact us](mailto:contact_email@example.com).

You can quote something by placing a > at the beginning of the line.

If you're already familiar with HTML you can also use that instead, though it is a little less readable for humans. Or mix it with Markdown! You may for example use it to go beyond Markdown's features and e.g. add icons to your text using <i class="fa fa-smile-o"></i> to get for instance. Check the full set of available icons at Font Awesome.

Knitr

If you want to customise the text or generate custom feedback, including plots, you can use Knitr. Thanks to Knitr you can freely mix Markdown and chunks of R. You can load data using R commands, but the data you just collected for this participant will automatically be made available as R data frames. See R helpers for more information. Some examples:

  • Today is `r date()` shows today's date.
  • Hello `r demographics$name` greets someone using the variable "name" from the survey "demographics".
  • Dear `r ifelse(demographics$sex == 1, 'Sir', 'Madam')` greets someone differently based on the variable "sex" from the survey "demographics".
  • You can also plot someone's extraversion on the standard normal distribution.
    ```{r}
    library(formr)
    # build scales automatically
    big5 = formr_aggregate(results = big5)
    # standardise
    big5$extraversion = scale(big5$extraversion, center = 3.2, scale = 2.1)
    
    # plot
    qplot_on_normal(big$extraversion, xlab = "Extraversion")
    ```
            
    yields
    Graph of extraversion bell curve feedback

R Helpers


R in formr

In formr, you can use R to write simple and complex code. Various places allow you to specify either R code (e.g., showif column, value column, SkipForward/SkipBackward, External, Pause button conditions) or R code interspersed with Markdown (as in knitr, e.g., labels, Stop button, Pause button texts). The R code you wrote will be automatically enriched with the data objects you name and processed using OpenCPU. By default, your participants cannot view the R code you write.

Automatically enriched data

When you write R code in formr, we try to automatically determine what data you need and supply it. To do so, formr has a look up table of all the available data (the surveys defined in the run, the items defined in these surveys, as well as some metadata about the participant and run progress).

For example, to obtain somebody's age, you need only write demographics$age. formr will then automatically create a data frame named "demographics" containing the variable "age". To give another example, to see whether a participant ever reported a headache in your diary, you might just write any(diary$headache > 1). In this case, formr would create a data frame containing all responses to the headache question. It's important to note that formr simply checks whether the name of the survey exists anywhere in the text and whether the name of the item exists anywhere else. So, demographics$age works, but so does demographics[, 'age']. If an item name exists in multiple surveys that you have named, it will be supplied for all surveys.

Available data
user_id
The unique user code which we use for logging people in, e.g., NqbpASFVlcci5cnVvpMZG4ueILaYvFk39fDND305XvPLh3KW4xzrP0ygJ1phs1gf.
.formr
$login_link $login_code $nr_of_participants $session_last_active
Useful shortcuts to obtain the link to the personalised study link, the login code (currently the same as user_id), the total number of participants in the run (even those who only saw the first page), as well as a date-time when the current participant was last active.
YourSurvey
$YourItem1
$YourItem2
Any of the surveys that are part of the run and any of their items can be requested in this way. In addition, if you have named items belonging to a scale with a numeric suffix and an optional R, you need only name the scale (e.g., extraversion) and all items (e.g., extraversion1, extraversion2R, extraversion3) will be supplied.
survey_users
$created
$modified
$user_code
$email
$email_verified
$mobile_number
$mobile_verified
This data frame contains user account information, such as when the account was created, the user's contact details, and whether they have been verified. This is usually empty, because most study participants don't sign up on formr.
survey_run_sessions
$session
$created
$last_access
$ended
$position
$current_unit_id
$deactivated
$no_email
This data frame tracks user sessions in the run/study, including when they started the study (created), last accessed it, ended it (reached a Stop button), the current position in the run, and whether the user has opted out of email notifications.
survey_unit_sessions
$created
$ended
$expired
$unit_id
$position
$type
This data frame contains metadata about the progression of the user through the run/study, including when they reached each unit (created), left it (ended), and so on.
externals
$created
$ended
$position
Metadata about external units linked to the study, i.e. when users were sent there, whether they returned/completed the external unit (ended) and the position in the run.
survey_items_display
$created
$answered_time
$answered
$displaycount
$item_id
This data frame tracks the display and response behavior for survey items, including timestamps for when they were displayed and answered.
survey_email_log
$email_id
$created
$recipient
This data frame logs email interactions, including when an email was sent and its recipient.
shuffle
$unit_id
$created
$group
This data frame tracks shuffled units and the group they belong to for randomisation purposes.

Packages

Wherever you use R in formr you can also use the functions in its R package. If you want to use the package in a different environment, you'll need to install it using the following code.

install.packages('formr', repos = c('https://rforms.r-universe.dev', 'https://cloud.r-project.org'))

The package currently has the following feature sets

  • Some shorthand functions for frequently needed operations on the site:
    first(cars) # first non-missing value
    last(cars) # last non-missing value
    current(cars) # last value, even if missing
    "formr." %contains% "mr." # will yield TRUE
    "formr." %contains_word% "mr" # will yield FALSE
    "12, 15" %contains% "1" # will yield TRUE
    "12, 15" %contains_word% "1" # will yield FALSE
  • Some helper functions to make it easier to correctly deal with dates and times:
    time_passed(hours = 7) 
    next_day()
    in_time_window(time1, time2)
  • Connecting to formr, importing your data, correctly typing all variables, automatically aggregating scales.
  • Easily making feedback plots e.g.
    qplot_on_normal(0.8, "Extraversion")
    The package also has a function to simulate possible data, so you can try to make feedback plots ahead of collecting data.

Further data

Sometimes, you need more than the data that formr auto-enriches your study with. For example, you might have designed a couples' diary study and need the partner's data to synchronize participation. In these cases, you will have to explicitly load the data using formr's API.

Other times, you might want to import data from elsewhere on the web. You can R packages and functions to, for example, read a participant's social media posts or to look up information in an external, online database.

Features


The following designs and many more are possible:

  • simple surveys with and without feedback
  • complex surveys (using skipping logic, personalised text, complex feedback)
  • surveys with eligibility limitations
  • diary studies including completely flexible automated email/text message reminders
  • longitudinal studies (e.g. automatically re-contact participants after they return from their exchange year). The items of later waves need not exist in final form at wave 1.
  • longitudinal social networks and other studies that require rating a variable number of things or persons

Core strengths

  • generates very pretty feedback live, including ggplot2, and interactive ggvis plots and htmlwidgets. We find that this greatly increases interest and retention in our studies.
  • automates complex experience sampling, diary and training studies, including automated reminders via email, push notifications, or text message
  • looks nice on a phone (about 30-40% of participants fill out our surveys on a mobile device), can be installed as a PWA (Progressive Web App) on the home screen
  • easily share, swap and combine surveys (they're simply spreadsheets) and runs (you can share complete designs, e.g. "daily diary study")
  • you can use R to do basically anything that R can do (i.e. complicated stuff, like using a sentiment analysis of a participant's Twitter feed to decide when the survey happens)
  • not jealous at all – feel free to integrate other components (other survey engines, reaction time tasks, whatever you are used to) with formr, we tried our best to make it easy.

Features

  • manage access to and eligibility for studies
  • longitudinal studies
  • send text messages (see the HowTo) and push notifications
  • works on all somewhat modern devices and degrades gracefully where it doesn't
  • formats text using Github-flavoured Markdown (a.k.a. the easiest and least bothersome way to mark up text)
  • file, image, video, sound uploads for users (as survey items) and admins (to supply study materials)
  • complex conditional items
  • a dedicated formr R package: makes pretty feedback graphs and complex run logic even simpler. Simplifies data wrangling (importing, aggregating, simulating data from surveys).
  • a nice editor, Ace, for editing Markdown & R in runs.

Plans:

  • work offline on mobile phones and other devices with intermittent internet access (in the meantime enketo is pretty good and free too, but geared towards humanitarian aid)
  • social networks, round robin studies - at the moment they can be implemented, but are a bit bothersome at first. There is a dedicated module already which might also get released as open source if there's time.
  • more planned enhancements on Github

formr API


The formr API is the way to get data out of, push surveys into, and orchestrate runs on the platform from your own code. There are two surfaces:

  • v1 (recommended) — a resource-oriented REST API rooted at /api/v1/. Covers runs, surveys, sessions, results, files, and your account profile. OAuth2 client_credentials grant for authentication, scope-based authorisation, optional per-credential restriction to specific runs.
  • Legacy /get/results — the older results-fetching endpoint. Still works for back-compat, documented at the bottom of this page. New integrations should use v1.

The easiest way to consume the v1 API is the formr R package (the formr_api_* family). The reference below is the underlying HTTP contract for callers in any language.

API base URL:
https://api.researchmixtape.com

API credentials are not the same as a study's “R Secrets”. To let someone read a run's data from outside formr — without giving them your account login — you create an API credential on your account page (Account → API Credentials), as in step 1 below. A key you add under a run's Settings → R Secrets tab is a different thing: it is a value (e.g. an external service's key) that your own R code can read while the study runs, reached as .formr$secret_<name>. R Secrets never grant anyone access to the formr API. If you created one (e.g. secret_TestAPI) in order to share data, that is the wrong place — delete it and follow step 1 instead.

1. Get API credentials

This section covers calling the API from outside formr. If you only want to use the API from R code running inside your own study (showif, value, label, condition, page or email body, overview script, External unit), you don't need a credential at all — see “Using the formr API in R” below.

API access requires admin level 2 on your account. If you only have admin level 1 (the default for new accounts), open the API Credentials tab on your account page and follow the support-email prompt to request access.

Once your level is set, open Account → API Credentials (the API tab on your account page). You can hold multiple credentials side by side — each one with its own scope set and run allowlist. A common pattern is one narrow read-only credential for a dashboard, plus a separate broader credential for a cron job. Deleting one credential does not affect the others.

To create a credential you will be asked to:

  1. Give the credential a label. Used only to tell credentials apart in the UI (e.g. dashboard, cron-2026). Must be unique within your account; the label internal is reserved.
  2. Pick the scopes this credential should grant. Each scope is one verb on one resource family:
    user:read / user:writeRead / update your account profile
    survey:read / survey:writeRead survey definitions / upload + edit them
    run:read / run:writeRead run metadata / create + update + delete runs
    session:read / session:writeRead participant sessions / create + advance them
    data:readRead participant response data
    file:read / file:writeDownload / upload files attached to runs
    A credential with only run:read will succeed on GET /v1/runs/{name} and 403 on PATCH /v1/runs/{name}.
  3. Optionally restrict the credential to specific runs. Leave the run picker empty to allow this credential to act on all of your runs. Tick one or more runs to narrow it. A run-restricted credential implicitly restricts which surveys it can touch — only surveys that appear as units in one of the allowlisted runs are reachable. Brand-new survey creation is blocked for run-restricted credentials (the new survey would be unreachable until you linked it into a run).
  4. Click Create credential. The client_id and client_secret are shown once. Copy both immediately — the server stores only a SHA-256 hash, so a forgotten secret has to be rotated, not recovered.

Once you have at least one credential, the API tab shows a table of all of them with their labels, scopes, and run counts. Each row has a Rotate button (mints a new client_secret while keeping the same client_id; optionally update the scopes / runs at the same time) and a Delete button (revokes the credential immediately; any service still using it will start getting 401 on the next call).

Recipe: give a collaborator read-only access to one run's data

A common case: you want a colleague to download one study's responses, but they must not see your other runs, change anything, or get your account login. Create a credential shaped exactly for that:

  1. Label it for the person or purpose, e.g. colleague-datadownload.
  2. Tick only data:read (add run:read if they also need the run's metadata, and file:read for participant-uploaded files). Leave every :write scope unticked — the token then 403s on any attempt to change something.
  3. In the run picker, select only that one run. This is the allowlist: the credential 403s on every other run, and on any survey that isn't a unit of the selected run.
  4. Create it, then send your collaborator three things: the client_id, the one-time client_secret, and the API base URL (https://api.researchmixtape.com). That is all they need — no formr account, no password.

They fetch the responses with GET /v1/runs/{name}/results (scope data:read), or the equivalent helper in the formr R package. When the project ends, click Delete on the credential and their access is revoked immediately — your other runs were never reachable through it.

2. Mint an access token

Exchange the client credentials for a bearer access token. Tokens are short-lived (1 hour by default) and stored as a SHA-256 hash on the server, so a database compromise alone does not expose replayable tokens.


POST /oauth/access_token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id={client-id}&client_secret={client-secret}

Successful response:


{
    "access_token": "XXXXXX3635f0dc13d563504b4d",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "run:read run:write survey:read"
}

The scope field echoes the scopes you picked when generating the credential. If the field is empty (""), the credential was created with no scopes selected and every API call will 403 — rotate it on the API Credentials tab and pick at least one scope.

Error response (invalid client):


{
    "error": "invalid_client",
    "error_description": "The client credentials are invalid"
}

3. Call resource endpoints

Send the token in the Authorization header on every request:


Authorization: Bearer {access-token}

Resources currently exposed under /api/v1/:

EndpointMethod → scope required
/v1/user/meGET → user:read
/v1/runsGET (list) → run:read
/v1/runs/{name}GET → run:read; POST / PATCH / DELETE → run:write
/v1/runs/{name}/sessionsGET → session:read; POST / DELETE → session:write
/v1/runs/{name}/unit_sessionsGET → session:read
/v1/runs/{name}/resultsGET → data:read
/v1/runs/{name}/filesGET → file:read; POST / DELETE → file:write
/v1/runs/{name}/structureGET → run:read; PUT → run:write
/v1/surveysGET (list) → survey:read; POST (upload) → survey:write
/v1/surveys/{name}GET → survey:read; PATCH / DELETE → survey:write

A scope check happens before the resource is looked up — so a token without run:write gets a 403 on PATCH /v1/runs/foo regardless of whether foo exists or belongs to your account.

Response envelope

Success bodies are the resource (or array of resources) directly. List endpoints (GET /v1/runs, GET /v1/surveys) return a bare JSON array. Detail endpoints return a single object. Error bodies are {"code": <int>, "message": "<text>"} with the HTTP status carrying the same code.

Error shapes you'll see when a scope or allowlist is wrong

// Token is missing the verb scope this endpoint needs.
HTTP/1.1 403 Forbidden
{"code": 403, "message": "Insufficient permissions: 'run:write' scope required."}

// Credential's run allowlist doesn't include this run.
HTTP/1.1 403 Forbidden
{"code": 403, "message": "This API client is not authorized for run 'foo'."}

// Credential's run allowlist doesn't include any run that uses this survey.
HTTP/1.1 403 Forbidden
{"code": 403, "message": "This API client is not authorized for survey 'bar'."}

// Run-restricted credentials cannot create brand-new surveys.
HTTP/1.1 403 Forbidden
{"code": 403, "message": "Cannot create surveys with a run-restricted API client; add the survey to a run via the admin UI first, then update it via the API."}

All four fix paths route through the same place: open the API Credentials tab, find the credential row in the table, click Rotate, adjust the scope tickboxes or the run picker, and confirm. The client_id stays the same; only the client_secret changes. (Or create a fresh credential with the right shape and delete the old one once your callers have migrated.)

4. Example: list runs and read one


# 1) Get a token
ACCESS_TOKEN=$(curl -s -X POST https://api.researchmixtape.com/oauth/access_token \
  -d grant_type=client_credentials \
  -d client_id=$CLIENT_ID \
  -d client_secret=$CLIENT_SECRET | jq -r .access_token)

# 2) List runs visible to this credential
curl -s https://api.researchmixtape.com/v1/runs \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

# 3) Read one run (subject to the credential's run allowlist if any)
curl -s https://api.researchmixtape.com/v1/runs/my-diary \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

5. Using the formr API in R

The R package handles auth, token refresh, and scoping-aware error hints. Installation and a full walk-through are in the Getting Started vignette.

Calling the API from inside your own study? You need no credential at all. When your R code runs on this server via OpenCPU — a showif, an item value or label, a branch condition, a page or email body, an External unit URL, or an overview script — formr auto-fills the credentials for you. Just call formr_api_authenticate() with no arguments (formr injects the host and a token the moment your code contains that call), then use the formr_api_*() helpers as normal. The injected token is owner-scoped, restricted to the current run, carries user:read session:read session:write run:read data:read, and expires after 180 seconds — enough for one OpenCPU call, with no long-lived secret left in your study code. The formr_store_keys() step below is only for driving the API from outside formr (your laptop, a dashboard, a cron job).

library(formr)

# One-time: store the credentials you generated at admin/account#api
formr_store_keys(
    host = "https://api.researchmixtape.com",
    client_id = "YOUR_CLIENT_ID",
    client_secret = "YOUR_CLIENT_SECRET"
)

# Authenticate (auto-picks up stored keys; also auto-picks up the
# embedded token when called inside an OpenCPU R block on this server)
formr_api_authenticate(host = "https://api.researchmixtape.com")

# Inspect which scopes the credential carries
formr_api_session()$scope
#> [1] "run:read run:write survey:read"

# Call resource helpers
runs    <- formr_api_runs()
details <- formr_api_get_run("my-diary")

# Per-unit interaction history — every (participant x unit x iteration)
# row, ordered so consecutive entries per session are trajectory edges.
# This is what the default Overview script's Sankey is built on.
us <- formr_api_unit_sessions("my-diary", testing = FALSE)

Unit-session history

GET /v1/runs/{name}/unit_sessions is the history view that complements /v1/runs/{name}/sessions (which exposes only each participant's current unit). One JSON row per survey_unit_sessions row, ordered by (session, created, unit_session_id) so consecutive rows per participant are trajectory edges.

Filters: ?session=<code> (or comma-list) to narrow to specific participants; ?testing=true|false to split real vs. test sessions; ?since=<ISO 8601> for incremental polling. Pagination via limit (default 1000, max 10000) and offset. Scope: session:read.

Special units (OverviewScriptPage, ServiceMessagePage, ReminderEmail) surface with position = null because they live outside the ordered run flow. The Track A state enum is one of PENDING, RUNNING, WAITING_USER, WAITING_TIMER, ENDED, EXPIRED, or SUPERSEDED (NULL on legacy rows from before patch 047).

Legacy: /get/results (V0)

The older results endpoint still works for back-compat. Same OAuth token flow as v1; the difference is the URL shape and that /get/results returns all surveys of a run in one call rather than per-survey under /v1/runs/{name}/results. New integrations should use the v1 endpoints above.


GET /get/results?
     run[name]={name of the run}
    &run[sessions]={comma-separated list of session codes; empty = all}
    &surveys[survey_name_1]={comma-separated items; empty = all}
    &surveys[survey_name_2]={comma-separated items; empty = all}

Authorization: Bearer {access-token}

Response: an object keyed by survey name, each value an array of session-keyed result rows.

Help


Where to get help

If you're a participant in one of the studies implemented in formr, please reach out to the person running the study.

If you're running a study yourself, there's several places to look for help.

  • this documentation is a good start, just click on any of the tabs above.
  • There is a Wiki on Github. You can find a number of HowTos there and contribute yourself.
  • You'll find answers to some frequently asked questions there too.
  • You can ask (and answer!) questions in our Github Discussions forum for community support. Previously, we used a mailing list, but spam made it unusable. You can still browse it.
  • If you find a bug, this is the place to describe it (preferably in a way that allows us to reproduce it, but we're also accepting Yeti reports).
  • Please do not email the creators of formr for general support unless you've bought one of our consulting packages. Do reach out if you think sharing your concerns publicly could result in security concerns.