14  Saving and Labeling Data

learning goals
  • Understand how jsPsych stores data and the difference between browser memory and permanent storage
  • Add meaningful labels using the data parameter, timeline data, and timeline variables
  • Apply global properties to all trials using jsPsych.data.addProperties()
  • Save data locally for testing using jsPsych.data.get().localSave()

14.1 How jsPsych Stores Data

Before we dive into labeling data, it’s important to understand how jsPsych actually saves your experimental data. jsPsych uses a simple but powerful principle: one event per row. Every time something happens in your experiment (a participant responds to a stimulus, views an instruction screen, or completes any trial) jsPsych creates a new row of data.

Let’s start with a simple example to see this in action:

// 1. Initialize jsPsych
const jsPsych = initJsPsych();

// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<p>Welcome to the experiment!</p><p>Press any key to continue.</p>'
};

const question1 = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>',
  choices: ['y', 'n']
};

const question2 = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>',
  choices: ['y', 'n']
};

const goodbye = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<p>Thank you for participating!</p><p>Press any key to finish.</p>'
};

// 3. Run jsPsych with our trials
jsPsych.run([welcome, question1, question2, goodbye]);
Live JsPsych Demo Click inside the demo to activate demo

When this simple experiment runs, jsPsych automatically collects data for each trial. Here’s what the resulting CSV file would look like:

rt stimulus response trial_type trial_index plugin_version time_elapsed
4128 <p>Welcome to the experiment!</p><p>Press any key to continue.</p> k html-keyboard-response 0 2.1.0 4128
1556 <p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p> y html-keyboard-response 1 2.1.0 5689
411 <p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p> y html-keyboard-response 2 2.1.0 6101

Notice that we have three rows, one for each trial that ran. jsPsych automatically includes several pieces of information in each row. The trial_type column tells us which plugin was used, while trial_index shows the order in which trials occurred (starting from 0). The time_elapsed column records the total time since the experiment started in milliseconds, and rt shows the response time for this specific trial. The stimulus column contains what was presented to the participant, response records what key they pressed, and plugin_version tells us which version of jsPsych was used.

It’s important to understand that jsPsych saves a row for everything that happens in your experiment. Instruction screens, fixation crosses, feedback screens, and actual experimental trials all get their own rows. This means you’ll often need to filter your data during analysis to focus only on the trials that contain your actual experimental responses.

While this data captures the basic mechanics of what happened, notice how little meaningful information we actually have about our experiment. Looking at row 2, we can see that the participant pressed ‘y’ in response to the chocolate question, but we have no easy way to know what ‘y’ means, whether this was correct, or what type of question this was. The same problem exists for row 3 with the vanilla question. Without additional context, we can’t easily determine what their response pattern means or how to categorize these responses for analysis. .

To best understand why this is a problem, we can return to the memory experiment in Chapter 11. Let’s have a look at our code and the resulting data:

// 1. Initialize jsPsych
const jsPsych = initJsPsych();

// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `Welcome to the Experiment! Press any key to begin.`,
  choices: "ALL_KEYS", 
  post_trial_gap: 500
}

const instructions = {
  type: jsPsychInstructions,
  pages: [
    // Page 1: Welcome and Overview
    `<div class='instructionStyle'>
      <p>You will participate in a memory experiment with two phases:</p>
     <p><strong>Phase 1:</strong> Study a list of words</p>
     <p><strong>Phase 2:</strong> Decide if words are OLD or NEW</p>
     <p>Click Next to continue.</p>
     </div>`,

    // Page 2: Study Phase Instructions  
    `<div class='instructionStyle'>
      <p>During the study phase, you will see words appear one at a time. Read each word carefully and try to remember it.</p>
     <p>Each word appears for 1 second.You will study about 15 words total.</p>
     <p>You do not need to press any keys during this phase.</p>
     </div>`,

    // Page 3: Test Phase Instructions
    `<div class='instructionStyle'>
      <p>During the test phase, you will see words one at a time. Some words are OLD (from the study list). Some words are NEW (not from the study list).</p>
     <p>Press the 'OLD' button if the word was old.</p>
     <p>Press the 'NEW' button if the word was new</p>
     </div>`,

    // Page 4: Stay focused
    `<div class='instructionStyle'>
      <p>Trust your first instinct.</p>
      <p>If unsure, make your best guess.</p>
    </div>`,

    // Page 5: Final Instructions
    `<div class='instructionStyle'>
      <p>Try to stay focused throughout the experiment. The experiment only takes a few minutes.</p>
      <p>Click Next to start the study phase.</p>
     </div>`
  ],
  key_forward: 'ArrowRight',
  key_backward: 'ArrowLeft',
  allow_backward: true,
  show_clickable_nav: true,
  button_label_previous: 'Back',
  button_label_next: 'Next'
};


let study = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: "+",
      choices: "NO_KEYS",
      post_trial_gap: 250,
      trial_duration: 500,
      css_classess: "wordStyle"
    },
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable("word"),
      choices: "NO_KEYS",
      post_trial_gap: 500,
      trial_duration: 1000,
      css_classess: "wordStyle"
    }
  ],
  timeline_variables: [
        {word: "BED"},
        {word: "REST"},
        {word: "AWAKE"},
        {word: "TIRED"},
        {word: "DREAM"},
        {word: "WAKE"},
        {word: "SNOOZE"},
        {word: "BLANKET"},
        {word: "DOZE"},
        {word: "SLUMBER"},
        {word: "SNORE"},
        {word: "NAP"},
        {word: "PEACE"},
        {word: "YAWN"},
        {word: "DROWSY"}
  ],
  randomize_order: true
}

let test = {
  timeline: [
    {
      type: jsPsychHtmlButtonResponse,
      stimulus: jsPsych.timelineVariable("word"),
      post_trial_gap: 500,
      choices: ["OLD", "NEW"],
      css_classess: "wordStyle"
    }
  ],
  timeline_variables: [
         // OLD WORDS 
        {word: "BED"},
        {word: "REST"},
        {word: "AWAKE"},
        {word: "TIRED"},
        {word: "DREAM"},
        {word: "WAKE"},
        {word: "SNOOZE"},
        {word: "BLANKET"},
        {word: "DOZE"},
        {word: "SLUMBER"},
        {word: "SNORE"},
        {word: "NAP"},
        {word: "PEACE"},
        {word: "YAWN"},
        {word: "DROWSY"},
        // NEW WORDS
        {word: "DOCTOR"},
        {word: "NURSE"},
        {word: "SICK"},
        {word: "LAWYER"},
        {word: "MEDICINE"},
        {word: "HEALTH"},
        {word: "HOSPITAL"},
        {word: "DENTIST"},
        {word: "PHYSICIAN"},
        {word: "ILL"},
        {word: "PATIENT"},
        {word: "OFFICE"},
        {word: "STETHOSCOPE"},
        {word: "SURGEON"},
        {word: "CLINIC"},
        {word: "CURE"},
        // critical word 
        {word: "SLEEP"}
  ],
  randomize_order: true
}

