diff --git a/cmake/modules/FairMacros.cmake b/cmake/modules/FairMacros.cmake
index f95bd2f4330bb7448308cb7b80e88ddb827b96a4..4dc7871bbc3d780792e42aba1ad791d7c20d2264 100644
--- a/cmake/modules/FairMacros.cmake
+++ b/cmake/modules/FairMacros.cmake
@@ -285,14 +285,25 @@ ENDMACRO (GENERATE_TEST_SCRIPT)
 
 Macro(Generate_Exe_Script _Path _ExeName)
 
-  Message("PATH: ${_Path}")
-  Message("ExeName: ${_ExeName}")
   set(shell_script_name "${_ExeName}.sh")
-  Message("shell_script_name: ${shell_script_name}")
 
-  string(REPLACE ${PROJECT_SOURCE_DIR}
-         ${PROJECT_BINARY_DIR} new_path ${_Path}
-        )
+  # Check if _Path is in the build directory
+  string(FIND ${_Path} ${PROJECT_BINARY_DIR} result)
+
+  # replace path only if not in build directory
+  if(${result} EQUAL -1)
+    string(REPLACE ${PROJECT_SOURCE_DIR}
+           ${PROJECT_BINARY_DIR} new_path ${_Path}
+          )
+  else()
+    set(new_path ${_Path})
+  endif()
+
+
+#  Message("PATH: ${_Path}")
+#  Message("ExeName: ${_ExeName}")
+#  Message("shell_script_name: ${shell_script_name}")
+#  Message("new_path: ${new_path}")
 
   set(my_exe_name ${EXECUTABLE_OUTPUT_PATH}/${_ExeName})
   Write_Geant4Data_Variables_sh()
diff --git a/cmake/scripts/run_binary.sh.in b/cmake/scripts/run_binary.sh.in
index f465cbfd837ec7a82abfce6dfedec5a5d3c4e2a2..da3f83522133fb03d517785eb5e151b6b0e86fe0 100644
--- a/cmake/scripts/run_binary.sh.in
+++ b/cmake/scripts/run_binary.sh.in
@@ -2,4 +2,4 @@
 
 source @CMAKE_BINARY_DIR@/config.sh
 
-@my_exe_name@
+@my_exe_name@ "$@"
diff --git a/core/data/CbmDefs.h b/core/data/CbmDefs.h
index d55c7d9c45e2b8bedc57cdfa61685058201b4b04..64ddc7fcbf744f1c2d92fbd6a0a193803b3a6629 100644
--- a/core/data/CbmDefs.h
+++ b/core/data/CbmDefs.h
@@ -148,6 +148,16 @@ enum class ECbmTreeAccess
   kRandom
 };
 
+/** @enum ECbmRecoMode
+ ** @brief Reconstruct the full time slice or event-by-event
+ **/
+enum class ECbmRecoMode
+{
+  Timeslice,
+  EventByEvent,
+  Undefined
+};
+
 
 /** Global functions for particle masses **/
 inline double CbmProtonMass() { return 0.938272046; }
diff --git a/external/InstallKFParticle.cmake b/external/InstallKFParticle.cmake
index c3b30403716fe60c3f2f709a8e7d5305307bbbf8..d95717419bcd33b48de86f427f872dcc2e53effb 100644
--- a/external/InstallKFParticle.cmake
+++ b/external/InstallKFParticle.cmake
@@ -50,6 +50,7 @@ ExternalProject_Add(KFPARTICLE
              -DFIXTARGET=TRUE
              -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH}
              -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON
+             -DCMAKE_MACOSX_RPATH=TRUE
   INSTALL_COMMAND  ${CMAKE_COMMAND} --build . --target install
 )
 
@@ -62,11 +63,6 @@ target_link_libraries(KFParticle INTERFACE Vc::Vc)
 
 add_dependencies(KFParticle KFPARTICLE)
 
