diff --git a/flowdrop_node_session.module b/flowdrop_node_session.module
index 79aadad..6137c90 100644
--- a/flowdrop_node_session.module
+++ b/flowdrop_node_session.module
@@ -11,6 +11,9 @@
 
 declare(strict_types=1);
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\flowdrop_node_session\Hook\FlowdropNodeSessionHooks;
+use Drupal\Core\Url;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface;
 
@@ -21,19 +24,9 @@ use Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface;
  * These provide fullscreen HTML and minimal page templates to remove Drupal's
  * default page chrome (toolbar, admin menu, etc.) for an immersive experience.
  */
+#[LegacyHook]
 function flowdrop_node_session_theme(): array {
-  return [
-    "html__flowdrop_entity_playground_fullscreen" => [
-      "render element" => "elements",
-      "base hook" => "html",
-      "template" => "html--flowdrop-entity-playground-fullscreen",
-    ],
-    "page__flowdrop_entity_playground" => [
-      "render element" => "elements",
-      "base hook" => "page",
-      "template" => "page--flowdrop-entity-playground",
-    ],
-  ];
+  return \Drupal::service(FlowdropNodeSessionHooks::class)->theme();
 }
 
 /**
@@ -43,11 +36,10 @@ function flowdrop_node_session_theme(): array {
  * This removes Drupal's default page chrome (toolbar, admin menu, etc.)
  * to provide an immersive fullscreen experience.
  */