// 3. Run jsPsych with our trials
jsPsych.run([
  welcome,
  instructions,
  study,
  test
]);

The resulting data from running this code looks like this:

rt stimulus response trial_type trial_index plugin_version time_elapsed view_history
9689 Welcome … a html-keyboard-response 0 2.1.0 9690
3973 instructions 1 2.1.0 14173 [{" page_index":0, "viewing_time":1014} ... { "page_index":4, "viewing_time":1093 }]
null + null html-keyboard-response 2 2.1.0 14685
null SNOOZE null html-keyboard-response 3 2.1.0 15952
null + null html-keyboard-response 4 2.1.0 16954
null AWAKE null html-keyboard-response 5 2.1.0 18205
null + null html-keyboard-response 6 2.1.0 19206
null BLANKET null html-keyboard-response 7 2.1.0 20491
null + null html-keyboard-response 8 2.1.0 21491
null PEACE null html-keyboard-response 9 2.1.0 22743
null + null html-keyboard-response 10 2.1.0 23743
null SNORE null html-keyboard-response 11 2.1.0 24994
null + null html-keyboard-response 12 2.1.0 26012
null YAWN null html-keyboard-response 13 2.1.0 27262
null + null html-keyboard-response 14 2.1.0 28264
null SLUMBER null html-keyboard-response 15 2.1.0 29531
null + null html-keyboard-response 16 2.1.0 30532
null REST null html-keyboard-response 17 2.1.0 31783
null + null html-keyboard-response 18 2.1.0 32801
null TIRED null html-keyboard-response 19 2.1.0 34051
null + null html-keyboard-response 20 2.1.0 35053
null DROWSY null html-keyboard-response 21 2.1.0 36320
null + null html-keyboard-response 22 2.1.0 37321
null DREAM null html-keyboard-response 23 2.1.0 38571
null + null html-keyboard-response 24 2.1.0 39573
null WAKE null html-keyboard-response 25 2.1.0 40823
null + null html-keyboard-response 26 2.1.0 41824
null BED null html-keyboard-response 27 2.1.0 43075
null + null html-keyboard-response 28 2.1.0 44092
null NAP null html-keyboard-response 29 2.1.0 45344
null + null html-keyboard-response 30 2.1.0 46347
null DOZE null html-keyboard-response 31 2.1.0 47615
2016 STETHOSCOPE 0 html-button-response 32 2.1.0 50150
407 BLANKET 1 html-button-response 33 2.1.0 51072
313 DROWSY 0 html-button-response 34 2.1.0 51894
302 REST 0 html-button-response 35 2.1.0 52700
328 HEALTH 0 html-button-response 36 2.1.0 53546
362 SLUMBER 1 html-button-response 37 2.1.0 54413
250 PATIENT 0 html-button-response 38 2.1.0 55170
388 YAWN 0 html-button-response 39 2.1.0 56072
412 DENTIST 0 html-button-response 40 2.1.0 56997
568 ILL 0 html-button-response 41 2.1.0 58071
962 DREAM 0 html-button-response 42 2.1.0 59535
433 SLEEP 1 html-button-response 43 2.1.0 60474
389 MEDICINE 1 html-button-response 44 2.1.0 61380
426 LAWYER 1 html-button-response 45 2.1.0 62319
449 NAP 0 html-button-response 46 2.1.0 63275
444 DOCTOR 0 html-button-response 47 2.1.0 64221
308 CURE 0 html-button-response 48 2.1.0 65034
333 PEACE 0 html-button-response 49 2.1.0 65877
1072 OFFICE 0 html-button-response 50 2.1.0 67451
544 TIRED 1 html-button-response 51 2.1.0 68524
359 SICK 1 html-button-response 52 2.1.0 69390
417 NURSE 0 html-button-response 53 2.1.0 70313
533 AWAKE 0 html-button-response 54 2.1.0 71347
285 SNOOZE 1 html-button-response 55 2.1.0 72134
241 HOSPITAL 0 html-button-response 56 2.1.0 72893
476 SURGEON 1 html-button-response 57 2.1.0 73877
252 SNORE 0 html-button-response 58 2.1.0 74636
315 BED 0 html-button-response 59 2.1.0 75467
409 WAKE 0 html-button-response 60 2.1.0 76379
628 PHYSICIAN 0 html-button-response 61 2.1.0 77514
614 DOZE 0 html-button-response 62 2.1.0 78635
1271 CLINIC 1 html-button-response 63 2.1.0 80410

If you’ve just collected this data how would you analyze it? How would you know which words were the studied words and which were the new ones? Which rows are part of the study phase versus the test phase? Which word is the critical lure?

Of course, you could go through this file row-by-row and figure it out, but if we had properly labelled each row, then we’ll have a much easier time analyzing the data when we have 100 of these files. This is exactly why we need to add our own data labels to make the dataset meaningful and analysis-ready. We’ll return to our memory experiment later to illustrate proper data labeling.

14.2 Saving Data

14.2.1 Where Data Lives: Browser Memory vs. Permanent Storage

A quick aside before discussing data labeling: It’s important to understand that when jsPsych collects this data, it’s initially stored only in the browser’s memory. This means the data exists only as long as the browser tab is open. If the participant closes the browser window or refreshes the page, all the data disappears forever.

For a real experiment, you need to transfer this data from the browser’s temporary memory to somewhere permanent. There are several options for permanent storage:

  1. Save to a server database (most common for online studies)
  2. Save to a file on a server (requires server setup)
  3. Save locally to the participant’s computer (good for testing and development)

In this chapter, we’ll focus on saving data locally, which is perfect for testing your experiments during development. Later in the book, we’ll cover how to set up server-based data storage for real data collection.

14.2.2 Saving Data Locally

For development and testing purposes, jsPsych provides a convenient way to save data directly to your computer using the localSave() method. This downloads the data as a file that you can open in Excel or other data analysis programs.

