diff --git a/cmlreaders/cmlreader.py b/cmlreaders/cmlreader.py
index 6ae4bfd..7d35bc4 100644
--- a/cmlreaders/cmlreader.py
+++ b/cmlreaders/cmlreader.py
@@ -1,4 +1,4 @@
-from typing import Optional
+from typing import List, Optional
 
 import numpy as np
 import pandas as pd
@@ -233,3 +233,47 @@ class CMLReader(object):
             })
 
         return self.load('eeg', **kwargs)
+
+    @classmethod
+    def load_events(cls, subjects: Optional[List[str]] = None,
+                    experiments: Optional[List[str]] = None,
+                    rootdir: Optional[str] = None) -> pd.DataFrame:
+        """Load events from multiple sessions.
+
+        Parameters
+        ----------
+        subjects
+            List of subjects.
+        experiments
+            List of experiments to include.
+        rootdir
+            Path to root data directory.
+
+        """
+        if subjects is None and experiments is None:
+            raise ValueError(
+                "Please specify at least one subject or experiment."
+            )
+
+        rootdir = get_root_dir(rootdir)
+        df = get_data_index("all", rootdir=rootdir)
+
+        if subjects is None:
+            subjects = df["subject"].unique()
+
+        if experiments is None:
+            experiments = df["experiment"].unique()
+
+        events = []
+
+        for subject in subjects:
+            for experiment in experiments:
+                mask = (df["subject"] == subject) & (df["experiment"] == experiment)
+                sessions = df[mask]["session"].unique()
+
+                for session in sessions:
+                    reader = CMLReader(subject, experiment, session,
+                                       rootdir=rootdir)
+                    events.append(reader.load("events"))
+
+        return pd.concat(events)

Multi-session event reading
A common use case when working with events is to load multiple sessions at a time. Right now, this is not as easy as it should be.

```python
all_events = []
for session in sessions_completed:
    sess_events = cml.CMLReader(subject="R1409D", experiment="FR6", session=session,
                                localization=0, montage=0, rootdir=rhino_root).load('task_events')
    all_events.append(sess_events)
all_sessions_df = pd.concat(all_events)
```

The proposed API is to have a special method associated with cml_reader to allow loading multiple sessions of events. The other option is to allow additional kwargs in .load(), but then it becomes extremely difficult to document that function since it would take different parameters depending on the data type being loaded. Instead, we want to mimic the behavior of load_eeg and have it be a separate method associated with the class.  Single sessions of events can be loaded using either reader.load() after having specified a session when creating the reader, or by using reader.load_events(sessions=[1]).  At a minimum, the following cases should be handled:

- Given a single experiment, load all completed sessions
- Given a single experiment, load a specific subset of sessions

```python
reader = CMLReader(subject="R1409D",  experiment="FR6")

# Load all sessions
all_fr6_events = reader.load_events()

# Load specific sessions
subset_fr5_events = reader.load_events(sessions=[0, 1])

```

- Given multiple experiments, load all completed sessions from each experiment
- Given a reader with no experiment specified, raise an error if load_events is called

```python
reader = CMLReader(subject="R1409D")

# Invalid Request
all_events = reader.load_events()

# Load sessions across experiments
all_record_only_events = reader.load_events(experiments=['catFR1', 'FR1'])
```

Depending on if it is important enough of a use case, it could also handle the following cases:

- Given multiple experiments and a specific session, load that session number of each experiment, i.e. the first session of FR1 and catFR1 for a particular subject
- Given multiple experiments and a specific set of sessions, load those specific sessions for each experiment given, raising an error if any of the requested session/experiment combinations are not available

```python
reader = CMLReader(subject="R1409D")

# Multi-experiment, single session
multi_exp_single_sess = reader.load_events(experiments=['catFR1', 'FR1'], sessions=[0])

# Multi-experiment, multi-session
multi_exp_multi_sess =  reader.load_events(experiments=['catFR1', 'FR1'], sessions=[0, 1])
```












diff --git a/cmlreaders/test/test_cmlreader.py b/cmlreaders/test/test_cmlreader.py
index f222842..614fb8e 100644
--- a/cmlreaders/test/test_cmlreader.py
+++ b/cmlreaders/test/test_cmlreader.py
@@ -200,3 +200,25 @@ class TestLoadMontage:
 
         with pytest.raises(exc.MissingDataError):
             reader.load(kind, read_categories=True)