-function flowdrop_node_session_theme_suggestions_html_alter(array &$suggestions, array $variables): void {
-  $route_name = \Drupal::routeMatch()->getRouteName();
-  if ($route_name === "flowdrop_node_session.playground.entity") {
-    $suggestions[] = "html__flowdrop_entity_playground_fullscreen";
-  }
+#[LegacyHook]
+function flowdrop_node_session_theme_suggestions_html_alter(array &$suggestions, array $variables): void
+{
+    \Drupal::service(FlowdropNodeSessionHooks::class)->themeSuggestionsHtmlAlter($suggestions, $variables);
 }
 
 /**
@@ -56,11 +48,10 @@ function flowdrop_node_session_theme_suggestions_html_alter(array &$suggestions,
  * Suggests a minimal page template for the entity context playground route.
  * This renders only the content without local actions, breadcrumbs, etc.
  */
-function flowdrop_node_session_theme_suggestions_page_alter(array &$suggestions, array $variables): void {
-  $route_name = \Drupal::routeMatch()->getRouteName();
-  if ($route_name === "flowdrop_node_session.playground.entity") {
-    $suggestions[] = "page__flowdrop_entity_playground";
-  }
+#[LegacyHook]
+function flowdrop_node_session_theme_suggestions_page_alter(array &$suggestions, array $variables): void
+{
+    \Drupal::service(FlowdropNodeSessionHooks::class)->themeSuggestionsPageAlter($suggestions, $variables);
 }
 
 /**
@@ -70,41 +61,10 @@ function flowdrop_node_session_theme_suggestions_page_alter(array &$suggestions,
  * Operations are defined in the module settings at
  * /admin/config/flowdrop/node-session.
  */
-function flowdrop_node_session_entity_operation(EntityInterface $entity): array {
-  $operations = [];
-
-  // Only add operations for nodes.
-  if ($entity->getEntityTypeId() !== 'node') {
-    return $operations;
-  }
-
-  // Check if user has permission to create playground sessions.
-  if (!\Drupal::currentUser()->hasPermission('create flowdrop_session')) {
-    return $operations;
-  }
-
-  // Load configured entity operations.
-  $config = \Drupal::config('flowdrop_node_session.settings');
-  $entity_operations = $config->get('entity_operations') ?: [];
-
-  foreach ($entity_operations as $index => $operation) {
-    $url = \Drupal\Core\Url::fromRoute('flowdrop_node_session.playground.entity', [
-      'workflow_id' => $operation['workflow_id'],
-    ], [
-      'query' => [
-        'entity_type' => 'node',
-        'entity_id' => $entity->id(),
-      ],
-    ]);
-
-    $operations['flowdrop_' . $operation['workflow_id']] = [
-      'title' => t('@label', ['@label' => $operation['label']]),
-      'url' => $url,
-      'weight' => 50 + $index,
-    ];
-  }
-
-  return $operations;
+#[LegacyHook]
+function flowdrop_node_session_entity_operation(EntityInterface $entity): array
+{
+    return \Drupal::service(FlowdropNodeSessionHooks::class)->entityOperation($entity);
 }
 
 /**
@@ -114,14 +74,8 @@ function flowdrop_node_session_entity_operation(EntityInterface $entity): array
  * entity context configured. This allows EntityContext nodes in workflows to
  * receive the pre-loaded entity data via initialData.
  */
-function flowdrop_node_session_entity_presave(EntityInterface $entity): void {
-  // Only process playground message entities.
-  if (!$entity instanceof FlowDropSessionMessageInterface) {
-    return;
-  }
-
-  // Get the injector service and inject entity context.
-  /** @var \Drupal\flowdrop_node_session\EventSubscriber\EntityContextInjectorSubscriber $injector */
-  $injector = \Drupal::service("flowdrop_node_session.entity_context_injector");
-  $injector->injectEntityContext($entity);
+#[LegacyHook]
+function flowdrop_node_session_entity_presave(EntityInterface $entity): void
+{
+    \Drupal::service(FlowdropNodeSessionHooks::class)->entityPresave($entity);
 }
diff --git a/flowdrop_node_session.services.yml b/flowdrop_node_session.services.yml
index 740609f..f23701a 100644
--- a/flowdrop_node_session.services.yml
+++ b/flowdrop_node_session.services.yml
@@ -18,3 +18,7 @@ services:
       - "@entity_type.manager"
     tags:
       - { name: event_subscriber }
+
+  Drupal\flowdrop_node_session\Hook\FlowdropNodeSessionHooks:
+    class: Drupal\flowdrop_node_session\Hook\FlowdropNodeSessionHooks
+    autowire: true
diff --git a/src/Hook/FlowdropNodeSessionHooks.php b/src/Hook/FlowdropNodeSessionHooks.php
new file mode 100644
index 0000000..1f1b1b4
--- /dev/null
+++ b/src/Hook/FlowdropNodeSessionHooks.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Drupal\flowdrop_node_session\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for flowdrop_node_session.
+ */
+class FlowdropNodeSessionHooks
+{
+    use StringTranslationTrait;
+    /**
+     * Implements hook_theme().
+     *
+     * Registers custom theme implementations for the entity context playground.
+     * These provide fullscreen HTML and minimal page templates to remove Drupal's
+     * default page chrome (toolbar, admin menu, etc.) for an immersive experience.
+     */
+    #[Hook('theme')]
+    public static function theme(): array
+    {
+        return [
+            "html__flowdrop_entity_playground_fullscreen" => [
+                "render element" => "elements",
+                "base hook" => "html",
+                "template" => "html--flowdrop-entity-playground-fullscreen",
+            ],
+            "page__flowdrop_entity_playground" => [
+                "render element" => "elements",
+                "base hook" => "page",
+                "template" => "page--flowdrop-entity-playground",
+            ],
+        ];
+    }
+    /**
+     * Implements hook_theme_suggestions_html_alter().
+     *
+     * Suggests a fullscreen HTML template for the entity context playground route.
+     * This removes Drupal's default page chrome (toolbar, admin menu, etc.)
+     * to provide an immersive fullscreen experience.
+     */
+    #[Hook('theme_suggestions_html_alter')]
+    public static function themeSuggestionsHtmlAlter(array &$suggestions, array $variables): void
+    {
+        $route_name = \Drupal::routeMatch()->getRouteName();
+        if ($route_name === "flowdrop_node_session.playground.entity") {
+            $suggestions[] = "html__flowdrop_entity_playground_fullscreen";
+        }
+    }
+    /**
+     * Implements hook_theme_suggestions_page_alter().
+     *
+     * Suggests a minimal page template for the entity context playground route.
+     * This renders only the content without local actions, breadcrumbs, etc.
+     */
+    #[Hook('theme_suggestions_page_alter')]
+    public static function themeSuggestionsPageAlter(array &$suggestions, array $variables): void
+    {
+        $route_name = \Drupal::routeMatch()->getRouteName();
+        if ($route_name === "flowdrop_node_session.playground.entity") {
+            $suggestions[] = "page__flowdrop_entity_playground";
+        }
+    }
+    /**
+     * Implements hook_entity_operation().
+     *
+     * Adds configured FlowDrop workflow operations to nodes in the content list.
+     * Operations are defined in the module settings at
+     * /admin/config/flowdrop/node-session.
+     */
+    #[Hook('entity_operation')]
+    public function entityOperation(\Drupal\Core\Entity\EntityInterface $entity): array
+    {
+        $operations = [
+        ];
+        // Only add operations for nodes.
+        if ($entity->getEntityTypeId() !== 'node') {
+            return $operations;
+        }
+        // Check if user has permission to create playground sessions.
+        if (!\Drupal::currentUser()->hasPermission('create flowdrop_session')) {
+            return $operations;
+        }
+        // Load configured entity operations.
+        $config = \Drupal::config('flowdrop_node_session.settings');
+        $entity_operations = $config->get('entity_operations') ?: [
+        ];
+        foreach ($entity_operations as $index => $operation) {
+            $url = \Drupal\Core\Url::fromRoute('flowdrop_node_session.playground.entity', [
+                'workflow_id' => $operation['workflow_id'],
+            ], [
+                'query' => [
+                    'entity_type' => 'node',
+                    'entity_id' => $entity->id(),
+                ],
+            ]);
+            $operations['flowdrop_' . $operation['workflow_id']] = [
+                'title' => $this->t('@label', [
+                    '@label' => $operation['label'],
+                ]),
+                'url' => $url,
+                'weight' => 50 + $index,
+            ];
+        }
+        return $operations;
+    }
+    /**
+     * Implements hook_entity_presave().
+     *
+     * Injects entity context data into playground messages for sessions that have
+     * entity context configured. This allows EntityContext nodes in workflows to
+     * receive the pre-loaded entity data via initialData.
+     */
+    #[Hook('entity_presave')]
+    public static function entityPresave(\Drupal\Core\Entity\EntityInterface $entity): void
+    {
+        // Only process playground message entities.
+        if (!$entity instanceof \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface) {
+            return;
+        }
+        // Get the injector service and inject entity context.
+        /** @var \Drupal\flowdrop_node_session\EventSubscriber\EntityContextInjectorSubscriber $injector */
+        $injector = \Drupal::service("flowdrop_node_session.entity_context_injector");
+        $injector->injectEntityContext($entity);
+    }
+}