The syntax to call this function looks like this:

jsPsych.data.get().localSave('csv', 'experiment_data.csv');
  • The jsPsych.data.get() part retrieves all of the data.
  • The .localSave('csv', 'experiment_data.csv') saves the data as a csv file, with the file name ‘experiment_data.csv’ (you can change that part to be whatever you want it to be).

In order to give participants the ability to download their own data, however, we need to incorporate this method into our experiment as a custom trial.

14.2.3 Creating a Data Saving Trial

To save data, we need to create a special trial that gives participants a way to download their data. This requires some techniques we haven’t covered yet, so let’s break down each part:

const saveData = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `
    <div style='text-align: center;'>
      <p>Experiment complete!</p>
      <p>Click the button below to save your data locally:</p>
      <button id='save-btn' style='padding: 10px 20px; font-size: 16px; cursor: pointer;'>
        Click here to save the data locally
      </button>
    </div>
  `,
  choices: 'NO_KEYS',
  trial_duration: null,
  on_load: function() {
    document.getElementById('save-btn').addEventListener('click', function() {
      jsPsych.data.get().localSave('csv', 'experiment_data.csv');
    });
  }
};

Let’s understand what each part does:

14.2.3.1 (1) The HTML Structure:

stimulus: `
  <div style='text-align: center;'>
    <p>Experiment complete!</p>
    <p>Click the button below to save your data locally:</p>
    <button id='save-btn' style='padding: 10px 20px; font-size: 16px; cursor: pointer;'>
      Click here to save the data locally
    </button>
  </div>
  `

We create HTML that includes a clickable button. The button has an id='save-btn' so we can reference it later.

14.2.3.2 (2) Disabling Keyboard Input

choices: 'NO_KEYS',
trial_duration: null,

This should be familiar:

  • ‘NO_KEYS’ means participants can’t press keys to continue
  • null duration means the trial stays active until we end it programmatically
  • This forces participants to use the button instead of keyboard shortcuts

This means that participants cannot click or press anything to make this screen change.

14.2.3.3 (3) The Button Functionality:

on_load: function() {
  document.getElementById('save-btn').addEventListener('click', function() {
    jsPsych.data.get().localSave('csv', 'experiment_data.csv');
  });
}

This is where the magic happens. The on_load is a parameter that takes a function. This function will run when the trial first appears on the screen.

The rest of the code is common JavaScript that finds our button element in the HTML and tells the webpage what to do when someone clicks that button:

  1. document.getElementById('save-btn') finds our button in the HTML
  2. addEventListener('click', ...) tells the button what to do when clicked and ‘listens’ or waits for a click
  3. jsPsych.data.get().localSave('csv', 'experiment_data.csv') downloads the data

Important: Don’t worry if you don’t fully understand (or remember) this code! Now that we’ve written this generic save trial once, we’ll be able to copy-paste it into our lab experiments and re-use it whenever we need it.

14.2.4 Save Data Example

Let’s see a full example of the local data saving. Take note that we need to add our saveData to the end of our timeline when we run our experiment.

<!DOCTYPE html>
<html>
<head>
    <title>Saving Data</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-html-keyboard-response.js"></script>

    <!-- custom CSS -->
    <link href="jspsych/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
// 1. Initialize jsPsych
const jsPsych = initJsPsych();

// 2. Define our trials
const welcome = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `<p>Welcome to the Experiment!</p>
             <p>Press any key to begin.</p>`,
    choices: "ALL_KEYS",
    post_trial_gap: 1000
}

const questions = {
    timeline: [
        {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: jsPsych.timelineVariable("question"),
            choices: ['y', 'n'],
            post_trial_gap: 250
        }
    ],
    timeline_variables: [
        {question: `<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>`},
        {question: `<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>`},
    ]
}

const saveData = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `
    <div style='text-align: center;'>
      <p>Experiment complete!</p>
      <p>Click the button below to save your data locally:</p>
      <button id='save-btn' style='padding: 10px 20px; font-size: 16px; cursor: pointer;'>
        Click here to save the data locally
      </button>
    </div>
  `,
    choices: 'NO_KEYS', // Disable keyboard responses
    trial_duration: null, // Keep trial active indefinitely
    on_load: function() {
        document.getElementById('save-btn').addEventListener('click', function() {
            // Save the data as a CSV file
            jsPsych.data.get().localSave('csv', 'experiment_data.csv');
        });
    }
};

jsPsych.run(
    [
        welcome,
        questions,
        saveData
    ]
);
Live JsPsych Demo Click inside the demo to activate demo

We’ll use this pattern throughout the course for testing and development. Note that localSave() only works for local testing and development. For actual data collection with participants, you’ll need a more robust solution for storing data permanently on a server.

14.3 Labeling Data

While jsPsych automatically saves basic information, notice what’s missing from our first data table above. Looking at the data, we can see that trial 1 showed a question about chocolate and the participant pressed ‘y’, while trial 2 asked about vanilla and they pressed ‘n’. However, we have no idea what these questions were actually asking (we have to read the full HTML stimulus), what each response means, or how to categorize these trials for analysis.

This is where data labeling becomes important. We need to add meaningful information to each trial so that our data tells the complete story of what happened during the experiment.

14.3.1 Adding Context with the data Parameter

The data parameter is always available and allows us to add custom information to any trial. Whenever we add a data parameter, jsPsych creates a new column with that name and adds that label when it’s present. The data parameter takes an object with key-value pairs. Each key-value pair indicates the column label (key) and the value that should be included in that column, on that row (value).

Let’s look at the first example from above and try to improve our data by adding some context:

const question1 = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>',
  choices: ['y', 'n'],
  data: {
    question_type: 'preference',
    food_item: 'chocolate'
  }
};

const question2 = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>',
  choices: ['y', 'n'],
  data: {
    question_type: 'preference',
    food_item: 'vanilla'
  }
};

Now our CSV output becomes much more informative:

trial_type trial_index time_elapsed rt stimulus response question_type food_item
html-keyboard-response 0 1247 1247 <p>Welcome...</p> ” ”
html-keyboard-response 1 3891 2644 <p>Do you like chocolate?</p>... y preference chocolate
html-keyboard-response 2 6234 2343 <p>Do you like vanilla?</p>... n preference vanilla

Notice how our custom data (question_type and food_item) appears as new columns in the CSV. The welcome trial has empty cells for these columns because we didn’t add this data to that trial. Now, in your analysis software you could filter it by indicating that you only want to see rows where question_type is equal to "preference". That would filter out the instructions and leave you with just the analysis-relevant questions.