+
+
+@pytest.mark.only
+@pytest.mark.rhino
+class TestLoadAggregate:
+    @pytest.mark.parametrize("subjects,experiments,unique_sessions", [
+        (None, None, None),
+        (["R1111M", "R1260D"], ["FR1"], 5),
+        (["R1111M"], None, 22),
+        (["R1111M"], ["PS2"], 6),
+        (None, ["FR2"], 79),
+    ])
+    def test_load_events(self, subjects, experiments, unique_sessions,
+                         rhino_root):
+        if subjects is experiments is None:
+            with pytest.raises(ValueError):
+                CMLReader.load_events(subjects, experiments, rootdir=rhino_root)
+            return
+
+        events = CMLReader.load_events(subjects, experiments, rootdir=rhino_root)
+        size = len(events.groupby(["subject", "experiment", "session"]).size())
+        assert size == unique_sessions

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov
apt-get update apt-get install -y gcc
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
async-generator==1.10
attrs==22.2.0
Babel==2.11.0
bleach==4.1.0
cached-property==1.5.2
certifi==2021.5.30
charset-normalizer==2.0.12
-e git+https://github.com/pennmem/cmlreaders.git@828f57fa7cae9033917802ef646c1ad334790ed3#egg=cmlreaders
codecov==2.1.13
coverage==6.2
cycler==0.11.0
decorator==5.1.1
defusedxml==0.7.1
docutils==0.18.1
entrypoints==0.4
flake8==3.9.2
h5py==3.1.0
idna==3.10
imagesize==1.4.1
importlib-metadata==4.8.3
iniconfig==1.1.1
ipython-genutils==0.2.0
Jinja2==3.0.3
jsonschema==3.2.0
jupyter-client==7.1.2
jupyter-core==4.9.2
jupyterlab-pygments==0.1.2
kiwisolver==1.3.1
MarkupSafe==2.0.1
matplotlib==3.3.4
mccabe==0.6.1
mistune==0.8.4
mne==0.23.4
nbclient==0.5.9
nbconvert==6.0.7
nbformat==5.1.3
nbsphinx==0.8.8
nest-asyncio==1.6.0
numpy==1.19.5
packaging==21.3
pandas==1.1.5
pandocfilters==1.5.1
Pillow==8.4.0
pluggy==1.0.0
py==1.11.0
pycodestyle==2.7.0
pyflakes==2.3.1
Pygments==2.14.0
pyparsing==3.1.4
pyrsistent==0.18.0
pytest==7.0.1
pytest-cov==4.0.0
python-dateutil==2.9.0.post0
pytz==2025.2
pyzmq==25.1.2
requests==2.27.1
scipy==1.5.4
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinx-rtd-theme==2.0.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
testpath==0.6.0
tomli==1.2.3
tornado==6.1
traitlets==4.3.3
typing_extensions==4.1.1
urllib3==1.26.20
webencodings==0.5.1
zipp==3.6.0

name: cmlreaders
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - async-generator==1.10
      - attrs==22.2.0
      - babel==2.11.0
      - bleach==4.1.0
      - cached-property==1.5.2
      - charset-normalizer==2.0.12
      - codecov==2.1.13
      - coverage==6.2
      - cycler==0.11.0
      - decorator==5.1.1
      - defusedxml==0.7.1
      - docutils==0.18.1
      - entrypoints==0.4
      - flake8==3.9.2
      - h5py==3.1.0
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - ipython-genutils==0.2.0
      - jinja2==3.0.3
      - jsonschema==3.2.0
      - jupyter-client==7.1.2
      - jupyter-core==4.9.2
      - jupyterlab-pygments==0.1.2
      - kiwisolver==1.3.1
      - markupsafe==2.0.1
      - matplotlib==3.3.4
      - mccabe==0.6.1
      - mistune==0.8.4
      - mne==0.23.4
      - nbclient==0.5.9
      - nbconvert==6.0.7
      - nbformat==5.1.3
      - nbsphinx==0.8.8
      - nest-asyncio==1.6.0
      - numpy==1.19.5
      - packaging==21.3
      - pandas==1.1.5
      - pandocfilters==1.5.1
      - pillow==8.4.0
      - pluggy==1.0.0
      - py==1.11.0
      - pycodestyle==2.7.0
      - pyflakes==2.3.1
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pyrsistent==0.18.0
      - pytest==7.0.1
      - pytest-cov==4.0.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyzmq==25.1.2
      - requests==2.27.1
      - scipy==1.5.4
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinx-rtd-theme==2.0.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - testpath==0.6.0
      - tomli==1.2.3
      - tornado==6.1
      - traitlets==4.3.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - webencodings==0.5.1
      - zipp==3.6.0
