diff --git a/CHANGES.rst b/CHANGES.rst
index 55dbe75c9681b67083671ae54999c63af5109eb8..99f5bd330690bd22a8b047f8839abe3602bb618d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -38,6 +38,10 @@ Changelog
     to the superevent. The automated pipeline is launched and is blocked before sending
     if ``EM_SelectedConfident`` is found to be applied.
 
+-   Add O3 replay MDC testing with RAVEN pipeline. This will run on the 
+    emfollow-playground server, creating mock coincidences with a frequency
+    given by the ``joint_O3_replay_freq`` variable.
+
 2.0.3 "Ugly Merman" (2023-02-16)
 --------------------------------
 
diff --git a/gwcelery/conf/__init__.py b/gwcelery/conf/__init__.py
index 3afe4c63c39a763929e29aba8a66b09d3124cab1..0ca1de48ffafcc5d605db55cf0b40e2ddc0948a3 100644
--- a/gwcelery/conf/__init__.py
+++ b/gwcelery/conf/__init__.py
@@ -353,6 +353,11 @@ joint_mdc_freq = 2
 MDC superevent to test the RAVEN alert pipeline, i.e for every x
 MDC superevents an external MDC event is created."""
 
+joint_O3_replay_freq = 10
+"""Determines how often an external replay event will be created near an
+superevent to test the RAVEN alert pipeline, i.e for every x
+O3 replay superevents an external MDC event is created."""
+
 bilby_default_mode = 'fast_test'
 """Sampling mode of bilby"""
 
diff --git a/gwcelery/tasks/first2years_external.py b/gwcelery/tasks/first2years_external.py
index e09270a0e6b8de412ec6849c5cfc78f93b7ec8a6..24a40db4dcad043b28bbbc74524f0abc7bf24a0a 100644
--- a/gwcelery/tasks/first2years_external.py
+++ b/gwcelery/tasks/first2years_external.py
@@ -13,20 +13,31 @@ from . import external_triggers
 from . import igwn_alert
 
 
-def create_grb_event(gpstime, pipeline):
-
+def create_grb_event(gpstime, pipeline, se_search):
+    """ Create a random GRB event for a certain percentage of MDC or O3-replay
+     superevents.
+
+    Parameters
+    ----------
+    gpstime : float
+        Event's gps time
+    pipeline : str
+        External trigger pipeline name
+    se_search : str
+        Search field for preferred event, 'MDC' or 'AllSky'
+    """
     new_date = str(Time(gpstime, format='gps', scale='utc').isot) + 'Z'
     new_TrigID = str(int(gpstime))
-
     fname = str(Path(__file__).parent /
                 '../tests/data/{}_grb_gcn.xml'.format(pipeline.lower()))
 
     root = etree.parse(fname)
-
-    # Change ivorn to indicate is an MDC event
+    # Change ivorn to indicate if this is an MDC event or O3 replay event
     root.xpath('.')[0].attrib['ivorn'] = \
-        'ivo://lvk.internal/{0}#MDC-test_event{1}'.format(
-            pipeline if pipeline != 'Swift' else 'SWIFT', new_date).encode()
+        'ivo://lvk.internal/{0}#{1}_event{2}'.format(
+            pipeline if pipeline != 'Swift' else 'SWIFT',
+            'MDC-test' if se_search == 'MDC' else 'O3-replay',
+            new_date).encode()
 
     # Change times to chosen time
     root.find("./Who/Date").text = str(new_date).encode()
@@ -56,55 +67,89 @@ def create_grb_event(gpstime, pipeline):
                           pretty_print=True)
 
 
-def _offset_time(gpstime):
-    # Reverse when searching around superevents
-    th_cbc, tl_cbc = app.conf['raven_coincidence_windows']['GRB_CBC']
-    return gpstime + random.uniform(-tl_cbc, -th_cbc)
+def _offset_time(gpstime, group):
+    """ This function checks coincident time windows for superevents if they
+    are of Burst or CBC group.
+
+       Parameters
+       ----------
+       gpstime : float
+           Event's gps time
+       group : str
+           Burst or CBC
+    """
+    if group == 'Burst':
+        th, tl = app.conf['raven_coincidence_windows']['GRB_Burst']
+    elif group == 'CBC':
+        th, tl = app.conf['raven_coincidence_windows']['GRB_CBC']
+    else:
+        raise AssertionError(
+            'Invalid group {}. Use only CBC or Burst.'.format(group))
+    return gpstime + random.uniform(-tl, -th)
 
 
-def _is_joint_mdc(graceid):
-    """Upload external event to every ten MDCs
+def _is_joint_mdc(graceid, se_search):
+    """Upload external events to the user-defined frequency of MDC or AllSky
+    superevents.
 
     Looks at the ending letters of a superevent (e.g. 'ac' from 'MS190124ac'),
     converts to a number, and checks if divisible by a number given in the
     configuration file.
 
-    For example, if the configuration number 'joint_mdc_freq' is 10,
+    For example, if the configuration number
+    :obj:`~gwcelery.conf.joint_mdc_freq` is 10,
     this means joint events with superevents ending with 'j', 't', 'ad', etc.
     """
     end_string = re.split(r'\d+', graceid)[-1].lower()
     val = 0
     for i in range(len(end_string)):
         val += (ord(end_string[i]) - 96) * 26 ** (len(end_string) - i - 1)