I could also use the food_item column to determine which question was asked on each row without having to figure it out from the stimulus HTML; Much better!

14.3.2 Adding Data to Timelines

One of the powerful features of jsPsych’s data system is that you can add data labels to entire timelines. When you add data to a timeline, that information is automatically included in every trial within that timeline. This creates a hierarchical data system where information can be applied at different levels.

Consider this example where we’re organizing our questions into blocks:

const preference_block = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: '<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>',
      choices: ['y', 'n']
    },
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: '<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>',
      choices: ['y', 'n']
    }
  ],
  data: {
    block_type: 'preferences',
    category: 'food'
  }
}

In this example, both trials will automatically include block_type: 'preferences' and category: 'food' in their data. This saves you from having to add the same information to each individual trial.

You can combine timeline-level and trial-level data. The trial-level data will be added to the timeline-level data:

const preference_block = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: '<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>',
      choices: ['y', 'n'],
      data: { food_item: 'chocolate' }
    },
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: '<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>',
      choices: ['y', 'n'],
      data: { food_item: 'vanilla' }
    }
  ],
  data: {
    block_type: 'preferences',
    category: 'food'
  }
}

Each trial will now have both the timeline data (block_type and category) and its specific trial data (food_item). This hierarchical approach lets you organize your data labels efficiently, applying broad categories at the timeline level and specific details at the trial level.

14.3.3 Dynamic Data with Timeline Variables

When you’re using timeline variables to create multiple similar trials, you can use jsPsych.timelineVariable() to dynamically pull data labels from your timeline variables. This is extremely powerful for creating well-labeled data without repetition.

Here’s an example using timeline variables for our food preference questions. Take note of how I include relevant data labels in the timeline_variables and reference them in my data object after.

// Create the timeline
const preference_block = {
  timeline: [
      {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable('question'),
      choices: ['y', 'n'],
      data: {
        food_item: jsPsych.timelineVariable('food_item'),
        food_category: jsPsych.timelineVariable('food_category'),
        expected_popularity: jsPsych.timelineVariable('expected_popularity'),
        question_type: 'preference'
      }
  }],
  timeline_variables: [
      {
        question: '<p>Do you like chocolate?</p><p>Press Y for yes, N for no.</p>',
        food_item: 'chocolate',
        food_category: 'sweet',
        expected_popularity: 'high'
      },
      {
        question: '<p>Do you like vanilla?</p><p>Press Y for yes, N for no.</p>',
        food_item: 'vanilla',
        food_category: 'sweet',
        expected_popularity: 'medium'
      },
      {
        question: '<p>Do you like broccoli?</p><p>Press Y for yes, N for no.</p>',
        food_item: 'broccoli',
        food_category: 'vegetable',
        expected_popularity: 'low'
      }
  ],
  randomize_order: true,
  data: {
    block_type: 'food_preferences',
    block_number: 1
  }
};

This approach automatically pulls the appropriate data labels for each trial from the timeline variables. Each trial will have:

  1. Timeline-level data:block_type: 'food_preferences' and block_number: 1
  2. Static trial data: question_type: 'preference'
  3. Dynamic trial data: food_item, food_category, and expected_popularity that change based on the timeline variable for that specific trial

The resulting data will look like this:

trial_type trial_index stimulus response block_type block_number question_type food_item food_category expected_popularity
html-keyboard-response 0 <p>Do you like chocolate?</p>... y food_preferences 1 preference chocolate sweet high
html-keyboard-response 1 <p>Do you like broccoli?</p>... n food_preferences 1 preference broccoli vegetable low
html-keyboard-response 2 <p>Do you like vanilla?</p>... y food_preferences 1 preference vanilla sweet medium

14.3.4 Saving Trial Parameters

14.3.5 Adding Global Properties to All Trials

One of the most common uses of the data module is adding information that applies to every trial in your experiment. The jsPsych.data.addProperties() function lets you add properties to all trials, both past and future ones.

This function takes an object as input, with each key-value pair indicating a new column and value to add to every row.

// Add these properties to every trial in the experiment
jsPsych.data.addProperties({
  subject_id: "001",
  condition: "control",
  experiment_version: "2.1",
  session_date: new Date().toISOString().split('T')[0] // YYYY-MM-DD format
});

This is useful because it’s (1) retroactive: Even trials that already happened get these properties and (2) it’s persistent: All future trials automatically include these properties

Here are some common properties that you might want to add to all trials:

jsPsych.data.addProperties({
    // Participant identification
    subject_id: jsPsych.randomization.randomID(8),

    // Experimental design
    condition: "experimental", // or randomly assigned
    experiment_version: "1.0",
    researcher: "Dr. Smith",

    // Session information
    session_date: new Date().toISOString().split('T')[0], // YYYY-MM-DD format
    start_time:  new Date().toISOString().split('T')[1],
    browser: navigator.userAgent,
    screen_width: screen.width,
    screen_height: screen.height
});

14.3.6 Data Labels: Full Example

Now let’s return to our memory experiment and apply the concepts we’ve just learned to apply proper data labels.

For this example, I’ve added (1) global data properties that get added to every row, (2) trial- and timeline-level data labels, and (3) added the save data trial to the end of the experiment.

You can try running this in the example below and you can save the resulting data to your computer.

<!DOCTYPE html>
<html>
<head>
    <title>Saving Data</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-html-keyboard-response.js"></script>
    <script src="jspsych/plugin-instructions.js"></script>

    <!-- custom CSS -->
    <link href="jspsych/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
// 1. Initialize jsPsych
const jsPsych = initJsPsych();

jsPsych.data.addProperties({
    // Participant identification
    subject_id: jsPsych.randomization.randomID(8),

    // Experimental design
    condition: "experimental", // or randomly assigned
    experiment_version: "1.0",
    researcher: "Dr. Smith",

    // Session information
    session_date: new Date().toISOString().split('T')[0], // YYYY-MM-DD format
    start_time:  new Date().toISOString().split('T')[1],
    browser: navigator.userAgent,
    screen_width: screen.width,
    screen_height: screen.height
});

// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `Welcome to the Experiment! Press any key to begin.`,
  choices: "ALL_KEYS", 
  post_trial_gap: 500,
  data: {
    phase: "welcome",
    trial_type: "instruction"
  }
}

