diff --git a/external/.gitignore b/external/.gitignore
index 95ce981061e9e672bcb76b01d85e66818d21b1b1..a0c5783cbd6a3c95168e0762783c305c6564d094 100644
--- a/external/.gitignore
+++ b/external/.gitignore
@@ -8,4 +8,4 @@ flib_dpb/flib_dpb
 ipc/ipc
 jsroot
 googletest
-
+yaml-cpp/
diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt
index 1768203c4db82c927a0e7e470775a2c2d2bb2b20..298f0fc5bd86fe3cf916c19a0f646b61aef21b11 100644
--- a/external/CMakeLists.txt
+++ b/external/CMakeLists.txt
@@ -43,6 +43,8 @@ if(DOWNLOAD_EXTERNALS)
   Include(InstallParameter.cmake)
   Include(InstallInput.cmake)
   Include(InstallGeometry.cmake)
+
+  Include(InstallYamlCpp.cmake)
 else()
   # Define targets which are needed by CbmRoot but are not available
   # whithout the external packages
diff --git a/external/InstallYamlCpp.cmake b/external/InstallYamlCpp.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..6c8ff0cda742785c0c338ff05f16422c15c53128
--- /dev/null
+++ b/external/InstallYamlCpp.cmake
@@ -0,0 +1,44 @@
+set(YAMLCPP_VERSION 0579ae3d976091d7d664aa9d2527e0d0cff25763) # version 0.7.0
+
+set(YAMLCPP_SRC_URL "https://github.com/jbeder/yaml-cpp")
+set(YAMLCPP_DESTDIR "${CMAKE_BINARY_DIR}/external/YAMLCPP-prefix")
+
+#set(YAMLCPP_BYPRODUCT "${PROJECT_BINARY_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}yaml-cpp${CMAKE_SHARED_LIBRARY_SUFFIX}")
+
+download_project_if_needed(PROJECT  yaml-cpp
+        GIT_REPOSITORY  ${YAMLCPP_SRC_URL}
+        GIT_TAG         ${YAMLCPP_VERSION}
+        SOURCE_DIR      ${CMAKE_CURRENT_SOURCE_DIR}/yaml-cpp
+        TEST_FILE       CMakeLists.txt
+        )
+
+If(ProjectUpdated)
+    File(REMOVE_RECURSE ${YAMLCPP_DESTDIR})
+    Message("yaml-cpp source directory was changed so build directory was deleted")
+EndIf()
+
+ExternalProject_Add(
+  yaml-cpp
+  SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/yaml-cpp
+  GIT_CONFIG advice.detachedHead=false
+  BUILD_IN_SOURCE 0
+  LOG_DOWNLOAD 1 LOG_CONFIGURE 1 LOG_BUILD 1 LOG_INSTALL 1
+  CMAKE_ARGS -DYAML_CPP_BUILD_CONTRIB=OFF
+             -DYAML_CPP_BUILD_TOOLS=OFF
+             -DYAML_CPP_BUILD_TESTS=OFF
+             -DYAML_BUILD_SHARED_LIBS=OFF
+             -DCMAKE_POSITION_INDEPENDENT_CODE=ON
+  BUILD_COMMAND ${CMAKE_COMMAND} --build . --target yaml-cpp --parallel 1
+  BUILD_BYPRODUCTS ${PROJECT_BINARY_DIR}/external/yaml-cpp-prefix/src/yaml-cpp-build/${CMAKE_STATIC_LIBRARY_PREFIX}yaml-cpp${CMAKE_STATIC_LIBRARY_SUFFIX}
+  INSTALL_COMMAND ""
+)
+
+# pre-create empty directory to make INTERFACE_INCLUDE_DIRECTORIES happy
+file(MAKE_DIRECTORY ${CMAKE_SOURCE_DIR}/external/yaml-cpp/include)
+
+add_library(external::yaml-cpp STATIC IMPORTED GLOBAL)
+add_dependencies(external::yaml-cpp yaml-cpp)
+set_target_properties(external::yaml-cpp PROPERTIES
+  IMPORTED_LOCATION ${PROJECT_BINARY_DIR}/external/yaml-cpp-prefix/src/yaml-cpp-build/${CMAKE_STATIC_LIBRARY_PREFIX}yaml-cpp${CMAKE_STATIC_LIBRARY_SUFFIX}
+  INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/external/yaml-cpp/include
+)
diff --git a/reco/CMakeLists.txt b/reco/CMakeLists.txt
index 51754b710c8ea438c15f8875a585bc0524b710fe..2df13eab8925ed3d772dee185cbf1e2f9e200b3b 100644
--- a/reco/CMakeLists.txt
+++ b/reco/CMakeLists.txt
@@ -14,4 +14,5 @@ add_subdirectory(tracking)
 add_subdirectory(qa)
 if (${CMAKE_CXX_STANDARD} EQUAL 17)
   add_subdirectory (tasks)
