diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 278712691127d34236a2cf9a6812309fa1cd9ac0..7b89a91772b63dfc731b7ba24d33536158d356f9 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -64,5 +64,8 @@ CORSIKA_REGISTER_EXAMPLE (corsika RUN_OPTIONS --energy=100 --zenith=10 --nevent=
 add_executable (mars mars.cpp)
 target_link_libraries (mars CORSIKA8::CORSIKA8)
 
+add_executable (water water.cpp)
+target_link_libraries (water CORSIKA8::CORSIKA8)
+
 add_executable (environment environment.cpp)
 target_link_libraries (environment CORSIKA8::CORSIKA8)
diff --git a/examples/water.cpp b/examples/water.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..73accbb53f691659e9c504674e5c4a603d707ddc
--- /dev/null
+++ b/examples/water.cpp
@@ -0,0 +1,297 @@
+/*
+ * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * This software is distributed under the terms of the GNU General Public
+ * Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+ * the license.
+ */
+
+/* clang-format off */
+// InteractionCounter used boost/histogram, which
+// fails if boost/type_traits have been included before. Thus, we have
+// to include it first...
+#include <corsika/framework/process/InteractionCounter.hpp>
+/* clang-format on */
+#include <corsika/framework/core/Cascade.hpp>
+#include <corsika/framework/process/ProcessSequence.hpp>
+#include <corsika/framework/process/SwitchProcessSequence.hpp>
+#include <corsika/framework/random/RNGManager.hpp>
+#include <corsika/media/Environment.hpp>
+#include <corsika/media/HomogeneousMedium.hpp>
+#include <corsika/media/IMediumModel.hpp>
+#include <corsika/media/MediumProperties.hpp>
+#include <corsika/media/MediumPropertyModel.hpp>
+#include <corsika/media/ShowerAxis.hpp>
+#include <corsika/modules/ObservationPlane.hpp>
+#include <corsika/modules/LongitudinalProfile.hpp>
+#include <corsika/modules/writers/SubWriter.hpp>
+#include <corsika/modules/writers/LongitudinalWriter.hpp>
+#include <corsika/modules/writers/EnergyLossWriter.hpp>
+#include <corsika/modules/PROPOSAL.hpp>
+#include <corsika/modules/ParticleCut.hpp>
+#include <corsika/modules/Pythia8.hpp>
+#include <corsika/modules/Random.hpp>
+#include <corsika/modules/Sibyll.hpp>
+#include <corsika/modules/UrQMD.hpp>
+#include <corsika/modules/tracking/TrackingStraight.hpp>
+
+#include <corsika/output/OutputManager.hpp>
+#include <corsika/stack/GeometryNodeStackExtension.hpp>
+#include <corsika/stack/WeightStackExtension.hpp>
+#include <corsika/stack/VectorStack.hpp>
+
+#include <corsika/setup/SetupStack.hpp>
+
+#include <CLI/App.hpp>
+#include <CLI/Config.hpp>
+#include <CLI/Formatter.hpp>
+
+using namespace corsika;
+
+using IMediumType = IMediumPropertyModel<IMediumModel>;
+using EnvType = Environment<IMediumType>;
+using StackActive = setup::Stack<EnvType>;
+using StackView = StackActive::stack_view_type;
+using Particle = StackActive::particle_type;
+
+void registerRandomStreams(int seed) {
+  RNGManager<>::getInstance().registerRandomStream("cascade");
+  RNGManager<>::getInstance().registerRandomStream("qgsjet");
+  RNGManager<>::getInstance().registerRandomStream("sibyll");
+  RNGManager<>::getInstance().registerRandomStream("epos");
+  RNGManager<>::getInstance().registerRandomStream("pythia");
+  RNGManager<>::getInstance().registerRandomStream("urqmd");
+  RNGManager<>::getInstance().registerRandomStream("proposal");
+  if (seed == 0) {
+    std::random_device rd;
+    seed = rd();
+    CORSIKA_LOG_INFO("random seed (auto) {} ", seed);
+  } else {
+    CORSIKA_LOG_INFO("random seed {} ", seed);
+  }
+  RNGManager<>::getInstance().setSeed(seed);
+}
+
+int main(int argc, char** argv) {
+  // * process input
+  Code primaryType;
+  HEPEnergyType e0, eCut;
+  int A, Z, n_event;
+  int randomSeed;
+  std::string output_dir;
+  CLI::App app{"Neutrino event generator"};
+  // we start by definining a sub-group for the primary ID
+  auto opt_Z = app.add_option("-Z", Z, "Atomic number for primary")
+                   ->check(CLI::Range(0, 26))
+                   ->group("Primary");
+  auto opt_A = app.add_option("-A", A, "Atomic mass number for primary")
+                   ->needs(opt_Z)
+                   ->check(CLI::Range(1, 58))
+                   ->group("Primary");
+  app.add_option("-p,--pdg", "PDG code for primary.")
+      ->excludes(opt_A)
+      ->excludes(opt_Z)
+      ->group("Primary");
+  app.add_option("-E,--energy", "Primary energy in GeV")
+      ->required()
+      ->check(CLI::PositiveNumber)
+      ->group("Primary");
+  app.add_option("--eCut", "Cut energy in GeV")->default_val(1.);
+  app.add_option("-N,--nevent", n_event, "The number of events/showers to run.")
+      ->default_val(1)
+      ->check(CLI::PositiveNumber);
+  app.add_option("-f,--filename", output_dir, "Filename for output library")
+      ->check(CLI::NonexistentPath)
+      ->default_val("output");
+  app.add_option("-v", "Verbosity level: warn, info, debug, trace.")
+      ->default_val("info")
+      ->check(CLI::IsMember({"warn", "info", "debug", "trace"}));
+  app.add_option("-s", randomSeed, "Seed for random number")
+      ->check(CLI::NonNegativeNumber)
+      ->default_val(0);
+  CLI11_PARSE(app, argc, argv);
+
+  // check that we got either PDG or A/Z
+  // this can be done with option_groups but the ordering
+  // gets all messed up
+  if (app.count("--pdg") == 0) {
+    if ((app.count("-A") == 0) || (app.count("-Z") == 0)) {
+      CORSIKA_LOG_ERROR("If --pdg is not provided, then both -A and -Z are required.");
+      return 1;
+    }
+  }
+  // check if we want to use a PDG code instead
+  if (app.count("--pdg") > 0) {
+    primaryType = convert_from_PDG(PDGCode(app["--pdg"]->as<int>()));
+  } else {
+    // check manually for proton and neutrons
+    if ((A == 1) && (Z == 1))
+      primaryType = Code::Proton;
+    else if ((A == 1) && (Z == 0))
+      primaryType = Code::Neutron;
+    else
+      primaryType = get_nucleus_code(A, Z);
+  }
+
+  e0 = app["-E"]->as<double>() * 1_GeV;
+  eCut = app["--eCut"]->as<double>() * 1_GeV;
+  std::string_view const loglevel = app["-v"]->as<std::string_view>();
+  if (loglevel == "warn") {
+    logging::set_level(logging::level::warn);
+  } else if (loglevel == "info") {
+    logging::set_level(logging::level::info);
+  } else if (loglevel == "debug") {
+    logging::set_level(logging::level::debug);
+  } else if (loglevel == "trace") {
+#ifndef _C8_DEBUG_
+    CORSIKA_LOG_ERROR("trace log level requires a Debug build.");
+    return 1;
+#endif
+    logging::set_level(logging::level::trace);
+  }
+
+  registerRandomStreams(randomSeed);
+
+  // * environment and universe
+  EnvType env;
+  auto& universe = env.getUniverse();
+  auto const& rootCS = env.getCoordinateSystem();
+
+  // * Water geometry
+  {
+    Point const center{rootCS, 0_m, 0_m, 0_m};
+    auto sphere = std::make_unique<Sphere>(center, 100_m);
+    auto node = std::make_unique<VolumeTreeNode<IMediumType>>(std::move(sphere));
+    // Hydrogen is not supported by UrQMD yet. See #456
+    auto comp = NuclearComposition({{Code::Oxygen}, {1.0}});
+    // density of sea water
+    auto density = 1.02_g / (1_cm * 1_cm * 1_cm);
+    auto water_medium =
+        std::make_shared<MediumPropertyModel<HomogeneousMedium<IMediumType>>>(
+            Medium::WaterLiquid, density, comp);
+    node->setModelProperties(water_medium);
+    universe->addChild(std::move(node));
+  }
+
+  // * detector geometry
+  auto injectorLength = 50_m;
+  Point const injectorPos = Point(rootCS, {0_m, 0_m, injectorLength});
+  auto const& injectCS = make_translation(rootCS, injectorPos.getCoordinates());
+  DirectionVector upVec(rootCS, {0., 0., 1.});
+  DirectionVector leftVec(rootCS, {1., 0., 0.});
+  DirectionVector downVec(rootCS, {0., 0., -1.});
+
+  // * observation plane
+  std::vector<ObservationPlane<tracking_line::Tracking>> obsPlanes;
+  const int nPlane = 5;
+  for (int i = 0; i < nPlane - 1; i++) {
+    Point planeCenter{injectCS, {0_m, 0_m, -(i + 1) * 3_m}};
+    obsPlanes.push_back({Plane(planeCenter, upVec), leftVec, false});
+  }
+  auto& obsPlaneFinal = obsPlanes.emplace_back(
+      Plane{Point{injectCS, {0_m, 0_m, -50_m}}, upVec}, leftVec, true);
+
+  // * longitutional profile
+  ShowerAxis const showerAxis{injectorPos, 1.2 * injectorLength * downVec, env};
+  LongitudinalWriter longiWriter{showerAxis, 5500, 1_g / square(1_cm)};
+  LongitudinalProfile<SubWriter<decltype(longiWriter)>> longprof{longiWriter};
+
+  // * energy loss profile
+  EnergyLossWriter dEdX{showerAxis, 1_g / square(1_cm), 5500};
+
+  // * physical process list
+  // particle production threshold
+  HEPEnergyType const emCut = eCut;
+  HEPEnergyType const hadCut = eCut;
+  ParticleCut<SubWriter<decltype(dEdX)>> cut(emCut, emCut, hadCut, hadCut, true, dEdX);
+
+  // hadronic interactions
+  HEPEnergyType heHadronModelThreshold = 63.1_GeV;
+  corsika::sibyll::Interaction sibyll(env);
+  corsika::urqmd::UrQMD urqmd;
+  InteractionCounter urqmdCounted(urqmd);
+  struct EnergySwitch {
+    HEPEnergyType cutE_;
+    EnergySwitch(HEPEnergyType cutE)
+        : cutE_(cutE) {}
+    bool operator()(const Particle& p) const { return (p.getKineticEnergy() < cutE_); }
+  };
+  // auto lowModel = make_sequence(urqmd);
+  auto hadronSequence =
+      make_select(EnergySwitch(heHadronModelThreshold), urqmdCounted, sibyll);
+
+  // decay process
+  corsika::pythia8::Decay decayPythia;
+  corsika::sibyll::Decay decaySibyll{{
+      Code::N1440Plus,
+      Code::N1440MinusBar,
+      Code::N1440_0,
+      Code::N1440_0Bar,
+      Code::N1710Plus,
+      Code::N1710MinusBar,
+      Code::N1710_0,
+      Code::N1710_0Bar,
+      Code::Pi1300Plus,
+      Code::Pi1300Minus,
+      Code::Pi1300_0,
+      Code::KStar0_1430_0,
+      Code::KStar0_1430_0Bar,
+      Code::KStar0_1430_Plus,
+      Code::KStar0_1430_MinusBar,
+  }};
+  auto decaySequence = make_sequence(decayPythia, decaySibyll);
+
+  // EM process
+  corsika::proposal::Interaction emCascade(env, sibyll.getHadronInteractionModel(),
+                                           heHadronModelThreshold);
+  corsika::proposal::ContinuousProcess<SubWriter<decltype(dEdX)>> emContinuous(env, dEdX);
+
+  // total physics list
+  auto physics_sequence =
+      make_sequence(emCascade, emContinuous, hadronSequence, decaySequence);
+
+  // * output module
+  OutputManager output(output_dir);
+  for (int i = 0; i < nPlane; i++) {
+    output.add(fmt::format("particles_{:}", i), obsPlanes[i]);
+  }
+  // hard coded
+  auto obsPlaneSequence =
+      make_sequence(obsPlanes[0], obsPlanes[1], obsPlanes[2], obsPlanes[3], obsPlanes[4]);
+  output.add("longi_profile", longiWriter);
+  output.add("energy_loss", dEdX);
+
+  // * the final process sequence
+  auto sequence = make_sequence(physics_sequence, cut, obsPlaneSequence, longprof);
+
+  // * tracking and stack
+  tracking_line::Tracking tracking;
+  StackActive stack;
+
+  // * cascade manager
+  Cascade EAS(env, tracking, sequence, output, stack);
+
+  // * main loop
+  output.startOfLibrary();
+  for (int i_shower = 0; i_shower < n_event; i_shower++) {
+    stack.clear();
+    CORSIKA_LOG_INFO("Event: {} / {}", i_shower, n_event);
+
+    // * inject primary
+    auto primary = stack.addParticle(std::make_tuple(
+        primaryType, e0 - get_mass(primaryType), downVec, injectorPos, 0_ns));
+
+    EAS.run();
+
+    // * report energy loss result
+    HEPEnergyType const Efinal = dEdX.getEnergyLost() + obsPlaneFinal.getEnergyGround();
+    CORSIKA_LOG_INFO(
+        "total energy budget (TeV): {:.2f} (dEdX={:.2f} ground={:.2f}), "
+        "relative difference (%): {:.3f}",
+        e0 / 1_TeV, dEdX.getEnergyLost() / 1_TeV, obsPlaneFinal.getEnergyGround() / 1_TeV,
+        (Efinal / e0 - 1.) * 100.);
+  }
+  output.endOfLibrary();
+
+  return 0;
+}