const instructions = {
  type: jsPsychInstructions,
  pages: [
    // Page 1: Welcome and Overview
    `<div class='instructionStyle'>
      <p>You will participate in a memory experiment with two phases:</p>
     <p><strong>Phase 1:</strong> Study a list of words</p>
     <p><strong>Phase 2:</strong> Decide if words are OLD or NEW</p>
     <p>Click Next to continue.</p>
     </div>`,

    // Page 2: Study Phase Instructions  
    `<div class='instructionStyle'>
      <p>During the study phase, you will see words appear one at a time. Read each word carefully and try to remember it.</p>
     <p>Each word appears for 1 second. You will study about 15 words total.</p>
     <p>You do not need to press any keys during this phase.</p>
     </div>`,

    // Page 3: Test Phase Instructions
    `<div class='instructionStyle'>
      <p>During the test phase, you will see words one at a time. Some words are OLD (from the study list). Some words are NEW (not from the study list).</p>
     <p>Press the 'OLD' button if the word was old.</p>
     <p>Press the 'NEW' button if the word was new</p>
     </div>`,

    // Page 4: Stay focused
    `<div class='instructionStyle'>
      <p>Trust your first instinct.</p>
      <p>If unsure, make your best guess.</p>
    </div>`,

    // Page 5: Final Instructions
    `<div class='instructionStyle'>
      <p>Try to stay focused throughout the experiment. The experiment only takes a few minutes.</p>
      <p>Click Next to start the study phase.</p>
     </div>`
  ],
  key_forward: 'ArrowRight',
  key_backward: 'ArrowLeft',
  allow_backward: true,
  show_clickable_nav: true,
  button_label_previous: 'Back',
  button_label_next: 'Next',
  data: {
    phase: "instructions",
    trial_type: "instruction"
  }
};

let study = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: "+",
      choices: "NO_KEYS",
      post_trial_gap: 250,
      trial_duration: 500,
      css_classes: "wordStyle",
      data: {
        phase: "study",
        trial_type: "fixation"
      }
    },
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable("word"),
      choices: "NO_KEYS",
      post_trial_gap: 500,
      trial_duration: 1000,
      css_classes: "wordStyle",
      data: {
        phase: "study",
        trial_type: "word_presentation",
        word: jsPsych.timelineVariable("word"),
        word_type: "study_item"
      }
    }
  ],
  timeline_variables: [
        {word: "BED"},
        {word: "REST"},
        {word: "AWAKE"},
        {word: "TIRED"},
        {word: "DREAM"},
        {word: "WAKE"},
        {word: "SNOOZE"},
        {word: "BLANKET"},
        {word: "DOZE"},
        {word: "SLUMBER"},
        {word: "SNORE"},
        {word: "NAP"},
        {word: "PEACE"},
        {word: "YAWN"},
        {word: "DROWSY"}
  ],
  randomize_order: true
}

let test = {
  timeline: [
    {
      type: jsPsychHtmlButtonResponse,
      stimulus: jsPsych.timelineVariable("word"),
      post_trial_gap: 500,
      choices: ["OLD", "NEW"],
      css_classes: "wordStyle",
      data: {
        phase: "test",
        trial_type: "recognition_test",
        word: jsPsych.timelineVariable("word"),
        word_type: jsPsych.timelineVariable("word_type"),
        correct_response: jsPsych.timelineVariable("correct_response")
      }
    }
  ],
  timeline_variables: [
         // OLD WORDS (should respond "OLD")
        {word: "BED", word_type: "old", correct_response: "OLD"},
        {word: "REST", word_type: "old", correct_response: "OLD"},
        {word: "AWAKE", word_type: "old", correct_response: "OLD"},
        {word: "TIRED", word_type: "old", correct_response: "OLD"},
        {word: "DREAM", word_type: "old", correct_response: "OLD"},
        {word: "WAKE", word_type: "old", correct_response: "OLD"},
        {word: "SNOOZE", word_type: "old", correct_response: "OLD"},
        {word: "BLANKET", word_type: "old", correct_response: "OLD"},
        {word: "DOZE", word_type: "old", correct_response: "OLD"},
        {word: "SLUMBER", word_type: "old", correct_response: "OLD"},
        {word: "SNORE", word_type: "old", correct_response: "OLD"},
        {word: "NAP", word_type: "old", correct_response: "OLD"},
        {word: "PEACE", word_type: "old", correct_response: "OLD"},
        {word: "YAWN", word_type: "old", correct_response: "OLD"},
        {word: "DROWSY", word_type: "old", correct_response: "OLD"},
        // NEW WORDS (should respond "NEW")
        {word: "DOCTOR", word_type: "new", correct_response: "NEW"},
        {word: "NURSE", word_type: "new", correct_response: "NEW"},
        {word: "SICK", word_type: "new", correct_response: "NEW"},
        {word: "LAWYER", word_type: "new", correct_response: "NEW"},
        {word: "MEDICINE", word_type: "new", correct_response: "NEW"},
        {word: "HEALTH", word_type: "new", correct_response: "NEW"},
        {word: "HOSPITAL", word_type: "new", correct_response: "NEW"},
        {word: "DENTIST", word_type: "new", correct_response: "NEW"},
        {word: "PHYSICIAN", word_type: "new", correct_response: "NEW"},
        {word: "ILL", word_type: "new", correct_response: "NEW"},
        {word: "PATIENT", word_type: "new", correct_response: "NEW"},
        {word: "OFFICE", word_type: "new", correct_response: "NEW"},
        {word: "STETHOSCOPE", word_type: "new", correct_response: "NEW"},
        {word: "SURGEON", word_type: "new", correct_response: "NEW"},
        {word: "CLINIC", word_type: "new", correct_response: "NEW"},
        {word: "CURE", word_type: "new", correct_response: "NEW"},
        // CRITICAL LURE (never studied, but related to study words)
        {word: "SLEEP", word_type: "critical_lure", correct_response: "NEW"}
  ],
  randomize_order: true
}

const saveData = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `
    <div style='text-align: center;'>
      <p>Experiment complete!</p>
      <p>Click the button below to save your data locally:</p>
      <button id='save-btn' style='padding: 10px 20px; font-size: 16px; cursor: pointer;'>
        Click here to save the data locally
      </button>
    </div>
  `,
    choices: 'NO_KEYS', // Disable keyboard responses
    trial_duration: null, // Keep trial active indefinitely
    on_load: function() {
        document.getElementById('save-btn').addEventListener('click', function() {
            // Save the data as a CSV file
            jsPsych.data.get().localSave('csv', 'recognition_data.csv');
        });
    }
};