+  add_subdirectory (app)
 endif()
diff --git a/reco/app/CMakeLists.txt b/reco/app/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c81df5543f7b511317bfe6bf35bb78ebe481e215
--- /dev/null
+++ b/reco/app/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(cbmreco_fairrun)
diff --git a/reco/app/cbmreco_fairrun/Application.cxx b/reco/app/cbmreco_fairrun/Application.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..4c60fc4090fec9d4b4107e918ac4d515e0800870
--- /dev/null
+++ b/reco/app/cbmreco_fairrun/Application.cxx
@@ -0,0 +1,16 @@
+/* Copyright (C) 2022 Johann Wolfgang Goethe-Universitaet Frankfurt, Frankfurt am Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Jan de Cuveland [committer] */
+
+#include "Application.h"
+
+Application::Application(ProgramOptions const& opt) : fOpt(opt)
+{
+  CbmRecoConfig config;
+  config.LoadYaml(fOpt.ConfigYamlFile());
+  if (!fOpt.SaveConfigYamlFile().empty()) { config.SaveYaml(fOpt.SaveConfigYamlFile()); }
+
+  fCbmReco = std::make_unique<CbmReco>(fOpt.InputUri(), fOpt.OutputRootFile(), fOpt.MaxNumTs(), config);
+}
+
+void Application::Run() { fCbmReco->Run(); }
diff --git a/reco/app/cbmreco_fairrun/Application.h b/reco/app/cbmreco_fairrun/Application.h
new file mode 100644
index 0000000000000000000000000000000000000000..9f877b9166f3af1fa4dd30676e0b938b25d13781
--- /dev/null
+++ b/reco/app/cbmreco_fairrun/Application.h
@@ -0,0 +1,43 @@
+/* Copyright (C) 2022 Johann Wolfgang Goethe-Universitaet Frankfurt, Frankfurt am Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Jan de Cuveland [committer] */
+
+#ifndef APP_CBMRECO_APPLICATION_H
+#define APP_CBMRECO_APPLICATION_H
+
+#include "CbmReco.h"
+
+#include <memory>
+
+#include "ProgramOptions.h"
+#include "log.hpp"
+
+/** @class Application
+ ** @brief Main class of the "cbmreco_fairrun" application
+ ** @author Jan de Cuveland <cuveland@compeng.uni-frankfurt.de>
+ ** @since 16 March 2022
+ **
+ ** This class implements a stand-alone command-line application.
+ ** It instantiatates and configures a CbmReco object, which executes
+ ** the CBM reconstruction steps using FairTasks and FairRunOnline.
+ **/
+class Application {
+public:
+  /** @brief Standard constructor, initialize the application **/
+  explicit Application(ProgramOptions const& opt);
+
+  /** @brief Copy constructor forbidden **/
+  Application(const Application&) = delete;
+
+  /** @brief Assignment operator forbidden **/
+  void operator=(const Application&) = delete;
+
+  /** @brief Run the application **/
+  void Run();
+
+private:
+  ProgramOptions const& fOpt;         ///< Program options object
+  std::unique_ptr<CbmReco> fCbmReco;  ///< CBM reconstruction steering class instance
+};
+
+#endif
diff --git a/reco/app/cbmreco_fairrun/CMakeLists.txt b/reco/app/cbmreco_fairrun/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7d2068e4d5415f207dbd4f8fdae2232d279fb578
--- /dev/null
+++ b/reco/app/cbmreco_fairrun/CMakeLists.txt
@@ -0,0 +1,29 @@
+# Copyright (C) 2022 Johann Wolfgang Goethe-Universitaet Frankfurt, Frankfurt am Main
+# SPDX-License-Identifier: GPL-3.0-only
+# Authors: Jan de Cuveland [committer]
+
+file(GLOB APP_SOURCES *.cxx)
+file(GLOB APP_HEADERS *.h)
+
+add_executable(cbmreco_fairrun ${APP_SOURCES} ${APP_HEADERS})
+
+target_include_directories(cbmreco_fairrun SYSTEM PUBLIC ${Boost_INCLUDE_DIRS})
+
+# This will no longer be necessary when the CbmRecoTasks library is set up to include this property
+target_include_directories(cbmreco_fairrun
+  PUBLIC ${CMAKE_SOURCE_DIR}/reco/tasks
+)
+
+# This will no longer be necessary when the Fairroot libraries are set up to include this property
+target_link_directories(cbmreco_fairrun
+  PUBLIC ${FAIRROOT_LIBRARY_DIR}
+)
+
+target_link_libraries(cbmreco_fairrun
+  fles_logging
+  CbmRecoTasks
+  Core
+  ${Boost_LIBRARIES}
+)
+
+install(TARGETS cbmreco_fairrun DESTINATION bin)
diff --git a/reco/app/cbmreco_fairrun/ProgramOptions.cxx b/reco/app/cbmreco_fairrun/ProgramOptions.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..ce629965462193485fa31f3e6923f9a54289c1af
--- /dev/null
+++ b/reco/app/cbmreco_fairrun/ProgramOptions.cxx
@@ -0,0 +1,97 @@
+/* Copyright (C) 2022 Johann Wolfgang Goethe-Universitaet Frankfurt, Frankfurt am Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Jan de Cuveland [committer] */
+
+#include "ProgramOptions.h"
+
+#include <boost/program_options.hpp>
+
+#include <fstream>
+#include <iostream>
+
+#include "log.hpp"
+
+namespace po = boost::program_options;
+
+void ProgramOptions::ParseOptions(int argc, char* argv[])
+{
+  unsigned log_level  = 2;
+  unsigned log_syslog = 2;
+  std::string log_file;
+  std::string options_file;
+
+  // --- Define generic options
+  po::options_description generic("Generic options");
+  auto generic_add = generic.add_options();
+  generic_add("options-file,f", po::value<std::string>(&options_file)->value_name("<filename>"),
+              "read program options from file");
+  generic_add("log-level,l", po::value<unsigned>(&log_level)->default_value(log_level)->value_name("<n>"),
+              "set the file log level (all:0)");
+  generic_add("log-file,L", po::value<std::string>(&log_file)->value_name("<filename>"), "write log output to file");
+  generic_add("log-syslog,S", po::value<unsigned>(&log_syslog)->implicit_value(log_syslog)->value_name("<n>"),
+              "enable logging to syslog at given log level");
+  generic_add("help,h", "display this help and exit");
+  generic_add("version,V", "output version information and exit");
+
+  // --- Define configuration options
+  po::options_description config("Configuration");
+  auto config_add = config.add_options();
+  config_add(
+    "input,i",
+    po::value<std::vector<std::string>>()->multitoken()->value_name("scheme://host/path?param=value ... | <filename>"),
+    "uri of a timeslice source");
+  config_add("output-root,o",
+             po::value<std::string>(&fOutputRootFile)->default_value(fOutputRootFile)->value_name("<filename>"),
+             "name of an output root file to write");
+  config_add("config,c", po::value<std::string>(&fConfigYamlFile)->value_name("<filename>"),
+             "name of a yaml config file containing the reconstruction chain configuration");
+  config_add("save-config", po::value<std::string>(&fSaveConfigYamlFile)->value_name("<filename>"),
+             "save configuration to yaml file (mostly for debugging)");
+  config_add("max-timeslice-number,n", po::value<int32_t>(&fMaxNumTs)->value_name("<n>"),
+             "quit after processing given number of timeslices (default: unlimited)");
+
+  po::options_description cmdline_options("Allowed options");
+  cmdline_options.add(generic).add(config);
+
+  po::variables_map vm;
+  po::store(po::parse_command_line(argc, argv, cmdline_options), vm);
+  po::notify(vm);
+
+  // --- Read program options from file if requested
+  if (!options_file.empty()) {
+    std::ifstream ifs(options_file.c_str());
+    if (!ifs) { throw ProgramOptionsException("cannot open options file: " + options_file); }
+    po::store(po::parse_config_file(ifs, config), vm);
+    notify(vm);
+  }
+
+  if (vm.count("help") != 0u) {
+    std::cout << cmdline_options << std::endl;
+    exit(EXIT_SUCCESS);
+  }
+
+  if (vm.count("version") != 0u) {
+    std::cout << "cbmreco, version (unspecified)" << std::endl;
+    exit(EXIT_SUCCESS);
+  }
+
+  // --- Set up logging
+  logging::add_console(static_cast<severity_level>(log_level));
+  if (vm.count("log-file") != 0u) {
+    L_(info) << "Logging output to " << log_file;
+    logging::add_file(log_file, static_cast<severity_level>(log_level));
+  }
+  if (vm.count("log-syslog") != 0u) {
+    logging::add_syslog(logging::syslog::local0, static_cast<severity_level>(log_syslog));
+  }
+
+  if (vm.count("input") == 0u) { throw ProgramOptionsException("no input source specified"); }
+  fInputUri = vm["input"].as<std::vector<std::string>>();
+
+  if (vm.count("config") == 0u) { throw ProgramOptionsException("no configuration file specified"); }
+
+  L_(debug) << "input sources (" << fInputUri.size() << "):";
+  for (auto& inputUri : fInputUri) {
+    L_(debug) << "  " << inputUri;
+  }
+}
diff --git a/reco/app/cbmreco_fairrun/ProgramOptions.h b/reco/app/cbmreco_fairrun/ProgramOptions.h
new file mode 100644
index 0000000000000000000000000000000000000000..d57563303cee74f8a10709ded325d5801baf655b
--- /dev/null
+++ b/reco/app/cbmreco_fairrun/ProgramOptions.h
@@ -0,0 +1,65 @@
+/* Copyright (C) 2022 Johann Wolfgang Goethe-Universitaet Frankfurt, Frankfurt am Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Jan de Cuveland [committer] */
+
+#ifndef APP_CBMRECO_PROGRAMOPTIONS_H
+#define APP_CBMRECO_PROGRAMOPTIONS_H
+
+#include <cstdint>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+/** @class ProgramOptionsException
+ ** @brief Program options exception class
+ ** @author Jan de Cuveland <cuveland@compeng.uni-frankfurt.de>
+ ** @since 16 March 2022
+ **/
+class ProgramOptionsException : public std::runtime_error {
+public:
+  explicit ProgramOptionsException(const std::string& what_arg = "") : std::runtime_error(what_arg) {}
+};
+
+/** @class ProgramOptions
+ ** @brief Program options class for the "cbmreco_fairrun" application
+ ** @author Jan de Cuveland <cuveland@compeng.uni-frankfurt.de>
+ ** @since 16 March 2022
+ **/
+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 **/
+  void operator=(const ProgramOptions&) = delete;
+
+  /** @brief Get URI specifying input timeslice stream source(s) **/
+  [[nodiscard]] const std::vector<std::string>& InputUri() const { return fInputUri; }
+
+  /** @brief Get output file name (.root format) **/
+  [[nodiscard]] const std::string& OutputRootFile() const { return fOutputRootFile; }
+
+  /** @brief Get configuration file name (.yaml format) **/
+  [[nodiscard]] const std::string& ConfigYamlFile() const { return fConfigYamlFile; }
+
+  /** @brief Get save configuration file name (.yaml format) **/
+  [[nodiscard]] const std::string& SaveConfigYamlFile() const { return fSaveConfigYamlFile; }
+
+  /** @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[]);
+
+  std::vector<std::string> fInputUri;         ///< URI(s) specifying input timeslice stream source(s)
+  std::string fOutputRootFile = "/dev/null";  ///< Output file name (.root format)
+  std::string fConfigYamlFile;                ///< Configuration file name (.yaml format)
+  std::string fSaveConfigYamlFile;            ///< Save configuration file name (.yaml format)
+  int32_t fMaxNumTs = INT32_MAX;              ///< Maximum number of timeslices to process
+};
+
+#endif /* APP_CBMRECO_PROGRAMOPTIONS_H */
diff --git a/reco/app/cbmreco_fairrun/main.cxx b/reco/app/cbmreco_fairrun/main.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..461e8b6d1c1e40f4aef153c1229a8a67210e62f4
--- /dev/null
+++ b/reco/app/cbmreco_fairrun/main.cxx
@@ -0,0 +1,23 @@
+/* Copyright (C) 2022 Johann Wolfgang Goethe-Universitaet Frankfurt, Frankfurt am Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Jan de Cuveland [committer] */
+
+#include "Application.h"
+#include "ProgramOptions.h"
+#include "log.hpp"
+
+int main(int argc, char* argv[])
+{
+  try {
+    ProgramOptions opt(argc, argv);
+    Application app(opt);
+    app.Run();
+  }
+  catch (std::exception const& e) {
+    L_(fatal) << e.what();
+    return EXIT_FAILURE;
+  }
+
+  L_(info) << "exiting";
+  return EXIT_SUCCESS;
+}
diff --git a/reco/tasks/CMakeLists.txt b/reco/tasks/CMakeLists.txt
index 7d98de2467e7d82c137bfb90a86f20ceb3bf3e22..0bca7c915bf925af763a7d375b1be84c2e365d60 100644
--- a/reco/tasks/CMakeLists.txt
+++ b/reco/tasks/CMakeLists.txt
@@ -68,6 +68,7 @@ Base
 CbmBase
 CbmData
 Algo