-    return val % int(app.conf['joint_mdc_freq']) == 0
+    return val % int(app.conf['joint_mdc_freq']) == 0 if se_search == 'MDC' \
+        else val % int(app.conf['joint_O3_replay_freq']) == 0
 
 
 @igwn_alert.handler('mdc_superevent',
+                    'superevent',
                     queue='exttrig',
                     shared=False)
 def upload_external_event(alert):
-    """Upload a random GRB event for a certain percentage of MDC superevents.
-
-    Every n MDC superevents, upload a Fermi-like GRB candidate within the
-    standard CBC-GRB search window, where the frequency n is determined by
-    the configuration variable 'joint_mdc_freq'.
+    """Upload a random GRB event for a certain percentage of MDC
+    or O3-replay superevents.
+
+    Notes
+    -----
+    Every n superevents, upload a GRB candidate within the
+    standard CBC-GRB or Burst-GRB search window, where the frequency n is
+    determined by the configuration variable
+    :obj:`~gwcelery.conf.joint_mdc_freq` or
+    :obj:`~gwcelery.conf.joint_O3_replay_freq`.
+
+    For O3 replay testing with RAVEN pipeline, only run on gracedb-playground.
     """
-
-    # Only create external MDC for the occasional MDC superevent
-    if not _is_joint_mdc(alert['uid']) or alert['alert_type'] != 'new':
+    if alert['alert_type'] != 'new':
         return
+    se_search = alert['object']['preferred_event_data']['search']
+    group = alert['object']['preferred_event_data']['group']
+    is_gracedb_playground = app.conf['gracedb_host'] \
+        == 'gracedb-playground.ligo.org'
+    joint_mdc_alert = se_search == 'MDC' and _is_joint_mdc(alert['uid'], 'MDC')
+    joint_allsky_alert = se_search == 'AllSky' and \
+        _is_joint_mdc(alert['uid'], 'AllSky') and is_gracedb_playground
+    if not (joint_mdc_alert or joint_allsky_alert):
+        return
+
     # Potentially upload 1, 2, or 3 GRB events
     num = 1 + np.random.choice(np.arange(3), p=[.6, .3, .1])
     events = []
     pipelines = []
     for i in range(num):
         gpstime = float(alert['object']['t_0'])
-        new_time = _offset_time(gpstime)
+        new_time = _offset_time(gpstime, group)
 
         # Choose external grb pipeline to simulate
         pipeline = np.random.choice(['Fermi', 'Swift', 'INTEGRAL', 'AGILE'],
                                     p=[.5, .3, .1, .1])
-        ext_event = create_grb_event(new_time, pipeline)
+        ext_event = create_grb_event(new_time, pipeline, se_search)
 
         # Upload as from GCN
         external_triggers.handle_grb_gcn(ext_event)
diff --git a/gwcelery/tests/test_tasks_first2years_external.py b/gwcelery/tests/test_tasks_first2years_external.py
index a2d1583652e5f2250c75d928c1cdfeb203b0b444..1ec7fcbd076c52eb565b170dce734a0cbd89a7a0 100644
--- a/gwcelery/tests/test_tasks_first2years_external.py
+++ b/gwcelery/tests/test_tasks_first2years_external.py
@@ -1,40 +1,90 @@
-from unittest.mock import call, patch
+import pytest
+from unittest.mock import call, Mock
 
 from . import data
+from .. import app
 from ..tasks import first2years_external
 from ..util import read_json
 
 
-@patch('gwcelery.tasks.external_skymaps.create_upload_external_skymap.run')
-@patch('gwcelery.tasks.external_skymaps.get_upload_external_skymap.run')
-@patch('gwcelery.tasks.detchar.check_vectors.run')
-@patch('gwcelery.tasks.gracedb.create_event.run', return_value={
-    'graceid': 'E1', 'gpstime': 1, 'instruments': '', 'pipeline': 'Fermi',
-    'search': 'GRB',
-    'extra_attributes': {'GRB': {'trigger_duration': 1, 'trigger_id': 123,
-                                 'ra': 0., 'dec': 0., 'error_radius': 10.}},
-    'links': {'self': 'https://gracedb.ligo.org/events/E356793/'}})
-@patch('gwcelery.tasks.gracedb.get_events', return_value=[])
-def test_handle_create_grb_event(mock_get_events,
-                                 mock_create_event,
-                                 mock_check_vectors,
-                                 mock_get_upload_external_skymap,
-                                 mock_create_upload_external_skymap):
-
+@pytest.mark.parametrize(
+    'host,se_search,group,superevent_id,expected_result',
+    [['gracedb-playground.ligo.org', 'MDC', 'CBC', 'MS180616j', True],
+     ['gracedb-playground.ligo.org', 'AllSky', 'CBC', 'MS180616j', True],
+     ['gracedb-playground.ligo.org', 'AllSky', 'Burst', 'MS180616j', True],
+     ['gracedb-playground.ligo.org', 'BBH', 'CBC', 'MS180616j', False],
+     ['gracedb-playground.ligo.org', 'AllSky', 'Test', 'TS180616j', False],
+     ['gracedb.ligo.org', 'MDC', 'CBC', 'MS180616j', True],
+     ['gracedb.ligo.org', 'AllSky', 'CBC', 'MS180616j', False],
+     ['gracedb.ligo.org', 'AllSky', 'Burst', 'MS180616j', False],
+     ['gracedb.ligo.org', 'MDC', 'CBC', 'MS180616k', False]])
+def test_handle_create_grb_event(monkeypatch,
+                                 host,
+                                 se_search,
+                                 group,
+                                 superevent_id,
+                                 expected_result):
     # Test IGWN alert payload.
     alert = read_json(data, 'igwn_alert_superevent_creation.json')
-
-    alert['uid'] = 'MS180616j'
+    alert['uid'] = superevent_id
     alert['object']['superevent_id'] = alert['uid']
-    alert['object']['preferred_event_data']['search'] = 'MDC'
-    events, pipelines = first2years_external.upload_external_event(alert)
+    alert['object']['preferred_event_data']['search'] = se_search
+    alert['object']['preferred_event_data']['group'] = group
+
+    mock_create_upload_external_skymap = Mock()
+    mock_get_upload_external_skymap = Mock()
+    mock_check_vectors = Mock()
+    mock_create_event = Mock(
+        return_value={'graceid': 'E1',
+                      'gpstime': 1,
+                      'instruments': '',
+                      'pipeline': 'Fermi',
+                      'search': 'GRB',
+                      'extra_attributes':
+                      {'GRB': {'trigger_duration': 1,
+                               'trigger_id': 123,
+                               'ra': 0., 'dec': 0.,
+                               'error_radius': 10.}},
+                      'links': {
+                          'self':
+                              'https://gracedb.ligo.org/events/E356793/'}})
+    mock_get_events = Mock(return_value=[])
+
+    monkeypatch.setattr(
+        'gwcelery.tasks.external_skymaps.create_upload_external_skymap.run',
+        mock_create_upload_external_skymap)
+    monkeypatch.setattr(
+        'gwcelery.tasks.external_skymaps.get_upload_external_skymap.run',
+        mock_get_upload_external_skymap)
+    monkeypatch.setattr('gwcelery.tasks.detchar.check_vectors.run',
+                        mock_check_vectors)
+    monkeypatch.setattr('gwcelery.tasks.gracedb.create_event.run',
+                        mock_create_event)
+    monkeypatch.setattr('gwcelery.tasks.gracedb.get_events',
+                        mock_get_events)
+    monkeypatch.setattr(app.conf, 'gracedb_host', host)
+    if group == 'Test':
+        with pytest.raises(AssertionError):
+            first2years_external.upload_external_event(alert)
+        res = None
+    else:
+        res = first2years_external.upload_external_event(alert)
+    if not expected_result:
+        assert res is None
+        events, pipelines = [], []
+    else:
+        events, pipelines = res
 
     calls = []
     for i in range(len(events)):
         calls.append(call(filecontents=events[i],
-                          search='MDC',
+                          search='MDC' if se_search == 'MDC' else 'GRB',
                           pipeline=pipelines[i],
                           group='External',
                           labels=None))
-    mock_create_event.assert_has_calls(calls)
-    mock_create_upload_external_skymap.assert_called()
+    if expected_result:
+        mock_create_event.assert_has_calls(calls)
+        mock_create_upload_external_skymap.assert_called()
+    else:
+        mock_create_event.assert_not_called()
+        mock_create_upload_external_skymap.assert_not_called()