// 3. Run jsPsych with our trials
jsPsych.run([
  welcome,
  instructions,
  study,
  test,
  saveData
]);
Live JsPsych Demo Click inside the demo to activate demo

Now, let’s look at the data that would result from this new labeling:

phase trial_type rt stimulus response trial_index plugin_version time_elapsed subject_id condition experiment_version researcher session_date start_time browser screen_width screen_height view_history word word_type correct_response
welcome html-keyboard-response 2257 Welcome to the Experiment! Press any key to begin. f 0 2.1.0 2258 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
instructions instructions 3088 1 2.1.0 5851 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 [{“page_index”: 0,“viewing_time”: 1297} … {“page_index”: 4,“viewing_time”:384 }]
study html-keyboard-response null + null 2 2.1.0 6356 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null WAKE null 3 2.1.0 7621 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 WAKE study_item
study html-keyboard-response null + null 4 2.1.0 8692 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null DROWSY null 5 2.1.0 9963 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 DROWSY study_item
study html-keyboard-response null + null 6 2.1.0 11012 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null SLUMBER null 7 2.1.0 12269 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 SLUMBER study_item
study html-keyboard-response null + null 8 2.1.0 13286 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null DREAM null 9 2.1.0 14651 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 DREAM study_item
study html-keyboard-response null + null 10 2.1.0 15691 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null BLANKET null 11 2.1.0 16977 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 BLANKET study_item
study html-keyboard-response null + null 12 2.1.0 18010 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null TIRED null 13 2.1.0 19285 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 TIRED study_item
study html-keyboard-response null + null 14 2.1.0 110847 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null YAWN null 15 2.1.0 112112 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 YAWN study_item
study html-keyboard-response null + null 16 2.1.0 113136 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null BED null 17 2.1.0 114391 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 BED study_item
study html-keyboard-response null + null 18 2.1.0 115456 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null DOZE null 19 2.1.0 116821 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 DOZE study_item
study html-keyboard-response null + null 20 2.1.0 117823 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null AWAKE null 21 2.1.0 119200 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 AWAKE study_item
study html-keyboard-response null + null 22 2.1.0 120220 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null NAP null 23 2.1.0 121481 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 NAP study_item
study html-keyboard-response null + null 24 2.1.0 122498 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null SNORE null 25 2.1.0 123824 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 SNORE study_item
study html-keyboard-response null + null 26 2.1.0 124842 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null SNOOZE null 27 2.1.0 126112 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 SNOOZE study_item
study html-keyboard-response null + null 28 2.1.0 127132 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null PEACE null 29 2.1.0 128385 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 PEACE study_item
study html-keyboard-response null + null 30 2.1.0 129418 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100
study html-keyboard-response null REST null 31 2.1.0 130701 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 REST study_item
test html-button-response 3542 HEALTH 0 32 2.1.0 134745 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 HEALTH new NEW
test html-button-response 611 BED 1 33 2.1.0 135857 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 BED old OLD
test html-button-response 289 TIRED 0 34 2.1.0 136658 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 TIRED old OLD
test html-button-response 279 REST 0 35 2.1.0 137459 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 REST old OLD
test html-button-response 763 HOSPITAL 1 36 2.1.0 138731 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 HOSPITAL new NEW
test html-button-response 366 DOZE 0 37 2.1.0 139620 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 DOZE old OLD
test html-button-response 455 SURGEON 0 38 2.1.0 140587 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 SURGEON new NEW
test html-button-response 326 SNORE 0 39 2.1.0 141428 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 SNORE old OLD
test html-button-response 386 CURE 1 40 2.1.0 142322 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 CURE new NEW
test html-button-response 361 AWAKE 0 41 2.1.0 143198 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 AWAKE old OLD
test html-button-response 431 DROWSY 0 42 2.1.0 144140 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 DROWSY old OLD
test html-button-response 646 SICK 0 43 2.1.0 145288 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 SICK new NEW
test html-button-response 531 OFFICE 0 44 2.1.0 146344 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 OFFICE new NEW
test html-button-response 385 PATIENT 0 45 2.1.0 147269 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 PATIENT new NEW
test html-button-response 363 DOCTOR 0 46 2.1.0 148150 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 DOCTOR new NEW
test html-button-response 363 LAWYER 0 47 2.1.0 149049 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 LAWYER new NEW
test html-button-response 505 ILL 1 48 2.1.0 150077 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 ILL new NEW
test html-button-response 369 BLANKET 1 49 2.1.0 150959 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 BLANKET old OLD
test html-button-response 316 YAWN 1 50 2.1.0 151792 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 YAWN old OLD
test html-button-response 563 PEACE 0 51 2.1.0 152871 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 PEACE old OLD
test html-button-response 434 CLINIC 0 52 2.1.0 153813 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 CLINIC new NEW
test html-button-response 387 SLEEP 0 53 2.1.0 154751 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 SLEEP critical_lure NEW
test html-button-response 469 SLUMBER 0 54 2.1.0 155731 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 SLUMBER old OLD
test html-button-response 470 NURSE 0 55 2.1.0 156717 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 NURSE new NEW
test html-button-response 368 SNOOZE 0 56 2.1.0 157603 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 SNOOZE old OLD
test html-button-response 351 DREAM 1 57 2.1.0 158471 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 DREAM old OLD
test html-button-response 400 NAP 1 58 2.1.0 159391 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 NAP old OLD
test html-button-response 767 PHYSICIAN 0 59 2.1.0 160663 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 PHYSICIAN new NEW
test html-button-response 526 WAKE 0 60 2.1.0 161700 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 WAKE old OLD
test html-button-response 615 MEDICINE 0 61 2.1.0 162844 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 MEDICINE new NEW
test html-button-response 697 STETHOSCOPE 0 62 2.1.0 164044 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 STETHOSCOPE new NEW
test html-button-response 1572 DENTIST 0 63 2.1.0 166119 m8kx3pqw experimental 1.0 Dr. Smith 2025-09-14 13:56:07.382Z Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 1760 1100 DENTIST new NEW

What we’ve improved:

Consistent phase labeling: Every trial now has a phase field (welcome, instructions, study, test)

Trial type classification: Each trial has a trial_type field describing what kind of trial it is (fixation, word_presentation, recognition_test, etc.)

