diff --git a/Cargo.lock b/Cargo.lock
index 165fb01a8235f24bb474ca4b6bab1545f27e26bf..33c33d2241d5966243e085c507a73ca7ae5123d2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1227,7 +1227,7 @@ dependencies = [
  "bitflags 2.9.0",
  "cexpr",
  "clang-sys",
- "itertools 0.12.1",
+ "itertools 0.11.0",
  "lazy_static",
  "lazycell",
  "log",
@@ -4368,15 +4368,6 @@ dependencies = [
  "either",
 ]
 
-[[package]]
-name = "itertools"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
-
 [[package]]
 name = "itertools"
 version = "0.14.0"
@@ -6071,9 +6062,9 @@ dependencies = [
 
 [[package]]
 name = "opentalk-types-api-v1"
-version = "0.33.0"
+version = "0.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8565d68e6994ab6d0d6b5bdbc37d94c4edff34900215309c88d0fe3e248a5924"
+checksum = "a7ffb9ca739d7b748de0e188c7a44f5fed08ecdd2d7063ac383e2c92eb2ba527"
 dependencies = [
  "actix-web",
  "base64 0.22.1",
@@ -6094,9 +6085,9 @@ dependencies = [
 
 [[package]]
 name = "opentalk-types-common"
-version = "0.32.0"
+version = "0.32.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc7b6f5082ce4410a8a4b483a33ac523863a974344be8ffeeb339e37624a9fd"
+checksum = "13523221c1cf75be9cf89b5149542013472d58eafdadb085f6bdd93def143921"
 dependencies = [
  "actix-http",
  "actix-web-httpauth",
@@ -6336,9 +6327,9 @@ dependencies = [
 
 [[package]]
 name = "opentalk-types-signaling-training-participation-report"
-version = "0.2.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8658e3a6b4303b0d3f637d152029f3ee206ff1bdfdd411bd680094c15f9b004"
+checksum = "94dc459b07a8f2f60c2bd7c8c5713b38b053a9527e3995dee7c7ccd904040c4e"
 dependencies = [
  "opentalk-types-common",
  "opentalk-types-signaling",
@@ -7147,7 +7138,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
 dependencies = [
  "bytes",
  "heck 0.5.0",
- "itertools 0.12.1",
+ "itertools 0.11.0",
  "log",
  "multimap",
  "once_cell",
@@ -7187,7 +7178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
 dependencies = [
  "anyhow",
- "itertools 0.12.1",
+ "itertools 0.11.0",
  "proc-macro2",
  "quote",
  "syn",
diff --git a/Cargo.toml b/Cargo.toml
index 77408c9b7307df4e90f0bb22d4d50cdd1807a986..56b9a9d8bf29e49673c3eb31c739b46bb42a8d29 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -92,8 +92,8 @@ opentalk-mail-worker-protocol = "0.32"
 opentalk-nextcloud-client = { version = "0.2.0", default-features = false, features = [
   "rustls-tls-native-roots",
 ] }
-opentalk-types-api-v1 = "0.33"
-opentalk-types-common = "0.32"
+opentalk-types-api-v1 = "0.34"
+opentalk-types-common = "0.32.1"
 opentalk-types-signaling = "0.32"
 opentalk-types-signaling-breakout = "0.32"
 opentalk-types-signaling-chat = "0.32"
@@ -108,7 +108,7 @@ opentalk-types-signaling-recording-service = "0.32"
 opentalk-types-signaling-shared-folder = "0.32"
 opentalk-types-signaling-subroom-audio = "0.32"
 opentalk-types-signaling-timer = "0.32"
-opentalk-types-signaling-training-participation-report = "0.2"
+opentalk-types-signaling-training-participation-report = "0.3"
 opentalk-types-signaling-whiteboard = "0.32"
 opentalk-version = "0.1.1"
 opentelemetry = { version = "0.27", default-features = false, features = [
diff --git a/api/controller/frontend_api.yaml b/api/controller/frontend_api.yaml
index e72ceaa0fcee629fc004747353872957ef8c2476..be3db6e1f3e124db5a74427b0363fedfa8e54fbd 100644
--- a/api/controller/frontend_api.yaml
+++ b/api/controller/frontend_api.yaml
@@ -3095,6 +3095,13 @@ components:
         title:
           $ref: "#/components/schemas/EventTitle"
           description: Title of the event
+        training_participation_report:
+          $ref: "#/components/schemas/TrainingParticipationReportParameterSet"
+          description: |-
+            The training participation report parameter set for the event.
+
+            When present, the training participation report will be started
+            automatically in the meeting.
         type:
           $ref: "#/components/schemas/EventType"
           description: "Must always be `instance`"
@@ -3158,6 +3165,13 @@ components:
           timezone: Europe/Berlin
         status: ok
         title: Team Event
+        training_participation_report:
+          checkpoint_interval:
+            after: 300
+            within: 400
+          initial_checkpoint_delay:
+            after: 100
+            within: 200
         type: recurring
         updated_at: "2024-07-20T14:16:19Z"
         updated_by:
@@ -3315,6 +3329,13 @@ components:
             streaming_endpoint: "https://ingress.streaming.example.com/"
             streaming_key: aabbccddeeff
         title: Team Event
+        training_participation_report:
+          checkpoint_interval:
+            after: 300
+            within: 400
+          initial_checkpoint_delay:
+            after: 100
+            within: 200
         type: single
         updated_at: "2024-07-20T14:16:19Z"
         updated_by:
@@ -3453,6 +3474,13 @@ components:
             Title of the event
 
             For display purposes
+        training_participation_report:
+          $ref: "#/components/schemas/TrainingParticipationReportParameterSet"
+          description: |-
+            The training participation report parameter set for the event.
+
+            When present, the training participation report will be started
+            automatically in the meeting.
         type:
           $ref: "#/components/schemas/EventType"
           description: |-
@@ -3520,6 +3548,13 @@ components:
             streaming_endpoint: "https://ingress.streaming.example.com/"
             streaming_key: aabbccddeeff
         title: Team Event
+        training_participation_report:
+          checkpoint_interval:
+            after: 300
+            within: 400
+          initial_checkpoint_delay:
+            after: 100
+            within: 200
         type: single
         updated_at: "2024-07-20T14:16:19Z"
         updated_by:
@@ -3666,6 +3701,13 @@ components:
             timezone: Europe/Berlin
           status: ok
           title: Team Event
+          training_participation_report:
+            checkpoint_interval:
+              after: 300
+              within: 400
+            initial_checkpoint_delay:
+              after: 100
+              within: 200
           type: recurring
           updated_at: "2024-07-20T14:16:19Z"
           updated_by:
@@ -4037,6 +4079,13 @@ components:
         title:
           $ref: "#/components/schemas/EventTitle"
           description: Patch the title of th event
+        training_participation_report:
+          $ref: "#/components/schemas/TrainingParticipationReportParameterSet"
+          description: |-
+            The training participation report parameter set for the event.
+
+            When present, the training participation report will be started
+            automatically in the meeting.
         waiting_room:
           type: boolean
           description: Patch the presence of a waiting room
@@ -4243,6 +4292,13 @@ components:
         title:
           $ref: "#/components/schemas/EventTitle"
           description: Title of the event
+        training_participation_report:
+          $ref: "#/components/schemas/TrainingParticipationReportParameterSet"
+          description: |-
+            The training participation report parameter set for the event.
+
+            When present, the training participation report will be started
+            automatically in the meeting.
         waiting_room:
           type: boolean
           description: Should the created event have a waiting room?
@@ -4269,6 +4325,13 @@ components:
             streaming_endpoint: "https://ingress.streaming.example.com/"
             streaming_key: aabbccddeeff
         title: Teammeeting
+        training_participation_report:
+          checkpoint_interval:
+            after: 300
+            within: 400
+          initial_checkpoint_delay:
+            after: 100
+            within: 200
         waiting_room: false
     PostInviteRequestBody:
       type: object
@@ -4983,6 +5046,26 @@ components:
     TicketToken:
       type: string
       description: A ticket token
+    TimeRange:
+      type: object
+      description: A time range within which checkpoints can be randomly created
+      required:
+        - after
+        - within
+      properties:
+        after:
+          type: integer
+          format: int64
+          description: The earliest number of seconds after which the checkpoint can be created.
+          minimum: 0
+        within:
+          type: integer
+          format: int64
+          description: "The number of seconds within which the checkpoint can be created after the `after` value."
+          minimum: 0
+      example:
+        after: 1200
+        within: 600
     TimeZone:
       type: string
       description: A time zone
@@ -4995,6 +5078,26 @@ components:
         A UTC DateTime wrapper that implements ToRedisArgs and FromRedisValue.
 
         The values are stores as unix timestamps in redis.
+    TrainingParticipationReportParameterSet:
+      type: object
+      description: The parameters for a training participant report checkpoint procedure.
+      required:
+        - initial_checkpoint_delay
+        - checkpoint_interval
+      properties:
+        checkpoint_interval:
+          $ref: "#/components/schemas/TimeRange"
+          description: The time range definition for the subsequent checkpoints.
+        initial_checkpoint_delay:
+          $ref: "#/components/schemas/TimeRange"
+          description: The time range definition for the initial checkpoint delay.
+      example:
+        checkpoint_interval:
+          after: 300
+          within: 400
+        initial_checkpoint_delay:
+          after: 100
+          within: 200
     TurnServer:
       type: object
       description: TURN access credentials for users.
diff --git a/crates/opentalk-controller-core/src/api/v1/events/instances.rs b/crates/opentalk-controller-core/src/api/v1/events/instances.rs
index 56d50276b77fea0881c4171646a4f5c692c4d7c2..36dc1ec4efbb128db70552000767de71866d3227 100644
--- a/crates/opentalk-controller-core/src/api/v1/events/instances.rs
+++ b/crates/opentalk-controller-core/src/api/v1/events/instances.rs
@@ -36,6 +36,7 @@ use opentalk_types_api_v1::{
 use opentalk_types_common::{
     events::{invites::EventInviteStatus, EventId},
     shared_folders::SharedFolder,
+    training_participation_report::TrainingParticipationReportParameterSet,
 };
 use rrule::RRuleSet;
 
@@ -151,8 +152,16 @@ async fn get_event_instances_inner(
 
     let mut conn = db.get_conn().await?;
 
-    let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        invite,
+        room,
+        sip_config,
+        is_favorite,
+        shared_folder,
+        tariff,
+        training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
     let (invitees, invitees_truncated) =
         super::get_invitees_for_event(settings, &mut conn, event.id, invitees_max).await?;
@@ -198,6 +207,9 @@ async fn get_event_instances_inner(
         .fetch(settings, &mut conn)
         .await?;
 
+    let training_participation_report = training_participation_report_parameter_set
+        .map(TrainingParticipationReportParameterSet::from);
+
     drop(conn);
 
     let room = EventRoomInfo::from_room(settings, room, sip_config, &tariff);
@@ -225,6 +237,7 @@ async fn get_event_instances_inner(
             invitees_truncated,
             can_edit,
             shared_folder.clone(),
+            training_participation_report.clone(),
         )?;
 
         instances.push(instance);
@@ -341,8 +354,16 @@ async fn get_event_instance_inner(
 ) -> DefaultApiResult<GetEventInstanceResponseBody, CaptureApiError> {
     let mut conn = db.get_conn().await?;
 
-    let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        invite,
+        room,
+        sip_config,
+        is_favorite,
+        shared_folder,
+        tariff,
+        training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
     verify_recurrence_date(&event, instance_id.into())?;
 
     let (invitees, invitees_truncated) =
@@ -376,6 +397,7 @@ async fn get_event_instance_inner(
         invitees_truncated,
         can_edit,
         shared_folder,
+        training_participation_report_parameter_set.map(Into::into),
     )?;
 
     let event_instance = EventInstance {
@@ -490,8 +512,16 @@ async fn patch_event_instance_inner(
 
     let mut conn = db.get_conn().await?;
 
-    let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        invite,
+        room,
+        sip_config,
+        is_favorite,
+        shared_folder,
+        tariff,
+        training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
     if !event.is_recurring.unwrap_or_default() {
         return Err(ApiError::not_found().into());
@@ -648,6 +678,7 @@ async fn patch_event_instance_inner(
         invitees_truncated,
         can_edit,
         shared_folder,
+        training_participation_report_parameter_set.map(Into::into),
     )?;
 
     let event_instance = EventInstance {
@@ -677,6 +708,7 @@ fn create_event_instance(
     invitees_truncated: bool,
     can_edit: bool,
     shared_folder: Option<SharedFolder>,
+    training_participation_report: Option<TrainingParticipationReportParameterSet>,
 ) -> opentalk_database::Result<EventInstance> {
     let mut status = EventStatus::Ok;
 
@@ -741,6 +773,7 @@ fn create_event_instance(
         is_favorite,
         can_edit,
         shared_folder,
+        training_participation_report,
     })
 }
 
@@ -849,6 +882,7 @@ mod tests {
             is_favorite: false,
             can_edit: false,
             shared_folder: None,
+            training_participation_report: None,
         };
 
         assert_eq_json!(
diff --git a/crates/opentalk-controller-core/src/api/v1/events/invites.rs b/crates/opentalk-controller-core/src/api/v1/events/invites.rs
index 22612b8eac59be0f1968ffa8008bbdd995da80ba..624403f37050fd6e30658ebd56f463ee25679ef5 100644
--- a/crates/opentalk-controller-core/src/api/v1/events/invites.rs
+++ b/crates/opentalk-controller-core/src/api/v1/events/invites.rs
@@ -912,8 +912,16 @@ async fn delete_invite_to_event_inner(
     let mut conn = db.get_conn().await?;
 
     // TODO(w.rabl) Further DB access optimization (replacing call to get_with_invite_and_room)?
-    let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        _invite,
+        room,
+        sip_config,
+        _is_favorite,
+        shared_folder,
+        _tariff,
+        _training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
     let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?;
 
     let created_by = if event.created_by == current_user.id {
@@ -1069,8 +1077,16 @@ async fn delete_email_invite_to_event_inner(
 
     let mut conn = db.get_conn().await?;
 
-    let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        _invite,
+        room,
+        sip_config,
+        _is_favorite,
+        shared_folder,
+        _tariff,
+        _training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
     let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?;
 
     let created_by = if event.created_by == current_user.id {
diff --git a/crates/opentalk-controller-core/src/api/v1/events/mod.rs b/crates/opentalk-controller-core/src/api/v1/events/mod.rs
index f8240a3f33ee6f98182570e6369fd0bb45c9a5bf..9ac8d14ade404254030a9cb43d9423514c024423 100644
--- a/crates/opentalk-controller-core/src/api/v1/events/mod.rs
+++ b/crates/opentalk-controller-core/src/api/v1/events/mod.rs
@@ -30,7 +30,8 @@ use opentalk_database::{Db, DbConnection};
 use opentalk_db_storage::{
     events::{
         email_invites::EventEmailInvite, shared_folders::EventSharedFolder, Event, EventException,
-        EventExceptionKind, EventInvite, NewEvent, UpdateEvent,
+        EventExceptionKind, EventInvite, EventTrainingParticipationReportParameterSet, NewEvent,
+        UpdateEvent, UpdateEventTrainingParticipationReportParameterSet,
     },
     invites::Invite,
     rooms::{NewRoom, Room, UpdateRoom},
@@ -63,6 +64,7 @@ use opentalk_types_common::{
     shared_folders::{SharedFolder, SharedFolderAccess},
     streaming::{RoomStreamingTarget, StreamingTarget},
     time::{DateTimeTz, RecurrencePattern, TimeZone, Timestamp},
+    training_participation_report::TrainingParticipationReportParameterSet,
     users::UserId,
 };
 use rrule::{Frequency, RRuleSet};
@@ -334,6 +336,7 @@ async fn new_event_inner(
                         streaming_targets,
                         has_shared_folder: _,
                         show_meeting_details,
+                        training_participation_report
                     } if recurrence_pattern.is_empty() => {
                         create_time_independent_event(
                             settings,
@@ -348,6 +351,7 @@ async fn new_event_inner(
                             streaming_targets,
                             show_meeting_details,
                             query,
+                            training_participation_report
                         )
                             .await?
                     }
@@ -366,6 +370,7 @@ async fn new_event_inner(
                         streaming_targets,
                         has_shared_folder: _,
                         show_meeting_details,
+                        training_participation_report
                     } => {
                         create_time_dependent_event(
                             settings,
@@ -384,6 +389,7 @@ async fn new_event_inner(
                             streaming_targets,
                             show_meeting_details,
                             query,
+                            training_participation_report
                         )
                             .await?
                     }
@@ -458,6 +464,34 @@ async fn store_event_streaming_targets(
     Ok(room_streaming_targets)
 }
 
+async fn store_training_participation_report(
+    conn: &mut DbConnection,
+    event_id: EventId,
+    TrainingParticipationReportParameterSet {
+        initial_checkpoint_delay,
+        checkpoint_interval,
+    }: TrainingParticipationReportParameterSet,
+) -> Result<Option<TrainingParticipationReportParameterSet>, CaptureApiError> {
+    let initial_checkpoint_delay_after =
+        i64::try_from(initial_checkpoint_delay.after).unwrap_or(i64::MAX);
+    let initial_checkpoint_delay_within =
+        i64::try_from(initial_checkpoint_delay.within).unwrap_or(i64::MAX);
+    let checkpoint_interval_after = i64::try_from(checkpoint_interval.after).unwrap_or(i64::MAX);
+    let checkpoint_interval_within = i64::try_from(checkpoint_interval.within).unwrap_or(i64::MAX);
+
+    let inserted = EventTrainingParticipationReportParameterSet {
+        event_id,
+        initial_checkpoint_delay_after,
+        initial_checkpoint_delay_within,
+        checkpoint_interval_after,
+        checkpoint_interval_within,
+    }
+    .try_insert(conn)
+    .await?;
+
+    Ok(inserted.map(Into::into))
+}
+
 struct MailResource {
     pub current_user: User,
     pub event: Event,
@@ -480,6 +514,7 @@ async fn create_time_independent_event(
     streaming_targets: Vec<StreamingTarget>,
     show_meeting_details: bool,
     query: EventOptionsQuery,
+    training_participation_report: Option<TrainingParticipationReportParameterSet>,
 ) -> Result<(EventResource, Option<MailResource>), CaptureApiError> {
     let room = NewRoom {
         created_by: current_user.id,
@@ -518,6 +553,12 @@ async fn create_time_independent_event(
     let streaming_targets =
         store_event_streaming_targets(conn, event.id, streaming_targets).await?;
 
+    let training_participation_report = if let Some(parameters) = training_participation_report {
+        store_training_participation_report(conn, event.id, parameters).await?
+    } else {
+        None
+    };
+
     let tariff = Tariff::get_by_user_id(conn, &current_user.id).await?;
 
     let suppress_email_notification = is_adhoc || query.suppress_email_notification;
@@ -554,6 +595,7 @@ async fn create_time_independent_event(
             shared_folder: None,
             streaming_targets,
             show_meeting_details,
+            training_participation_report,
         },
         mail_resource,
     ))
@@ -578,6 +620,7 @@ async fn create_time_dependent_event(
     streaming_targets: Vec<StreamingTarget>,
     show_meeting_details: bool,
     query: EventOptionsQuery,
+    training_participation_report: Option<TrainingParticipationReportParameterSet>,
 ) -> Result<(EventResource, Option<MailResource>), CaptureApiError> {
     let recurrence_pattern = recurrence_pattern.to_multiline_string();
 
@@ -664,6 +707,7 @@ async fn create_time_dependent_event(
             shared_folder: None,
             streaming_targets,
             show_meeting_details,
+            training_participation_report,
         },
         mail_resource,
     ))
@@ -771,7 +815,7 @@ async fn get_events_inner(
     )
     .await?;
 
-    for (event, _, _, _, exceptions, _, _, _) in &events {
+    for (event, _, _, _, exceptions, _, _, _, _) in &events {
         users.add(event);
         users.add(exceptions);
     }
@@ -809,7 +853,17 @@ async fn get_events_inner(
     let mut ret_cursor_data = None;
 
     for (
-        (event, invite, room, sip_config, exceptions, is_favorite, shared_folder, tariff),
+        (
+            event,
+            invite,
+            room,
+            sip_config,
+            exceptions,
+            is_favorite,
+            shared_folder,
+            tariff,
+            training_participation_report,
+        ),
         (mut invites_with_user, mut email_invites),
     ) in events.into_iter().zip(invites_grouped_by_event)
     {
@@ -884,6 +938,7 @@ async fn get_events_inner(
             shared_folder,
             streaming_targets: Vec::new(),
             show_meeting_details: event.show_meeting_details,
+            training_participation_report,
         }));
 
         for exception in exceptions {
@@ -994,8 +1049,16 @@ async fn get_event_inner(
 ) -> DefaultApiResult<EventResource, CaptureApiError> {
     let mut conn = db.get_conn().await?;
 
-    let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        invite,
+        room,
+        sip_config,
+        is_favorite,
+        shared_folder,
+        tariff,
+        training_participation_report,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
     let room_streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?;
     let (invitees, invitees_truncated) =
         get_invitees_for_event(settings, &mut conn, event_id, query.invitees_max).await?;
@@ -1047,6 +1110,7 @@ async fn get_event_inner(
         shared_folder,
         streaming_targets: room_streaming_targets,
         show_meeting_details: event.show_meeting_details,
+        training_participation_report: training_participation_report.map(Into::into),
     };
 
     let event_resource = EventResource {
@@ -1165,8 +1229,16 @@ async fn patch_event_inner(
 
     let mut conn = db.get_conn().await?;
 
-    let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        invite,
+        room,
+        sip_config,
+        is_favorite,
+        shared_folder,
+        tariff,
+        training_participation_report,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
     let room = if patch.password.is_some() || patch.waiting_room.is_some() {
         // Update the event's room if at least one of the fields is set
@@ -1205,6 +1277,19 @@ async fn patch_event_inner(
         None => {}
     }
 
+    let training_participation_report = match &patch.training_participation_report {
+        Some(Some(parameter_set)) => Some(
+            UpdateEventTrainingParticipationReportParameterSet::from(parameter_set.clone())
+                .apply(&mut conn, event.id)
+                .await?,
+        ),
+        Some(None) => {
+            EventTrainingParticipationReportParameterSet::delete_by_id(&mut conn, event.id).await?;
+            None
+        }
+        None => training_participation_report,
+    };
+
     // Special case: if the patch only modifies the password do not update the event
     let event = if patch.only_modifies_room() {
         event
@@ -1304,6 +1389,7 @@ async fn patch_event_inner(
         shared_folder: shared_folder.clone(),
         streaming_targets: streaming_targets.clone(),
         show_meeting_details: event.show_meeting_details,
+        training_participation_report: training_participation_report.map(Into::into),
     };
 
     if send_email_notification {
@@ -1344,8 +1430,16 @@ pub async fn notify_event_invitees_by_room_about_update(
     let event = Event::get_for_room(conn, room_id).await?;
 
     if let Some(event) = event {
-        let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) =
-            Event::get_with_related_items(conn, current_user.id, event.id).await?;
+        let (
+            event,
+            _invite,
+            room,
+            sip_config,
+            _is_favorite,
+            shared_folder,
+            _tariff,
+            _training_participation_report_parameter_set,
+        ) = Event::get_with_related_items(conn, current_user.id, event.id).await?;
 
         let shared_folder_for_user =
             shared_folder_for_user(shared_folder, event.created_by, current_user.id);
@@ -1734,8 +1828,16 @@ async fn delete_event_inner(
     let mut conn = db.get_conn().await?;
 
     // TODO(w.rabl) Further DB access optimization (replacing call to get_with_invite_and_room)?
-    let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        _invite,
+        room,
+        sip_config,
+        _is_favorite,
+        shared_folder,
+        _tariff,
+        _training_participation_report,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
     let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?;
 
@@ -2253,7 +2355,10 @@ mod tests {
     use std::time::SystemTime;
 
     use opentalk_test_util::assert_eq_json;
-    use opentalk_types_common::events::invites::InviteRole;
+    use opentalk_types_common::{
+        events::invites::InviteRole, rooms::RoomId, training_participation_report::TimeRange,
+        users::UserId,
+    };
 
     use super::*;
 
@@ -2330,6 +2435,16 @@ mod tests {
             shared_folder: None,
             streaming_targets: Vec::new(),
             show_meeting_details: true,
+            training_participation_report: Some(TrainingParticipationReportParameterSet {
+                initial_checkpoint_delay: TimeRange {
+                    after: 100,
+                    within: 200,
+                },
+                checkpoint_interval: TimeRange {
+                    after: 300,
+                    within: 400,
+                },
+            }),
         };
 
         assert_eq_json!(
@@ -2396,6 +2511,16 @@ mod tests {
                 "can_edit": true,
                 "is_adhoc": false,
                 "show_meeting_details": true,
+                "training_participation_report": {
+                    "initial_checkpoint_delay": {
+                        "after": 100,
+                        "within": 200,
+                    },
+                    "checkpoint_interval": {
+                        "after": 300,
+                        "within": 400,
+                    },
+                },
             }
         );
     }
@@ -2457,6 +2582,16 @@ mod tests {
             shared_folder: None,
             streaming_targets: Vec::new(),
             show_meeting_details: false,
+            training_participation_report: Some(TrainingParticipationReportParameterSet {
+                initial_checkpoint_delay: TimeRange {
+                    after: 100,
+                    within: 200,
+                },
+                checkpoint_interval: TimeRange {
+                    after: 300,
+                    within: 400,
+                },
+            }),
         };
 
         assert_eq_json!(
@@ -2519,6 +2654,16 @@ mod tests {
                 "can_edit": false,
                 "is_adhoc": false,
                 "show_meeting_details": false,
+                "training_participation_report": {
+                    "initial_checkpoint_delay": {
+                        "after": 100,
+                        "within": 200,
+                    },
+                    "checkpoint_interval": {
+                        "after": 300,
+                        "within": 400,
+                    },
+                },
             }
         );
     }
diff --git a/crates/opentalk-controller-core/src/api/v1/events/shared_folder.rs b/crates/opentalk-controller-core/src/api/v1/events/shared_folder.rs
index c81426b25563a5a94e241a40b306134484bd0ac2..f546032933293a96a5c05bbff35b784a200fa661 100644
--- a/crates/opentalk-controller-core/src/api/v1/events/shared_folder.rs
+++ b/crates/opentalk-controller-core/src/api/v1/events/shared_folder.rs
@@ -200,8 +200,16 @@ async fn put_shared_folder_for_event_inner(
 
     let (shared_folder, created) = put_shared_folder(settings, event_id, &mut conn).await?;
 
-    let (event, _invite, room, sip_config, _is_favorite, _shared_folder, _tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        _invite,
+        room,
+        sip_config,
+        _is_favorite,
+        _shared_folder,
+        _tariff,
+        _training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
     if send_email_notification {
         let shared_folder_for_user = shared_folder_for_user(
@@ -552,8 +560,16 @@ async fn delete_shared_folder_for_event_inner(
 
     let mut conn = db.get_conn().await?;
 
-    let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) =
-        Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+    let (
+        event,
+        _invite,
+        room,
+        sip_config,
+        _is_favorite,
+        shared_folder,
+        _tariff,
+        _training_participation_report_parameter_set,
+    ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
 
     if let Some(shared_folder) = shared_folder {
         let shared_folders = std::slice::from_ref(&shared_folder);
diff --git a/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs b/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ce9cd25be8d16cd59f2cbacaca420a47c1b8118b
--- /dev/null
+++ b/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs
@@ -0,0 +1,455 @@
+// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
+//
+// SPDX-License-Identifier: EUPL-1.2
+
+use std::collections::HashSet;
+
+use chrono::{Days, NaiveDate, Utc};
+use log::warn;
+use opentalk_controller_service_facade::RequestUser;
+use opentalk_controller_settings::Settings;
+use opentalk_controller_utils::CaptureApiError;
+use opentalk_database::DbConnection;
+use opentalk_db_storage::{
+    events::{
+        shared_folders::{EventSharedFolder, NewEventSharedFolder},
+        Event,
+    },
+    streaming_targets::get_room_streaming_targets,
+    tenants::Tenant,
+    users::User,
+};
+use opentalk_nextcloud_client::{Client, ShareId, SharePermission, ShareType};
+use opentalk_types_api_v1::{
+    error::ApiError,
+    events::{DeleteSharedFolderQuery, PutSharedFolderQuery},
+};
+use opentalk_types_common::{
+    events::EventId,
+    shared_folders::{SharedFolder, SharedFolderAccess},
+};
+use snafu::Report;
+
+use crate::{
+    events::{notifications::notify_event_invitees_about_update, shared_folder_for_user},
+    ControllerBackend,
+};
+
+impl ControllerBackend {
+    pub(crate) async fn get_shared_folder_for_event(
+        &self,
+        current_user: RequestUser,
+        event_id: EventId,
+    ) -> Result<SharedFolder, CaptureApiError> {
+        let mut conn = self.db.get_conn().await?;
+
+        let event = Event::get(&mut conn, event_id).await?;
+
+        let shared_folder = SharedFolder::from(
+            EventSharedFolder::get_for_event(&mut conn, event_id)
+                .await?
+                .ok_or_else(ApiError::not_found)?,
+        );
+
+        let shared_folder = if event.created_by == current_user.id {
+            shared_folder
+        } else {
+            shared_folder.without_write_access()
+        };
+
+        Ok(shared_folder)
+    }
+
+    pub(crate) async fn put_shared_folder_for_event(
+        &self,
+        current_user: RequestUser,
+        event_id: EventId,
+        query: PutSharedFolderQuery,
+    ) -> Result<(SharedFolder, bool), CaptureApiError> {
+        let settings = self.settings.load();
+        let mut conn = self.db.get_conn().await?;
+
+        let send_email_notification = !query.suppress_email_notification;
+
+        let (shared_folder, created) = put_shared_folder(&settings, event_id, &mut conn).await?;
+
+        let (
+            event,
+            _invite,
+            room,
+            sip_config,
+            _is_favorite,
+            _shared_folder,
+            _tariff,
+            _training_participation_report,
+        ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+
+        if send_email_notification {
+            let shared_folder_for_user = shared_folder_for_user(
+                Some(shared_folder.clone()),
+                event.created_by,
+                current_user.id,
+            );
+
+            let current_tenant = Tenant::get(&mut conn, current_user.tenant_id).await?;
+            let current_user = User::get(&mut conn, current_user.id).await?;
+            let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?;
+
+            notify_event_invitees_about_update(
+                &self.kc_admin_client,
+                &settings,
+                &self.mail_service,
+                current_tenant,
+                current_user,
+                &mut conn,
+                event,
+                room,
+                sip_config,
+                shared_folder_for_user,
+                streaming_targets,
+            )
+            .await?;
+        }
+
+        Ok((SharedFolder::from(shared_folder), created))
+    }
+
+    pub(crate) async fn delete_shared_folder_for_event(
+        &self,
+        current_user: RequestUser,
+        event_id: EventId,
+        query: DeleteSharedFolderQuery,
+    ) -> Result<(), CaptureApiError> {
+        let settings = self.settings.load();
+        let mut conn = self.db.get_conn().await?;
+
+        let send_email_notification = !query.suppress_email_notification;
+
+        let (
+            event,
+            _invite,
+            room,
+            sip_config,
+            _is_favorite,
+            shared_folder,
+            _tariff,
+            _training_participation_report,
+        ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?;
+
+        if let Some(shared_folder) = shared_folder {
+            let shared_folders = std::slice::from_ref(&shared_folder);
+            let deletion = delete_shared_folders(&settings, shared_folders).await;
+
+            let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?;
+
+            match deletion {
+                Ok(()) => {
+                    shared_folder.delete(&mut conn).await?;
+
+                    if send_email_notification {
+                        let current_tenant = Tenant::get(&mut conn, current_user.tenant_id).await?;
+                        let current_user = User::get(&mut conn, current_user.id).await?;
+
+                        notify_event_invitees_about_update(
+                            &self.kc_admin_client,
+                            &settings,
+                            &self.mail_service,
+                            current_tenant,
+                            current_user,
+                            &mut conn,
+                            event,
+                            room,
+                            sip_config,
+                            None,
+                            streaming_targets,
+                        )
+                        .await?;
+                    }
+
+                    Ok(())
+                }
+                Err(e) => {
+                    if query.force_delete_reference_if_shared_folder_deletion_fails {
+                        warn!(
+                            "Deleting local shared folder reference anyway, because \
+                        `force_delete_reference_if_shared_folder_deletion_fails` is set to true"
+                        );
+                        shared_folder.delete(&mut conn).await?;
+
+                        if send_email_notification {
+                            let current_tenant =
+                                Tenant::get(&mut conn, current_user.tenant_id).await?;
+                            let current_user = User::get(&mut conn, current_user.id).await?;
+
+                            notify_event_invitees_about_update(
+                                &self.kc_admin_client,
+                                &settings,
+                                &self.mail_service,
+                                current_tenant,
+                                current_user,
+                                &mut conn,
+                                event,
+                                room,
+                                sip_config,
+                                None,
+                                streaming_targets,
+                            )
+                            .await?;
+                        }
+
+                        Ok(())
+                    } else {
+                        Err(e.into())
+                    }
+                }
+            }
+        } else {
+            Ok(())
+        }
+    }
+}
+
+/// Adds a shared folder to the specified event
+pub async fn put_shared_folder(
+    settings: &Settings,
+    event_id: EventId,
+    conn: &mut DbConnection,
+) -> Result<(EventSharedFolder, bool), CaptureApiError> {
+    let shared_folder = EventSharedFolder::get_for_event(conn, event_id).await?;
+
+    if let Some(shared_folder) = shared_folder {
+        return Ok((shared_folder, false));
+    }
+    let shared_folder_settings = settings.shared_folder.as_ref().ok_or_else(|| {
+        ApiError::bad_request().with_message("No shared folder configured for this server")
+    })?;
+
+    match shared_folder_settings {
+        opentalk_controller_settings::SharedFolder::Nextcloud {
+            url,
+            username,
+            password,
+            directory,
+            expiry,
+        } => {
+            let client =
+                Client::new(url.clone(), username.clone(), password.clone()).map_err(|e| {
+                    warn!("Error creating NextCloud client: {}", Report::from_error(e));
+                    ApiError::internal().with_message("Error creating NextCloud client")
+                })?;
+            let path = format!(
+                "{}/opentalk-event-{}",
+                directory.trim_matches('/'),
+                event_id
+            );
+            let user_path = format!("files/{username}/{path}");
+            client.create_folder(&user_path).await.map_err(|e| {
+                warn!(
+                    "Error creating folder on NextCloud: {}",
+                    Report::from_error(e)
+                );
+                ApiError::internal().with_message("Error creating folder on NextCloud")
+            })?;
+
+            let expire_date = expiry
+                .as_ref()
+                .map(|days| Utc::now().date_naive() + Days::new(*days));
+
+            let write_permissions = HashSet::from([
+                SharePermission::Read,
+                SharePermission::Create,
+                SharePermission::Update,
+                SharePermission::Delete,
+            ]);
+            let read_permissions = HashSet::from([SharePermission::Read]);
+
+            async fn create_share(
+                client: &Client,
+                path: &str,
+                permissions: HashSet<SharePermission>,
+                label: &str,
+                password: String,
+                expire_date: Option<NaiveDate>,
+            ) -> Result<(ShareId, SharedFolderAccess), ApiError> {
+                let mut creator = client
+                    .create_share(path, ShareType::PublicLink)
+                    .password(&password)
+                    .label(label);
+                for permission in &permissions {
+                    creator = creator.permission(*permission);
+                }
+                if let Some(expire_date) = expire_date {
+                    creator = creator.expire_date(expire_date);
+                }
+                let share = creator.send().await.map_err(|e| {
+                    warn!(
+                        "Error creating share on NextCloud: {}",
+                        Report::from_error(e)
+                    );
+                    ApiError::internal().with_message("Error creating share on NextCloud")
+                })?;
+
+                // Workaround for NextCloud up to version 25 not processing the share permissions
+                // on folder creation. We just need to change them with a subsequent update request.
+                //
+                // See: https://github.com/nextcloud/server/issues/32611
+                if share.data.permissions != permissions {
+                    _ = client
+                        .update_share(share.data.id.clone())
+                        .permissions(permissions)
+                        .await
+                        .map_err(|e| {
+                            warn!(
+                                "Error setting permissions for share on NextCloud: {}",
+                                Report::from_error(e)
+                            );
+                            ApiError::internal()
+                                .with_message("Error setting permissions for share on NextCloud")
+                        })?;
+                }
+
+                Ok((
+                    share.data.id,
+                    SharedFolderAccess {
+                        url: share.data.url,
+                        password,
+                    },
+                ))
+            }
+
+            let write_password = generate_password(&client).await?;
+            let read_password = generate_password(&client).await?;
+
+            let (
+                write_share_id,
+                SharedFolderAccess {
+                    url: write_url,
+                    password: write_password,
+                },
+            ) = create_share(
+                &client,
+                &path,
+                write_permissions,
+                "OpenTalk read-write",
+                write_password,
+                expire_date,
+            )
+            .await?;
+            let (
+                read_share_id,
+                SharedFolderAccess {
+                    url: read_url,
+                    password: read_password,
+                },
+            ) = create_share(
+                &client,
+                &path,
+                read_permissions,
+                "OpenTalk read-only",
+                read_password,
+                expire_date,
+            )
+            .await?;
+
+            let new_shared_folder = NewEventSharedFolder {
+                event_id,
+                path,
+                write_share_id: write_share_id.to_string(),
+                write_url,
+                write_password,
+                read_share_id: read_share_id.to_string(),
+                read_url,
+                read_password,
+            };
+
+            let shared_folder = new_shared_folder
+                .try_insert(conn)
+                .await?
+                .ok_or_else(ApiError::internal)?;
+
+            Ok((shared_folder, true))
+        }
+    }
+}
+
+/// Deletes the shared folders for the specified event
+pub async fn delete_shared_folders(
+    settings: &Settings,
+    shared_folders: &[EventSharedFolder],
+) -> Result<(), ApiError> {
+    if shared_folders.is_empty() {
+        return Ok(());
+    }
+
+    let shared_folder_settings = if let Some(settings) = settings.shared_folder.as_ref() {
+        settings
+    } else {
+        return Err(
+            ApiError::bad_request().with_message("No shared folder configured for this server")
+        );
+    };
+
+    match shared_folder_settings {
+        opentalk_controller_settings::SharedFolder::Nextcloud {
+            url,
+            username,
+            password,
+            ..
+        } => {
+            let client =
+                Client::new(url.clone(), username.clone(), password.clone()).map_err(|e| {
+                    warn!("Error creating NextCloud client: {}", Report::from_error(e));
+                    ApiError::internal().with_message("Error creating NextCloud client")
+                })?;
+            for shared_folder in shared_folders {
+                let path = &shared_folder.path;
+                if path.trim_matches('/').is_empty() {
+                    warn!("Preventing recursive deletion of empty shared folder path, this is probably harmful and not intended");
+                    return Err(ApiError::internal());
+                }
+                let user_path = format!("files/{username}/{path}");
+                if let Err(e) = client
+                    .delete_share(ShareId::from(shared_folder.read_share_id.clone()))
+                    .await
+                {
+                    warn!(
+                        "Could not delete NextCloud read share: {}",
+                        Report::from_error(e)
+                    );
+                }
+                if let Err(e) = client
+                    .delete_share(ShareId::from(shared_folder.write_share_id.clone()))
+                    .await
+                {
+                    warn!(
+                        "Could not delete NextCloud write share: {}",
+                        Report::from_error(e)
+                    );
+                }
+                match client.delete(&user_path).await {
+                    Ok(()) | Err(opentalk_nextcloud_client::Error::FileNotFound { .. }) => {}
+                    Err(e) => {
+                        warn!(
+                            "Error deleting folder on NextCloud: {}",
+                            Report::from_error(e)
+                        );
+                        return Err(
+                            ApiError::internal().with_message("Error deleting folder on NextCloud")
+                        );
+                    }
+                };
+            }
+            Ok(())
+        }
+    }
+}
+
+async fn generate_password(client: &Client) -> Result<String, ApiError> {
+    client.generate_password().await.map_err(|e| {
+        warn!(
+            "Error generating share password on NextCloud: {}",
+            Report::from_error(e)
+        );
+        ApiError::internal().with_message("Error generating share password NextCloud")
+    })
+}
diff --git a/crates/opentalk-controller-service/src/events/notifications.rs b/crates/opentalk-controller-service/src/events/notifications.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fc1ba56b6d58ad38737ecaab6ed7907d06c48308
--- /dev/null
+++ b/crates/opentalk-controller-service/src/events/notifications.rs
@@ -0,0 +1,184 @@
+// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
+//
+// SPDX-License-Identifier: EUPL-1.2
+
+//! Handles event-related notifications.
+
+use opentalk_controller_settings::Settings;
+use opentalk_controller_utils::CaptureApiError;
+use opentalk_database::DbConnection;
+use opentalk_db_storage::{
+    events::{Event, EventException},
+    invites::Invite,
+    rooms::Room,
+    sip_configs::SipConfig,
+    streaming_targets::get_room_streaming_targets,
+    tenants::Tenant,
+    users::User,
+};
+use opentalk_keycloak_admin::KeycloakAdminClient;
+use opentalk_types_common::{
+    rooms::RoomId, shared_folders::SharedFolder, streaming::RoomStreamingTarget,
+};
+use snafu::Report;
+
+use crate::{
+    events::{enrich_from_keycloak, get_invited_mail_recipients_for_event, shared_folder_for_user},
+    services::{MailRecipient, MailService},
+};
+
+/// Provides information for event update notifications (e.g. via email)
+#[derive(Debug)]
+pub struct UpdateNotificationValues {
+    /// The tenant id
+    pub tenant: Tenant,
+    /// The user who has created the event
+    pub created_by: User,
+    /// The event that was updated
+    pub event: Event,
+    /// The event exception that was updated
+    pub event_exception: Option<EventException>,
+    /// The room of the updated event
+    pub room: Room,
+    /// The SIP configuration of the updated event
+    pub sip_config: Option<SipConfig>,
+    /// The users to notify about the update
+    pub users_to_notify: Vec<MailRecipient>,
+    /// The updated invite
+    pub invite_for_room: Invite,
+}
+
+/// Notifies the invitees of an event belonging to the specified room
+pub async fn notify_event_invitees_by_room_about_update(
+    kc_admin_client: &KeycloakAdminClient,
+    settings: &Settings,
+    mail_service: &MailService,
+    current_tenant: Tenant,
+    current_user: User,
+    conn: &mut DbConnection,
+    room_id: RoomId,
+) -> Result<(), CaptureApiError> {
+    let event = Event::get_for_room(conn, room_id).await?;
+
+    if let Some(event) = event {
+        let (
+            event,
+            _invite,
+            room,
+            sip_config,
+            _is_favorite,
+            shared_folder,
+            _tariff,
+            _training_participation_report,
+        ) = Event::get_with_related_items(conn, current_user.id, event.id).await?;
+
+        let shared_folder_for_user =
+            shared_folder_for_user(shared_folder, event.created_by, current_user.id);
+
+        let streaming_targets = get_room_streaming_targets(conn, room.id).await?;
+
+        notify_event_invitees_about_update(
+            kc_admin_client,
+            settings,
+            mail_service,
+            current_tenant,
+            current_user,
+            conn,
+            event,
+            room,
+            sip_config,
+            shared_folder_for_user,
+            streaming_targets,
+        )
+        .await?;
+    }
+    Ok(())
+}
+
+/// Notifies the invitees of an event about updates
+#[allow(clippy::too_many_arguments)]
+pub async fn notify_event_invitees_about_update(
+    kc_admin_client: &KeycloakAdminClient,
+    settings: &Settings,
+    mail_service: &MailService,
+    current_tenant: Tenant,
+    current_user: User,
+    conn: &mut DbConnection,
+    event: Event,
+    room: Room,
+    sip_config: Option<SipConfig>,
+    shared_folder_for_user: Option<SharedFolder>,
+    streaming_targets: Vec<RoomStreamingTarget>,
+) -> Result<(), CaptureApiError> {
+    let invited_users = get_invited_mail_recipients_for_event(conn, event.id).await?;
+    let current_user_mail_recipient = MailRecipient::Registered(current_user.clone().into());
+    let users_to_notify = invited_users
+        .into_iter()
+        .chain(std::iter::once(current_user_mail_recipient))
+        .collect::<Vec<_>>();
+    let invite_for_room =
+        Invite::get_first_or_create_for_room(conn, room.id, current_user.id).await?;
+    let created_by = if event.created_by == current_user.id {
+        current_user
+    } else {
+        User::get(conn, event.created_by).await?
+    };
+
+    let notification_values = UpdateNotificationValues {
+        tenant: current_tenant,
+        created_by,
+        event,
+        event_exception: None,
+        room,
+        sip_config,
+        users_to_notify,
+        invite_for_room,
+    };
+
+    notify_invitees_about_update(
+        settings,
+        notification_values,
+        mail_service,
+        kc_admin_client,
+        shared_folder_for_user,
+        streaming_targets,
+    )
+    .await;
+    Ok(())
+}
+
+/// Notifies the invitees of an event about updates
+pub async fn notify_invitees_about_update(
+    settings: &Settings,
+    notification_values: UpdateNotificationValues,
+    mail_service: &MailService,
+    kc_admin_client: &KeycloakAdminClient,
+    shared_folder: Option<SharedFolder>,
+    streaming_targets: Vec<RoomStreamingTarget>,
+) {
+    for user in notification_values.users_to_notify {
+        let invited_user =
+            enrich_from_keycloak(settings, user, &notification_values.tenant, kc_admin_client)
+                .await;
+
+        if let Err(e) = mail_service
+            .send_event_update(
+                notification_values.created_by.clone(),
+                notification_values.event.clone(),
+                notification_values.event_exception.clone(),
+                notification_values.room.clone(),
+                notification_values.sip_config.clone(),
+                invited_user,
+                notification_values.invite_for_room.id.to_string(),
+                shared_folder.clone(),
+                streaming_targets.clone(),
+            )
+            .await
+        {
+            log::error!(
+                "Failed to send event update with MailService, {}",
+                Report::from_error(e)
+            );
+        }
+    }
+}
diff --git a/crates/opentalk-db-storage/src/events/mod.rs b/crates/opentalk-db-storage/src/events/mod.rs
index f5d7d7d87c7271415499c1bd714f1b2a5000f578..b0d97c724a2f9f8d70d16fa93af272f73395d7ac 100644
--- a/crates/opentalk-db-storage/src/events/mod.rs
+++ b/crates/opentalk-db-storage/src/events/mod.rs
@@ -27,6 +27,7 @@ use opentalk_types_common::{
     sql_enum,
     tenants::TenantId,
     time::TimeZone,
+    training_participation_report::{TimeRange, TrainingParticipationReportParameterSet},
     users::UserId,
 };
 use redis_args::{FromRedisValue, ToRedisArgs};
@@ -36,8 +37,9 @@ use self::shared_folders::EventSharedFolder;
 use crate::{
     rooms::Room,
     schema::{
-        event_exceptions, event_favorites, event_invites, event_shared_folders, events, rooms,
-        sip_configs, tariffs, users,
+        event_exceptions, event_favorites, event_invites, event_shared_folders,
+        event_training_participation_report_parameter_sets, events, rooms, sip_configs, tariffs,
+        users,
     },
     sip_configs::SipConfig,
     tariffs::Tariff,
@@ -352,36 +354,43 @@ impl Event {
         bool,
         Option<EventSharedFolder>,
         Tariff,
+        Option<EventTrainingParticipationReportParameterSet>,
     )> {
-        let query = events::table
-            .left_join(
-                event_invites::table.on(event_invites::event_id
-                    .eq(events::id)
-                    .and(event_invites::invitee.eq(user_id))),
-            )
-            .left_join(
-                event_favorites::table.on(event_favorites::event_id
-                    .eq(events::id)
-                    .and(event_favorites::user_id.eq(user_id))),
-            )
-            .left_join(
-                event_shared_folders::table.on(event_shared_folders::event_id.eq(events::id)),
-            )
-            .inner_join(rooms::table.on(events::room.eq(rooms::id)))
-            .left_join(sip_configs::table.on(rooms::id.eq(sip_configs::room)))
-            .inner_join(users::table.on(users::id.eq(rooms::created_by)))
-            .inner_join(tariffs::table.on(tariffs::id.eq(users::tariff_id)))
-            .select((
-                events::all_columns,
-                event_invites::all_columns.nullable(),
-                rooms::all_columns,
-                sip_configs::all_columns.nullable(),
-                event_favorites::user_id.nullable().is_not_null(),
-                event_shared_folders::all_columns.nullable(),
-                tariffs::all_columns,
-            ))
-            .filter(events::id.eq(event_id));
-
+        let query =
+            events::table
+                .left_join(
+                    event_invites::table.on(event_invites::event_id
+                        .eq(events::id)
+                        .and(event_invites::invitee.eq(user_id))),
+                )
+                .left_join(
+                    event_favorites::table.on(event_favorites::event_id
+                        .eq(events::id)
+                        .and(event_favorites::user_id.eq(user_id))),
+                )
+                .left_join(
+                    event_shared_folders::table.on(event_shared_folders::event_id.eq(events::id)),
+                )
+                .inner_join(rooms::table.on(events::room.eq(rooms::id)))
+                .left_join(sip_configs::table.on(rooms::id.eq(sip_configs::room)))
+                .inner_join(users::table.on(users::id.eq(rooms::created_by)))
+                .inner_join(tariffs::table.on(tariffs::id.eq(users::tariff_id)))
+                .inner_join(
+                    event_training_participation_report_parameter_sets::table
+                        .on(event_training_participation_report_parameter_sets::event_id
+                            .eq(events::id)),
+                )
+                .select((
+                    events::all_columns,
+                    event_invites::all_columns.nullable(),
+                    rooms::all_columns,
+                    sip_configs::all_columns.nullable(),
+                    event_favorites::user_id.nullable().is_not_null(),
+                    event_shared_folders::all_columns.nullable(),
+                    tariffs::all_columns,
+                    event_training_participation_report_parameter_sets::all_columns.nullable(),
+                ))
+                .filter(events::id.eq(event_id));
         Ok(query.first(conn).await?)
     }
 
@@ -431,6 +440,7 @@ impl Event {
             bool,
             Option<EventSharedFolder>,
             Tariff,
+            Option<TrainingParticipationReportParameterSet>,
         )>,
     > {
         // Filter applied to all events which validates that the event is either created by
@@ -585,6 +595,8 @@ impl Event {
             } else {
                 vec![]
             };
+            // TODO: remove this
+            let training_participation_report_parameter_set = None;
 
             events_with_invite_room_and_exceptions.push((
                 event,
@@ -595,6 +607,7 @@ impl Event {
                 is_favorite,
                 shared_folders,
                 tariff,
+                training_participation_report_parameter_set,
             ));
         }
 
@@ -1132,3 +1145,139 @@ impl NewEventFavorite {
         }
     }
 }
+
+#[derive(Debug, Insertable, Queryable, Identifiable, Associations)]
+#[diesel(table_name = event_training_participation_report_parameter_sets)]
+#[diesel(primary_key(event_id))]
+#[diesel(belongs_to(Event, foreign_key = event_id))]
+pub struct EventTrainingParticipationReportParameterSet {
+    pub event_id: EventId,
+    pub initial_checkpoint_delay_after: i64,
+    pub initial_checkpoint_delay_within: i64,
+    pub checkpoint_interval_after: i64,
+    pub checkpoint_interval_within: i64,
+}
+
+impl EventTrainingParticipationReportParameterSet {
+    #[tracing::instrument(err, skip_all)]
+    pub async fn get_for_event(conn: &mut DbConnection, event_id: EventId) -> Result<Option<Self>> {
+        let parameter_set = event_training_participation_report_parameter_sets::table
+            .filter(event_training_participation_report_parameter_sets::event_id.eq(event_id))
+            .get_result(conn)
+            .await
+            .optional()?;
+
+        Ok(parameter_set)
+    }
+
+    /// Tries to insert the EventTrainingParticipationParameterSet into the database
+    ///
+    /// When yielding a unique key violation, None is returned.
+    #[tracing::instrument(err, skip_all)]
+    pub async fn try_insert(self, conn: &mut DbConnection) -> Result<Option<Self>> {
+        let query = self.insert_into(event_training_participation_report_parameter_sets::table);
+
+        let result = query.get_result(conn).await;
+
+        match result {
+            Ok(event_invite) => Ok(Some(event_invite)),
+            Err(diesel::result::Error::DatabaseError(
+                diesel::result::DatabaseErrorKind::UniqueViolation,
+                ..,
+            )) => Ok(None),
+            Err(e) => Err(e.into()),
+        }
+    }
+
+    #[tracing::instrument(err, skip_all)]
+    pub async fn delete_by_id(conn: &mut DbConnection, event_id: EventId) -> Result<()> {
+        let query = diesel::delete(
+            event_training_participation_report_parameter_sets::table
+                .filter(event_training_participation_report_parameter_sets::event_id.eq(event_id)),
+        );
+
+        query.execute(conn).await?;
+
+        Ok(())
+    }
+}
+
+impl From<EventTrainingParticipationReportParameterSet>
+    for TrainingParticipationReportParameterSet
+{
+    fn from(
+        EventTrainingParticipationReportParameterSet {
+            event_id: _,
+            initial_checkpoint_delay_after,
+            initial_checkpoint_delay_within,
+            checkpoint_interval_after,
+            checkpoint_interval_within,
+        }: EventTrainingParticipationReportParameterSet,
+    ) -> Self {
+        Self {
+            initial_checkpoint_delay: TimeRange {
+                after: u64::try_from(initial_checkpoint_delay_after).unwrap_or_default(),
+                within: u64::try_from(initial_checkpoint_delay_within).unwrap_or_default(),
+            },
+            checkpoint_interval: TimeRange {
+                after: u64::try_from(checkpoint_interval_after).unwrap_or_default(),
+                within: u64::try_from(checkpoint_interval_within).unwrap_or_default(),
+            },
+        }
+    }
+}
+
+#[derive(AsChangeset)]
+#[diesel(table_name = event_training_participation_report_parameter_sets)]
+pub struct UpdateEventTrainingParticipationReportParameterSet {
+    pub initial_checkpoint_delay_after: Option<i64>,
+    pub initial_checkpoint_delay_within: Option<i64>,
+    pub checkpoint_interval_after: Option<i64>,
+    pub checkpoint_interval_within: Option<i64>,
+}
+
+impl UpdateEventTrainingParticipationReportParameterSet {
+    /// Apply the update to the invite where `user_id` is the invitee
+    #[tracing::instrument(err, skip_all)]
+    pub async fn apply(
+        self,
+        conn: &mut DbConnection,
+        event_id: EventId,
+    ) -> Result<EventTrainingParticipationReportParameterSet> {
+        let query = diesel::update(event_training_participation_report_parameter_sets::table)
+            .filter(event_training_participation_report_parameter_sets::event_id.eq(event_id))
+            .set(self)
+            // change it here
+            .returning(event_training_participation_report_parameter_sets::all_columns);
+
+        let event_training_participation_report_parameter_sets = query.get_result(conn).await?;
+
+        Ok(event_training_participation_report_parameter_sets)
+    }
+}
+
+impl From<TrainingParticipationReportParameterSet>
+    for UpdateEventTrainingParticipationReportParameterSet
+{
+    fn from(
+        TrainingParticipationReportParameterSet {
+            initial_checkpoint_delay,
+            checkpoint_interval,
+        }: TrainingParticipationReportParameterSet,
+    ) -> Self {
+        Self {
+            initial_checkpoint_delay_after: Some(
+                i64::try_from(initial_checkpoint_delay.after).unwrap_or(i64::MAX),
+            ),
+            initial_checkpoint_delay_within: Some(
+                i64::try_from(initial_checkpoint_delay.within).unwrap_or(i64::MAX),
+            ),
+            checkpoint_interval_after: Some(
+                i64::try_from(checkpoint_interval.after).unwrap_or(i64::MAX),
+            ),
+            checkpoint_interval_within: Some(
+                i64::try_from(checkpoint_interval.within).unwrap_or(i64::MAX),
+            ),
+        }
+    }
+}
diff --git a/crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql b/crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql
new file mode 100644
index 0000000000000000000000000000000000000000..c9b3ed6f9e30799a7c4dd46f3785fb57668dfd09
--- /dev/null
+++ b/crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql
@@ -0,0 +1,7 @@
+CREATE TABLE event_training_participation_report_parameter_sets (
+    event_id UUID PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE NOT NULL,
+    initial_checkpoint_delay_after BIGINT NOT NULL,
+    initial_checkpoint_delay_within BIGINT NOT NULL,
+    checkpoint_interval_after BIGINT NOT NULL,
+    checkpoint_interval_within BIGINT NOT NULL
+);
diff --git a/crates/opentalk-db-storage/src/schema.rs b/crates/opentalk-db-storage/src/schema.rs
index f41661531b35bcd229d01cd8ef0f7ea61ecb7f9e..3e091cc2a6ea7ccbd14504820aa630cbf6c02b56 100644
--- a/crates/opentalk-db-storage/src/schema.rs
+++ b/crates/opentalk-db-storage/src/schema.rs
@@ -113,6 +113,18 @@ diesel::table! {
     }
 }
 
+diesel::table! {
+    use crate::sql_types::*;
+
+    event_training_participation_report_parameter_sets (event_id) {
+        event_id -> Uuid,
+        initial_checkpoint_delay_after -> Int8,
+        initial_checkpoint_delay_within -> Int8,
+        checkpoint_interval_after -> Int8,
+        checkpoint_interval_within -> Int8,
+    }
+}
+
 diesel::table! {
     use crate::sql_types::*;
 
@@ -384,6 +396,7 @@ diesel::joinable!(event_favorites -> events (event_id));
 diesel::joinable!(event_favorites -> users (user_id));
 diesel::joinable!(event_invites -> events (event_id));
 diesel::joinable!(event_shared_folders -> events (event_id));
+diesel::joinable!(event_training_participation_report_parameter_sets -> events (event_id));
 diesel::joinable!(events -> rooms (room));
 diesel::joinable!(events -> tenants (tenant_id));
 diesel::joinable!(external_tariffs -> tariffs (tariff_id));
@@ -413,6 +426,7 @@ diesel::allow_tables_to_appear_in_same_query!(
     event_favorites,
     event_invites,
     event_shared_folders,
+    event_training_participation_report_parameter_sets,
     events,
     external_tariffs,
     groups,
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/lib.rs b/crates/opentalk-signaling-module-training-participation-report/src/lib.rs
index 2361ae8d376217bf03a7fa0ec9518529f1737518..d9f58596c5c3948a9934426c61353503e392e14a 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/lib.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/lib.rs
@@ -52,6 +52,7 @@ use opentalk_types_common::{
     modules::ModuleId,
     rooms::RoomId,
     time::{TimeZone, Timestamp},
+    training_participation_report::TimeRange,
     users::{DisplayName, UserId},
 };
 use opentalk_types_signaling::ParticipantId;
@@ -63,7 +64,7 @@ use opentalk_types_signaling_training_participation_report::{
         PresenceLoggingStartedReason, TrainingParticipationReportEvent,
     },
     state::{ParticipationLoggingState, TrainingParticipationReportState},
-    TimeRange, MODULE_ID,
+    MODULE_ID,
 };
 use rand::Rng as _;
 use snafu::{Report, ResultExt as _};
@@ -209,6 +210,8 @@ impl TrainingParticipationReport {
             .get_recorded_presence_state(self.room, self.participant)
             .await?;
 
+        let parameter_set = None;
+
         if control_data.is_room_owner {
             let (other_room_owners, trainees) = self
                 .load_already_present_room_owners_and_trainees(
@@ -225,8 +228,17 @@ impl TrainingParticipationReport {
                 // Don't ask the room owner for confirmation
                 state = ParticipationLoggingState::Enabled;
             }
+
+            *frontend_data = Some(TrainingParticipationReportState {
+                state,
+                parameter_set,
+            });
+        } else {
+            *frontend_data = Some(TrainingParticipationReportState {
+                state,
+                parameter_set: None,
+            });
         }
-        *frontend_data = Some(TrainingParticipationReportState { state });
         Ok(())
     }
 
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs
index 3f0a19e1bcc163b53ffe27a377a44e77e265b5c6..655a0e75c191a614f29c19f54aff984e20d1f808 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs
@@ -19,11 +19,9 @@ mod test_common {
     use std::collections::{BTreeMap, BTreeSet};
 
     use opentalk_signaling_core::SignalingModuleError;
-    use opentalk_types_common::rooms::RoomId;
+    use opentalk_types_common::{rooms::RoomId, training_participation_report::TimeRange};
     use opentalk_types_signaling::ParticipantId;
-    use opentalk_types_signaling_training_participation_report::{
-        state::ParticipationLoggingState, TimeRange,
-    };
+    use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState;
     use pretty_assertions::assert_eq;
 
     use super::TrainingParticipationReportStorage;
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs
index 2c7a0e1538ec473d7c5734ecf387eee8ee422946..9bf44eddd92dc036821f8fe2f60810b98fa39c01 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs
@@ -6,11 +6,11 @@ use std::collections::{BTreeMap, BTreeSet};
 
 use async_trait::async_trait;
 use opentalk_signaling_core::{NotFoundSnafu, RedisConnection, RedisSnafu, SignalingModuleError};
-use opentalk_types_common::{rooms::RoomId, time::Timestamp};
-use opentalk_types_signaling::ParticipantId;
-use opentalk_types_signaling_training_participation_report::{
-    state::ParticipationLoggingState, TimeRange,
+use opentalk_types_common::{
+    rooms::RoomId, time::Timestamp, training_participation_report::TimeRange,
 };
+use opentalk_types_signaling::ParticipantId;
+use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState;
 use redis::{AsyncCommands, ExistenceCheck, SetOptions};
 use redis_args::{FromRedisValue, ToRedisArgs};
 use serde::{Deserialize, Serialize};
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs
index 16e734e91412e659957c6a53d5f75aa20fad5dfb..ce960b89af7134d8fca90c028bad83b2fe8a739f 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs
@@ -4,9 +4,8 @@
 
 use std::collections::BTreeSet;
 
-use opentalk_types_common::time::Timestamp;
+use opentalk_types_common::{time::Timestamp, training_participation_report::TimeRange};
 use opentalk_types_signaling::ParticipantId;
-use opentalk_types_signaling_training_participation_report::TimeRange;
 
 use super::{Checkpoint, TrainingReportState};
 
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs
index 83648365e2c67a588aaec2afc210da0546fb02f8..1d0f5d87cf1b5ee42ad0ecafac9f2138c5b5e433 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs
@@ -6,11 +6,11 @@ use std::collections::BTreeSet;
 
 use async_trait::async_trait;
 use opentalk_signaling_core::SignalingModuleError;
-use opentalk_types_common::{rooms::RoomId, time::Timestamp};
-use opentalk_types_signaling::ParticipantId;
-use opentalk_types_signaling_training_participation_report::{
-    state::ParticipationLoggingState, TimeRange,
+use opentalk_types_common::{
+    rooms::RoomId, time::Timestamp, training_participation_report::TimeRange,
 };
+use opentalk_types_signaling::ParticipantId;
+use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState;
 
 use super::{RoomState, TrainingReportState};
 
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs
index 998f2ff60f1fc0d97fb41173100f580d30ec0402..2b2b4d85c7315e743a005a478d27e59d013db102 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs
@@ -5,11 +5,11 @@
 use std::collections::{BTreeMap, BTreeSet};
 
 use opentalk_signaling_core::{NotFoundSnafu, SignalingModuleError};
-use opentalk_types_common::{rooms::RoomId, time::Timestamp};
-use opentalk_types_signaling::ParticipantId;
-use opentalk_types_signaling_training_participation_report::{
-    state::ParticipationLoggingState, TimeRange,
+use opentalk_types_common::{
+    rooms::RoomId, time::Timestamp, training_participation_report::TimeRange,
 };
+use opentalk_types_signaling::ParticipantId;
+use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState;
 use snafu::{ensure_whatever, OptionExt as _};
 
 use crate::storage::{Checkpoint, RoomState, TrainingReportState};
diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs
index 40355229aabc37167ffb3e6b52dbbe6066900567..95ebf1e25c8613c550b361cccb7529d449ed8096 100644
--- a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs
+++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs
@@ -9,11 +9,11 @@ use std::{
 
 use async_trait::async_trait;
 use opentalk_signaling_core::{SignalingModuleError, VolatileStaticMemoryStorage};
-use opentalk_types_common::{rooms::RoomId, time::Timestamp};
-use opentalk_types_signaling::ParticipantId;
-use opentalk_types_signaling_training_participation_report::{
-    state::ParticipationLoggingState, TimeRange,
+use opentalk_types_common::{
+    rooms::RoomId, time::Timestamp, training_participation_report::TimeRange,
 };
+use opentalk_types_signaling::ParticipantId;
+use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState;
 use parking_lot::RwLock;
 
 use super::memory::TrainingParticipationReportState;
diff --git a/deny.toml b/deny.toml
index 8c842641410e3a6e980b8bec2ff7404bce81c183..9473a9cc33f8ce6cb2b91382633574e8c12a112b 100644
--- a/deny.toml
+++ b/deny.toml
@@ -319,7 +319,6 @@ skip = [
   { name = "indexmap", version = "1" },
   { name = "itertools", version = "0.10" },
   { name = "itertools", version = "0.11" },
-  { name = "itertools", version = "0.12" },
   { name = "jiff", version = "0.1" },
   { name = "linux-raw-sys", version = "0.4" },
   { name = "p256", version = "0.11" },
diff --git a/docs/developer/database.md b/docs/developer/database.md
index 8783ef729fd83c864611d7d9adff6392a7ced347..b885582a5ab678a062d8631bdf831b384d6bae7f 100644
--- a/docs/developer/database.md
+++ b/docs/developer/database.md
@@ -75,6 +75,13 @@ event_shared_folders {
     text write_share_id
     text write_url
 }
+event_training_participation_report_parameter_sets {
+    uuid event_id PK,FK
+    bigint checkpoint_interval_after
+    bigint checkpoint_interval_within
+    bigint initial_checkpoint_delay_after
+    bigint initial_checkpoint_delay_within
+}
 events {
     uuid id PK
     uuid created_by FK
@@ -241,6 +248,7 @@ event_invites }o--|| events: ""
 event_invites }o--|| users: ""
 event_invites }o--|| users: ""
 event_shared_folders |o--|| events: ""
+event_training_participation_report_parameter_sets |o--|| events: ""
 events }o--|| rooms: ""
 events }o--|| users: ""
 events }o--|| users: ""