diff --git a/PhysicsAnalysis/NtupleDumper/CMakeLists.txt b/PhysicsAnalysis/NtupleDumper/CMakeLists.txt
index fab78641d7ae2a3842904122968a80170bca9a28..607c58d892d201731098f64829a82d556f1a302a 100644
--- a/PhysicsAnalysis/NtupleDumper/CMakeLists.txt
+++ b/PhysicsAnalysis/NtupleDumper/CMakeLists.txt
@@ -1,11 +1,14 @@
 atlas_subdir( NtupleDumper )
 
+find_package( nlohmann_json )
+
 atlas_add_component(
         NtupleDumper
         src/NtupleDumperAlg.h
         src/NtupleDumperAlg.cxx
         src/component/NtupleDumper_entries.cxx
         LINK_LIBRARIES AthenaBaseComps StoreGateLib xAODFaserWaveform xAODFaserCalorimeter xAODFaserTrigger xAODFaserLHC ScintIdentifier FaserCaloIdentifier GeneratorObjects FaserActsGeometryLib TrackerSimEvent TrackerSimData TrackerIdentifier TrackerReadoutGeometry TrkTrack GeoPrimitives TrackerRIO_OnTrack TrackerSpacePoint FaserActsKalmanFilterLib
+	PRIVATE_LINK_LIBRARIES nlohmann_json::nlohmann_json 
 )
 
 atlas_install_python_modules( python/*.py )
diff --git a/PhysicsAnalysis/NtupleDumper/scripts/faser_ntuple_maker.py b/PhysicsAnalysis/NtupleDumper/scripts/faser_ntuple_maker.py
index 8ea26ebb33fef5490110206cab8c940eecfa716b..1f4b990c2fb7bbdef7e109898e91bf5e0171d49d 100755
--- a/PhysicsAnalysis/NtupleDumper/scripts/faser_ntuple_maker.py
+++ b/PhysicsAnalysis/NtupleDumper/scripts/faser_ntuple_maker.py
@@ -55,6 +55,9 @@ parser.add_argument("--unblind", action='store_true',
                     help="Don't apply signal blinding (default: do)")
 parser.add_argument("--onlyblind", action='store_true',
                     help="Only store events that were blinded (will override unblind arg)")
+
+parser.add_argument("--grl", default="",
+                    help="Specify GRL to apply")
  
 parser.add_argument("--fluka", action='store_true',
                     help="Add FLUKA weights to ntuple")
@@ -148,11 +151,6 @@ if filepath.is_dir():
     print(f"First = {firstseg}")
     print(f"Last  = {lastseg}")
     print(f"Tag  = {args.tag}")
-    print(f"Trigger Filter  = {args.trigFilt}")
-    print(f"Scintillator Filter  = {args.scintFilt}")
-    print(f"Track Filter = {not args.NoTrackFilt}")
-    print(f"Blind = {not args.unblind}")
-    print(f"OnlyBlinded = {args.onlyblind}")
 
     # Find any tags
     tagstr = firststem.replace(f"{firstfaser}-{firstshort}-{runstr}-{firstseg}", "")
@@ -212,6 +210,13 @@ if len(args.outfile) > 0:
 print("Output file:")
 print(outfile)
 
+print(f"Trigger Filter  = {args.trigFilt}")
+print(f"Scintillator Filter  = {args.scintFilt}")
+print(f"Track Filter = {not args.NoTrackFilt}")
+print(f"Blind = {not args.unblind}")
+print(f"OnlyBlinded = {args.onlyblind}")
+print(f"GRL = {args.grl}")
+
 # OK, lets run the job here
 from AthenaCommon.Logging import log, logging
 from AthenaCommon.Constants import DEBUG, VERBOSE, INFO
@@ -245,18 +250,25 @@ ConfigFlags.lock()
 acc = MainServicesCfg(ConfigFlags)
 acc.merge(PoolReadCfg(ConfigFlags))
 
+# Create kwargs for NtupleDumper
+grl_kwargs = {}
+if args.grl:
+    grl_kwargs['GoodRunsList'] = args.grl
+    grl_kwargs['ApplyGoodRunsList'] = True
+
+mc_kwargs = {}
+if args.genie:
+    mc_kwargs['UseGenieWeights'] = True
+if args.fluka:
+    mc_kwargs['UseFlukaWeights'] = True
+
 # algorithm
 from NtupleDumper.NtupleDumperConfig import NtupleDumperAlgCfg
 if args.isMC:
-    if args.genie:
-        acc.merge(NtupleDumperAlgCfg(ConfigFlags, outfile, UseGenieWeights=True))
-    elif args.fluka:
-        acc.merge(NtupleDumperAlgCfg(ConfigFlags, outfile, UseFlukaWeights=True))
-    else:
-        acc.merge(NtupleDumperAlgCfg(ConfigFlags, outfile))
+    acc.merge(NtupleDumperAlgCfg(ConfigFlags, outfile, **mc_kwargs))
 
 else:
-    acc.merge(NtupleDumperAlgCfg(ConfigFlags, outfile, DoBlinding=(not args.unblind), OnlyBlinded=args.onlyblind, DoScintFilter = args.scintFilt, DoTrackFilter = (not args.NoTrackFilt), DoTrigFilter = args.trigFilt))
+    acc.merge(NtupleDumperAlgCfg(ConfigFlags, outfile, DoBlinding=(not args.unblind), OnlyBlinded=args.onlyblind, DoScintFilter = args.scintFilt, DoTrackFilter = (not args.NoTrackFilt), DoTrigFilter = args.trigFilt, **grl_kwargs))
 
 if not args.verbose:
     from AthenaConfiguration.ComponentFactory import CompFactory
@@ -264,6 +276,10 @@ if not args.verbose:
     AthenaEventLoopMgr.EventPrintoutInterval=1000
     acc.addService(AthenaEventLoopMgr)
 
+else:
+    nd = acc.getEventAlgo("NtupleDumperAlg")
+    nd.OutputLevel = VERBOSE
+
 # Hack to avoid problem with our use of MC databases when isMC = False
 if not args.isMC:
     replicaSvc = acc.getService("DBReplicaSvc")
diff --git a/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.cxx b/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.cxx
index 8b6aba7341e4c4c01aa752d8e6acbda13eb053cd..59a2be962c55923976f578c7e403cd2aae64a899 100644
--- a/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.cxx
+++ b/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.cxx
@@ -120,6 +120,19 @@ StatusCode NtupleDumperAlg::initialize()
 
   ATH_CHECK(m_spacePointContainerKey.initialize());
 
+  // Read GRL if requested
+  if (!m_goodRunsList.value().empty()) {
+    ATH_MSG_INFO("Opening GRL file " << m_goodRunsList);
+    ATH_MSG_INFO(m_applyGoodRunsList);
+    std::ifstream f(m_goodRunsList);
+    m_grl = nlohmann::json::parse(f);
+    f.close();
+    ATH_MSG_INFO("Read GRL with " << m_grl.size() << " records");
+  } else {
+    // Make sure this is empty
+    m_grl.clear();
+  }
+
   if (m_useFlukaWeights)
   {
     m_baseEventCrossSection = (m_flukaCrossSection * kfemtoBarnsPerMilliBarn)/m_flukaCollisions;
@@ -140,6 +153,7 @@ StatusCode NtupleDumperAlg::initialize()
   m_tree->Branch("eventID", &m_event_number, "eventID/I");
   m_tree->Branch("eventTime", &m_event_time, "eventTime/I");
   m_tree->Branch("BCID", &m_bcid, "BCID/I");
+  m_tree->Branch("inGRL", &m_in_grl, "inGRL/I");
 
   m_tree->Branch("fillNumber", &m_fillNumber, "fillNumber/I");
   m_tree->Branch("betaStar", &m_betaStar, "betaStar/F");
@@ -417,6 +431,71 @@ StatusCode NtupleDumperAlg::execute(const EventContext &ctx) const
     isMC = true;
   }
 
+  // EventInfo data
+  m_run_number = ctx.eventID().run_number();
+  m_event_number = ctx.eventID().event_number();
+  m_event_time = ctx.eventID().time_stamp();
+  m_bcid = ctx.eventID().bunch_crossing_id();
+
+  // For real data, find if data is in GoodRunsList
+  if (!isMC && !m_grl.empty()) {
+    ATH_MSG_DEBUG("Checking GRL for run " << m_run_number << " event " << m_event_number);
+
+    // JSON keys are always strings
+    std::string run_string = std::to_string(m_run_number);
+    std::string reason("unknown");
+
+    if (m_grl.contains(run_string)) {
+      // m_in_grl set to 0 by clearTree
+      auto jrun = m_grl[run_string];
+
+      // Make sure this is in the stable beams range
+      if (jrun.contains("stable_list")) {
+	for ( auto& slist : jrun["stable_list"]) {
+	  // Check if event falls in this range
+	  if (slist["start_utime"] > m_event_time) continue;
+	  if (slist["stop_utime"] <= m_event_time) continue;
+	  m_in_grl = 1;
+	  break;
+	}
+
+	// Check exclude list (not all runs will have this)
+	if (jrun.contains("excluded_list")) {
+	  ATH_MSG_DEBUG("Checking excluded list");
+	  for ( auto& blist : jrun["excluded_list"]) {
+	    ATH_MSG_DEBUG("Checking excluded list start " << blist["start_utime"] << " stop " << blist["stop_utime"] << " event " << m_event_time);
+
+	    // Check if event falls in this range
+	    if (blist["start_utime"] > m_event_time) continue;
+	    if (blist["stop_utime"] <= m_event_time) continue;
+	    if (blist.contains("reason")) {
+	      reason = blist["reason"];
+	    } 
+	    m_in_grl = 0;
+	    break;
+	  }
+	}
+
+	if (m_in_grl == 0) {
+	  ATH_MSG_DEBUG("Run " << run_string << " event " << m_event_number << " time " << m_event_time << " excluded for " << reason);
+	}
+
+      } else {
+	ATH_MSG_WARNING("Run " << run_string << " has no stable_list!");
+      }
+    } else {
+      ATH_MSG_WARNING("Run " << run_string << " not found in GRL!");
+    }
+  }
+
+  // Skip this event?
+  if (m_applyGoodRunsList && (m_in_grl == 0)) {
+    ATH_MSG_DEBUG("Skipping run " << m_run_number 
+		  << " event " << m_event_number << " - outside GRL" );
+    m_eventsFailedGRL += 1;
+    return StatusCode::SUCCESS;
+  }
+
   // if real data, correct for diigitzer timing jitter with clock phase
   if (!isMC) {
     // correct waveform time with clock phase
@@ -577,11 +656,6 @@ StatusCode NtupleDumperAlg::execute(const EventContext &ctx) const
     m_scintHit = m_scintHit | 128;
   }
 
-  m_run_number = ctx.eventID().run_number();
-  m_event_number = ctx.eventID().event_number();
-  m_event_time = ctx.eventID().time_stamp();
-  m_bcid = ctx.eventID().bunch_crossing_id();
-
   if (isMC) { // if simulation find MC cross section and primary lepton
     // Work out effective cross section for MC
     if (m_useFlukaWeights) {
@@ -1161,7 +1235,8 @@ StatusCode NtupleDumperAlg::execute(const EventContext &ctx) const
 
 StatusCode NtupleDumperAlg::finalize()
 {
-  ATH_MSG_INFO("Number of events passed Ntuple selectioon = " << m_eventsPassed);
+  ATH_MSG_INFO("Number of events passed Ntuple selection = " << m_eventsPassed);
+  ATH_MSG_INFO("Number of events failing GRL selection   = " << m_eventsFailedGRL);
   return StatusCode::SUCCESS;
 }
 
@@ -1173,6 +1248,7 @@ NtupleDumperAlg::clearTree() const
   m_event_number = 0;
   m_event_time = 0;
   m_bcid = 0;
+  m_in_grl = 0;
 
   m_fillNumber = 0;
   m_betaStar = 0;
diff --git a/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.h b/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.h
index 322e38c90a68e917951a763b092c507efdb54c0a..339c0838b22047dd146d71163acebdc472f7d1f3 100644
--- a/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.h
+++ b/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.h
@@ -25,6 +25,7 @@
 #include "StoreGate/ReadDecorHandle.h"
 
 #include <vector>
+#include <nlohmann/json.hpp>
 
 class TTree;
 class TH1;
@@ -120,6 +121,12 @@ private:
   DoubleProperty m_blindingCaloThreshold {this, "BlindingCaloThreshold", 100000.0, "Blind events with Ecal energy above threshold (in MeV)"}; 
   DoubleProperty m_caloMC_FudgeFactor    {this, "CaloFudgeFactorMC", 1.088, "Correct the MC energy calibration by this fudge factor"};
 
+  BooleanProperty m_applyGoodRunsList    {this, "ApplyGoodRunsList", false, "Only write out events passing GRL (data only)"};
+  StringProperty m_goodRunsList          {this, "GoodRunsList", "", "GoodRunsList in json format"};
+
+  // json object to hold data read from GRL file (or empty if not)
+  nlohmann::json m_grl;
+
   double m_baseEventCrossSection {1.0};
   const double kfemtoBarnsPerMilliBarn {1.0e12};
 
@@ -131,6 +138,7 @@ private:
   mutable unsigned int m_event_number;
   mutable unsigned int m_event_time;
   mutable unsigned int m_bcid;
+  mutable unsigned int m_in_grl;
 
   mutable unsigned int m_fillNumber;
   mutable float m_betaStar;
@@ -344,7 +352,7 @@ private:
   mutable double m_crossSection;
 
   mutable int    m_eventsPassed = 0;
-
+  mutable int    m_eventsFailedGRL = 0;
 };
 
 inline const ServiceHandle <ITHistSvc> &NtupleDumperAlg::histSvc() const {