Stimulus tracking: Test trials include the actual word being presented and its word_type (old, new, critical_lure)

Response accuracy: Each test trial includes the correct_response, making it easy to calculate accuracy later

Critical lure identification: The word "SLEEP" is specifically labeled as a critical_lure. This is the key manipulation in DRM experiments, as it’s semantically related to the study words but was never actually presented.

With these labels, analyzing your data becomes straightforward. You can easily filter trials by phase, calculate accuracy rates for different word types, and examine false alarm rates for critical lures.

14.4 Basic Data Updates with on_finish

Often you’ll want to add computed information to your data after a trial completes (e.g., determine whether the response was accurate). The on_finish callback function is perfect for this. It receives the trial’s data as a parameter and allows you to modify or add to it.

When the on_finish function runs, it receives a data parameter that contains all the information collected during that specific trial. This includes:

Automatic jsPsych properties:

  • trial_type: The plugin that was used (e.g., “html-keyboard-response”)
  • trial_index: The position of this trial in the experiment (starts at 0)
  • time_elapsed: Total time since the experiment started (in milliseconds)
  • rt: Response time for this trial (in milliseconds)
  • response: The participant’s response (key pressed, button clicked, etc.)

Properties from your trial definition:

  • Any properties you added in the data object of your trial
  • Timeline variables that were active for this trial

Properties from addProperties():

  • Any global properties you added (like subject_id, condition, etc.)

First, here’s an example to show the proper syntax of adding the on_finish parameter:

const recognition_trial = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable("word"),
      choices: ['f', 'j'],
      data: {
        task: "recognition_test",
        word: jsPsych.timelineVariable("word"),
        word_type: jsPsych.timelineVariable("word_type"),
        correct_response: jsPsych.timelineVariable("correct_response")
      },
      on_finish: function(data) {
          // on_finish code goes inside here
      }
    }
  ],
  timeline_variables: [
    { word: "DOCTOR", word_type: "old", correct_response: "f" },
    { word: "PLANET", word_type: "new", correct_response: "j" },
    { word: "GARDEN", word_type: "old", correct_response: "f" },
    { word: "WINDOW", word_type: "new", correct_response: "j" },
    { word: "BRIDGE", word_type: "old", correct_response: "f" },
    { word: "FOREST", word_type: "new", correct_response: "j" }
  ]
};

Notice that we add it just like the other parameters on_finish:. However, it’s value is a function: function(data){ }. We add the data in the parentheses because this is how we add inputs to our functions and we want the data from that trial to be available inside the function.

Using that function, we can access any of trial properties by using standard object notation like data.rt or data["rt"]. Here’s an example showing what’s available by accessing them inside the function and printing them in the JavaScript console:

const recognition_trial = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable("word"),
      choices: ['f', 'j'],
      data: {
        task: "recognition_test",
        word: jsPsych.timelineVariable("word"),
        word_type: jsPsych.timelineVariable("word_type"),
        correct_response: jsPsych.timelineVariable("correct_response")
      },
      on_finish: function(data) {
          // At this point, 'data' contains:
          console.log("Trial type:", data.trial_type);           // "html-keyboard-response"
          console.log("Trial index:", data.trial_index);         // e.g., 15
          console.log("Time elapsed:", data.time_elapsed);       // e.g., 45230
          console.log("Response time:", data.rt);                // e.g., 1205
          console.log("Key pressed:", data.response);            // "f" or "j"
          console.log("Task:", data.task);                       // "recognition_test"
          console.log("Word shown:", data.word);                 // e.g., "DOCTOR"
          console.log("Word type:", data.word_type);             // e.g., "new"
          console.log("Correct response:", data.correct_response); // e.g., "j"
          console.log("Subject ID:", data.subject_id);           // from addProperties()
      
          // At this point, we're just examining the data, not changing it yet
          // This code would just print the data values in the console
      }
    }
  ],
  timeline_variables: [
    { word: "DOCTOR", word_type: "old", correct_response: "f" },
    { word: "PLANET", word_type: "new", correct_response: "j" },
    { word: "GARDEN", word_type: "old", correct_response: "f" },
    { word: "WINDOW", word_type: "new", correct_response: "j" },
    { word: "BRIDGE", word_type: "old", correct_response: "f" },
    { word: "FOREST", word_type: "new", correct_response: "j" }
  ]
};

However, we typically don’t just want to access the data, we want to do something with in. The on_finish function is your opportunity to add new properties or modify existing ones based on what happened during the trial. This is commonly used to assess accuracy and categorize performance.

14.4.1 Checking Accuracy

Let’s look at an example of how we can determine whether the participant provided the correct response at the end of the trial. This is probably the most common use-case for modifying the data at the end of the trial:

const recognition_trial = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable("word"),
      choices: ['f', 'j'],
      data: {
        task: "recognition_test",
        word: jsPsych.timelineVariable("word"),
        word_type: jsPsych.timelineVariable("word_type"),
        correct_response: jsPsych.timelineVariable("correct_response")
      },
      on_finish: function(data) {
        // Use compareKeys() to properly compare key responses
        if (jsPsych.pluginAPI.compareKeys(data.response, data.correct_response)) {
          data.correct = true;
        } else {
          data.correct = false;
        }
    
        // Or more concisely:
        // data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
      }
    }
  ],
  timeline_variables: [
    { word: "DOCTOR", word_type: "old", correct_response: "f" },
    { word: "PLANET", word_type: "new", correct_response: "j" },
    { word: "GARDEN", word_type: "old", correct_response: "f" },
    { word: "WINDOW", word_type: "new", correct_response: "j" },
    { word: "BRIDGE", word_type: "old", correct_response: "f" },
    { word: "FOREST", word_type: "new", correct_response: "j" }
  ]
};

In this code, I check whether data.response is equal to data.correct_response. If it is true, then I update data.correct to be true. If data.response is NOT equal to data.correct_response, then I set it to false. This will update my data file that is saved at the end with a new column called correct with true/false values.

Here is an example of the data output. Take note of the new columns that were added, including task, word, etc.

rt stimulus response trial_type trial_index plugin_version time_elapsed subject_id experiment_version task word word_type correct_response correct
2121 f html-keyboard-response 0 2.1.0 2121 q4jr9zbl 1
879 DOCTOR f html-keyboard-response 1 2.1.0 3002 q4jr9zbl 1 recognition_test DOCTOR old f TRUE
3264 PLANET j html-keyboard-response 2 2.1.0 6266 q4jr9zbl 1 recognition_test PLANET new j TRUE
767 GARDEN f html-keyboard-response 3 2.1.0 7033 q4jr9zbl 1 recognition_test GARDEN old f TRUE
808 WINDOW j html-keyboard-response 4 2.1.0 7842 q4jr9zbl 1 recognition_test WINDOW new j TRUE

