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.
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/
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.
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.
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.
?_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.
?_new_session=1&rated=P1 lands on ?code=…&rated=P1 and rated is still available to your survey's items.?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.
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.
Using an SMTP account (most email addresses come with one) that you can set up in the mail section, you can send emails to your participants, their friends or yourself. Using the tag {{login_link}}, you can send participants a personalised link to the run. You can also use {{login_code}} to use the session code to create custom links, e.g. for inviting peers to rate this person (informants). Many ISPs limit using their SMTP server to send automated email. Gmail users cannot send more than 500 emails a day and have to disable some advanced security features. Vendors like Sendgrid offer free student accounts as part of the Github education pack and are more amenable to automated emails via SMTP.
A simple one-shot survey with feedback. Let's say your run contains
email_address).
big5$email_address.
A participant fills out your survey. After completing it, they see the feedback page, which contains a bar chart of their individual big 5 scores. Before they see the page marked by the stop point, an email containing the same feedback is sent off to their email address - this way they get a take-home copy as well.
A simple one-shot survey after which you receive a notification.
'youremailaddress@example.org'. Note the single quotes, they mean that this is a constant.
A participant fills out your survey. After completing it, they see the thank you note at pos. 30. Before they see the page marked by the stop point, an email is sent off to youremailaddress@example.org - this way you (or whoever's email address you use here) would get an email notification for every participant. This might be helpful in longitudinal surveys where experimenter intervention is required to e.g. set up a phone interview, in an assessment context when you want the scores to be automatically generated, or in a clinical study where you want to do a structured interview after a screening task.
See the Knitr & Markdown section to find out how to generate personalised emails, which contain feedback, including plots. In the next section, you'll learn how to use the email module for invitations in a diary study.
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.
A simple diary. Let's say your run contains
nrow(diary) < 14 and the instructions to jump back to position 20, the pause, if that is true.
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.
But you can also make a loop that doesn't involve user action, to periodically check for external events:
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.
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.29.5 and 30.4 both mean position 30. Rounding happens first, so 1.9 becomes 2 and does jump.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.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 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).
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.
Let's say your run contains
depression$suicidal != 1. If the person is not suicidal, it skips forward to pos 40.
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.
Let's say your run contains
optimism$pessimist == 1. If the person is a pessimist, it skips forward to pos 50.
TRUE, so it always skips forward to pos 60.
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.
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.29.5 and 30.4 both mean position 30. Rounding happens first, so 1.9 becomes 2 and does jump.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.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 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.
Let's say your run contains
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 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:
__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 |
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:
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.
* in this column, you can turn items optional instead. Using ! requires a response to items that are optional by default (check, check_button).
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).
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:
An instruction is shown first, followed by items 1–4 in random order.
| name | block_order | item_order |
|---|---|---|
| instr | 1 | |
| item_1 | 2 | |
| item_2 | 2 | |
| item_3 | 2 | |
| item_4 | 2 |
Blocks A and B are presented in random order; within each block the items keep their order.
| name | block_order | item_order |
|---|---|---|
| item_1 | A | 1 |
| item_2 | A | 2 |
| item_3 | B | 1 |
| item_4 | B | 2 |
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.)
| name | block_order | item_order |
|---|---|---|
| item_1 | A | 1 |
| item_2 | A | 1 |
| submit1 | 2 | |
| item_3 | B | 3 |
| item_4 | B | 3 |
| submit2 | 4 |
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.
| name | block_order | item_order |
|---|---|---|
| item_1 | A | 1 |
| item_2 | A | 1 |
| submit1 | A | 2 |
| item_3 | B | 1 |
| item_4 | B | 1 |
| submit2 | B | 2 |
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 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 |
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.
showif or for dynamically generating item text have been given.
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.
text 100 defines the maximum number of characters that may be entered.
step defaults to 1, using any will allow any decimals.
A-Za-züäöß.;,!: ), no numbers.
1,100,1.
0,100,1. Works in both survey and Form (v2) units.
2013-01-01,2014-01-01 or -2years,now.
12:00,17:00. Stored as a time (HH:MM:SS), so in R you get a string, not a number.
datetime-local control). Stored as a DATETIME.
datetime control, so prefer datetime_local.
yyyy-mm. Stored as a DATE on the first of that month.
yyyy-Www. Stored as text.
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 but instead of the text appearing next to a small button, a big button contains each choice label
min,max,step in between. Defaults to 1,5,1.
mc_button with the ♂, ♀ symbols as choices
Europe/Berlin). No choice list needed; the chosen zone name is stored as text.
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.
3, 1, 2 for "3 ranked first" — strsplit(rank, ", ") in R recovers the ranking.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.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.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 3 makes the puzzle harder (difficulty 1–3, default from the server settings).verified is stored, never the token itself, so there is nothing participant-identifying in your results table.hidden item with the same value when the form needs the result, or compute it in the R of the unit that uses it.
server HTTP_USER_AGENT.
var from the query string, so in the example above get param1 would lead to 10 being stored.
(item1 + item2) > 100 to add further requirements.
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:
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.
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.
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:
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.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.
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:
calculate, see below) are refused at upload.
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:
| Setting | Default | What it does |
|---|---|---|
| Enable offline queue | on | A 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" button | off | Lets 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. |
| Layout | Default | Default — multiple items per page or Solo — one item per screen. See Layouts. |
| Participant language | en | A 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 labels | empty | Your 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 v2Two columns of the item table decide what a participant sees and what is pre-filled. In v2 they follow two rules:
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.value is R. Anything that is not empty or a plain number is sent to R on the server, as written.showifEvery 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):
| Helper | Meaning |
|---|---|
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.
valueA 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:
hidden item with the same value — see below;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:
| name | type | showif | value |
|---|---|---|---|
treat | hidden | ifelse(demographics$age > 30, 1, 2) | |
older_block | note | treat == 1 | |
younger_block | note | treat == 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.
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).
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:
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.| Column | Bucket | Meaning | What to do |
|---|---|---|---|
showif | empty | no expression | — |
| JS-OK | no R in it | nothing; 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$item | Rewrite 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 hidden | compute the value in a hidden item's value on this form and reference that item — see Bridging R into a showif | |
type | not supported in v2 (flagged, blocks the switch) | a calculate item, or another item that takes no input but carries R in value | replace it as described under calculate items are not supported |
value | empty | no default | — |
| literal | a number, used as-is | — | |
| R | everything else, including sticky; evaluated on the server | nothing — 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.
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).
submit item) are shown together, as in the classic engine. A progress line reads Page 2 of 5.submit item uses that button's label instead.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.
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).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.
| Moment | What happens | Indicator |
|---|---|---|
| An answer changes | Autosave. 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 closed | Pending 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 / Submit | Page 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 submitted | A 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.
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:
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.
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.
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.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).
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:
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.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.
| Setting | Default | What it does |
|---|---|---|
form_v2_enabled | false | Offers 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_limits | 30 / 90 / 10 / 20 per minute | Per 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_ttl | 300 s | How long a resolved value, label or choice label is reused for the same participant and the same answers. |
form_v2_offline_blob_max_mb | 10 | Largest file a page may carry into the offline queue. |
bot_check_* | see the operator's guide | Difficulty, memory and lifetime of the bot_check puzzle, and its signing secret (derived from the instance's encryption key by default). |
calculate item (how). Upload the corrected sheet.
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.
# 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  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.
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".```{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
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.
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.
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
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 FALSEtime_passed(hours = 7)
next_day()
in_time_window(time1, time2)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.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.
The following designs and many more are possible:
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:
/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.
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
.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.
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:
dashboard, cron-2026). Must be unique within your account; the label
internal is reserved.
user:read / user:write | Read / update your account profile |
survey:read / survey:write | Read survey definitions / upload + edit them |
run:read / run:write | Read run metadata / create + update + delete runs |
session:read / session:write | Read participant sessions / create + advance them |
data:read | Read participant response data |
file:read / file:write | Download / upload files attached to runs |
run:read will succeed on GET /v1/runs/{name} and 403 on PATCH /v1/runs/{name}.
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).
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:
colleague-datadownload.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.
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.
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"
}
Send the token in the Authorization header on every request:
Authorization: Bearer {access-token}
Resources currently exposed under /api/v1/:
| Endpoint | Method → scope required |
|---|---|
/v1/user/me | GET → user:read |
/v1/runs | GET (list) → run:read |
/v1/runs/{name} | GET → run:read; POST / PATCH / DELETE → run:write |
/v1/runs/{name}/sessions | GET → session:read; POST / DELETE → session:write |
/v1/runs/{name}/unit_sessions | GET → session:read |
/v1/runs/{name}/results | GET → data:read |
/v1/runs/{name}/files | GET → file:read; POST / DELETE → file:write |
/v1/runs/{name}/structure | GET → run:read; PUT → run:write |
/v1/surveys | GET (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.
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.
// 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.)
# 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
The R package handles auth, token refresh, and scoping-aware error hints. Installation and a full walk-through are in the Getting Started vignette.
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)
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).
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.
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.