prefix: /opt/conda/envs/cmlreaders

cmlreaders/test/test_cmlreader.py::TestLoadAggregate::test_load_events[None-None-None]
cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[voxel_coordinates-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[classifier_excluded_leads-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[jacksheet-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[good_leads-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[leads-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[electrode_coordinates-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[prior_stim_results-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[target_selection_table-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[electrode_categories-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[classifier_summary-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[math_summary-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[session_summary-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[pairs-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[contacts-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[localization-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[baseline_classifier-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[used_classifier-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[events-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[events-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[all_events-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[all_events-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[task_events-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[task_events-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[math_events-R1405E-FR1-1-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[math_events-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[baseline_classifier.zip] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[used_classifier.zip] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[baseline_classifier] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[used_classifier] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_ps4_events[R1354E-PS4_FR-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_ps2_events cmlreaders/test/test_cmlreader.py::TestLoadMontage::test_read_categories_rhino[True-contacts] cmlreaders/test/test_cmlreader.py::TestLoadMontage::test_read_categories_rhino[True-pairs] cmlreaders/test/test_cmlreader.py::TestLoadMontage::test_read_categories_rhino[False-contacts] cmlreaders/test/test_cmlreader.py::TestLoadMontage::test_read_categories_rhino[False-pairs] cmlreaders/test/test_cmlreader.py::TestLoadMontage::test_read_categories_missing[contacts] cmlreaders/test/test_cmlreader.py::TestLoadMontage::test_read_categories_missing[pairs] cmlreaders/test/test_cmlreader.py::TestLoadAggregate::test_load_events[subjects1-experiments1-5] cmlreaders/test/test_cmlreader.py::TestLoadAggregate::test_load_events[subjects2-None-22] cmlreaders/test/test_cmlreader.py::TestLoadAggregate::test_load_events[subjects3-experiments3-6] cmlreaders/test/test_cmlreader.py::TestLoadAggregate::test_load_events[None-experiments4-79]
cmlreaders/test/test_cmlreader.py::TestCMLReader::test_determine_localization_or_montage[R1278E-catFR1-0-0-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_determine_localization_or_montage[R1278E-catFR1-None-0-1] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_determine_localization_or_montage[R1278E-PAL1-None-2-2] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_determine_localization_or_montage[R1278E-PAL3-2-2-2] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_determine_localization_or_montage[R1278E-TH1-0-0-0] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_determine_localization_or_montage[R1278E-TH1-None-0-0] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_determine_localization_or_montage[LTP093-ltpFR2-0-None-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[voxel_coordinates-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[classifier_excluded_leads-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[jacksheet-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[good_leads-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[leads-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[electrode_coordinates-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[prior_stim_results-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[target_selection_table-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[electrode_categories-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[classifier_summary-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[math_summary-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[session_summary-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[pairs-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[contacts-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[localization-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[baseline_classifier-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_from_rhino[used_classifier-LTP093-ltpFR2-0-None] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[voxel_coordinates.txt] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[classifier_excluded_leads.txt] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[jacksheet.txt] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[good_leads.txt] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[leads.txt] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[electrode_coordinates.csv] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[prior_stim_results.csv] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[target_selection_table.csv] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[pairs.json] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[contacts.json] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[localization.json] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[all_events.json] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[math_events.json] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load[task_events.json] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[voxel_coordinates] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[classifier_excluded_leads] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[jacksheet] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[good_leads] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[leads] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[electrode_coordinates] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[prior_stim_results] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[target_selection_table] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[pairs] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[contacts] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[localization] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[all_events] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[math_events] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_get_reader[task_events] cmlreaders/test/test_cmlreader.py::TestCMLReader::test_load_unimplemented cmlreaders/test/test_cmlreader.py::TestLoadMontage::test_read_categories[True-contacts] cmlreaders/test/test_cmlreader.py::TestLoadMontage::test_read_categories[True-pairs] cmlreaders/test/test_cmlreader.py::TestLoadMontage::test_read_categories[False-contacts] cmlreaders/test/test_cmlreader.py::TestLoadMontage::test_read_categories[False-pairs]
cmlreaders/cmlreader.py
cmlreaders/cmlreader.py