diff --git a/DDG4/edm4hep/Geant4Output2EDM4hep.cpp b/DDG4/edm4hep/Geant4Output2EDM4hep.cpp
index 8d80de675ac5386f6a4e9c813ac1ffac50759104..f043add6133e44b2e2fc5ffddd8ba49bcc21a7ff 100644
--- a/DDG4/edm4hep/Geant4Output2EDM4hep.cpp
+++ b/DDG4/edm4hep/Geant4Output2EDM4hep.cpp
@@ -60,6 +60,7 @@ namespace dd4hep {
       stringmap_t                   m_eventParametersInt;
       stringmap_t                   m_eventParametersFloat;
       stringmap_t                   m_eventParametersString;
+      stringmap_t                   m_cellIDEncodingStrings{};
       std::string                   m_section_name      { "events" };
       int                           m_runNo             { 0 };
       int                           m_runNumberOffset   { 0 };
@@ -67,10 +68,10 @@ namespace dd4hep {
       int                           m_eventNumberOffset { 0 };
       bool                          m_filesByRun        { false };
       
-      /// create the podio collections for the particles and hits
-      void createCollections(OutputContext<G4Event>& ctxt) ;
       /// Data conversion interface for MC particles to EDM4hep format
       void saveParticles(Geant4ParticleMap* particles);
+      /// Store the metadata frame with e.g. the cellID encoding strings
+      void saveFileMetaData();
     public:
       /// Standard constructor
       Geant4Output2EDM4hep(Geant4Context* ctxt, const std::string& nam);
@@ -223,12 +224,22 @@ void Geant4Output2EDM4hep::beginRun(const G4Run* run)  {
 /// Callback to store the Geant4 run information
 void Geant4Output2EDM4hep::endRun(const G4Run* run)  {
   saveRun(run);
+  saveFileMetaData();
   if ( m_file )   {
     m_file->finish();
     m_file.reset();
   }
 }
 
+void Geant4Output2EDM4hep::saveFileMetaData() {
+  podio::Frame metaFrame{};
+  for (const auto& [name, encodingStr] : m_cellIDEncodingStrings) {
+    metaFrame.putParameter(name + "__CellIDEncoding", encodingStr);
+  }
+
+  m_file->writeFrame(metaFrame, "metadata");
+}
+
 /// Commit data at end of filling procedure
 void Geant4Output2EDM4hep::commit( OutputContext<G4Event>& /* ctxt */)   {
   if ( m_file )   {
@@ -428,6 +439,25 @@ void Geant4Output2EDM4hep::saveEvent(OutputContext<G4Event>& ctxt)  {
   }
 }
 
+/**
+ * Helper struct that can be used together with map::try_emplace to construct
+ * the encoding only once per collection (name).
+ */
+struct LazyEncodingExtraction {
+  /// Constructor that does effectively nothing. This will be called in every
+  /// try_emplace call
+  LazyEncodingExtraction(Geant4HitCollection* coll) : m_coll(coll) {}
+  /// Defer the real work to the implicit conversion to std::string that will
+  /// only be called if the value is actually emplaced into the map
+  operator std::string() const {
+    const auto* sd = m_coll->sensitive();
+    return dd4hep::sim::Geant4ConversionHelper::encoding(sd->sensitiveDetector());
+  }
+private:
+  Geant4HitCollection* m_coll{nullptr};
+};
+
+
 /// Callback to store each Geant4 hit collection
 void Geant4Output2EDM4hep::saveCollection(OutputContext<G4Event>& /*ctxt*/, G4VHitsCollection* collection)  {
   Geant4HitCollection* coll = dynamic_cast<Geant4HitCollection*>(collection);
@@ -439,6 +469,10 @@ void Geant4Output2EDM4hep::saveCollection(OutputContext<G4Event>& /*ctxt*/, G4VH
   size_t nhits = collection->GetSize();
   Geant4ParticleMap* pm = context()->event().extension<Geant4ParticleMap>(false);
   debug("+++ Saving EDM4hep collection %s with %d entries.", colName.c_str(), int(nhits));
+
+  // Using try_emplace here to only fill this the first time we come across
+  m_cellIDEncodingStrings.try_emplace(colName, LazyEncodingExtraction{coll});
+
   //-------------------------------------------------------------------
   if( typeid( Geant4Tracker::Hit ) == coll->type().type()  ){
     // Create the hit container even if there are no entries!