Writing HED into events.json: practical BIDS examples
Why HED usually belongs in events.json
In BIDS, the most maintainable way to annotate events is usually to place the HED information in the JSON sidecar rather than writing a full custom HED string for every single row.
This approach is easier to:
- read
- maintain
- validate
- reuse across many event rows
In most datasets, the event rows in events.tsv contain values such as event type, condition, stimulus file, and response label. The events.json file explains what those values mean and attaches HED annotations to them.
Example 1: a simple fixation -> face stimulus -> button press experiment
This is a beginner-friendly example similar to many EEG, MEG, and behavioral tasks.
events.tsv
onset duration trial_type face_type response stim_file
1.2 0.5 fixation n/a n/a cross.png
2.0 0.5 show_face unfamiliar_face n/a faces/u032.bmp
2.7 n/a left_press n/a correct
What the columns mean
| Column | Meaning |
|---|---|
trial_type |
Main event category recorded in this row |
face_type |
Whether the shown face is familiar or unfamiliar |
response |
Behavioral outcome |
stim_file |
Path or file name of the visual stimulus |
events.json
{
"trial_type": {
"Description": "Primary event type for each row",
"Levels": {
"fixation": "Fixation cross is displayed",
"show_face": "A face image is shown",
"left_press": "Participant presses the left response button"
},
"HED": {
"fixation": "Sensory-event, Visual-presentation, Instructional, (Cross, White-color)",
"show_face": "Sensory-event, Visual-presentation, Experimental-stimulus, (Image, Face)",
"left_press": "Agent-action, Press, (Experiment-participant, Human-agent), Correct-action"
}
},
"face_type": {
"Description": "Type of face shown on face trials",
"Levels": {
"unfamiliar_face": "The face is unfamiliar to the participant"
},
"HED": {
"unfamiliar_face": "(Condition-variable/Face-identity, Label/Unfamiliar-face)"
}
},
"response": {
"Description": "Behavioral classification of the participant response",
"Levels": {
"correct": "The response is correct"
},
"HED": {
"correct": "Correct-action"
}
},
"stim_file": {
"Description": "Path to the stimulus file",
"HED": "(Image, Pathname/#)"
}
}
What this achieves
For the row with show_face, the assembled annotation combines:
- the main event type from
trial_type - the condition detail from
face_type - the file path from
stim_file
That gives a much richer event description than the row alone.
Example 2: adding a cover story or task context
Many experiments do not present stimuli in isolation. They are embedded in a task story, block context, or instructional context.
HED can capture that context in a reusable way.
Scenario
Participants are told that they are working as an airport security screener. They see baggage images and must detect whether a target item is present.
This is a classic example where the cover story matters scientifically, because it shapes attention, expectations, and decision strategy.
events.tsv
onset duration event_type item_status response stim_file
0.0 2.0 instruction_screen n/a n/a instructions01.png
2.5 0.8 show_bag no_target n/a bags/bag001.png
3.4 n/a button_press n/a hit
4.2 0.8 show_bag target_present n/a bags/bag002.png
5.0 n/a button_press n/a miss
events.json
{
"event_type": {
"Description": "Main event type",
"Levels": {
"instruction_screen": "Instruction screen is shown",
"show_bag": "Baggage image is shown",
"button_press": "Participant button response"
},
"HED": {
"instruction_screen": "Sensory-event, Visual-presentation, Instructional, (Image, Text)",
"show_bag": "Sensory-event, Visual-presentation, Experimental-stimulus, (Image, Bag)",
"button_press": "Agent-action, Press, (Experiment-participant, Human-agent)"
}
},
"item_status": {
"Description": "Whether the bag contains the target item",
"Levels": {
"no_target": "No target object is present",
"target_present": "A target object is present"
},
"HED": {
"no_target": "Distractor",
"target_present": "Target"
}
},
"response": {
"Description": "Behavioral response category",
"Levels": {
"hit": "Participant correctly detects a target",
"miss": "Participant fails to report a target"
},
"HED": {
"hit": "Correct-action",
"miss": "Incorrect-action"
}
},
"stim_file": {
"Description": "Stimulus file path",
"HED": "(Image, Pathname/#)"
}
}
How to represent the cover story more explicitly
If the cover story is central to the experiment, you can represent it using a reusable definition.
Example HED definition
(Definition/Airport-security-task, (Task, Label/Airport-security-screening))
You can then include that concept where needed:
Def/Airport-security-task
For example, the show_bag entry could be extended conceptually to include that the stimulus belongs to the airport security task context.
This is especially useful when the same context applies across many rows.
Example 3: oddball EEG experiment
This type of example is highly relevant for neuroscientists.
events.tsv
onset duration event_type stimulus_type response reaction_time
0.5 0.1 play_tone standard n/a n/a
1.7 0.1 play_tone target n/a n/a
2.1 n/a button_press n/a correct 0.42
2.9 0.1 play_tone standard n/a n/a
events.json
{
"event_type": {
"Description": "Main type of event",
"Levels": {
"play_tone": "An auditory tone is presented",
"button_press": "Participant presses the response button"
},
"HED": {
"play_tone": "Sensory-event, Auditory-presentation, Experimental-stimulus, Tone",
"button_press": "Agent-action, Press, (Experiment-participant, Human-agent)"
}
},
"stimulus_type": {
"Description": "Role of the tone in the oddball task",
"Levels": {
"standard": "Frequent non-target tone",
"target": "Rare target tone"
},
"HED": {
"standard": "Distractor",
"target": "Target"
}
},
"response": {
"Description": "Outcome of the participant response",
"Levels": {
"correct": "Correct target response"
},
"HED": {
"correct": "Correct-action"
}
},
"reaction_time": {
"Description": "Reaction time in seconds",
"Units": "s",
"HED": "(Agent-action, Label/Reaction-time, Duration/# s)"
}
}
Why this works well for EEG
This structure clearly separates:
- the sensory event (
play_tone) - the task role (
standardvstarget) - the participant response (
button_press) - the behavioral outcome (
correct) - the value column (
reaction_time)
This makes the dataset much easier to search and analyze later.
Example 4: trial and block structure with reusable definitions
In many experiments, not every event is a stimulus or response. Some events define structure.
Examples:
- block start
- trial start
- rest period onset
- stimulus sequence onset and offset
HED is very good at representing this structure.
Conceptual example definition set
(Definition/Task-block, (Time-block, Task))
(Definition/Trial-context, (Experimental-trial, Task))
Possible event rows
onset duration event_type block_type
0.0 30 block_start face_block
30.0 2 trial_start n/a
32.0 0.5 show_face n/a
33.0 n/a button_press n/a
Possible sidecar idea
{
"event_type": {
"HED": {
"block_start": "Experiment-structure, Def/Task-block",
"trial_start": "Experiment-structure, Def/Trial-context",
"show_face": "Sensory-event, Visual-presentation, Experimental-stimulus, (Image, Face)",
"button_press": "Agent-action, Press, (Experiment-participant, Human-agent)"
}
},
"block_type": {
"HED": {
"face_block": "Label/Face-block"
}
}
}
This is a simplified pattern, but it shows how HED can annotate not only isolated stimuli but also the structure of the experiment.
How to think when writing events.json
A practical workflow for researchers is:
Step 1: identify the main columns
Usually these are columns like:
trial_typeevent_typestim_typeresponseconditionstim_file
Step 2: decide whether a column is categorical or value-based
| Column type | Typical examples | How to annotate |
|---|---|---|
| Categorical | show_face, left_press, target, incorrect |
Map each value to a HED string |
| Value-based | file path, RT, numeric ID, intensity value | Use a HED string with # |
Step 3: annotate meaning, not just code labels
Bad thinking:
code 13 means something important to my lab
Better thinking:
this row marks the onset of a visual face stimulus that serves as a target
Step 4: keep reusable meaning in the sidecar
If the same concept appears many times, put it in the sidecar once instead of rewriting it row by row.
Beginner mistakes to avoid in events.json
| Mistake | Why it is a problem | Better approach |
|---|---|---|
Writing everything only in events.tsv |
Hard to maintain, hard to reuse | Put shared meaning in events.json |
| Annotating only codes | Other users cannot understand the experiment | Annotate scientific meaning |
| Mixing categorical and value logic | Validation becomes confusing | Separate category maps from # value placeholders |
| Using overcomplicated tags too early | Makes documentation harder to read | Start simple, then refine |
| Ignoring task context | Loses important experimental meaning | Add context through properties or definitions |
A compact template you can adapt
{
"event_type": {
"Description": "Main event type",
"Levels": {
"stimulus": "A stimulus is shown",
"response": "Participant response"
},
"HED": {
"stimulus": "Sensory-event, Visual-presentation, Experimental-stimulus, (Image, Face)",
"response": "Agent-action, Press, (Experiment-participant, Human-agent)"
}
},
"condition": {
"Description": "Experimental condition",
"Levels": {
"target": "Target condition",
"distractor": "Distractor condition"
},
"HED": {
"target": "Target",
"distractor": "Distractor"
}
},
"stim_file": {
"Description": "Stimulus file",
"HED": "(Image, Pathname/#)"
}
}
Key takeaway
The goal of events.json is not just to satisfy BIDS metadata requirements.
It is to turn event rows into scientifically meaningful, machine-actionable annotations.
For beginners, the most useful pattern is:
- annotate the main event column first
- add condition and response meaning next
- use
#for row-wise values such as file names or reaction times - use definitions when complex concepts repeat
That approach is usually enough to make a BIDS dataset far easier to understand and reuse.