The most common way to lose data in an online experiment is also the most avoidable one: the participant finishes the last trial, gets redirected back to Prolific, SONA or Mechanical Turk, and the final upload never completes. This guide explains how Cognition protects that last step, and how to wire up participant IDs, completion codes and return URLs for each recruitment platform.
Recruitment platforms need two things from your experiment: a way to know who took part (a participant ID passed into your study URL) and a way to know that they finished (a completion code or a return URL). The second part is where things go wrong. If your code sends the participant away with window.location.href = ... while the browser is still uploading the last block of trials, the navigation cancels the request and the run ends up incomplete or empty.
This problem is well known in the jsPsych community. Reports of "data saving incomplete" when returning participants to Prolific (for example, jsPsych discussion #1850) almost always trace back to the same cause: calling jsPsych.endExperiment() or redirecting manually before the server has confirmed that it received the data. The jsPsych maintainers' recommendation is to redirect from the global on_finish callback and to let any asynchronous saving finish first. Since jsPsych 7, on_finish can return a Promise precisely for this reason.
When your task runs on Cognition, the platform wraps the on_finish callback you pass to initJsPsych(). The sequence at the end of every run is:
jsPsych.endExperiment() / jsPsych.abortExperiment()).on_finish.
Trials are also streamed to the server while the experiment is running, and a service worker buffers data locally if the participant goes offline, so the final upload is usually tiny. The practical rule is simple: put your redirect inside the global on_finish of initJsPsych() and nothing else. Don't redirect from the on_finish of an individual trial, from a call-function plugin, or from a setTimeout.
const jsPsych = initJsPsych({
on_finish: function () {
// Cognition has already uploaded every trial at this point.
window.location.href = "https://app.prolific.com/submissions/complete?cc=ABC123";
}
});
If you prefer to show a completion code instead of redirecting (SONA with manual credit granting, or MTurk where the worker pastes a code), display it in a final html-keyboard-response trial with choices: "NO_KEYS" and leave on_finish empty. Either way the data is safe.
Every recruitment platform appends identifiers to your study link as query parameters. Cognition reads every query parameter of the run URL and adds it as a column to every row of the collected data, so you don't need to write any code to store them. You can still read them in your code with jsPsych.data.getURLVariable() when you need to branch on them.
| Platform | Study URL to configure | Columns you will see in your data |
|---|---|---|
| Prolific | https://<task>.cognition.run?PROLIFIC_PID={{%PROLIFIC_PID%}}&STUDY_ID={{%STUDY_ID%}}&SESSION_ID={{%SESSION_ID%}} |
PROLIFIC_PID, STUDY_ID, SESSION_ID |
| SONA Systems | https://<task>.cognition.run?survey_code=%SURVEY_CODE% |
survey_code |
| Mechanical Turk | https://<task>.cognition.run?workerId=...&assignmentId=...&hitId=... (built by your HIT template) |
workerId, assignmentId, hitId |
Replace <task> with the token shown on your task page. Prolific fills in the curly-brace placeholders when it opens the study; SONA replaces %SURVEY_CODE% with a unique per-participant code.
Reading a parameter in code, for example to show a different welcome message or to reject an empty ID:
const prolificId = jsPsych.data.getURLVariable("PROLIFIC_PID");
if (!prolificId) {
// Opened outside Prolific (e.g. a preview): tag the run so you can filter it out later.
jsPsych.data.addProperties({ source: "direct" });
}
PROLIFIC_PID, STUDY_ID and SESSION_ID already appended. Use that link as is.https://app.prolific.com/submissions/complete?cc=XXXXXX.on_finish as shown above.
Prolific lets you define several completion codes with different actions (approve, flag for review, screen out). Because Cognition calls your on_finish only after the upload, you can safely pick the code at the very end, for example based on attention checks:
const jsPsych = initJsPsych({
on_finish: function () {
const failedChecks = jsPsych.data.get().filter({ task: "attention", correct: false }).count();
const code = failedChecks > 1 ? "REVIEW01" : "APPROVE1";
window.location.href = "https://app.prolific.com/submissions/complete?cc=" + code;
}
});
Declined consent. If you use Cognition's informed consent screen, set the decline URL of the task to your Prolific "screened out" or "return" URL. Participants who decline are sent straight back without creating a data file.
https://<task>.cognition.run?survey_code=%SURVEY_CODE%.https://yourschool.sona-systems.com/webstudy_credit.aspx?experiment_id=123&credit_token=abcd1234&survey_code=XXXX.on_finish, rebuild that URL with the survey_code of the current participant:
const jsPsych = initJsPsych({
on_finish: function () {
const surveyCode = jsPsych.data.getURLVariable("survey_code");
window.location.href =
"https://yourschool.sona-systems.com/webstudy_credit.aspx" +
"?experiment_id=123&credit_token=abcd1234&survey_code=" + surveyCode;
}
});
When the participant lands on that URL, SONA grants the credit automatically. Because survey_code is also stored as a column in your data, you can later match credits against completed runs without any manual bookkeeping.
MTurk does not redirect. The usual approach is a HIT that links to your Cognition task, followed by a completion code the worker pastes back into the HIT. Generate a code you can verify afterwards, store it in the data, and display it on the last screen:
const completionCode = jsPsych.randomization.randomID(8).toUpperCase();
jsPsych.data.addProperties({ completion_code: completionCode });
timeline.push({
type: jsPsychHtmlKeyboardResponse,
stimulus: "<p>Thank you! Your completion code is:</p><h2>" + completionCode + "</h2>" +
"<p>Copy it into the MTurk window to receive payment.</p>",
choices: "NO_KEYS"
});
Since workerId is saved with every trial, you can export the data, filter by completion_code and approve assignments in bulk from the MTurk requester interface. If you use a turkSubmitTo-style form instead, submit it from the global on_finish for the same reason as before: by then the data has been uploaded.
on_finish. Never from a trial, a plugin or a timer.beforeunload hacks or navigator.sendBeacon to "rescue" data; Cognition already uploads continuously and blocks accidental tab closing while data is pending.PROLIFIC_PID / survey_code column.window.CONDITION; you don't need to derive conditions from PROLIFIC_PID.
Losing the last block of data at the redirect is a solved problem on Cognition: the platform uploads every trial before it hands control back to your on_finish, so the only thing you need to remember is to put the return URL there. Participant identifiers arrive for free as columns in your data, and each recruitment platform only needs its return URL or completion code configured once. Create a free account, paste your jsPsych code, and share the study link with Prolific, SONA or MTurk.