Different plugins represent responses in different ways, so you’ll need to adjust your accuracy checking accordingly. When using the keyboard response plugin, we need to compare the key the participant pressed against the correct key. It’s important to use jsPsych’s built-in compareKeys() function rather than simple equality checks. This function handles case sensitivity and different key representations properly. For example, if the participant had caps lock on and the correct response was set to "e", then "e" === "E" would be false. We want that comparison to be true regardless of case, so we use the built-in function to handle the comparison correctly.

Here’s an example using the button response plugin, which represents button responses numerically (0 for the first button, 1 for the second, etc.):

const recognition_trial = {
  timeline: [
    {
      type: jsPsychHtmlButtonResponse,
      stimulus: jsPsych.timelineVariable("word"),
      choices: ['OLD', 'NEW'],
      data: {
        task: "recognition_test",
        word: jsPsych.timelineVariable("word"),
        word_type: jsPsych.timelineVariable("word_type"),
        correct_response: jsPsych.timelineVariable("correct_response") // This should be 0 or 1 (0 == 'OLD' and 1 == 'NEW')
      },
      on_finish: function(data) {
        // For button responses, we compare numbers directly
        if (data.response === data.correct_response) {
          data.correct = true;
        } else {
          data.correct = false;
        }
    
        // Or more concisely:
        // data.correct = data.response === data.correct_response;
      }
    }
  ],
  timeline_variables: [
    { word: "DOCTOR", word_type: "old", correct_response: "f" },
    { word: "PLANET", word_type: "new", correct_response: "j" },
    { word: "GARDEN", word_type: "old", correct_response: "f" },
    { word: "WINDOW", word_type: "new", correct_response: "j" },
    { word: "BRIDGE", word_type: "old", correct_response: "f" },
    { word: "FOREST", word_type: "new", correct_response: "j" }
  ]
};

Important Note: Always check the “Data Generated” section of each plugin’s documentation to understand how responses are represented. For keyboard plugins, responses are typically strings (the key pressed), while button plugins use numeric indices (0, 1, 2, etc.).

14.4.2 Categorizing Response Times:

To provide another example of modifying the data at the end of the trial, let’s look at how you can add categories and computed measures based on performance. This could be useful for adding feedback after the trial, something we’ll return to in the following chapters. It’s important to understand that on_finish is an arbitrary function, which means we can add whatever code we want here and it will be executed after the trials.

In this example, I will (1) check accuracy, (2) check the response time and categorize it based on speed, and (3) use BOTH response time and accuracy to categorize their performance.

const stroop_trial = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable("stimulus"),
      choices: ['r', 'g', 'b'],
      data: {
        task: "stroop",
        condition: jsPsych.timelineVariable("condition"),
        correct_key: jsPsych.timelineVariable("correct_key")
      },
      on_finish: function(data) {
        // Determine accuracy using compareKeys
        data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_key);
    
        // Categorize response time
        if (data.rt < 300) {
          data.rt_category = "fast";
        } else if (data.rt < 800) {
          data.rt_category = "normal";
        } else {
          data.rt_category = "slow";
        }
    
        // Add performance score combining accuracy and speed
        if (data.correct && data.rt < 600) {
          data.performance = "excellent";
        } else if (data.correct) {
          data.performance = "good";
        } else {
          data.performance = "needs_improvement";
        }
      }
    }  
  ],
  timeline_variables: [
    {stimulus: `<span style='color: red;'>RED</span>`, condition: "congruent", correct_key: "r"},
    {stimulus: `<span style='color: blue;'>RED</span>`, condition: "incongruent", correct_key: "b"},
    {stimulus: `<span style='color: green;'>GREEN</span>>`, condition: "incongruent", correct_key: "g"},
    {stimulus: `<span style='color: red;'>BLUE</span>`, condition: "incongruent", correct_key: "r"},
    {stimulus: `<span style='color: blue;'>BLUE</span>`, condition: "congruent", correct_key: "b"},
    {stimulus: `<span style='color: green;'>RED</span>`, condition: "incongruent", correct_key: "g"},
  ]
};

Again, we are updating the data that is being saved. So, our data output will now include new columns for our categories and values if one is provided:

rt stimulus response trial_type trial_index plugin_version time_elapsed subject_id experiment_version task condition correct_key correct rt_category performance
542 RED r html-keyboard-response 0 2.1.0 542 m8kx3pqw 1 stroop congruent r TRUE normal excellent
1205 RED b html-keyboard-response 1 2.1.0 1747 m8kx3pqw 1 stroop incongruent r FALSE slow needs_improvement
287 GREEN g html-keyboard-response 2 2.1.0 2034 m8kx3pqw 1 stroop congruent g TRUE fast excellent
934 BLUE r html-keyboard-response 3 2.1.0 2968 m8kx3pqw 1 stroop incongruent b FALSE slow needs_improvement
678 BLUE b html-keyboard-response 4 2.1.0 3646 m8kx3pqw 1 stroop congruent b TRUE normal good
445 RED g html-keyboard-response 5 2.1.0 4091 m8kx3pqw 1 stroop incongruent r FALSE normal needs_improvement

Some final points about on_finish:

  • Timing: on_finish runs immediately after the participant responds, before the next trial begins
  • Persistence: Any properties you add or modify in on_finish become part of the permanent data record for that trial
  • Flexibility: You can add as many new properties as you want, and they’ll all appear in your final dataset

The on_finish function is your opportunity to enrich your data with computed information that will be valuable during analysis. By adding accuracy scores, performance categories, and other derived measures, you make your data much more useful for statistical analysis.

14.5 Final Thoughts on Data Labeling

Data labeling is one of the most important skills in experimental programming because it determines whether your results will be analyzable and meaningful. Throughout this chapter, you’ve learned multiple techniques for adding context to your data including simple trial labels and global properties that track participants and experimental conditions. The key insight is to think about your data analysis needs before you start collecting data. What variables will you need to group trials? What information will help you identify different experimental conditions? By planning your data labels carefully and testing them with local saves, you’ll avoid the frustration of collecting unusable data and ensure your experiments produce clean, analysis-ready datasets from the start.