+external::yaml-cpp
 )
 # ---------------------------------------------------------
 
diff --git a/reco/tasks/CbmReco.cxx b/reco/tasks/CbmReco.cxx
index 373191c2b9e1233e406d4ddb8c95e7667775be81..ec47da729f594aa3a1e9759b64954cde52bbd7a6 100644
--- a/reco/tasks/CbmReco.cxx
+++ b/reco/tasks/CbmReco.cxx
@@ -22,12 +22,68 @@
 #include <memory>
 #include <string>
 
+#include <yaml-cpp/yaml.h>
+
 using std::cout;
 using std::endl;
 using std::make_unique;
 using std::string;
 
 
+// -----   Load configuration from YAML file   --------------------------------
+void CbmRecoConfig::LoadYaml(const std::string& filename)
+{
+  YAML::Node config = YAML::LoadFile(filename);
+  // --- Digi trigger
+  fTriggerDet       = ToCbmModuleIdCaseInsensitive(config["trigger"]["detector"].as<std::string>());
+  fTriggerWin       = config["trigger"]["window"].as<double>();
+  fTriggerThreshold = config["trigger"]["threshold"].as<size_t>();
+  fTriggerDeadTime  = config["trigger"]["deadtime"].as<double>();
+  // --- Event builder: (detector -> (tMin, tMax))
+  if (auto eventbuilder = config["eventbuilder"]) {
+    if (auto windows = eventbuilder["windows"]) {
+      for (YAML::const_iterator it = windows.begin(); it != windows.end(); ++it) {
+        auto det              = ToCbmModuleIdCaseInsensitive(it->first.as<std::string>());
+        auto lower            = it->second[0].as<double>();
+        auto upper            = it->second[1].as<double>();
+        fEvtbuildWindows[det] = std::make_pair(lower, upper);
+      }
+    }
+  }
+  // --- Branch persistence in output file
+  fStoreTimeslice = config["store"]["timeslice"].as<bool>();
+  fStoreTrigger   = config["store"]["triggers"].as<bool>();
+  fStoreEvents    = config["store"]["events"].as<bool>();
+}
+// ----------------------------------------------------------------------------
+
+
+// -----   Save configuration to YAML file   ----------------------------------
+void CbmRecoConfig::SaveYaml(const std::string& filename)
+{
+  YAML::Node config;
+  // --- Digi trigger
+  config["trigger"]["detector"]  = ToString(fTriggerDet);
+  config["trigger"]["window"]    = fTriggerWin;
+  config["trigger"]["threshold"] = fTriggerThreshold;
+  config["trigger"]["deadtime"]  = fTriggerDeadTime;
+  // --- Event builder: (detector -> (tMin, tMax))
+  for (const auto& [key, value] : fEvtbuildWindows) {
+    auto det = ToString(key);
+    config["eventbuilder"]["windows"][det].push_back(value.first);
+    config["eventbuilder"]["windows"][det].push_back(value.second);
+  };
+  // --- Branch persistence in output file
+  config["store"]["timeslice"] = fStoreTimeslice;
+  config["store"]["triggers"]  = fStoreTrigger;
+  config["store"]["events"]    = fStoreEvents;
+  // ---
+  std::ofstream fout(filename);
+  fout << config;
+}
+// ----------------------------------------------------------------------------
+
+
 // -----   Constructor from single source   -----------------------------------
 CbmReco::CbmReco(string source, TString outFile, int32_t numTs, const CbmRecoConfig& config, uint16_t port)
   : fSourceNames {source}