-set(KFParticle_LIB_DIR ${CMAKE_BINARY_DIR}/lib PARENT_SCOPE)
-set(KFParticle_LIBRARIES KFParticle PARENT_SCOPE)
-set(KFParticle_INCLUDE_DIR "${CMAKE_BINARY_DIR}/include" PARENT_SCOPE)
-set(KFParticle_FOUND TRUE PARENT_SCOPE)
-
 Install(FILES ${CMAKE_BINARY_DIR}/lib/${KFPARTICLE_LIBNAME} 
               ${CMAKE_BINARY_DIR}/lib/${CMAKE_SHARED_LIBRARY_PREFIX}KFParticle.rootmap
               ${CMAKE_BINARY_DIR}/lib/${CMAKE_SHARED_LIBRARY_PREFIX}KFParticle_rdict.pcm
diff --git a/macro/L1/run_reco_L1global.C b/macro/L1/run_reco_L1global.C
index 0d15ce7b10e3a6c087d8d4e3f5a3cba578691505..2d2130013cc65dd68087f182c36826da0460e8d0 100644
--- a/macro/L1/run_reco_L1global.C
+++ b/macro/L1/run_reco_L1global.C
@@ -274,8 +274,8 @@ void run_reco_L1global(TString input = "", Int_t nTimeSlices = -1, Int_t firstTi
 
   // -----   Local reconstruction in STS   ----------------------------------
   if (useSts) {
-    CbmRecoSts* stsReco = new CbmRecoSts(ECbmRecoMode::kCbmRecoTimeslice);
-    if (eventBased) stsReco->SetMode(ECbmRecoMode::kCbmRecoEvent);
+    CbmRecoSts* stsReco = new CbmRecoSts(ECbmRecoMode::Timeslice);
+    if (eventBased) stsReco->SetMode(ECbmRecoMode::EventByEvent);
     run->AddTask(stsReco);
     std::cout << "-I- " << myName << ": Added task " << stsReco->GetName() << std::endl;
   }
diff --git a/macro/L1/run_reco_LITglobal.C b/macro/L1/run_reco_LITglobal.C
index 3bca70a90d7292bf10ce829747f58a25b1d25c39..7647cd2cd3091882cb800fd241b23a145ad6c098 100644
--- a/macro/L1/run_reco_LITglobal.C
+++ b/macro/L1/run_reco_LITglobal.C
@@ -284,9 +284,9 @@ void run_reco_LITglobal(TString input = "", Int_t nTimeSlices = -1, Int_t firstT
 
   // -----   Local reconstruction in STS   ----------------------------------
   if (useSts) {
-    CbmRecoSts* stsReco = new CbmRecoSts(ECbmRecoMode::kCbmRecoTimeslice);
+    CbmRecoSts* stsReco = new CbmRecoSts(ECbmRecoMode::Timeslice);
     stsReco->SetUseGpuReco(stsRecoUseGpu);
-    if (eventBased) stsReco->SetMode(ECbmRecoMode::kCbmRecoEvent);
+    if (eventBased) stsReco->SetMode(ECbmRecoMode::EventByEvent);
     run->AddTask(stsReco);
     std::cout << "-I- " << myName << ": Added task " << stsReco->GetName() << std::endl;
   }
diff --git a/macro/PWG/common/production/run_reco_json_config.C b/macro/PWG/common/production/run_reco_json_config.C
index fd18cbd4a7e2125541db156852188a3c99a2f797..9d62d2fdcfed96877903ce2d1e23a6aaf482f880 100644
--- a/macro/PWG/common/production/run_reco_json_config.C
+++ b/macro/PWG/common/production/run_reco_json_config.C
@@ -309,8 +309,8 @@ void run_reco_json_config(TString input = "", Int_t nTimeSlices = -1, Int_t firs
 
   // -----   Local reconstruction in STS   ----------------------------------
   if (useSts) {
-    CbmRecoSts* stsReco = new CbmRecoSts(ECbmRecoMode::kCbmRecoTimeslice);
-    if (eventBased) stsReco->SetMode(ECbmRecoMode::kCbmRecoEvent);
+    CbmRecoSts* stsReco = new CbmRecoSts(ECbmRecoMode::Timeslice);
+    if (eventBased) stsReco->SetMode(ECbmRecoMode::EventByEvent);
     run->AddTask(stsReco);
     std::cout << "-I- " << myName << ": Added task " << stsReco->GetName() << std::endl;
   }
diff --git a/macro/analysis/dielectron/run_reco.C b/macro/analysis/dielectron/run_reco.C
index a51ba8c3dd85ce04ba66d3e19445c7dc9f046a59..d10f528e731b822ef82b4ef593a71f34ecbec7c0 100644
--- a/macro/analysis/dielectron/run_reco.C
+++ b/macro/analysis/dielectron/run_reco.C
@@ -89,7 +89,7 @@ void run_reco(const string& traFile, const string& parFile, const string& digiFi
 
   if (useSts) {
     CbmRecoSts* stsReco = new CbmRecoSts(ECbmRecoMode::kCbmRecoTimeslice);
-    if (eventBased) stsReco->SetMode(ECbmRecoMode::kCbmRecoEvent);
+    if (eventBased) stsReco->SetMode(ECbmRecoMode::EventByEvent);
     run->AddTask(stsReco);
   }
 
diff --git a/macro/beamtime/mcbm2020/mcbm_build_and_reco.C b/macro/beamtime/mcbm2020/mcbm_build_and_reco.C
index 3ecbc4f3128a1a20917d9cc296c740b89ed63d4a..b190ae0fa13e7b7dcdc8d88cc8eeda9ce958c24f 100644
--- a/macro/beamtime/mcbm2020/mcbm_build_and_reco.C
+++ b/macro/beamtime/mcbm2020/mcbm_build_and_reco.C
@@ -1,6 +1,6 @@
 /* Copyright (C) 2020-2021 PI-UHd, GSI
    SPDX-License-Identifier: GPL-3.0-only
-   Authors: Pierre-Alain Loizeau */
+   Authors: Pierre-Alain Loizeau [committer] */
 
 // --------------------------------------------------------------------------
 //
@@ -194,7 +194,7 @@ Bool_t mcbm_build_and_reco(UInt_t uRunId        = 831,
 
   // -----   Local reconstruction in STS   ----------------------------------
   CbmRecoSts* recoSts = new CbmRecoSts();
-  recoSts->SetMode(kCbmRecoEvent);
+  recoSts->SetMode(ECbmRecoMode::EventByEvent);
 
   //recoSts->SetTimeCutDigisAbs( 20 );// cluster finder: time cut in ns
   //recoSts->SetTimeCutClustersAbs(20.); // hit finder: time cut in ns
diff --git a/macro/beamtime/mcbm2020/mcbm_event_ana.C b/macro/beamtime/mcbm2020/mcbm_event_ana.C
index d66e84b0ee77690317539297d865186e28999b4c..442287da45f6b935f6d2d82228c487e335351a5b 100644
--- a/macro/beamtime/mcbm2020/mcbm_event_ana.C
+++ b/macro/beamtime/mcbm2020/mcbm_event_ana.C
@@ -128,7 +128,7 @@ void mcbm_event_ana(UInt_t uRunId         = 831,
 
   // -----   Local reconstruction in STS   ----------------------------------
   CbmRecoSts* recoSts = new CbmRecoSts();
-  recoSts->SetMode(kCbmRecoEvent);
+  recoSts->SetMode(ECbmRecoMode::EventByEvent);
 
   //recoSts->SetTimeCutDigisAbs( 20 );// cluster finder: time cut in ns
   //recoSts->SetTimeCutClustersAbs(20.); // hit finder: time cut in ns
diff --git a/macro/beamtime/mcbm2020/mcbm_event_reco.C b/macro/beamtime/mcbm2020/mcbm_event_reco.C
index 9b99527eb760bcf576c7b457fbf1630dc074b656..57331286060f44187cf6faefb7d1db835cd1225c 100644
--- a/macro/beamtime/mcbm2020/mcbm_event_reco.C
+++ b/macro/beamtime/mcbm2020/mcbm_event_reco.C
@@ -1,6 +1,6 @@
 /* Copyright (C) 2020-2021 PI-UHd, GSI
    SPDX-License-Identifier: GPL-3.0-only
-   Authors: Pierre-Alain Loizeau, Pascal Raisig */
+   Authors: Pierre-Alain Loizeau [committer], Pascal Raisig */
 
 // --------------------------------------------------------------------------
 //
@@ -138,7 +138,7 @@ Bool_t mcbm_event_reco(UInt_t uRunId        = 831,
 
   // -----   Local reconstruction in STS   ----------------------------------
   CbmRecoSts* recoSts = new CbmRecoSts();
-  recoSts->SetMode(kCbmRecoEvent);
+  recoSts->SetMode(ECbmRecoMode::EventByEvent);
 
   //recoSts->SetTimeCutDigisAbs( 20 );// cluster finder: time cut in ns
   //recoSts->SetTimeCutClustersAbs(20.); // hit finder: time cut in ns
diff --git a/macro/beamtime/mcbm2020/mcbm_event_reco_ana.C b/macro/beamtime/mcbm2020/mcbm_event_reco_ana.C
index 7b1583c5a31b3d293b113b1365d2395a67dc108d..e242f794d6bf7a39ed7b1cd852d48e79bc3350a4 100644
--- a/macro/beamtime/mcbm2020/mcbm_event_reco_ana.C
+++ b/macro/beamtime/mcbm2020/mcbm_event_reco_ana.C
@@ -94,7 +94,7 @@ void mcbm_event_reco_ana(Int_t runId = 831, Int_t nTimeslices = 1000)
 
   // -----   Local reconstruction in STS   ----------------------------------
   CbmRecoSts* recoSts = new CbmRecoSts();
-  recoSts->SetMode(kCbmRecoEvent);
+  recoSts->SetMode(ECbmRecoMode::EventByEvent);
 
   //recoSts->SetTimeCutDigisAbs( 20 );// cluster finder: time cut in ns
   //recoSts->SetTimeCutClustersAbs(20.); // hit finder: time cut in ns
diff --git a/macro/beamtime/mcbm2021/dis_reco_mcbm_digievent.C b/macro/beamtime/mcbm2021/dis_reco_mcbm_digievent.C
index 4e7e8b1bcdb310b1591d1207532039614bda6dc8..ea78004de38fe9539acfb70f1c050865005587f0 100644
--- a/macro/beamtime/mcbm2021/dis_reco_mcbm_digievent.C
+++ b/macro/beamtime/mcbm2021/dis_reco_mcbm_digievent.C
@@ -209,7 +209,7 @@ void dis_reco_mcbm_digievent(TString input = "", Int_t nTimeSlices = 1, Int_t fi
     //std::cout << "-I- " << myName << ": Adding task " << recoSts->GetName();
 
     CbmRecoSts* recoSts = new CbmRecoSts();
-    recoSts->SetMode(kCbmRecoEvent);
+    recoSts->SetMode(ECbmRecoMode::EventByEvent);
 
     recoSts->SetTimeCutDigisAbs(20.0);     // cluster finder: time cut in ns
     recoSts->SetTimeCutClustersAbs(20.0);  // hit finder: time cut in ns
diff --git a/macro/beamtime/mcbm2021/mcbm_event_reco.C b/macro/beamtime/mcbm2021/mcbm_event_reco.C
index 0a5b00426039dfb2a8bae33af028caccc02b1920..0950dacd3d84190225252f690fc2c37c110b6639 100644
--- a/macro/beamtime/mcbm2021/mcbm_event_reco.C
+++ b/macro/beamtime/mcbm2021/mcbm_event_reco.C
@@ -264,7 +264,7 @@ Bool_t mcbm_event_reco(UInt_t uRunId                   = 1588,
 
   if (geoSetup->IsActive(ECbmModuleId::kSts)) {
     CbmRecoSts* recoSts = new CbmRecoSts();
-    recoSts->SetMode(kCbmRecoEvent);
+    recoSts->SetMode(ECbmRecoMode::EventByEvent);
 
     recoSts->SetTimeCutDigisAbs(20.0);     // cluster finder: time cut in ns
     recoSts->SetTimeCutClustersAbs(20.0);  // hit finder: time cut in ns
diff --git a/macro/beamtime/mcbm2021/run_reco_mcbm_digievent.C b/macro/beamtime/mcbm2021/run_reco_mcbm_digievent.C
index 663807f1db9997f46c2b2d54f2b11d38b4edfc9d..440089db77a21e7568112e314240c418a4c58a2a 100644
--- a/macro/beamtime/mcbm2021/run_reco_mcbm_digievent.C
+++ b/macro/beamtime/mcbm2021/run_reco_mcbm_digievent.C
@@ -225,7 +225,7 @@ void run_reco_mcbm_digievent(TString input = "", Int_t nTimeSlices = -1, Int_t f
     //std::cout << "-I- " << myName << ": Adding task " << recoSts->GetName();
 
     CbmRecoSts* recoSts = new CbmRecoSts();
-    recoSts->SetMode(kCbmRecoEvent);
+    recoSts->SetMode(ECbmRecoMode::EventByEvent);
 
     recoSts->SetTimeCutDigisAbs(20.0);     // cluster finder: time cut in ns
     recoSts->SetTimeCutClustersAbs(20.0);  // hit finder: time cut in ns
diff --git a/macro/beamtime/mcbm2022/mcbm_digievent_reco.C b/macro/beamtime/mcbm2022/mcbm_digievent_reco.C
index b404b52458e8922b79043567e08a756f06aebe22..5a2260af0c60caa00ecff8e0e038d0e9c54da16c 100644
--- a/macro/beamtime/mcbm2022/mcbm_digievent_reco.C
+++ b/macro/beamtime/mcbm2022/mcbm_digievent_reco.C
@@ -277,7 +277,7 @@ Bool_t mcbm_digievent_reco(UInt_t uRunId               = 2365,
 
   if (bSTS && geoSetup->IsActive(ECbmModuleId::kSts)) {
     CbmRecoSts* recoSts = new CbmRecoSts();
-    recoSts->SetMode(kCbmRecoEvent);
+    recoSts->SetMode(ECbmRecoMode::EventByEvent);
 
     recoSts->SetTimeCutDigisAbs(20.0);     // cluster finder: time cut in ns
     recoSts->SetTimeCutClustersAbs(20.0);  // hit finder: time cut in ns
diff --git a/macro/beamtime/mcbm2022/mcbm_event_reco.C b/macro/beamtime/mcbm2022/mcbm_event_reco.C
index bc684474d85e6000904614a0ccd5900d56a4b496..b14ad914d3bd9c6a0f5e59a4b14b414046f9faf3 100644
--- a/macro/beamtime/mcbm2022/mcbm_event_reco.C
+++ b/macro/beamtime/mcbm2022/mcbm_event_reco.C
@@ -301,7 +301,7 @@ Bool_t mcbm_event_reco(UInt_t uRunId                   = 2391,
 
   if (geoSetup->IsActive(ECbmModuleId::kSts)) {
     CbmRecoSts* recoSts = new CbmRecoSts();
-    recoSts->SetMode(kCbmRecoEvent);
+    recoSts->SetMode(ECbmRecoMode::EventByEvent);
 
     recoSts->SetTimeCutDigisAbs(20.0);     // cluster finder: time cut in ns
     recoSts->SetTimeCutClustersAbs(20.0);  // hit finder: time cut in ns
diff --git a/macro/beamtime/mcbm2022/mcbm_reco.C b/macro/beamtime/mcbm2022/mcbm_reco.C
index 394bdcb7c3adcfb4060b968d66a7c3a451db505a..53a3578972a07d2224bb4f95f02bb8d87e2c12a8 100644
--- a/macro/beamtime/mcbm2022/mcbm_reco.C
+++ b/macro/beamtime/mcbm2022/mcbm_reco.C
@@ -287,7 +287,7 @@ Bool_t mcbm_reco(UInt_t uRunId                   = 2391,
 
   if (bSTS && geoSetup->IsActive(ECbmModuleId::kSts)) {
     CbmRecoSts* recoSts = new CbmRecoSts();
-    recoSts->SetMode(kCbmRecoEvent);
+    recoSts->SetMode(ECbmRecoMode::EventByEvent);
 
     recoSts->SetTimeCutDigisAbs(20.0);     // cluster finder: time cut in ns
     recoSts->SetTimeCutClustersAbs(20.0);  // hit finder: time cut in ns
diff --git a/macro/mcbm/mcbm_reco_event_tb_nh.C b/macro/mcbm/mcbm_reco_event_tb_nh.C
index 62b7366625d9be2987a675100bf28ca3ecbc4761..b530685210505d90e2f15f2c3b0d528ee5a76aae 100644
--- a/macro/mcbm/mcbm_reco_event_tb_nh.C
+++ b/macro/mcbm/mcbm_reco_event_tb_nh.C
@@ -237,7 +237,7 @@ void mcbm_reco_event_tb_nh(Int_t nEvents = 10, TString RunId = "test", TString I
   if (setup->IsActive(ECbmModuleId::kSts)) {
     CbmRecoSts* stsReco = NULL;
     if (timebased) {
-      stsReco = new CbmRecoSts(kCbmRecoEvent, kFALSE, kFALSE);
+      stsReco = new CbmRecoSts(ECbmRecoMode::EventByEvent, kFALSE, kFALSE);
       //      stsReco = new CbmRecoSts();
     }
     else {
diff --git a/macro/run/CMakeLists.txt b/macro/run/CMakeLists.txt
index c8846808a37c387d89f6f59384823eee4c792f82..dd8fc5160f1b46c2ef1024cefb9b6f473b4bd451 100644
--- a/macro/run/CMakeLists.txt
+++ b/macro/run/CMakeLists.txt
@@ -5,6 +5,8 @@ GENERATE_ROOT_TEST_SCRIPT(${CBMROOT_SOURCE_DIR}/macro/run/run_digi.C)
 GENERATE_ROOT_TEST_SCRIPT(${CBMROOT_SOURCE_DIR}/macro/run/run_reco.C)
 GENERATE_ROOT_TEST_SCRIPT(${CBMROOT_SOURCE_DIR}/macro/run/run_qa.C)
 
+GENERATE_EXE_SCRIPT(${CBMROOT_BINARY_DIR}/bin cbmreco_offline)
+
 Set(MACRO_DIR ${CMAKE_CURRENT_BINARY_DIR})
 # ============================================================================
 
@@ -165,13 +167,31 @@ foreach(setup IN LISTS cbm_setup)
   add_test(${testname}	 ${MACRODIR}/run_reco.sh
   	\"data/${sname}_ev\" -1 0 \"data/${sname}_eb_eb_ideal\" \"Ideal\" \"${setup}\" \"data/${sname}_coll\")
   set_tests_properties(${testname} PROPERTIES
-  	TIMEOUT ${timeOutTime}
+ 	TIMEOUT ${timeOutTime}
 	FAIL_REGULAR_EXPRESSION "segmentation violation"
   	PASS_REGULAR_EXPRESSION "Macro finished successfully"
 	FIXTURES_REQUIRED fixt_digi_ev_${setup}
 	FIXTURES_SETUP fixt_reco_ev_ideal_${setup}
  	RESOURCE_LOCK collParDb_${setup}
   )
+  
+  # --- Test run_reco_offline_ev_ideal
+  # --- Event-by-event reconstruction from event-based simulation, with binary cbmreco_offline
+  # --- Ideal raw event builder
+  set(testname run_${sname}_reco_offline_ev_ideal)
+  set(binary ${CBMROOT_BINARY_DIR}/bin/cbmreco_offline.sh)
+  set(inputname data/${sname}_ev.raw.root)
+  set(parfilename data/${sname}_coll.par.root)
+  set(outputname data/dummy.reco.root)
+  set(configname ${CBMROOT_SOURCE_DIR}/reco/offline/config/RecoConfig_event_ideal.yaml)
+  set(cl_options -i ${inputname} -o ${outputname} -p ${parfilename} -c ${configname} -s ${setup} -w)
+  add_test(NAME ${testname} COMMAND ${binary} ${cl_options})
+  set_tests_properties(${testname} PROPERTIES
+  	TIMEOUT ${timeOutTime}
+	FIXTURES_REQUIRED fixt_digi_ev_${setup}
+	FIXTURES_SETUP fixt_reco_offline_ev_ideal_${setup}
+ 	RESOURCE_LOCK collParDb_${setup}
+  )
 
   # --- Test run_reco_ev_real
   # --- Event-by-event reconstruction from event-based simulation
@@ -188,6 +208,24 @@ foreach(setup IN LISTS cbm_setup)
     FIXTURES_SETUP fixt_reco_ev_real_${setup}
     RESOURCE_LOCK collParDb_${setup}
   )
+  
+  # --- Test run_reco_offline_ev_real
+  # --- Event-by-event reconstruction from event-based simulation, with binary cbmreco_offline
+  # --- Real raw event builder
+  set(testname run_${sname}_reco_offline_ev_real)
+  set(binary ${CBMROOT_BINARY_DIR}/bin/cbmreco_offline.sh)
+  set(inputname data/${sname}_ev.raw.root)
+  set(parfilename data/${sname}_coll.par.root)
+  set(outputname data/dummy.reco.root)
+  set(configname ${CBMROOT_SOURCE_DIR}/reco/offline/config/RecoConfig_event_real.yaml)
+  set(cl_options -i ${inputname} -o ${outputname} -p ${parfilename} -c ${configname} -s ${setup} -w)
+  add_test(NAME ${testname} COMMAND ${binary} ${cl_options})
+  set_tests_properties(${testname} PROPERTIES
+  	TIMEOUT ${timeOutTime}
+	FIXTURES_REQUIRED fixt_digi_ev_${setup}
+	FIXTURES_SETUP fixt_reco_offline_ev_ideal_${setup}
+ 	RESOURCE_LOCK collParDb_${setup}
+  )
 
   # --- Test run_reco_ts_eb_ideal
   # --- Event-by-event reconstruction from time-based simulation
@@ -204,7 +242,25 @@ foreach(setup IN LISTS cbm_setup)
 	FIXTURES_SETUP fixt_reco_ts_eb_ideal_${setup}
  	RESOURCE_LOCK collParDb_${setup}
   )
-
+  
+  # --- Test run_reco_offline_ts_eb_ideal
+  # --- Event-by-event reconstruction from time-based simulation, with binary cbmreco_offline
+  # --- Real raw event builder
+  set(testname run_${sname}_reco_offline_ts_eb_ideal)
+  set(binary ${CBMROOT_BINARY_DIR}/bin/cbmreco_offline.sh)
+  set(inputname data/${sname}_ts.raw.root)
+  set(parfilename data/${sname}_coll.par.root)
+  set(outputname data/dummy.reco.root)
+  set(configname ${CBMROOT_SOURCE_DIR}/reco/offline/config/RecoConfig_event_ideal.yaml)
+  set(cl_options -i ${inputname} -o ${outputname} -p ${parfilename} -c ${configname} -s ${setup} -w)
+  add_test(NAME ${testname} COMMAND ${binary} ${cl_options})
+  set_tests_properties(${testname} PROPERTIES
+  	TIMEOUT ${timeOutTime}
+	FIXTURES_REQUIRED fixt_digi_ev_${setup}
+	FIXTURES_SETUP fixt_reco_offline_ev_ideal_${setup}
+ 	RESOURCE_LOCK collParDb_${setup}
+  )
+  
   # --- Test run_reco_ts_eb_real
   # --- Event-by-event reconstruction from time-based simulation
   # --- Real raw event builder
@@ -220,7 +276,25 @@ foreach(setup IN LISTS cbm_setup)
 	FIXTURES_SETUP fixt_reco_ts_eb_real_${setup}
  	RESOURCE_LOCK collParDb_${setup}
   )
-
+  
+  # --- Test run_reco_offline_ts_eb_real
+  # --- Event-by-event reconstruction from event-based simulation, with binary cbmreco_offline
+  # --- Real raw event builder
+  set(testname run_${sname}_reco_offline_ts_eb_real)
+  set(binary ${CBMROOT_BINARY_DIR}/bin/cbmreco_offline.sh)
+  set(inputname data/${sname}_ts.raw.root)
+  set(parfilename data/${sname}_coll.par.root)
+  set(outputname data/dummy.reco.root)
+  set(configname ${CBMROOT_SOURCE_DIR}/reco/offline/config/RecoConfig_event_real.yaml)
+  set(cl_options -i ${inputname} -o ${outputname} -p ${parfilename} -c ${configname} -s ${setup} -w)
+  add_test(NAME ${testname} COMMAND ${binary} ${cl_options})
+  set_tests_properties(${testname} PROPERTIES
+  	TIMEOUT ${timeOutTime}
+	FIXTURES_REQUIRED fixt_digi_ev_${setup}
+	FIXTURES_SETUP fixt_reco_offline_ev_ideal_${setup}
+ 	RESOURCE_LOCK collParDb_${setup}
+  )
+  
   # --- Test run_qa
   # --- Run QA tasks with Event-by-event reconstruction from time-based simulation
   # --- Real raw event builder
@@ -252,6 +326,24 @@ foreach(setup IN LISTS cbm_setup)
 	FIXTURES_SETUP fixt_reco_ts_tb_${setup}
  	RESOURCE_LOCK collParDb_${setup}
   )
+  
+  # --- Test run_reco_offline_ts_tb
+  # --- Time-based reconstruction from time-based simulation, with binary cbmreco_offline
+  set(testname run_${sname}_reco_offline_ts_tb)
+  set(binary ${CBMROOT_BINARY_DIR}/bin/cbmreco_offline.sh)
+  set(inputname data/${sname}_ts.raw.root)
+  set(parfilename data/${sname}_coll.par.root)
+  set(outputname data/dummy.reco.root)
+  set(configname ${CBMROOT_SOURCE_DIR}/reco/offline/config/RecoConfig_timeslice.yaml)
+  set(cl_options -i ${inputname} -o ${outputname} -p ${parfilename} -c ${configname} -s ${setup} -w)
+  add_test(NAME ${testname} COMMAND ${binary} ${cl_options})
+  set_tests_properties(${testname} PROPERTIES
+  	TIMEOUT ${timeOutTime}
+	FIXTURES_REQUIRED fixt_digi_ev_${setup}
+	FIXTURES_SETUP fixt_reco_offline_ev_ideal_${setup}
+ 	RESOURCE_LOCK collParDb_${setup}
+  )
+  
 
 
 endforeach(setup IN LISTS cbm_setup)
diff --git a/macro/run/run_reco.C b/macro/run/run_reco.C
index ba81a3b3c95e51520c807bc7a168f426e043d841..06c5501f44c82f36ca0cffa4f3c451a647389e33 100644
--- a/macro/run/run_reco.C
+++ b/macro/run/run_reco.C
@@ -288,9 +288,9 @@ void run_reco(TString input = "", Int_t nTimeSlices = -1, Int_t firstTimeSlice =
 
   // -----   Local reconstruction in STS   ----------------------------------
   if (useSts) {
-    CbmRecoSts* stsReco = new CbmRecoSts(ECbmRecoMode::kCbmRecoTimeslice);
+    CbmRecoSts* stsReco = new CbmRecoSts(ECbmRecoMode::Timeslice);
     stsReco->SetUseGpuReco(stsRecoUseGpu);
-    if (eventBased) stsReco->SetMode(ECbmRecoMode::kCbmRecoEvent);
+    if (eventBased) stsReco->SetMode(ECbmRecoMode::EventByEvent);
     run->AddTask(stsReco);
     std::cout << "-I- " << myName << ": Added task " << stsReco->GetName() << std::endl;
   }
diff --git a/macro/run/run_reco_digievent.C b/macro/run/run_reco_digievent.C
index f1963615b28ce0f0df9aa0b90794a34b4be07228..087f8848ae3e91668b1103136834a57910088afb 100644
--- a/macro/run/run_reco_digievent.C
+++ b/macro/run/run_reco_digievent.C
@@ -189,7 +189,7 @@ void run_reco_digievent(TString input = "", Int_t nTimeSlices = -1, Int_t firstT
 
   // -----   Local reconstruction in STS   ----------------------------------
   if (useSts) {
-    auto recoSts = std::make_unique<CbmRecoSts>(kCbmRecoEvent);
+    auto recoSts = std::make_unique<CbmRecoSts>(ECbmRecoMode::EventByEvent);
     std::cout << "-I- " << myName << ": Adding task " << recoSts->GetName();
     run->AddTask(recoSts.release());
   }
diff --git a/macro/run/run_reco_yaml.C b/macro/run/run_reco_yaml.C
new file mode 100644
index 0000000000000000000000000000000000000000..e6d404df3deee7595a0a2da6b7cf60c28dc6abde
--- /dev/null
+++ b/macro/run/run_reco_yaml.C
@@ -0,0 +1,133 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer], Dominik Smith */
+
+/** @file run_reco_yaml.C
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @since 3 June 2023
+ **/
+
+
+// --- Includes needed for IDE
+#include <RtypesCore.h>
+#if !defined(__CLING__)
+#include <FairMonitor.h>
+#include <FairSystemInfo.h>
+
+#include <TStopwatch.h>
+
+#include "reco/offline/steer/Run.h"
+#endif
+
+
+/** @brief Macro for CBM offline reconstruction, configured from YAML
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @since  3 June 2023
+ ** @param input          Name of input file (w/o extension .raw.root)
+ ** @param numTs          Number of time-slices to process
+ ** @param config         YAML configuration file
+ ** @param output         Name of output file (w/o extension .rec.root)
+ ** @param setup          Name of predefined geometry setup
+ ** @param paramFile      Parameter ROOT file (w/o extension .par.root)
+ **
+ ** This macro performs reconstruction from digis. It can be used
+ ** for simulated data (result of run_digi.C) or real data after unpacking.
+ **
+ ** The macro covers both time-based reconstruction and event-based
+ ** reconstruction using raw events build from digis.
+ **
+ ** The file names must be specified without extensions. The convention is
+ ** that the raw (input) file is [input].raw.root. The output file
+ ** will be [input].rec.root if not specified by the user. The parameter file
+ ** has the extension .par.root. It is assumed to be [input].par.root if
+ ** not specified by the user.
+ **
+ ** If no argument is specified, the input will be set to "test". This allows
+ ** to execute the macro chain (run_tra_file.C, run_digi.C and run_reco_yaml.C)
+ ** from the ROOT prompt without user intervention.
+ **
+ **/
+void run_reco_yaml(TString input = "", Int_t numTs = 0, TString config = "", TString output = "",
+                   TString setup = "sis100_electron", TString paramFile = "")
+{
+
+  // -----   Environment   --------------------------------------------------
+  TString myName = "run_reco_yaml";                // this macro's name for screen output
+  TString srcDir = gSystem->Getenv("VMCWORKDIR");  // top source directory
+  // ------------------------------------------------------------------------
+
+
+  // -----   In- and output file names   ------------------------------------
+  if (input.IsNull()) input = "test";
+  TString inFile = input + ".raw.root";
+  if (output.IsNull()) output = input;
+  TString outFile = output + ".reco.root";
+  TString monFile = output + ".moni_reco.root";
+  if (paramFile.IsNull()) paramFile = input;
+  TString parFile = paramFile + ".par.root";
+  std::cout << myName << ": Input file     " << inFile << std::endl;
+  std::cout << myName << ": Output file    " << outFile << std::endl;
+  std::cout << myName << ": Parameter file " << parFile << std::endl;
+  std::cout << myName << ": Configuration  " << (config.IsNull() ? "default" : config);
+  std::cout << std::endl;
+  std::cout << myName << ": Geometry setup " << setup << std::endl;
+  std::cout << myName << ": Number of Ts   ";
+  if (numTs == 0) std::cout << "max";
+  else
+    std::cout << numTs;
+  std::cout << std::endl;
+  // ------------------------------------------------------------------------
+
+
+  // -----   Configure run   ------------------------------------------------
+  // TODO: The run should created on the stack, but then there is a segmentation fault
+  // when destructing it at the end of the main function. This is caused by
+  // the destructor of CbmStsReco (StsHitfinderBase). Until this is resolved,
+  // the run is created on the heap.
+  std::cout << myName << ": Configuring run" << std::endl;
+  cbm::reco::offline::Run* run = new cbm::reco::offline::Run();
+  run->SetInput(inFile.Data());
+  run->SetOutput(outFile.Data());
+  run->SetParams(parFile.Data());
+  run->SetGeoSetupTag(setup.Data());
+  run->SetNumTs(numTs);
+  run->LoadConfig(config.Data());
+  // ------------------------------------------------------------------------
+
+
+  // -----   Start run   ----------------------------------------------------
+  TStopwatch timer;
+  timer.Start();
+  std::cout << myName << ": Starting run" << std::endl;
+  run->Exec();
+  timer.Stop();
+  // ------------------------------------------------------------------------
+
+
+  // -----   Finish   -------------------------------------------------------
+  FairMonitor::GetMonitor()->Print();
+  Double_t rtime = timer.RealTime();
+  Double_t ctime = timer.CpuTime();
+  std::cout << std::endl << std::endl;
+  std::cout << "Macro finished successfully." << std::endl;
+  std::cout << "Output file is    " << outFile << std::endl;
+  std::cout << "Parameter file is " << parFile << std::endl;
+  std::cout << "Real time " << rtime << " s, CPU time " << ctime << " s" << std::endl;
+  FairSystemInfo sysInfo;
+  Float_t maxMemory = sysInfo.GetMaxMemory();
+  std::cout << "<DartMeasurement name=\"MaxMemory\" type=\"numeric/double\">";
+  std::cout << maxMemory;
+  std::cout << "</DartMeasurement>" << std::endl;
+  Float_t cpuUsage = ctime / rtime;
+  std::cout << "<DartMeasurement name=\"CpuLoad\" type=\"numeric/double\">";
+  std::cout << cpuUsage;
+  std::cout << "</DartMeasurement>" << std::endl;
+  // ------------------------------------------------------------------------
+
+
+  // -----   This is to prevent a malloc error when exiting ROOT   ----------
+  // The source of the error is unknown. Related to TGeoManager.
+  RemoveGeoManager();
+  // ------------------------------------------------------------------------
+
+}  // End of main macro function
diff --git a/reco/CMakeLists.txt b/reco/CMakeLists.txt
index df891db26df4340d6de9713049871c353feec60a..6bcd5fd587f3d91485f9b88597118d9279539004 100644
--- a/reco/CMakeLists.txt
+++ b/reco/CMakeLists.txt
@@ -4,6 +4,8 @@
 # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")
 # set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address")
 
+add_subdirectory(alignment)
+add_subdirectory(app)
 add_subdirectory(base)
 add_subdirectory(calibration)
 add_subdirectory(detectors)
@@ -12,12 +14,11 @@ add_subdirectory(global)
 add_subdirectory(KF)
 add_subdirectory(L1)
 add_subdirectory(littrack)
+add_subdirectory(mq)
+add_subdirectory(offline)
+add_subdirectory(qa)
 add_subdirectory(steer)
+add_subdirectory(tasks)
 add_subdirectory(tracking)
-add_subdirectory(qa)
-add_subdirectory (tasks)
-add_subdirectory (app)
-add_subdirectory (mq)
-add_subdirectory (alignment)
 
 
diff --git a/reco/detectors/sts/CbmRecoSts.cxx b/reco/detectors/sts/CbmRecoSts.cxx
index 6ecf42a6f8b6f94735c6da42318b4d461f9140e2..39d76808a29fcbaddf2fbc974aa1c4d9b26146b5 100644
--- a/reco/detectors/sts/CbmRecoSts.cxx
+++ b/reco/detectors/sts/CbmRecoSts.cxx
@@ -229,7 +229,7 @@ void CbmRecoSts::Exec(Option_t*)
 
     // Old reco in time based mode
   }
-  else if (fMode == kCbmRecoTimeslice) {
+  else if (fMode == ECbmRecoMode::Timeslice) {
 
     ProcessData(nullptr);
 
@@ -295,7 +295,7 @@ void CbmRecoSts::Finish()
   else
     LOG(info) << "STS reconstruction ran multithreaded with OpenMP (nthreads = " << ompThreads << ")";
   LOG(info) << "Time slices            : " << fNofTs;
-  if (fMode == kCbmRecoEvent) LOG(info) << "Events                 : " << fNofEvents;
+  if (fMode == ECbmRecoMode::EventByEvent) LOG(info) << "Events                 : " << fNofEvents;
   LOG(info) << "Digis / TSlice         : " << fixed << setprecision(2) << fNofDigisRun / Double_t(fNofTs);
   LOG(info) << "Digis used / TSlice    : " << fixed << setprecision(2) << fNofDigisUsed / Double_t(fNofTs);
   LOG(info) << "Digis ignored / TSlice : " << fixed << setprecision(2) << fNofDigisIgnored / Double_t(fNofTs);
@@ -398,7 +398,7 @@ InitStatus CbmRecoSts::Init()
   fDigiManager->Init();
 
   // --- In event mode: get input array (CbmEvent)
-  if (fMode == kCbmRecoEvent) {
+  if (fMode == ECbmRecoMode::EventByEvent) {
     LOG(info) << GetName() << ": Using event-by-event mode";
     fEvents = dynamic_cast<TClonesArray*>(ioman->GetObject("CbmEvent"));
     if (nullptr == fEvents) {
@@ -726,7 +726,8 @@ void CbmRecoSts::ProcessData(CbmEvent* event)
 
 void CbmRecoSts::ProcessDataGpu()
 {
-  if (fMode == kCbmRecoEvent) throw std::runtime_error("STS GPU Reco does not yet support event-by-event mode.");
+  if (fMode == ECbmRecoMode::EventByEvent)
+    throw std::runtime_error("STS GPU Reco does not yet support event-by-event mode.");
 
   auto digis = fDigiManager->GetArray<CbmStsDigi>();
   fGpuReco(digis);
diff --git a/reco/detectors/sts/CbmRecoSts.h b/reco/detectors/sts/CbmRecoSts.h
index 9cfa4e017840463ff2c0f7a91ce5c815bd97f588..5650d3b43e30fc66bbbdada2e8bf24817fc543da 100644
--- a/reco/detectors/sts/CbmRecoSts.h
+++ b/reco/detectors/sts/CbmRecoSts.h
@@ -34,16 +34,6 @@ class CbmStsSensor;
 class CbmStsSetup;
 
 
-/** @enum ECbmMode
- ** @brief Time-slice or event-by-event mode
- **/
-enum ECbmRecoMode
-{
-  kCbmRecoTimeslice,
-  kCbmRecoEvent
-};
-
-
 /** @class CbmRecoSts
  ** @brief Task class for local reconstruction in the STS
  ** @author Volker Friese <v.friese@gsi.de>
@@ -65,7 +55,7 @@ class CbmRecoSts : public FairTask {
 
 public:
   /** @brief Constructor **/
-  CbmRecoSts(ECbmRecoMode mode = kCbmRecoTimeslice, Bool_t writeClusters = kFALSE);
+  CbmRecoSts(ECbmRecoMode mode = ECbmRecoMode::Timeslice, Bool_t writeClusters = kFALSE);
 
 
   /** @brief Copy constructor (disabled) **/
@@ -106,7 +96,7 @@ public:
      **
      ** Alternative to using SetMode.
      **/
-  void SetEventMode(Bool_t choice = kTRUE) { fMode = (choice ? kCbmRecoEvent : kCbmRecoTimeslice); }
+  void SetEventMode(Bool_t choice = kTRUE) { fMode = (choice ? ECbmRecoMode::EventByEvent : ECbmRecoMode::Timeslice); }
 
 
   /** @brief Set execution mode
@@ -286,12 +276,12 @@ private:
   CbmStsParSetSensorCond* fUserParSetCond = nullptr;
 
   // --- Settings
-  ECbmRecoMode fMode           = kCbmRecoEvent;  ///< Time-slice or event
-  Double_t fTimeCutDigisSig    = 3.;             ///< Time cut for cluster finding
-  Double_t fTimeCutDigisAbs    = -1.;            ///< Time cut for cluster finding [ns]
-  Double_t fTimeCutClustersSig = 4.;             ///< Time cut for hit finding
-  Double_t fTimeCutClustersAbs = -1.;            ///< Time cut for hit finding [ns]
-  Bool_t fWriteClusters        = kFALSE;         ///< Write clusters to tree
+  ECbmRecoMode fMode           = ECbmRecoMode::Timeslice;  ///< Time-slice or event-by-event
+  Double_t fTimeCutDigisSig    = 3.;                       ///< Time cut for cluster finding
+  Double_t fTimeCutDigisAbs    = -1.;                      ///< Time cut for cluster finding [ns]
+  Double_t fTimeCutClustersSig = 4.;                       ///< Time cut for hit finding
+  Double_t fTimeCutClustersAbs = -1.;                      ///< Time cut for hit finding [ns]
+  Bool_t fWriteClusters        = kFALSE;                   ///< Write clusters to tree
 
   // --- Timeslice counters
   Long64_t fNofDigis        = 0;   ///< Total number of digis processed
diff --git a/reco/detectors/sts/CbmRecoStsPixel.cxx b/reco/detectors/sts/CbmRecoStsPixel.cxx
index ed19abb7a78989836faf32ed7417e09f81e70405..20cda9824995c14c9817d4e098e7a4b5c595374c 100644
--- a/reco/detectors/sts/CbmRecoStsPixel.cxx
+++ b/reco/detectors/sts/CbmRecoStsPixel.cxx
@@ -82,7 +82,7 @@ InitStatus CbmRecoStsPixel::Init()
   assert(ioman);
 
   // --- In event mode: get input array (CbmEvent)
-  if (fMode == kCbmRecoEvent) {
+  if (fMode == ECbmRecoMode::EventByEvent) {
     LOG(info) << GetName() << ": Using event-by-event mode";
     fEvents = dynamic_cast<TClonesArray*>(ioman->GetObject("CbmEvent"));
     if (nullptr == fEvents) {
@@ -180,7 +180,7 @@ void CbmRecoStsPixel::Exec(Option_t*)
   // --- Reset cluster output array
   fClusters->Delete();
 
-  if (fMode == kCbmRecoTimeslice) {
+  if (fMode == ECbmRecoMode::Timeslice) {
     // --- Time-slice mode: process entire array
     ProcessData(nullptr);
   }
diff --git a/reco/detectors/sts/CbmRecoStsPixel.h b/reco/detectors/sts/CbmRecoStsPixel.h
index d450bd2020b96862a7b1075a7f8150c3aba9d7fa..582b3be3951837371f0193c8a78e497e2d617360 100644
--- a/reco/detectors/sts/CbmRecoStsPixel.h
+++ b/reco/detectors/sts/CbmRecoStsPixel.h
@@ -50,7 +50,7 @@ class CbmRecoStsPixel : public FairTask {
 
 public:
   /** @brief Constructor **/
-  CbmRecoStsPixel(ECbmRecoMode mode = kCbmRecoTimeslice);
+  CbmRecoStsPixel(ECbmRecoMode mode = ECbmRecoMode::Timeslice);
 
 
   /** @brief Copy constructor (disabled) **/
@@ -87,7 +87,7 @@ public:
      **
      ** Alternative to using SetMode.
      **/
-  void SetEventMode(Bool_t choice = kTRUE) { fMode = (choice ? kCbmRecoEvent : kCbmRecoTimeslice); }
+  void SetEventMode(Bool_t choice = kTRUE) { fMode = (choice ? ECbmRecoMode::EventByEvent : ECbmRecoMode::Timeslice); }
 
 
   /** @brief Set execution mode
@@ -129,7 +129,7 @@ private:
 
 
   // --- Settings
-  ECbmRecoMode fMode = kCbmRecoEvent;  ///< Time-slice or event
+  ECbmRecoMode fMode = ECbmRecoMode::Timeslice;  ///< Time-slice or event
 
   ClassDef(CbmRecoStsPixel, 0);
 };
diff --git a/reco/eventbuilder/digis/CbmAlgoBuildRawEvents.cxx b/reco/eventbuilder/digis/CbmAlgoBuildRawEvents.cxx
index 8c4c4385af80ba8cf5ca145f3027f829c84d93de..dee887ec98544a97fcb10b9bda6accbb15fa5f52 100644
--- a/reco/eventbuilder/digis/CbmAlgoBuildRawEvents.cxx
+++ b/reco/eventbuilder/digis/CbmAlgoBuildRawEvents.cxx
@@ -340,6 +340,7 @@ void CbmAlgoBuildRawEvents::CheckSeed(Double_t dSeedTime, UInt_t uSeedDigiIdx)
         return;
         break;
       }
+      default: break;
     }
   }  // if( prev Event exists and mode forbiden overlap present )
   else {
@@ -617,9 +618,9 @@ void CbmAlgoBuildRawEvents::SetBmonEventTime(CbmEvent* event)
   const int32_t iNbDigis = event->GetNofData(ECbmDataType::kT0Digi);
 
   if (0 < iNbDigis) {
-    uint idx                = event->GetIndex(ECbmDataType::kT0Digi, 0);
+    uint idx                 = event->GetIndex(ECbmDataType::kT0Digi, 0);
     const CbmBmonDigi* pDigi = GetDigi<CbmBmonDigi>(idx);
-    double eventTime        = pDigi->GetTime();
+    double eventTime         = pDigi->GetTime();
 
     for (int idigi = 1; idigi < iNbDigis; ++idigi) {
       idx   = event->GetIndex(ECbmDataType::kT0Digi, idigi);
diff --git a/reco/eventbuilder/digis/CbmAlgoBuildRawEvents.h b/reco/eventbuilder/digis/CbmAlgoBuildRawEvents.h
index 8a17452d69e293c71e861b904b18323ac0fe9cc6..f17e0ee2711b6843a4fb576dcd13a5c843a2e518 100644
--- a/reco/eventbuilder/digis/CbmAlgoBuildRawEvents.h
+++ b/reco/eventbuilder/digis/CbmAlgoBuildRawEvents.h
@@ -45,7 +45,8 @@ enum class EOverlapModeRaw
 {
   NoOverlap,
   MergeOverlap,
-  AllowOverlap
+  AllowOverlap,
+  Undefined
 };
 
 class RawEventBuilderDetector {
diff --git a/reco/offline/CMakeLists.txt b/reco/offline/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e8e0d8791b26cadc89262afed0b105510f4a3680
--- /dev/null
+++ b/reco/offline/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_subdirectory(app)
+add_subdirectory(steer)
diff --git a/reco/offline/app/Application.cxx b/reco/offline/app/Application.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..cc31543a139ccc28b4412c69d62582852c389d72
--- /dev/null
+++ b/reco/offline/app/Application.cxx
@@ -0,0 +1,46 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer] */
+
+/** @file Application.cxx
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @date 09.06.2023
+ **/
+
+#include "Application.h"
+
+#include "Config.h"
+
+using std::string;
+
+namespace cbm::reco::offline
+{
+
+  // -----   Constructor   ----------------------------------------------------
+  Application::Application(ProgramOptions const& opt) : fOpt(opt) {}
+  // --------------------------------------------------------------------------
+
+
+  // -----   Run the reconstruction   -----------------------------------------
+  void Application::Exec()
+  {
+
+    // --- Use program options
+    fRun.SetInput(fOpt.InputFile().c_str());
+    fRun.SetOutput(fOpt.OutputFile().c_str());
+    fRun.SetParams(fOpt.ParamFile().c_str());
+    fRun.SetGeoSetupTag(fOpt.SetupTag().c_str());
+    fRun.SetNumTs(fOpt.MaxNumTs());
+    if (fOpt.Overwrite()) fRun.AllowOverwrite();
+
+    // --- Read configuration from YAML file
+    cbm::reco::offline::Config config;
+    config.LoadYaml(fOpt.ConfigFile());
+    fRun.SetConfig(config);
+
+    // --- Execute reconstruction run
+    fRun.Exec();
+  }
+  // --------------------------------------------------------------------------
+
+}  // namespace cbm::reco::offline
diff --git a/reco/offline/app/Application.h b/reco/offline/app/Application.h
new file mode 100644
index 0000000000000000000000000000000000000000..650a74327df7a0b37fe206869d8ad89c005c2fd3
--- /dev/null
+++ b/reco/offline/app/Application.h
@@ -0,0 +1,54 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer] */
+
+/** @file Application.h
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @date 09.06.2023
+ **/
+
+
+#ifndef CBM_RECO_OFFLINE_APP_APPLICATION_H
+#define CBM_RECO_OFFLINE_APP_APPLICATION 1
+
+#include "ProgramOptions.h"
+#include "Run.h"
+
+namespace cbm::reco::offline
+{
+
+  class Application {
+
+
+  public:
+    /** @brief Standard constructor, initialises the application
+     ** @param opt  **/
+    explicit Application(ProgramOptions const& opt);
+
+    /** @brief Copy constructor forbidden **/
+    Application(const Application&) = delete;
+
+    /** @brief Assignment operator forbidden **/
+    void operator=(const Application&) = delete;
+
+    /** @brief Destructor **/
+    ~Application() = default;
+
+    /** @brief Run the application **/
+    void Exec();
+
+  private:
+    const std::string& OutputFile() const;
+    const std::string& ParamFile() const;
+    const std::string& SetupTag() const;
+    const std::string& ConfigFile() const;
+
+
+  private:
+    ProgramOptions const& fOpt;  ///< Program options object
+    Run fRun = {};               ///< Reconstruction run
+  };
+
+}  // namespace cbm::reco::offline
+
+#endif /* CBM_RECO_OFFLINE_APP_APPLICATION */
diff --git a/reco/offline/app/CMakeLists.txt b/reco/offline/app/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..feb7930a49e623365e9f5c262f67cbb64d72e100
--- /dev/null
+++ b/reco/offline/app/CMakeLists.txt
@@ -0,0 +1,25 @@
+# CMakeList file for binary cbmreco_offline
+# V. Friese,     13 May 2023
+
+set(SRCS
+  Application.cxx
+  ProgramOptions.cxx
+  main.cxx
+  )
+  
+set(HEADERS
+  Application.h
+  ProgramOptions.h
+ )
+  
+
+add_executable(cbmreco_offline ${SRCS} ${HEADERS})
+
+target_link_libraries(cbmreco_offline
+  PUBLIC 
+    CbmRecoOfflineSteer
+  PRIVATE 
+    Boost::program_options
+  )
+
+install(TARGETS cbmreco_offline DESTINATION bin)
diff --git a/reco/offline/app/ProgramOptions.cxx b/reco/offline/app/ProgramOptions.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..1b9740bd8ddc56220b5b52eba7dd4d69166bda7b
--- /dev/null
+++ b/reco/offline/app/ProgramOptions.cxx
@@ -0,0 +1,79 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer] */
+
+/** @file ProgramOptions.cxx
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @date 09.06.2023
+ **
+ ** Code taken from reco/app/cbmreco_fairrun/ProgramOptions.cxx (J. de Cuveland)
+ **/
+
+#include "ProgramOptions.h"
+
+#include <Logger.h>
+
+#include <boost/program_options.hpp>
+
+#include <iostream>
+
+namespace po = boost::program_options;
+
+using std::string;
+
+namespace cbm::reco::offline
+{
+
+  // -----   Parse command line   ---------------------------------------------
+  void ProgramOptions::ParseOptions(int argc, char* argv[])
+  {
+
+    // --- Define generic options
+    po::options_description generic("Generic options");
+    auto generic_add = generic.add_options();
+    generic_add("help,h", "display this help and exit");
+
+    // --- Define configuration options
+    string defconfig = std::getenv("VMCWORKDIR");
+    defconfig.append("/");
+    defconfig.append(DEFAULT_CONFIG);
+    po::options_description config("Configuration");
+    auto config_add = config.add_options();
+    config_add("input,i", po::value<string>(&fInput)->value_name("<file name>"),
+               "name of the input ROOT file containing digi data");
+    config_add("output,o", po::value<string>(&fOutput)->value_name("<file name>"),
+               "name of the output ROOT file with reconstructed data");
+    config_add("params,p", po::value<string>(&fParam)->value_name("<file name>"),
+               "name of a parameter ROOT file (FairRuntimeDb format)");
+    config_add("config,c", po::value<string>(&fConfig)->value_name("<file name>")->default_value(defconfig),
+               "name of a YAML file describing the configuration of reconstruction");
+    config_add("setup,s", po::value<string>(&fSetup)->value_name("<tag>")->default_value(DEFAULT_SETUP),
+               "geometry setup tag");
+    config_add("max-timeslice-number,n", po::value<int32_t>(&fMaxNumTs)->value_name("<n>")->default_value(INT32_MAX),
+               "maximum number of timeslices to process (default: all in input file)");
+    config_add("overwrite,w", po::bool_switch(&fOverwrite)->default_value(false),
+               "allow to overwite an existing output file");
+
+    // --- Allowed options
+    po::options_description cmdline_options("Allowed options");
+    cmdline_options.add(generic).add(config);
+
+    // --- Parse command line
+    po::variables_map vars;
+    po::store(po::parse_command_line(argc, argv, cmdline_options), vars);
+    po::notify(vars);
+
+    // --- Help: print help information and exit program
+    if (vars.count("help") != 0u) {
+      std::cout << cmdline_options << std::endl;
+      exit(EXIT_SUCCESS);
+    }
+
+    // --- Catch mandatory parameters not being specified
+    if (vars.count("input") == 0u) { throw std::runtime_error("no input file name specified"); }
+    if (vars.count("output") == 0u) { throw std::runtime_error("no output file name specified"); }
+    if (vars.count("params") == 0u) { throw std::runtime_error("no parameter file name specified"); }
+  }
+  // --------------------------------------------------------------------------
+
+}  // namespace cbm::reco::offline
diff --git a/reco/offline/app/ProgramOptions.h b/reco/offline/app/ProgramOptions.h
new file mode 100644
index 0000000000000000000000000000000000000000..cb5bdbda5d5d5146800a203b49959c6bc5889f64
--- /dev/null
+++ b/reco/offline/app/ProgramOptions.h
@@ -0,0 +1,83 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer], Jan de Cuveland */
+
+/** @file ProgramOptions.h
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @date 09.06.2023
+ **
+ ** Code taken from reco/app/cbmreco_fairrun/ProgramOptions.h (J. de Cuveland)
+ **/
+
+#ifndef CBM_RECO_OFFLINE_APP_PROGRAMOPTIONS_H
+#define CBM_RECO_OFFLINE_APP_PROGRAMOPTIONS_H 1
+
+#define DEFAULT_CONFIG "reco/offline/config/RecoConfig_event_ideal.yaml"
+#define DEFAULT_SETUP "sis100_electron"
+
+
+#include <string>
+
+namespace cbm::reco::offline
+{
+
+  /** @class ProgramOptions
+   ** @author Volker Friese <v.friese@gsi.de>
+   ** @since 9 June 2023
+   **
+   ** Program option class for the application cbmreco_offline
+   **/
+  class ProgramOptions {
+  public:
+    /** @brief Standard constructor with command line arguments **/
+    ProgramOptions(int argc, char* argv[]) { ParseOptions(argc, argv); }
+
+    /** @brief Copy constructor forbidden **/
+    ProgramOptions(const ProgramOptions&) = delete;
+
+    /** @brief Assignment operator forbidden **/
+    ProgramOptions& operator=(const ProgramOptions&) = delete;
+
+    /** @brief Destructor **/
+    ~ProgramOptions() = default;
+
+    /** @brief Get input file name **/
+    [[nodiscard]] const std::string& InputFile() const { return fInput; }
+
+    /** @brief Get output file name (.root format) **/
+    [[nodiscard]] const std::string& OutputFile() const { return fOutput; }
+
+    /** @brief Get configuration file name (YAML format) **/
+    [[nodiscard]] const std::string& ConfigFile() const { return fConfig; }
+
+    /** @brief Get overwrite option **/
+    [[nodiscard]] bool Overwrite() const { return fOverwrite; }
+
+    /** @brief Get parameter file name **/
+    [[nodiscard]] const std::string& ParamFile() const { return fParam; }
+
+    /** @brief Get geometry setup tag **/
+    [[nodiscard]] const std::string& SetupTag() const { return fSetup; }
+
+    /** @brief Get maximum number of timeslices to process **/
+    [[nodiscard]] int32_t MaxNumTs() const { return fMaxNumTs; }
+
+
+  private:
+    /** @brief Parse command line arguments using boost program_options **/
+    void ParseOptions(int argc, char* argv[]);
+
+
+  private:                            // members
+    std::string fInput  = "";         ///< Input file name (ROOT format)
+    std::string fOutput = "";         ///< Output file name (ROOT format)
+    std::string fParam  = "";         ///< Parameter file name (ROOT format)
+    std::string fConfig = "";         ///< Configuration file name (YAML format)
+    std::string fSetup  = "";         ///< Geometry setup tag
+    int32_t fMaxNumTs   = INT32_MAX;  ///< Maximum number of timeslices to process
+    bool fOverwrite     = false;      ///< Enable overwriting of existing output file
+  };
+
+}  // namespace cbm::reco::offline
+
+#endif /* CBM_RECO_OFFLINE_APP_PROGRAMOPTIONS_H */
diff --git a/reco/offline/app/main.cxx b/reco/offline/app/main.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..e80051dee517ff5cd1ae9e535e0153942244661e
--- /dev/null
+++ b/reco/offline/app/main.cxx
@@ -0,0 +1,26 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer], Jan de Cuveland */
+#include <Logger.h>
+
+#include "Application.h"
+#include "ProgramOptions.h"
+
+using namespace cbm::reco::offline;
+
+int main(int argc, char* argv[])
+{
+  LOG(info) << "*****   CBM Offline Reconstruction   *****";
+  try {
+    ProgramOptions opt(argc, argv);
+    Application app(opt);
+    app.Exec();
+  }
+  catch (std::exception const& e) {
+    LOG(error) << e.what() << "; terminating.";
+    return EXIT_FAILURE;
+  }
+
+  LOG(info) << "CBM Offline Reconstruction: Program completed successfully; exiting.";
+  return EXIT_SUCCESS;
+}
diff --git a/reco/offline/config/RecoConfig_event_ideal.yaml b/reco/offline/config/RecoConfig_event_ideal.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..28c0cd9378c4dec38a9a9d7fbd80b61fae49404b
--- /dev/null
+++ b/reco/offline/config/RecoConfig_event_ideal.yaml
@@ -0,0 +1,19 @@
+global:
+  log_level: INFO
+  log_verbose: LOW
+  mode: event
+evbuild:
+  type: ideal
+  overlap: allow
+  triggerDetector: sts
+  minNumDigis: 1000
+  maxNumDigis: -1
+  trigWinMin: -500
+  trigWinMax: 500
+sts:
+  usegpu: false
+trd:
+  trigThresh: 0.5e-6
+littrack:
+  trackingType: branch
+  mergingType: nearest_hit
diff --git a/reco/offline/config/RecoConfig_event_real.yaml b/reco/offline/config/RecoConfig_event_real.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b98ef71727ded07ce2f7cf2b4d18ddc227cad30f
--- /dev/null
+++ b/reco/offline/config/RecoConfig_event_real.yaml
@@ -0,0 +1,19 @@
+global:
+  log_level: INFO
+  log_verbose: LOW
+  mode: event
+evbuild:
+  type: real
+  overlap: allow
+  triggerDetector: sts
+  minNumDigis: 1000
+  maxNumDigis: -1
+  trigWinMin: -500
+  trigWinMax: 500
+sts:
+  usegpu: false
+trd:
+  trigThresh: 0.5e-6
+littrack:
+  trackingType: branch
+  mergingType: nearest_hit
diff --git a/reco/offline/config/RecoConfig_timeslice.yaml b/reco/offline/config/RecoConfig_timeslice.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..890a9751f5c18ad41d70ab8e666103f0a6602b3f
--- /dev/null
+++ b/reco/offline/config/RecoConfig_timeslice.yaml
@@ -0,0 +1,19 @@
+global:
+  log_level: INFO
+  log_verbose: LOW
+  mode: timeslice
+evbuild:
+  type: ideal
+  overlap: allow
+  triggerDetector: sts
+  minNumDigis: 1000
+  maxNumDigis: -1
+  trigWinMin: -500
+  trigWinMax: 500
+sts:
+  usegpu: false
+trd:
+  trigThresh: 0.5e-6
+littrack:
+  trackingType: branch
+  mergingType: nearest_hit
diff --git a/reco/offline/steer/CMakeLists.txt b/reco/offline/steer/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..597907cb4e5f87e1d10b4cec9d165db90d72a9ad
--- /dev/null
+++ b/reco/offline/steer/CMakeLists.txt
@@ -0,0 +1,69 @@
+# CMakeList file for library libCbmRecoOfflineSteer
+# V. Friese,     13 May 2023
+
+
+set(SRCS
+  Config.cxx
+  Run.cxx
+  TaskFactory.cxx
+  )
+  
+set(INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR})
+  
+if(OpenMP_CXX_FOUND)
+  list(APPEND DEPENDENCIES OpenMP::OpenMP_CXX)
+endif()
+
+
+set(LIBRARY_NAME CbmRecoOfflineSteer)
+set(LINKDEF RootLinkDef.h)
+set(PUBLIC_DEPENDENCIES
+  CbmData
+  FairRoot::Base
+  ROOT::Core
+  external::yaml-cpp
+  CbmEventBuilder
+  )
+
+set(PRIVATE_DEPENDENCIES
+  CbmBase
+  CbmTrdBase
+  FairLogger::FairLogger
+  FairRoot::Online
+  ROOT::Hist
+  ROOT::RHTTP
+  CbmGlobal
+  CbmRichBase
+  CbmTofBase
+  CbmMuchReco
+  CbmMvdReco
+  CbmRichReco
+  CbmRecoSts
+  CbmTofReco
+  CbmTrdReco
+  CbmSimSteer
+  L1
+  Algo
+  )
+
+# Check if the compiler supports std::execution in the respective STL
+# library
+CHECK_CXX_SOURCE_COMPILES("
+#include <numeric>
+#include <vector>
+#include <execution>
+
+int main(int argc, char *argv[])
+{
+    std::vector<double> v(10, 1);
+
+    auto result = std::reduce(std::execution::par, v.begin(), v.end());
+    return 0;
+}" HAS_STD_EXECUTION)
+
+if (HAS_STD_EXECUTION)
+  message("Execution is available in STL")
+  add_definitions(-DWITH_EXECUTION)
+endif()
+
+generate_cbm_library()
diff --git a/reco/offline/steer/Config.cxx b/reco/offline/steer/Config.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..82aa9cc6b47eecc14ae319481c2be410fc680540
--- /dev/null
+++ b/reco/offline/steer/Config.cxx
@@ -0,0 +1,174 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer] */
+
+/** @file .cxx
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @date 31.05.2023
+ **/
+
+
+#include "Config.h"
+
+#include <Logger.h>
+
+#include <fstream>
+
+#include <yaml-cpp/yaml.h>
+
+using std::string;
+
+namespace cbm::reco::offline
+{
+
+
+  // -----   Load settings from YAML file   -------------------------------------
+  void Config::LoadYaml(const string& fileName)
+  {
+
+    LOG(info) << "Config: Reading configuration from " << fileName;
+    YAML::Node settings = YAML::LoadFile(fileName);
+
+    // --- Global settings
+    f_glb_logLevel   = settings["global"]["log_level"].as<string>();
+    f_glb_logVerbose = settings["global"]["log_verbose"].as<string>();
+    f_glb_mode       = ToCbmRecoMode(settings["global"]["mode"].as<string>());
+
+    // --- Event builder
+    f_evbuild_type       = ToCbmEvbuildType(settings["evbuild"]["type"].as<string>());
+    f_evbuild_ovlapmode  = ToOverlapModeRaw(settings["evbuild"]["overlap"].as<string>());
+    f_evbuild_trigDet    = ToCbmModuleIdCaseInsensitive(settings["evbuild"]["triggerDetector"].as<string>());
+    f_evbuild_trigNumMin = settings["evbuild"]["minNumDigis"].as<int64_t>();
+    f_evbuild_trigNumMax = settings["evbuild"]["maxNumDigis"].as<int64_t>();
+    f_evbuild_trigWinMin = settings["evbuild"]["trigWinMin"].as<float>();
+    f_evbuild_trigWinMax = settings["evbuild"]["trigWinMax"].as<float>();
+
+    // --- STS
+    f_sts_usegpu = settings["sts"]["usegpu"].as<bool>();
+
+    // --- TRD
+    f_trd_trigThresh = settings["trd"]["trigThresh"].as<double>();
+
+    // --- littrack
+    f_lit_trackType = settings["littrack"]["trackingType"].as<string>();
+    f_lit_mergeType = settings["littrack"]["mergingType"].as<string>();
+  }
+  // ----------------------------------------------------------------------------
+
+
+  // ------   String to ECbmEvbuildType   ---------------------------------------
+  ECbmEvbuildType Config::ToCbmEvbuildType(string choice)
+  {
+    string temp = choice;
+    std::transform(temp.begin(), temp.end(), temp.begin(), [](unsigned char c) { return std::tolower(c); });
+    if (temp == "ideal") return ECbmEvbuildType::Ideal;
+    else if (temp == "real")
+      return ECbmEvbuildType::Real;
+    else
+      return ECbmEvbuildType::Undefined;
+  }
+  // ----------------------------------------------------------------------------
+
+
+  // ------   String to ECbmRecoMode   ------------------------------------------
+  ECbmRecoMode Config::ToCbmRecoMode(string choice)
+  {
+    string temp = choice;
+    std::transform(temp.begin(), temp.end(), temp.begin(), [](unsigned char c) { return std::tolower(c); });
+    if (temp == "timeslice") return ECbmRecoMode::Timeslice;
+    else if (temp == "event")
+      return ECbmRecoMode::EventByEvent;
+    else
+      return ECbmRecoMode::Undefined;
+  }
+  // ----------------------------------------------------------------------------
+
+
+  // ------   String to EOverlapModeRaw   ---------------------------------------
+  EOverlapModeRaw Config::ToOverlapModeRaw(string choice)
+  {
+    string temp = choice;
+    std::transform(temp.begin(), temp.end(), temp.begin(), [](unsigned char c) { return std::tolower(c); });
+    if (temp == "no") return EOverlapModeRaw::NoOverlap;
+    else if (temp == "allow")
+      return EOverlapModeRaw::AllowOverlap;
+    else if (temp == "merge")
+      return EOverlapModeRaw::MergeOverlap;
+    else
+      return EOverlapModeRaw::Undefined;
+  }
+  // ----------------------------------------------------------------------------
+
+
+  // -----   ECbmEvbuildType to string   ----------------------------------------
+  string Config::ToString(ECbmEvbuildType type)
+  {
+    if (type == ECbmEvbuildType::Ideal) return "ideal";
+    else if (type == ECbmEvbuildType::Real)
+      return "real";
+    else
+      return "undefined";
+  }
+  // ----------------------------------------------------------------------------
+
+
+  // -----   ECbmRecoMode to string   -------------------------------------------
+  string Config::ToString(ECbmRecoMode mode)
+  {
+    if (mode == ECbmRecoMode::Timeslice) return "timeslice";
+    else if (mode == ECbmRecoMode::EventByEvent)
+      return "event";
+    else
+      return "undefined";
+  }
+  // ----------------------------------------------------------------------------
+
+
+  // -----   EOverlapMode to string   -------------------------------------------
+  string Config::ToString(EOverlapModeRaw mode)
+  {
+    if (mode == EOverlapModeRaw::NoOverlap) return "no";
+    else if (mode == EOverlapModeRaw::AllowOverlap)
+      return "allow";
+    else if (mode == EOverlapModeRaw::MergeOverlap)
+      return "merge";
+    else
+      return "undefined";
+  }
+  // ----------------------------------------------------------------------------
+
+
+  // -----   Save settings to YAML node   ---------------------------------------
+  YAML::Node Config::ToYaml()
+  {
+    YAML::Node settings;
+
+    // --- Global settings
+    settings["global"]["log_level"]   = f_glb_logLevel;
+    settings["global"]["log_verbose"] = f_glb_logVerbose;
+    settings["global"]["mode"]        = ToString(f_glb_mode);
+
+    // --- Event builder
+    settings["evbuild"]["type"]            = ToString(f_evbuild_type);
+    settings["evbuild"]["overlap"]         = ToString(f_evbuild_ovlapmode);
+    settings["evbuild"]["triggerDetector"] = ::ToString(f_evbuild_trigDet);
+    settings["evbuild"]["minNumDigis"]     = f_evbuild_trigNumMin;
+    settings["evbuild"]["maxNumDigis"]     = f_evbuild_trigNumMax;
+    settings["evbuild"]["trigWinMin"]      = f_evbuild_trigWinMin;
+    settings["evbuild"]["trigWinMax"]      = f_evbuild_trigWinMax;
+
+    // --- STS
+    settings["sts"]["usegpu"] = f_sts_usegpu;
+
+    // --- TRD
+    settings["trd"]["trigThresh"] = f_trd_trigThresh;
+
+    // --- littrack
+    settings["littrack"]["trackingType"] = f_lit_trackType;
+    settings["littrack"]["mergingType"]  = f_lit_mergeType;
+
+    return settings;
+  }
+  // ----------------------------------------------------------------------------
+
+}  // namespace cbm::reco::offline
diff --git a/reco/offline/steer/Config.h b/reco/offline/steer/Config.h
new file mode 100644
index 0000000000000000000000000000000000000000..a013033884705ad6b042d7e6886a9a8888a86026
--- /dev/null
+++ b/reco/offline/steer/Config.h
@@ -0,0 +1,117 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer] */
+
+/** @file Config.h
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @date 31.05.2023
+ **/
+
+#ifndef CBM_RECO_OFFLINE_STEER_CONFIG_H
+#define CBM_RECO_OFFLINE_STEER_CONFIG_H 1
+
+#include "CbmAlgoBuildRawEvents.h"
+#include "CbmDefs.h"
+
+#include <array>
+#include <fstream>
+#include <sstream>
+#include <string>
+
+#include <yaml-cpp/yaml.h>
+
+namespace cbm::reco::offline
+{
+
+  enum class ECbmEvbuildType
+  {
+    Ideal,
+    Real,
+    Undefined
+  };
+
+
+  /** @class Config
+   ** @author Volker Friese <v.friese@gsi.de>
+   ** @date 31.05.2023
+   **
+   ** Configuration for the offline reconstruction chain, replacing run_reco.C.
+   ** With interfaces to YAML.
+   **/
+  class Config {
+  public:
+    /** @brief Constructor **/
+    Config() = default;
+
+
+    /** @brief Destructor **/
+    virtual ~Config() = default;
+
+
+    /** @brief Load from YAML file
+     ** @param filename  Name of input YAML file
+     **/
+    void LoadYaml(const std::string& filename);
+
+
+    /** @brief String output (YAML format) **/
+    std::string ToString()
+    {
+      std::stringstream out;
+      out << ToYaml();
+      return out.str();
+    }
+
+
+    /** @brief Save to YAML file
+     ** @param filename  Name of output YAML file
+     **/
+    void SaveYaml(const std::string& filename)
+    {
+      std::ofstream fout(filename);
+      fout << ToYaml();
+    }
+
+
+  private:
+    ECbmEvbuildType ToCbmEvbuildType(std::string tag);
+    ECbmRecoMode ToCbmRecoMode(std::string tag);
+    EOverlapModeRaw ToOverlapModeRaw(std::string tag);
+    std::string ToString(ECbmEvbuildType type);
+    std::string ToString(ECbmRecoMode mode);
+    std::string ToString(EOverlapModeRaw mode);
+
+    /** @brief Save to YAML node **/
+    YAML::Node ToYaml();
+
+
+  public:
+    // --- Global settings
+    std::string f_glb_logLevel   = "INFO";
+    std::string f_glb_logVerbose = "LOW";
+    ECbmRecoMode f_glb_mode      = ECbmRecoMode::Undefined;
+
+    // --- Event builder
+    ECbmEvbuildType f_evbuild_type      = ECbmEvbuildType::Undefined;
+    EOverlapModeRaw f_evbuild_ovlapmode = EOverlapModeRaw::Undefined;
+    ECbmModuleId f_evbuild_trigDet      = ECbmModuleId::kNotExist;
+    int64_t f_evbuild_trigNumMin        = 0;
+    int64_t f_evbuild_trigNumMax        = 0;
+    float f_evbuild_trigWinMin          = std::nanf("undefined");
+    float f_evbuild_trigWinMax          = std::nanf("undefined");
+
+    // --- STS
+    bool f_sts_usegpu = false;
+
+    // --- TRD
+    float f_trd_trigThresh = std::nanf("undefined");
+
+    // --- littrack
+    std::string f_lit_trackType = "";
+    std::string f_lit_mergeType = "";
+  };
+
+}  // namespace cbm::reco::offline
+
+
+#endif /* CBM_RECO_OFFLINE_STEER_CONFIG_H */
diff --git a/reco/offline/steer/RootLinkDef.h b/reco/offline/steer/RootLinkDef.h
new file mode 100644
index 0000000000000000000000000000000000000000..b0bb83217633e362d13c52a5731a35d8f60305d9
--- /dev/null
+++ b/reco/offline/steer/RootLinkDef.h
@@ -0,0 +1,17 @@
+/* Copyright (C) 2021 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer] */
+
+// ===== LinkDef for reco/task =====
+
+#ifdef __CINT__
+
+#pragma link off all globals;
+#pragma link off all classes;
+#pragma link off all functions;
+
+// --- Classes
+#pragma link C++ class cbm::reco::offline::Run + ;
+
+
+#endif /* __CINT__ */
diff --git a/reco/offline/steer/Run.cxx b/reco/offline/steer/Run.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..c11b30b037fa69bac728e3fcd5acdafec53ed1b6
--- /dev/null
+++ b/reco/offline/steer/Run.cxx
@@ -0,0 +1,234 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer], Florian Uhlig */
+
+/** @file Run.cxx
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @date 18.05.2018
+ **/
+
+#include "Run.h"
+
+#include "CbmSetup.h"
+
+#include <FairEventHeader.h>
+#include <FairFileSource.h>
+#include <FairMonitor.h>
+#include <FairParRootFileIo.h>
+#include <FairRootFileSink.h>
+#include <FairRunAna.h>
+#include <FairRuntimeDb.h>
+#include <FairTask.h>
+#include <Logger.h>
+
+#include "TaskFactory.h"
+#include <TFile.h>
+#include <TGeoManager.h>
+#include <TStopwatch.h>
+#include <TString.h>
+#include <TTree.h>
+
+#include <cassert>
+#include <iostream>
+
+#include <sys/stat.h>
+
+namespace cbm::reco::offline
+{
+
+
+  // -----   Constructor   ----------------------------------------------------
+  Run::Run() : TNamed("Run", "CBM Reconstruction Run") {}
+  // --------------------------------------------------------------------------
+
+
+  // -----   Destructor   -----------------------------------------------------
+  Run::~Run() { LOG(debug) << "Destructing " << fName; }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Add a reconstruction task   --------------------------------------
+  void Run::AddTask(FairTask* task)
+  {
+    fRun.AddTask(task);
+    LOG(info) << GetName() << ": Added task " << task->GetName();
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Check a digi branch   --------------------------------------------
+  void Run::CheckDigiBranch(TTree* tree, ECbmModuleId detector)
+  {
+    TString branchName = ToString(detector);
+    branchName += "Digi";
+    if (tree->GetBranchStatus(branchName.Data())) {
+      LOG(info) << GetName() << ": Found branch " << branchName;
+      fDataPresent.insert(detector);
+    }
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Check existence of a file   --------------------------------------
+  bool Run::CheckFile(const char* fileName)
+  {
+    struct stat buffer;
+    return (stat(fileName, &buffer) == 0);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Check which input digi branches are present   --------------------
+  void Run::CheckInputBranches(FairFileSource* source)
+  {
+
+    TFile* inFile = source->GetInFile();
+    if (!inFile) throw std::runtime_error("No input file");
+    TTree* inTree = dynamic_cast<TTree*>(inFile->Get("cbmsim"));
+    if (!inTree) throw std::runtime_error("No input tree");
+
+    for (ECbmModuleId detector = ECbmModuleId::kMvd; detector != ECbmModuleId::kNofSystems; ++detector)
+      CheckDigiBranch(inTree, detector);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Create the topology   --------------------------------------------
+  void Run::CreateTopology()
+  {
+
+    TaskFactory fact(this);
+
+    // --- Timeslice processing
+    if (fConfig.f_glb_mode == ECbmRecoMode::Timeslice) {
+      fact.RegisterStsReco();            // Local reconstruction in STS
+      fact.RegisterRichHitFinder();      // Hit finding in RICH
+      fact.RegisterMuchReco();           // Local reconstruction in MUCH
+      fact.RegisterTrdReco();            // Local reconstruction in TRD
+      fact.RegisterTofReco();            // Local reconstruction in TOF
+      fact.RegisterCaTracking();         // CA track finder in STS and MVD
+      fact.RegisterTrackEventBuilder();  // Event building from STS tracks
+    }
+
+    // --- Event-by-event processing
+    else if (fConfig.f_glb_mode == ECbmRecoMode::EventByEvent) {
+      fact.RegisterDigiEventBuilder();  // Event building from digis
+      fact.RegisterMvdReco();           // Local reconstruction in MVD
+      fact.RegisterStsReco();           // Local reconstruction in STS
+      fact.RegisterRichHitFinder();     // Hit finding in RICH
+      fact.RegisterMuchReco();          // Local reconstruction in MUCH
+      fact.RegisterTrdReco();           // Local reconstruction in TRD
+      fact.RegisterTofReco();           // Local reconstruction in TOF
+      fact.RegisterCaTracking();        // CA track finder in STS and MVD
+      fact.RegisterPvFinder();          // Primary vertex finding
+      fact.RegisterGlobalTracking();    // Global tracking
+      fact.RegisterTrdPid();            // PID in TRD
+      fact.RegisterRichReco();          // Local RICH reconstruction
+      fact.RegisterTzeroReco();         // Reconstruction of T0 from BMON
+    }
+
+    // --- Mode not defined
+    else
+      throw std::out_of_range("Reconstruction mode not defined");
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Execute reconstruction run   -------------------------------------
+  void Run::Exec()
+  {
+
+    // --- Mirror options and configuration
+    LOG(info) << GetName() << ": Input file is     " << fInput;
+    LOG(info) << GetName() << ": Parameter file is " << fParams;
+    LOG(info) << GetName() << ": Output file is    " << fOutput;
+    LOG(info) << GetName() << ": Geometry setup is " << fSetupTag;
+    LOG(info) << "Configuration: \n" << fConfig.ToString();
+
+    // --- Timer
+    TStopwatch timer;
+    timer.Start();
+
+    // --- Run info
+    fRun.SetGenerateRunInfo(true);
+
+    // --- Check input, output and parameter files
+    if (!CheckFile(fInput.Data())) throw std::runtime_error("Input file does not exist");
+    if (!CheckFile(fParams.Data())) throw std::runtime_error("Parameter file does not exist");
+    if (CheckFile(fOutput.Data()) && !fOverwrite) throw std::runtime_error("Output file already exists");
+
+    // --- Input and output
+    FairFileSource* source = new FairFileSource(fInput);
+    fRun.SetSource(source);
+    fRun.SetSink(new FairRootFileSink(fOutput));
+
+    // --- Check presence of input (digi) branches
+    LOG(info) << GetName() << ": Checking digi input...";
+    CheckInputBranches(source);
+
+    // --- Geometry setup
+    LOG(info) << GetName() << ": Loading setup " << fSetupTag;
+    fSetup = CbmSetup::Instance();
+    fSetup->LoadSetup(fSetupTag);
+    // TODO: This CbmSetup business with singleton is not nice. Should be replaced eventually.
+
+    // --- Topology
+    LOG(info) << GetName() << ": Creating topology...";
+    CreateTopology();
+
+    // --- Parameter database
+    LOG(info) << GetName() << ": Set runtime DB...";
+    FairRuntimeDb* rtdb       = fRun.GetRuntimeDb();
+    FairParRootFileIo* parIo1 = new FairParRootFileIo();
+    parIo1->open(fParams.Data(), "UPDATE");
+    rtdb->setFirstInput(parIo1);
+
+    // --- Initialisation
+    LOG(info) << GetName() << ": Initialise FairRun..." << std::endl;
+    fRun.Init();
+    rtdb->setOutput(parIo1);
+    rtdb->saveOutput();
+    rtdb->print();
+    timer.Stop();
+    double timeInit = timer.RealTime();
+    timer.Start();
+
+    // TODO: Register light ions (see run_reco.C). But what for?
+
+    // --- Execute run
+    std::cout << std::endl << std::endl;
+    LOG(info) << GetName() << ": Starting run" << std::endl;
+    fRun.Run(fNumTs, 0);
+
+    // --- Finish
+    timer.Stop();
+    double timeExec = timer.RealTime();
+    FairMonitor::GetMonitor()->Print();
+    std::cout << std::endl << std::endl;
+    LOG(info) << GetName() << ": Execution successful";
+    LOG(info) << GetName() << ": Input file was    " << fInput;
+    LOG(info) << GetName() << ": Parameter file is " << fParams;
+    LOG(info) << GetName() << ": Output file is    " << fOutput;
+    LOG(info) << GetName() << ": Execution time: Init " << timeInit << " s, Exec " << timeExec << "s";
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----  Read configuration from YAML file   -------------------------------
+  void Run::LoadConfig(const char* fileName)
+  {
+
+    TString file(fileName);
+    if (file.IsNull()) {
+      file = std::getenv("VMCWORKDIR");
+      file += "/reco/offline/config/RecoConfig_event_ideal.yaml";
+    }
+    LOG(info) << GetName() << ": Loading configuration from " << file;
+    fConfig.LoadYaml(file.Data());
+  }
+  // --------------------------------------------------------------------------
+
+
+}  // namespace cbm::reco::offline
+
+ClassImp(cbm::reco::offline::Run)
diff --git a/reco/offline/steer/Run.h b/reco/offline/steer/Run.h
new file mode 100644
index 0000000000000000000000000000000000000000..6c819ad499ffc330ce2136a7196823c5d019b173
--- /dev/null
+++ b/reco/offline/steer/Run.h
@@ -0,0 +1,160 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer] */
+
+/** @file Run.h
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @date 12.05.2023
+ **/
+
+#ifndef CBM_RECO_OFFLINE_STEER_RUN_H
+#define CBM_RECO_OFFLINE_STEER_RUN_H 1
+
+#include "CbmDefs.h"
+
+#include <FairRunAna.h>
+
+#include <TNamed.h>
+#include <TTree.h>
+
+#include <string>
+
+#include "Config.h"
+
+class TGeoManager;
+class CbmSetup;
+class FairTask;
+
+namespace cbm::reco::offline
+{
+
+  class Run : public TNamed {
+
+  public:
+    /** @brief Constructor **/
+    Run();
+
+
+    /** @brief Destructor  **/
+    virtual ~Run();
+
+
+    /** @brief Add a task to the run **/
+    void AddTask(FairTask* task);
+
+
+    /** @brief Allow overwriting if output file already exists **/
+    void AllowOverwrite() { fOverwrite = true; }
+
+
+    /** @brief Run reconstruction **/
+    void Exec();
+
+
+    /** @brief Settings object **/
+    const Config& GetConfig() const { return fConfig; }
+
+
+    /** @brief Presence of input digi data
+     ** @param detector Detector data to check
+     ** @return true if data present, else false
+     **/
+    bool IsDataPresent(ECbmModuleId detector) const { return (fDataPresent.count(detector) == 0 ? false : true); }
+
+
+    /** @brief Set configuration file name
+     ** @param fileName  Configuration file name
+     **
+     ** Legacy interface for running from ROOT prompt. In the executable, the config is read in
+     ** by the application.
+     **/
+    void LoadConfig(const char* fileName);
+
+
+    /** @brief Set configuration
+     ** @param fileName  Configuration object
+     **/
+    void SetConfig(const Config& config) { fConfig = config; }
+
+
+    /** @brief Set geometry setup tag
+     ** @param tag  Geometry setup tag
+     **/
+    void SetGeoSetupTag(const char* tag) { fSetupTag = tag; }
+
+
+    /** @brief Set input file name
+     ** @param fileName  Input file name
+     **/
+    void SetInput(const char* fileName) { fInput = fileName; }
+
+
+    /** @brief Set number of timeslices to process
+     ** @param numTs Number of timeslice to process
+     **/
+    void SetNumTs(int32_t num) { fNumTs = num; }
+
+
+    /** @brief Set output file name
+     ** @param fileName  Output file name
+     **/
+    void SetOutput(const char* fileName) { fOutput = fileName; }
+
+
+    /** @brief Set parameter file name
+     ** @param fileName  Parameter file name
+     **/
+    void SetParams(const char* fileName) { fParams = fileName; }
+
+
+  private:
+    /** @brief Copy constructor forbidden **/
+    Run(const Run&) = delete;
+
+
+    /** @brief Assignment operator forbidden **/
+    Run operator=(const Run&) = delete;
+
+
+    /** @brief Check and mark presence of a digi branch
+     ** @param detector ECbmModuleId
+     ** @param tree Pointer to ROOT Tree
+     **/
+    void CheckDigiBranch(TTree* tree, ECbmModuleId detector);
+
+
+    /** @brief Check existence of a file
+     ** @param fileName  File name (absolute or relative to current directory)
+     ** @return true if file exists
+     **/
+    bool CheckFile(const char* fileName);
+
+
+    /** @brief Check the presence of digi input branches **/
+    void CheckInputBranches(FairFileSource* source);
+
+
+    /** @brief Create the reconstruction task topology (chain) **/
+    void CreateTopology();
+
+
+  private:
+    FairRunAna fRun {};
+    TString fInput    = "";
+    TString fParams   = "";
+    TString fOutput   = "";
+    TString fSetupTag = "";
+    CbmSetup* fSetup  = nullptr;
+    size_t fNumTs     = 0;
+    bool fOverwrite   = false;
+
+    Config fConfig                      = {};
+    std::set<ECbmModuleId> fDataPresent = {};
+
+
+    ClassDef(cbm::reco::offline::Run, 1);
+  };
+
+}  // namespace cbm::reco::offline
+
+#endif /* CBM_RECO_OFFLINE_STEER_RUN_H */
diff --git a/reco/offline/steer/TaskFactory.cxx b/reco/offline/steer/TaskFactory.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..565d4b4cf13d1fe482504163dad79bd3ac880126
--- /dev/null
+++ b/reco/offline/steer/TaskFactory.cxx
@@ -0,0 +1,338 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer] */
+
+/** @file TaskFactory.cxx
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @date 06.06.2023
+ **/
+
+#include "TaskFactory.h"
+
+#include "CbmBuildEventsFromTracksReal.h"
+#include "CbmBuildEventsIdeal.h"
+#include "CbmFindPrimaryVertex.h"
+#include "CbmKF.h"
+#include "CbmL1.h"
+#include "CbmL1StsTrackFinder.h"
+#include "CbmLitFindGlobalTracks.h"
+#include "CbmMuchFindHitsGem.h"
+#include "CbmMuchGeoScheme.h"
+#include "CbmMvdClusterfinder.h"
+#include "CbmMvdHitfinder.h"
+#include "CbmPVFinderKF.h"
+#include "CbmRecoSts.h"
+#include "CbmRecoTzero.h"
+#include "CbmRichHitProducer.h"
+#include "CbmRichReconstruction.h"
+#include "CbmSetup.h"
+#include "CbmStsFindTracks.h"
+#include "CbmStsFindTracksEvents.h"
+#include "CbmTaskBuildRawEvents.h"
+#include "CbmTofSimpClusterizer.h"
+#include "CbmTrackingDetectorInterfaceInit.h"
+#include "CbmTrdClusterFinder.h"
+#include "CbmTrdHitProducer.h"
+#include "CbmTrdSetTracksPidLike.h"
+
+#include <TSystem.h>
+
+
+namespace cbm::reco::offline
+{
+
+
+  // -----   Constructor   ----------------------------------------------------
+  TaskFactory::TaskFactory(Run* run) : fRun(run) {}
+  // --------------------------------------------------------------------------
+
+
+  // -----   CA tracking   ----------------------------------------------------
+  void TaskFactory::RegisterCaTracking()
+  {
+    assert(fRun);
+
+    // --- Tracking geometry interface
+    auto interface = new CbmTrackingDetectorInterfaceInit();
+    fRun->AddTask(interface);
+
+    // --- Kalman Filter
+    auto kf = new CbmKF();
+    fRun->AddTask(kf);
+
+    // --- CA Track Finder
+    auto ca = new CbmL1("CA Track Finder", 0, 0, 4);
+    if (fRun->IsDataPresent(ECbmModuleId::kMvd)) {
+      TString mvdGeoTag;
+      if (CbmSetup::Instance()->GetGeoTag(ECbmModuleId::kMvd, mvdGeoTag)) {
+        TString parFileMvd = gSystem->Getenv("VMCWORKDIR");
+        parFileMvd += "/parameters/mvd/mvd_matbudget_" + mvdGeoTag + ".root";
+        ca->SetMvdMaterialBudgetFileName(parFileMvd.Data());
+      }
+      else
+        throw std::runtime_error("CA: No geo tag for MVD");
+    }
+    if (fRun->IsDataPresent(ECbmModuleId::kSts)) {
+      TString stsGeoTag;
+      if (CbmSetup::Instance()->GetGeoTag(ECbmModuleId::kSts, stsGeoTag)) {
+        TString parFileSts = gSystem->Getenv("VMCWORKDIR");
+        parFileSts += "/parameters/sts/sts_matbudget_" + stsGeoTag + ".root";
+        ca->SetStsMaterialBudgetFileName(parFileSts.Data());
+      }
+      else
+        throw std::runtime_error("CA: No geo tag for STS");
+    }
+    fRun->AddTask(ca);
+
+    // --- Track finder steering class
+    auto trackFinder = new CbmL1StsTrackFinder();
+
+    // --- Track finder task
+    FairTask* findTracks = nullptr;
+    if (fRun->GetConfig().f_glb_mode == ECbmRecoMode::Timeslice)
+      findTracks = new CbmStsFindTracks(0, trackFinder, true);
+    else
+      findTracks = new CbmStsFindTracksEvents(trackFinder, true);
+    fRun->AddTask(findTracks);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Event building from digis   --------------------------------------
+  void TaskFactory::RegisterDigiEventBuilder()
+  {
+    assert(fRun);
+    if (fRun->GetConfig().f_evbuild_type == cbm::reco::offline::ECbmEvbuildType::Undefined)
+      throw std::out_of_range("Undefined event builder type");
+
+    // --- Ideal digi event builder (using MC truth information)
+    if (fRun->GetConfig().f_evbuild_type == cbm::reco::offline::ECbmEvbuildType::Ideal) {
+      FairTask* evBuildRaw = new CbmBuildEventsIdeal();
+      fRun->AddTask(evBuildRaw);
+    }  //? Ideal event builder
+
+    // --- Real digi event builder
+    // TODO: This should be replaced by the newer algorithm algo/evbuild/EventBuilder
+    else {
+
+      const cbm::reco::offline::Config& config = fRun->GetConfig();
+      ECbmModuleId triggerDetector             = config.f_evbuild_trigDet;
+
+      if (!fRun->IsDataPresent(triggerDetector)) throw std::runtime_error("No input data for trigger detector");
+
+      // --- Task
+      CbmTaskBuildRawEvents* evBuildRaw = new CbmTaskBuildRawEvents();
+
+      // --- Mode
+      evBuildRaw->SetEventOverlapMode(config.f_evbuild_ovlapmode);
+
+      // --- Reference detector
+      switch (triggerDetector) {
+        case ECbmModuleId::kSts: evBuildRaw->SetReferenceDetector(kRawEventBuilderDetSts); break;
+        case ECbmModuleId::kMuch: evBuildRaw->SetReferenceDetector(kRawEventBuilderDetMuch); break;
+        case ECbmModuleId::kTrd: evBuildRaw->SetReferenceDetector(kRawEventBuilderDetTrd); break;
+        case ECbmModuleId::kTrd2d: evBuildRaw->SetReferenceDetector(kRawEventBuilderDetTrd2D); break;
+        case ECbmModuleId::kTof: evBuildRaw->SetReferenceDetector(kRawEventBuilderDetTof); break;
+        case ECbmModuleId::kRich: evBuildRaw->SetReferenceDetector(kRawEventBuilderDetRich); break;
+        case ECbmModuleId::kPsd: evBuildRaw->SetReferenceDetector(kRawEventBuilderDetPsd); break;
+        case ECbmModuleId::kT0: evBuildRaw->SetReferenceDetector(kRawEventBuilderDetT0); break;
+        default: throw std::out_of_range("Event builder: Undefined trigger detector"); break;
+      }
+
+      // --- Make BMON (default reference detector) a selected detector (with default parameters)
+      if (triggerDetector != ECbmModuleId::kT0) evBuildRaw->AddDetector(kRawEventBuilderDetT0);
+
+      // --- Remove detectors of which there are no input data
+      if (!fRun->IsDataPresent(ECbmModuleId::kRich)) evBuildRaw->RemoveDetector(kRawEventBuilderDetRich);
+      if (!fRun->IsDataPresent(ECbmModuleId::kMuch)) evBuildRaw->RemoveDetector(kRawEventBuilderDetMuch);
+      if (!fRun->IsDataPresent(ECbmModuleId::kPsd)) evBuildRaw->RemoveDetector(kRawEventBuilderDetPsd);
+      if (!fRun->IsDataPresent(ECbmModuleId::kTof)) evBuildRaw->RemoveDetector(kRawEventBuilderDetTof);
+      if (!fRun->IsDataPresent(ECbmModuleId::kTrd)) evBuildRaw->RemoveDetector(kRawEventBuilderDetTrd);
+
+      // --- Timeslice parameters
+      evBuildRaw->SetTsParameters(0.0, 1.e7, 0.0);
+
+      // --- Use CbmMuchDigi instead of CbmMuchBeamtimeDigi
+      // TODO: Seems legacy.
+      evBuildRaw->ChangeMuchBeamtimeDigiFlag(kFALSE);
+
+      // --- Set trigger parameters
+      evBuildRaw->SetTriggerMinNumber(triggerDetector, config.f_evbuild_trigNumMin);
+      evBuildRaw->SetTriggerMaxNumber(triggerDetector, config.f_evbuild_trigNumMax);
+      evBuildRaw->SetTriggerWindow(triggerDetector, config.f_evbuild_trigWinMin, config.f_evbuild_trigWinMax);
+
+      fRun->AddTask(evBuildRaw);
+
+    }  //? Real event builder
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   LIT Global Tracking   --------------------------------------------
+  void TaskFactory::RegisterGlobalTracking()
+  {
+    assert(fRun);
+    CbmLitFindGlobalTracks* finder = new CbmLitFindGlobalTracks();
+    finder->SetTrackingType(fRun->GetConfig().f_lit_trackType);
+    finder->SetMergerType(fRun->GetConfig().f_lit_mergeType);
+    fRun->AddTask(finder);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   MUCH reconstruction   --------------------------------------------
+  void TaskFactory::RegisterMuchReco()
+  {
+    assert(fRun);
+    if (!fRun->IsDataPresent(ECbmModuleId::kMuch)) return;
+
+    // --- Parameter file name
+    TString geoTag;
+    if (CbmSetup::Instance()->GetGeoTag(ECbmModuleId::kMuch, geoTag)) {
+      Int_t muchFlag  = (geoTag.Contains("mcbm") ? 1 : 0);
+      TString parFile = gSystem->Getenv("VMCWORKDIR");
+      parFile += "/parameters/much/much_" + geoTag(0, 4) + "_digi_sector.root";
+
+      auto muchGeoScheme = CbmMuchGeoScheme::Instance();
+      if (!muchGeoScheme->IsInitialized()) { muchGeoScheme->Init(parFile, muchFlag); }
+
+      FairTask* muchReco = new CbmMuchFindHitsGem(parFile.Data(), muchFlag);
+      fRun->AddTask(muchReco);
+    }
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   MVD reconstruction   ---------------------------------------------
+  void TaskFactory::RegisterMvdReco()
+  {
+    assert(fRun);
+    if (!fRun->IsDataPresent(ECbmModuleId::kMvd)) return;
+
+    CbmMvdClusterfinder* mvdCluster = new CbmMvdClusterfinder("MVD Cluster Finder", 0, 0);
+    fRun->AddTask(mvdCluster);
+
+    CbmMvdHitfinder* mvdHit = new CbmMvdHitfinder("MVD Hit Finder", 0, 0);
+    mvdHit->UseClusterfinder(kTRUE);
+    fRun->AddTask(mvdHit);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Primary Vertex Finding   -----------------------------------------
+  void TaskFactory::RegisterPvFinder()
+  {
+    assert(fRun);
+    CbmPrimaryVertexFinder* pvFinder = new CbmPVFinderKF();
+    CbmFindPrimaryVertex* findVertex = new CbmFindPrimaryVertex(pvFinder);
+    fRun->AddTask(findVertex);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   RICH hit finding   -----------------------------------------------
+  void TaskFactory::RegisterRichHitFinder()
+  {
+    assert(fRun);
+    if (!fRun->IsDataPresent(ECbmModuleId::kRich)) return;
+
+    CbmRichHitProducer* richHitProd = new CbmRichHitProducer();
+    fRun->AddTask(richHitProd);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   RICH reconstruction   --------------------------------------------
+  void TaskFactory::RegisterRichReco()
+  {
+    assert(fRun);
+    if (!fRun->IsDataPresent(ECbmModuleId::kRich)) return;
+
+    CbmRichReconstruction* richReco = new CbmRichReconstruction();
+    fRun->AddTask(richReco);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   STS reconstruction   ---------------------------------------------
+  void TaskFactory::RegisterStsReco()
+  {
+    assert(fRun);
+    if (!fRun->IsDataPresent(ECbmModuleId::kSts)) return;
+
+    CbmRecoSts* stsReco = new CbmRecoSts(fRun->GetConfig().f_glb_mode);
+    stsReco->SetUseGpuReco(fRun->GetConfig().f_sts_usegpu);
+    fRun->AddTask(stsReco);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   TOF reconstruction   ---------------------------------------------
+  void TaskFactory::RegisterTofReco()
+  {
+    assert(fRun);
+    if (!fRun->IsDataPresent(ECbmModuleId::kTof)) return;
+
+    CbmTofSimpClusterizer* tofCluster = new CbmTofSimpClusterizer("TOF Simple Clusterizer", 0);
+    tofCluster->SetOutputBranchPersistent("TofHit", kTRUE);
+    tofCluster->SetOutputBranchPersistent("TofDigiMatch", kTRUE);
+    fRun->AddTask(tofCluster);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Event building from tracks   -------------------------------------
+  void TaskFactory::RegisterTrackEventBuilder()
+  {
+    assert(fRun);
+    auto eventBuilder = new CbmBuildEventsFromTracksReal();
+    fRun->AddTask(eventBuilder);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   TRD PID   --------------------------------------------------------
+  void TaskFactory::RegisterTrdPid()
+  {
+    assert(fRun);
+    if (!fRun->IsDataPresent(ECbmModuleId::kTrd)) return;
+
+    CbmTrdSetTracksPidLike* trdPid = new CbmTrdSetTracksPidLike("TRDLikelihood", "TRDLikelihood");
+    trdPid->SetUseMCInfo(kTRUE);
+    trdPid->SetUseMomDependence(kTRUE);
+    fRun->AddTask(trdPid);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   TRD reconstruction   ---------------------------------------------
+  void TaskFactory::RegisterTrdReco()
+  {
+    assert(fRun);
+    if (!fRun->IsDataPresent(ECbmModuleId::kTrd)) return;
+
+    CbmTrdClusterFinder* trdCluster = new CbmTrdClusterFinder();
+    if (fRun->GetConfig().f_glb_mode == ECbmRecoMode::EventByEvent) trdCluster->SetTimeBased(kFALSE);
+    else
+      trdCluster->SetTimeBased(kTRUE);
+    trdCluster->SetNeighbourEnable(true, false);
+    trdCluster->SetMinimumChargeTH(fRun->GetConfig().f_trd_trigThresh);
+    trdCluster->SetRowMerger(true);
+    fRun->AddTask(trdCluster);
+
+    CbmTrdHitProducer* trdHit = new CbmTrdHitProducer();
+    fRun->AddTask(trdHit);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   T0 reconstruction   ----------------------------------------------
+  void TaskFactory::RegisterTzeroReco()
+  {
+    assert(fRun);
+    CbmRecoTzero* recoT0 = new CbmRecoTzero();
+    fRun->AddTask(recoT0);
+  }
+  // --------------------------------------------------------------------------
+
+}  // namespace cbm::reco::offline
diff --git a/reco/offline/steer/TaskFactory.h b/reco/offline/steer/TaskFactory.h
new file mode 100644
index 0000000000000000000000000000000000000000..5fd7bf11952008a61017131f66ad3581bda0cf01
--- /dev/null
+++ b/reco/offline/steer/TaskFactory.h
@@ -0,0 +1,52 @@
+/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer] */
+
+/** @file TaskFactory.h
+ ** @author Volker Friese <v.friese@gsi.de>
+ ** @date 06.06.2023
+ **/
+
+#ifndef CBM_RECO_OFFLINE_STEER_TASKFACTORY_H
+#define CBM_RECO_OFFLINE_STEER_TASKFACTORY_H 1
+
+#include "Run.h"
+
+namespace cbm::reco::offline
+{
+
+  /** @class TaskFactory
+   ** @brief Factory class for the instantiation of CBM reconstruction tasks
+   ** @author Volker Friese <v.friese@gsi.de>
+   ** @since 6 June 2023
+   **/
+  class TaskFactory {
+  public:
+    /** @brief Constructor **/
+    TaskFactory(Run* steer = nullptr);
+
+    /** @brief Destructor **/
+    virtual ~TaskFactory() {};
+
+    void RegisterCaTracking();         /// CA track finding
+    void RegisterDigiEventBuilder();   /// Event building from digis
+    void RegisterGlobalTracking();     /// Global track finding
+    void RegisterMuchReco();           /// Local reconstruction for MUCH
+    void RegisterMvdReco();            /// Local reconstruction for MVD
+    void RegisterPvFinder();           /// Primary vertex finding
+    void RegisterRichHitFinder();      /// Hit finding in RICH
+    void RegisterRichReco();           /// Local reconstruction for RICH
+    void RegisterStsReco();            /// Local reconstruction for STS
+    void RegisterTofReco();            /// Local reconstruction for TOF
+    void RegisterTrackEventBuilder();  /// Event building from tracks
+    void RegisterTrdReco();            /// Local reconstruction for TRD
+    void RegisterTrdPid();             /// PID with TRD
+    void RegisterTzeroReco();          /// Reconstruction of T0
+
+  private:  //members
+    Run* fRun = nullptr;
+  };
+
+}  // namespace cbm::reco::offline
+
+#endif /* CBM_RECO_OFFLINE_STEER_TASKFACTORY_H */