diff --git a/reco/tasks/CbmReco.h b/reco/tasks/CbmReco.h
index 28aaf291efaecd9cab7d6fef7b65fbebbe922d9e..61992fc1a7da684d188250f9d56decad538d979c 100644
--- a/reco/tasks/CbmReco.h
+++ b/reco/tasks/CbmReco.h
@@ -32,6 +32,9 @@ public:
   bool fStoreTimeslice = false;
   bool fStoreTrigger   = false;
   bool fStoreEvents    = false;
+  // --- Load/save using yaml-cpp
+  void LoadYaml(const std::string& filename);
+  void SaveYaml(const std::string& filename);
   // --- Destructor
   virtual ~CbmRecoConfig() {};
 
diff --git a/reco/tasks/CbmRecoConfigExample.yaml b/reco/tasks/CbmRecoConfigExample.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..69bd607e3bd7811a52a98b0cad9abd4bae93e6c6
--- /dev/null
+++ b/reco/tasks/CbmRecoConfigExample.yaml
@@ -0,0 +1,12 @@
+trigger:
+  detector: STS
+  window: 10
+  threshold: 100
+  deadtime: 50
+eventbuilder:
+  windows:
+    STS: [-20, 30]
+store:
+  timeslice: false
+  triggers: false
+  events: true