diff --git a/entity_workflow.module b/entity_workflow.module
index f08cd24..8c0478b 100644
--- a/entity_workflow.module
+++ b/entity_workflow.module
@@ -1,5 +1,7 @@
 <?php
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\entity_workflow\Hook\EntityWorkflowHooks;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityInterface;
@@ -22,80 +24,34 @@ const SYSTEM_TRANSITION = '__system';
 /**
  * Implements hook_field_info_alter().
  */
+#[LegacyHook]
 function entity_workflow_field_info_alter(&$info) {
-  // Add temporary BC field type for sites that are using the core patch.
-  // @see https://www.drupal.org/project/drupal/issues/2835545
-  if (!isset($info['workflow_state']) && isset($info['entity_workflow_state'])) {
-    $info['workflow_state'] = $info['entity_workflow_state'];
-    $info['workflow_state']['id'] = 'workflow_state';
-  }
+  \Drupal::service(EntityWorkflowHooks::class)->fieldInfoAlter($info);
 }
 
 /**
  * Implements hook_entity_base_field_info().
  */
+#[LegacyHook]
 function entity_workflow_entity_base_field_info(EntityTypeInterface $entity_type) {
-  if ($workflows = \Drupal::service('entity_workflow.info')->getWorkflowsInfoForEntityType($entity_type->id())) {
-    $fields = [];
-    foreach ($workflows as $workflow_id => $workflow_label) {
-      $field_name = entity_workflow_get_field_name($workflow_id);
-      $fields[$field_name] = BaseFieldDefinition::create('entity_workflow_state')
-        ->setLabel(t('Entity workflow: @label', ['@label' => $workflow_label]))
-        ->setDescription(t('The workflow state of this entity for the @label workflow.', ['@label' => $workflow_label]))
-        ->setTranslatable(TRUE)
-        ->setRevisionable(TRUE)
-        ->setSetting('workflow', $workflow_id)
-        ->setDisplayOptions('view', [
-          'region' => 'hidden',
-        ]);
-    }
-
-    return $fields;
-  }
+  return \Drupal::service(EntityWorkflowHooks::class)->entityBaseFieldInfo($entity_type);
 }
 
 /**
  * Implements hook_entity_bundle_field_info().
  */
+#[LegacyHook]
 function entity_workflow_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
-  if ($workflows = \Drupal::service('entity_workflow.info')->getWorkflowsInfoForEntityType($entity_type->id())) {
-    foreach ($workflows as $workflow_id => $workflow_label) {
-      $field_name = entity_workflow_get_field_name($workflow_id);
-      if (isset($base_field_definitions[$field_name])) {
-        // Add the target bundle to the workflow state field. Since each bundle
-        // can be attached to a different workflow, adding this information to
-        // the field definition allows the associated workflow to be derived
-        // where a field definition is present.
-        $base_field_definitions[$field_name]->setTargetBundle($bundle);
-        return [
-          $field_name => $base_field_definitions[$field_name],
-        ];
-      }
-    }
-  }
+  return \Drupal::service(EntityWorkflowHooks::class)->entityBundleFieldInfo($entity_type, $bundle, $base_field_definitions);
 }
 
 /**
  * Implements hook_entity_bundle_info_alter().
  */
-function entity_workflow_entity_bundle_info_alter(&$bundles) {
-  /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
-  $entity_workflow_info = \Drupal::service('entity_workflow.info');
-
-  foreach ($entity_workflow_info->getWorkflowEntities() as $workflow_id => $workflow) {
-    /** @var \Drupal\entity_workflow\WorkflowType\EntityWorkflowTypeInterface $workflow_plugin */
-    $workflow_plugin = $workflow->getTypePlugin();
-
-    foreach ($workflow_plugin->getEntityTypes() as $entity_type_id) {
-      if (isset($bundles[$entity_type_id])) {
-        foreach (array_keys($bundles[$entity_type_id]) as $bundle_id) {
-          if ($workflow_plugin->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
-            $bundles[$entity_type_id][$bundle_id]['entity_workflows'][$workflow_id] = $workflow->label();
-          }
-        }
-      }
-    }
-  }
+#[LegacyHook]
+function entity_workflow_entity_bundle_info_alter(&$bundles)
+{
+    \Drupal::service(EntityWorkflowHooks::class)->entityBundleInfoAlter($bundles);
 }
 
 /**
@@ -103,46 +59,19 @@ function entity_workflow_entity_bundle_info_alter(&$bundles) {
  *
  * Installs the workflow state base field when a new workflow is created.
  */
-function entity_workflow_workflow_insert(WorkflowInterface $workflow) {
-  /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
-  $entity_workflow_info = \Drupal::service('entity_workflow.info');
-
-  if ($entity_workflow_info->isEntityWorkflow($workflow)) {
-    /** @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_manager */
-    $entity_definition_manager = \Drupal::service('entity.definition_update_manager');
-    $storage_definition = BaseFieldDefinition::create('entity_workflow_state')
-      ->setLabel(t('Entity workflow: @label', ['@label' => $workflow->label()]))
-      ->setDescription(t('The workflow state of this entity for the @label workflow.', ['@label' => $workflow->label()]))
-      ->setTranslatable(TRUE)
-      ->setRevisionable(TRUE);
-
-    foreach ($workflow->getTypePlugin()->getEntityTypes() as $entity_type_id) {
-      $field_name = entity_workflow_get_field_name($workflow->id());
-      $entity_definition_manager->installFieldStorageDefinition($field_name, $entity_type_id, 'entity_workflow', $storage_definition);
-    }
-
-    // When a workflow is added, the router needs to be rebuilt to add the
-    // corresponding tabs and local actions.
-    \Drupal::service('router.builder')->setRebuildNeeded();
-
-    // The bundle info cache also needs to be cleared in order to take the new
-    // workflow into account when field definitions are rebuilt.
-    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
-  }
+#[LegacyHook]
+function entity_workflow_workflow_insert(WorkflowInterface $workflow)
+{
+    \Drupal::service(EntityWorkflowHooks::class)->workflowInsert($workflow);
 }
 
 /**
  * Implements hook_ENTITY_TYPE_update() for the 'workflow' entity type.
  */
-function entity_workflow_workflow_update(WorkflowInterface $workflow) {
-  /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
-  $entity_workflow_info = \Drupal::service('entity_workflow.info');
-
-  if ($entity_workflow_info->isEntityWorkflow($workflow)) {
-    // When a workflow is updated, the router needs to be rebuilt to account for
-    // possible changes to menu local tasks and local actions.
-    \Drupal::service('router.builder')->setRebuildNeeded();
-  }
+#[LegacyHook]
+function entity_workflow_workflow_update(WorkflowInterface $workflow)
+{
+    \Drupal::service(EntityWorkflowHooks::class)->workflowUpdate($workflow);
 }
 
 /**
@@ -150,71 +79,28 @@ function entity_workflow_workflow_update(WorkflowInterface $workflow) {
  *
  * Uninstalls the workflow_state base field when a workflow is deleted.
  */
-function entity_workflow_workflow_delete(WorkflowInterface $workflow) {
-  /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
-  $entity_workflow_info = \Drupal::service('entity_workflow.info');
-
-  if ($entity_workflow_info->isEntityWorkflow($workflow)) {
-    /** @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_manager */
-    $entity_definition_manager = \Drupal::service('entity.definition_update_manager');
-
-    foreach ($workflow->getTypePlugin()->getEntityTypes() as $entity_type_id) {
-      $field_name = entity_workflow_get_field_name($workflow->id());
-      if ($storage_definition = $entity_definition_manager->getFieldStorageDefinition($field_name, $entity_type_id)) {
-        $entity_definition_manager->uninstallFieldStorageDefinition($storage_definition);
-      }
-    }
-
-    // When a workflow is deleted, the router needs to be rebuilt to add the
-    // corresponding tabs and local actions.
-    \Drupal::service('router.builder')->setRebuildNeeded();
-  }
+#[LegacyHook]
+function entity_workflow_workflow_delete(WorkflowInterface $workflow)
+{
+    \Drupal::service(EntityWorkflowHooks::class)->workflowDelete($workflow);
 }
 
 /**
  * Implements hook_entity_delete().
  */
-function entity_workflow_entity_delete(EntityInterface $entity) {
-  $workflow_transition_log_storage = \Drupal::entityTypeManager()->getStorage('workflow_transition_log');
-
-  // Remove transition logs when a workspace is deleted.
-  if ($entity->getEntityTypeId() === 'workspace') {
-    $result = $workflow_transition_log_storage->getQuery()
-      ->condition('workspace_id', $entity->id())
-      ->accessCheck(FALSE)
-      ->execute();
-
-    $logs = $workflow_transition_log_storage->loadMultiple($result);
-    $workflow_transition_log_storage->delete($logs);
-  }
-
-  if (!\Drupal::service('entity_workflow.info')->isEntityTypeSupported($entity->getEntityTypeId())) {
-    return;
-  }
-
-  // Remove transition logs when a supported entity is deleted.
-  $logs = entity_workflow_get_history($entity->getEntityTypeId(), [$entity->id()]);
-  $workflow_transition_log_storage->delete($logs);
+#[LegacyHook]
+function entity_workflow_entity_delete(EntityInterface $entity)
+{
+    \Drupal::service(EntityWorkflowHooks::class)->entityDelete($entity);
 }
 
 /**
  * Implements hook_entity_revision_delete().
  */
-function entity_workflow_entity_revision_delete(EntityInterface $entity) {
-  /** @var \Drupal\Core\Entity\RevisionableInterface $entity */
-  if (!\Drupal::service('entity_workflow.info')->isEntityTypeSupported($entity->getEntityTypeId())) {
-    return;
-  }
-
-  // Remove transition logs when a supported entity revision is deleted.
-  $workflow_transition_log_storage = \Drupal::entityTypeManager()->getStorage('workflow_transition_log');
-  $result = $workflow_transition_log_storage->getQuery()
-    ->condition('entity_revision_id', $entity->getRevisionId())
-    ->accessCheck(FALSE)
-    ->execute();
-
-  $logs = $workflow_transition_log_storage->loadMultiple($result);
-  $workflow_transition_log_storage->delete($logs);
+#[LegacyHook]
+function entity_workflow_entity_revision_delete(EntityInterface $entity)
+{
+    \Drupal::service(EntityWorkflowHooks::class)->entityRevisionDelete($entity);
 }
 
 /**
@@ -233,156 +119,50 @@ function entity_workflow_module_implements_alter(&$implementations, $hook) {
 /**
  * Implements hook_entity_presave().
  */
+#[LegacyHook]
 function entity_workflow_entity_presave(EntityInterface $entity) {
-  /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
-  // Ensure that only changing the workflow state of an entity does not create
-  // a new revision.
-  if (isset($entity->_entityWorkflowEnforceNoNewRevision)) {
-    $entity->setNewRevision(FALSE);
-  }
+  \Drupal::service(EntityWorkflowHooks::class)->entityPresave($entity);
 }
 
 /**
  * Implements hook_ENTITY_TYPE_view_alter() for the 'workspace' entity type.
  */
+#[LegacyHook]
 function entity_workflow_workspace_view_alter(array &$build, EntityInterface $workspace, EntityViewDisplayInterface $display) {
-  if ($build['#view_mode'] == 'full') {
-    /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
-    $entity_workflow_info = \Drupal::service('entity_workflow.info');
-
-    $header = &$build['changes']['list']['#header'];
-
-    // Ensure that we take into account only the revisions that are displayed
-    // on the page.
-    $tracked_entities = [];
-    foreach (Element::children($build['changes']['list']) as $key) {
-      [$entity_type_id, $entity_id] = explode(':', $key, 2);
-      $revision_id = $build['changes']['list'][$key]['#entity']->getRevisionId();
-      $tracked_entities[$entity_type_id][$revision_id] = $entity_id;
-    }
-
-    // Add entity workflow state information to the workspace manage page.
-    foreach ($tracked_entities as $entity_type_id => $entity_ids) {
-      if (!$entity_workflow_info->isEntityTypeSupported($entity_type_id)) {
-        continue;
-      }
-
-      // Gather the latest transition logs for every entity workflow of this
-      // entity type.
-      foreach (array_keys($entity_workflow_info->getWorkflowsInfoForEntityType($entity_type_id)) as $workflow_id) {
-        $latest_transition_history[$workflow_id] = entity_workflow_get_history($entity_type_id, $entity_ids, $workflow_id, $workspace->id(), TRUE);
-      }
-
-      foreach ($entity_ids as $entity_id) {
-        $render = &$build['changes']['list'][$entity_type_id . ':' . $entity_id];
-
-        /** @var \Drupal\Core\Entity\ContentEntityInterface $tracked_entity */
-        $tracked_entity = $render['#entity'];
-
-        foreach ($entity_workflow_info->getWorkflowsInfoForEntityType($entity_type_id) as $workflow_id => $workflow_label) {
-          $workflow_render_key = $workflow_id . '_workflow';
-
-          // Insert a new header column before 'Operations';
-          $workflow_header = [
-            $workflow_render_key => t('@label workflow', ['@label' => $workflow_label]),
-          ];
-          $pos = (int) array_search('operations', array_keys($header)) ?: count($header);
-          $header = array_merge(array_slice($header, 0, $pos), $workflow_header, array_slice($header, $pos));
-
-          // Link to the workflow state form if the user has access to it.
-          $workflow_field = entity_workflow_get_field($tracked_entity, $workflow_id);
-          $label = $workflow_field->getStateLabel();
-
-          // Look up the route name to make sure it exists.
-          try {
-            $url = Url::fromRoute("entity.$entity_type_id.workflow", [
-              $entity_type_id => $tracked_entity->id(),
-              'workflow' => $workflow_id,
-            ],
-            [
-              'query' => ['destination' => \Drupal::request()->getRequestUri()],
-            ]);
-          }
-          catch (\Exception $exception) {
-            $url = NULL;
-          }
-
-          // Entities given the state by default might not have a uid or
-          // timestamp so be cautious.
-          if (isset($latest_transition_history[$workflow_id][$tracked_entity->id()])) {
-            $transition_log = $latest_transition_history[$workflow_id][$tracked_entity->id()];
-            $state_summary = [
-              '#theme' => 'entity_workflow_state_summary',
-              '#state' => $label,
-              '#url' => $url,
-              '#url_access' => $url && $url->access(),
-              '#user' => [
-                '#theme' => 'username',
-                '#account' => $transition_log->uid->entity,
-              ],
-              '#date' => $transition_log->getCreatedTime(),
-            ];
-          }
-          else {
-            $state_summary = [
-              '#markup' => $label,
-            ];
-          }
-
-          // Insert a new column before 'Operations';
-          $pos = (int) array_search('operations', array_keys($render)) ?: count($header);
-          $workflow_data = [$workflow_render_key => $state_summary];
-          $render = array_merge(array_slice($render, 0, $pos), $workflow_data, array_slice($render, $pos));
-        }
-      }
-    }
-  }
+  \Drupal::service(EntityWorkflowHooks::class)->workspaceViewAlter($build, $workspace, $display);
 }
 
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function entity_workflow_theme() {
-  return [
-    'entity_workflow_state_summary' => [
-      'variables' => [
-        'state' => NULL,
-        'url' => NULL,
-        'url_access' => NULL,
-        'user' => NULL,
-        'date' => NULL,
-      ],
-      'template' => 'entity-workflow-state-summary',
-    ],
-  ];
+  return \Drupal::service(EntityWorkflowHooks::class)->theme();
 }
 
 /**
  * Implements hook_views_data_alter().
  */
-function entity_workflow_views_data_alter(&$data) {
-  // Use our custom filter for the source entity ID of transition logs, which
-  // can be either numeric or string.
-  $data['workflow_transition_log']['entity_id']['argument']['id'] = 'workflow_transition_log_source_entity_id';
-  $data['workflow_transition_log']['entity_id_string']['argument']['id'] = 'workflow_transition_log_source_entity_id';
+#[LegacyHook]
+function entity_workflow_views_data_alter(&$data)
+{
+    \Drupal::service(EntityWorkflowHooks::class)->viewsDataAlter($data);
 }
 
 /**
  * Implements hook_field_widget_info_alter().
  */
+#[LegacyHook]
 function entity_workflow_field_widget_info_alter(array &$info) {
-  if (isset($info['options_select'])) {
-    $info['options_select']['field_types'][] = 'entity_workflow_state';
-  }
+  \Drupal::service(EntityWorkflowHooks::class)->fieldWidgetInfoAlter($info);
 }
 
 /**
  * Implements hook_field_formatter_info_alter().
  */
+#[LegacyHook]
 function entity_workflow_field_formatter_info_alter(array &$info) {
-  if (isset($info['list_default'])) {
-    $info['list_default']['field_types'][] = 'entity_workflow_state';
-  }
+  \Drupal::service(EntityWorkflowHooks::class)->fieldFormatterInfoAlter($info);
 }
 
 // -----------------------------------------------------------------------
diff --git a/entity_workflow.services.yml b/entity_workflow.services.yml
index a2d9289..7d31fa6 100644
--- a/entity_workflow.services.yml
+++ b/entity_workflow.services.yml
@@ -21,3 +21,7 @@ services:
     arguments: ['@entity.definition_update_manager', '@entity.last_installed_schema.repository', '@entity_workflow.info']
     tags:
       - { name: 'event_subscriber' }
+
+  Drupal\entity_workflow\Hook\EntityWorkflowHooks:
+    class: Drupal\entity_workflow\Hook\EntityWorkflowHooks
+    autowire: true
diff --git a/modules/entity_workflow_content/entity_workflow_content.module b/modules/entity_workflow_content/entity_workflow_content.module
index c0a342e..47ce025 100644
--- a/modules/entity_workflow_content/entity_workflow_content.module
+++ b/modules/entity_workflow_content/entity_workflow_content.module
@@ -1,5 +1,7 @@
 <?php
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\entity_workflow_content\Hook\EntityWorkflowContentHooks;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
@@ -26,44 +28,10 @@ function entity_workflow_content_module_implements_alter(&$implementations, $hoo
  *
  * @todo Every default provided below should be configurable in the UI.
  */
-function entity_workflow_content_entity_workflow_type_alter(array &$configuration, $plugin_id) {
-  if ($plugin_id !== 'entity_workflow_content') {
-    return;
-  }
-
-  // Fill in default supported entity types for now.
-  $supported_entity_types = array_filter(\Drupal::entityTypeManager()->getDefinitions(), function (EntityTypeInterface $entity_type) {
-    return \Drupal::service('workspaces.information')->isEntityTypeSupported($entity_type);
-  });
-  $configuration['entity_types'] = array_fill_keys(array_keys($supported_entity_types), []);
-
-  // Fill in other defaults for now.
-  $configuration['default_state'] = 'draft';
-  $configuration['default_transition'] = 'new_draft';
-
-  // Alter in the access callback to all transitions to make things easier, but
-  // only if it hasn't been set.
-  foreach ($configuration['transitions'] as &$transition_info) {
-    if (empty($transition_info['access_callback'])) {
-      $transition_info['access_callback'] = 'entity_workflow_content_transition_access';
-    }
-  }
-
-  // Add a special access callback for the 'approve_immediately' transition.
-  if (isset($configuration['transitions']['approve_immediately'])) {
-    $configuration['transitions']['approve_immediately']['additional_access_callbacks'] = ['entity_workflow_content_can_not_approve'];
-  }
-
-  // This content workflow implementation requires an active workspace for
-  // any entity operation (add, edit, etc.). The exclusion list below allows
-  // specified entity types or only a subset of their bundles to bypass the
-  // workspace requirement, so entity operation can also happen in 'Live'.
-  // Exclusions are arrays keyed by entity type IDs, with values either an
-  // empty array, which means all bundles are excluded, or a specific list
-  // of bundle names to be excluded.
-  if (!isset($configuration['exclude_from_workspace_requirement'])) {
-    $configuration['exclude_from_workspace_requirement'] = [];
-  }
+#[LegacyHook]
+function entity_workflow_content_entity_workflow_type_alter(array &$configuration, $plugin_id)
+{
+    return \Drupal::service(EntityWorkflowContentHooks::class)->entityWorkflowTypeAlter($configuration, $plugin_id);
 }
 
 /**
@@ -151,28 +119,8 @@ function _entity_workflow_content_process_access_hook_results(array $access) {
 /**
  * Implements hook_form_alter().
  */
-function entity_workflow_content_form_alter(&$form, FormStateInterface $form_state, $form_id) {
-  if (\Drupal::service('workspaces.manager')->hasActiveWorkspace()) {
-    return;
-  }
-
-  $form_object = $form_state->getFormObject();
-  if ($form_object instanceof ViewsForm) {
-    $view = $form_state->getBuildInfo()['args'][0];
-
-    $bulk_form_field_name = NULL;
-    foreach ($view->field as $field_name => $field) {
-      if ($field instanceof BulkForm) {
-        $bulk_form_field_name = $field_name;
-        break;
-      }
-    }
-
-    // Hide Views bulk operations form if we are in Live.
-    if ($bulk_form_field_name) {
-      $form[$bulk_form_field_name]['#access'] = FALSE;
-      $form['header'][$bulk_form_field_name]['#access'] = FALSE;
-      $form['actions']['#access'] = FALSE;
-    }
-  }
+#[LegacyHook]
+function entity_workflow_content_form_alter(&$form, FormStateInterface $form_state, $form_id)
+{
+    \Drupal::service(EntityWorkflowContentHooks::class)->formAlter($form, $form_state, $form_id);
 }
diff --git a/modules/entity_workflow_content/entity_workflow_content.services.yml b/modules/entity_workflow_content/entity_workflow_content.services.yml
index 068068e..ff0c4d5 100644
--- a/modules/entity_workflow_content/entity_workflow_content.services.yml
+++ b/modules/entity_workflow_content/entity_workflow_content.services.yml
@@ -9,3 +9,7 @@ services:
     arguments: ['@entity_type.manager', '@plugin.manager.workflows.type']
     tags:
       - { name: route_enhancer }
+
+  Drupal\entity_workflow_content\Hook\EntityWorkflowContentHooks:
+    class: Drupal\entity_workflow_content\Hook\EntityWorkflowContentHooks
+    autowire: true
diff --git a/modules/entity_workflow_content/src/Hook/EntityWorkflowContentHooks.php b/modules/entity_workflow_content/src/Hook/EntityWorkflowContentHooks.php
new file mode 100644
index 0000000..65083c6
--- /dev/null
+++ b/modules/entity_workflow_content/src/Hook/EntityWorkflowContentHooks.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\entity_workflow_content\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\views\Form\ViewsForm;
+use Drupal\views\Plugin\views\field\BulkForm;
+use Drupal\workflows\WorkflowInterface;
+use Drupal\workspaces\WorkspaceInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+/**
+ * Hook implementations for entity_workflow_content.
+ */
+class EntityWorkflowContentHooks
+{
+    /**
+     * Implements hook_entity_workflow_type_alter().
+     *
+     * @todo Every default provided below should be configurable in the UI.
+     */
+    #[Hook('entity_workflow_type_alter')]
+    public function entityWorkflowTypeAlter(array &$configuration, $plugin_id)
+    {
+        if ($plugin_id !== 'entity_workflow_content') {
+            return;
+        }
+        // Fill in default supported entity types for now.
+        $supported_entity_types = array_filter(\Drupal::entityTypeManager()->getDefinitions(), function (\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
+            return \Drupal::service('workspaces.information')->isEntityTypeSupported($entity_type);
+        });
+        $configuration['entity_types'] = array_fill_keys(array_keys($supported_entity_types), [
+        ]);
+        // Fill in other defaults for now.
+        $configuration['default_state'] = 'draft';
+        $configuration['default_transition'] = 'new_draft';
+        // Alter in the access callback to all transitions to make things easier, but
+        // only if it hasn't been set.
+        foreach ($configuration['transitions'] as &$transition_info) {
+            if (empty($transition_info['access_callback'])) {
+                $transition_info['access_callback'] = 'entity_workflow_content_transition_access';
+            }
+        }
+        // Add a special access callback for the 'approve_immediately' transition.
+        if (isset($configuration['transitions']['approve_immediately'])) {
+            $configuration['transitions']['approve_immediately']['additional_access_callbacks'] = [
+                'entity_workflow_content_can_not_approve',
+            ];
+        }
+        // This content workflow implementation requires an active workspace for
+        // any entity operation (add, edit, etc.). The exclusion list below allows
+        // specified entity types or only a subset of their bundles to bypass the
+        // workspace requirement, so entity operation can also happen in 'Live'.
+        // Exclusions are arrays keyed by entity type IDs, with values either an
+        // empty array, which means all bundles are excluded, or a specific list
+        // of bundle names to be excluded.
+        if (!isset($configuration['exclude_from_workspace_requirement'])) {
+            $configuration['exclude_from_workspace_requirement'] = [
+            ];
+        }
+    }
+    /**
+     * Implements hook_form_alter().
+     */
+    #[Hook('form_alter')]
+    public function formAlter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id)
+    {
+        if (\Drupal::service('workspaces.manager')->hasActiveWorkspace()) {
+            return;
+        }
+        $form_object = $form_state->getFormObject();
+        if ($form_object instanceof \Drupal\views\Form\ViewsForm) {
+            $view = $form_state->getBuildInfo()['args'][0];
+            $bulk_form_field_name = NULL;
+            foreach ($view->field as $field_name => $field) {
+                if ($field instanceof \Drupal\views\Plugin\views\field\BulkForm) {
+                    $bulk_form_field_name = $field_name;
+                    break;
+                }
+            }
+            // Hide Views bulk operations form if we are in Live.
+            if ($bulk_form_field_name) {
+                $form[$bulk_form_field_name]['#access'] = FALSE;
+                $form['header'][$bulk_form_field_name]['#access'] = FALSE;
+                $form['actions']['#access'] = FALSE;
+            }
+        }
+    }
+}
diff --git a/modules/entity_workflow_workspace/entity_workflow_workspace.module b/modules/entity_workflow_workspace/entity_workflow_workspace.module
index 9ca8697..5a3d376 100644
--- a/modules/entity_workflow_workspace/entity_workflow_workspace.module
+++ b/modules/entity_workflow_workspace/entity_workflow_workspace.module
@@ -1,5 +1,7 @@
 <?php
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\entity_workflow_workspace\Hook\EntityWorkflowWorkspaceHooks;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
@@ -28,18 +30,10 @@ function entity_workflow_workspace_module_implements_alter(&$implementations, $h
 /**
  * Implements hook_entity_workflow_type_alter().
  */
-function entity_workflow_workspace_entity_workflow_type_alter(array &$configuration, $plugin_id) {
-  if ($plugin_id !== 'entity_workflow_workspace') {
-    return;
-  }
-
-  // Alter in the access callback to all transitions to make things easier, but
-  // only if it hasn't been set.
-  foreach ($configuration['transitions'] as &$transition_info) {
-    if (empty($transition_info['access_callback'])) {
-      $transition_info['access_callback'] = 'entity_workflow_workspace_transition_access';
-    }
-  }
+#[LegacyHook]
+function entity_workflow_workspace_entity_workflow_type_alter(array &$configuration, $plugin_id)
+{
+    \Drupal::service(EntityWorkflowWorkspaceHooks::class)->entityWorkflowTypeAlter($configuration, $plugin_id);
 }
 
 /**
@@ -63,49 +57,10 @@ function entity_workflow_workspace_transition_access(EntityInterface $entity, Wo
 /**
  * Implements hook_ENTITY_TYPE_access().
  */
-function entity_workflow_workspace_workspace_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  // Don't alter access for standard ops (like view, create, edit, delete)
-  // where this module doesn't define any state transition permissions; see
-  // \Drupal\entity_workflow_workspace\Plugin\WorkflowType\EntityWorkflowWorkspace::getPermissions().
-  $workspace_workflow = Workflow::load('workspace')->getTypePlugin();
-  if (!in_array($operation, array_keys($workspace_workflow->getConfiguration()['transitions']), TRUE)) {
-    return AccessResult::neutral();
-  }
-
-  // Be sure to enforce transition permissions.
-  if (!$account->hasPermission('use workspace transition ' . $operation)
-      && !$account->hasPermission('use all workspace transitions')) {
-    return AccessResult::forbidden();
-  }
-
-  /** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */
-  $workspace_association = \Drupal::service('workspaces.association');
-  $tracked_entities = $workspace_association->getTrackedEntities($entity->id());
-
-  // Don't allow any transition (except new drafts and unpublishing) for empty
-  // workspaces.
-  if (!in_array($operation, ['new_draft', 'unpublish'], TRUE) && empty($tracked_entities)) {
-    return AccessResult::forbidden();
-  }
-
-  // Publish op only allowed if all entities in the workspace are in approved
-  // state.
-  if ($operation === 'publish') {
-    /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
-    $entity_workflow_info = \Drupal::service('entity_workflow.info');
-
-    foreach ($tracked_entities as $entity_type_id => $entities) {
-      if (!$entity_workflow_info->isEntityTypeSupported($entity_type_id)) {
-        continue;
-      }
-
-      if (entity_workflow_has_not_entity_state('content', $entity->id(), $entity_type_id, [], 'approved')) {
-        // Just return, we don't need to process more.
-        return AccessResult::forbidden();
-      }
-    }
-  }
-  return AccessResult::allowedIfHasPermission($account, 'use workspace transition ' . $operation);
+#[LegacyHook]
+function entity_workflow_workspace_workspace_access(EntityInterface $entity, $operation, AccountInterface $account)
+{
+    return \Drupal::service(EntityWorkflowWorkspaceHooks::class)->workspaceAccess($entity, $operation, $account);
 }
 
 /**
@@ -114,90 +69,27 @@ function entity_workflow_workspace_workspace_access(EntityInterface $entity, $op
  * Swaps the workspace list builder so we can customize their operations and the
  * toolbar switcher.
  */
-function entity_workflow_workspace_entity_type_alter(array &$entity_types) {
-  $entity_types['workspace']->setListBuilderClass(EntityWorkflowWorkspaceListBuilder::class);
-
-  /** @var \Drupal\workspaces\WorkspaceInformationInterface $workspace_info */
-  $workspace_info = \Drupal::service('workspaces.information');
-  foreach ($entity_types as $entity_type) {
-    if ($workspace_info->isEntityTypeSupported($entity_type)) {
-      $entity_type->addConstraint('LockedWorkspace');
-    }
-  }
+#[LegacyHook]
+function entity_workflow_workspace_entity_type_alter(array &$entity_types)
+{
+    \Drupal::service(EntityWorkflowWorkspaceHooks::class)->entityTypeAlter($entity_types);
 }
 
 /**
  * Implements hook_ENTITY_TYPE_view_alter() for the 'workspace' entity type.
  */
+#[LegacyHook]
 function entity_workflow_workspace_workspace_view_alter(array &$build, EntityInterface $workspace, EntityViewDisplayInterface $display) {
-  if ($build['#view_mode'] == 'full') {
-    $workflow = \Drupal::entityTypeManager()->getStorage('workflow')->load('workspace');
-
-    $state = entity_workflow_get_entity_state($workspace, $workflow->id());
-    $build['state'] = [
-      '#type' => 'item',
-      '#title' => t('State'),
-      '#markup' => $state ? $workflow->getTypePlugin()->getState($state)->label() : t('N/A'),
-      '#wrapper_attributes' => ['class' => ['container-inline'], 'style' => 'font-size: 1.2em'],
-      '#weight' => -10,
-    ];
-  }
+  \Drupal::service(EntityWorkflowWorkspaceHooks::class)->workspaceViewAlter($build, $workspace, $display);
 }
 
 /**
  * Implements hook_form_alter().
  */
-function entity_workflow_workspace_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  $is_simple_transition_form = $form_id === 'entity_workflow_simple_transition_form';
-  $is_workflow_form = $form_id === 'entity_workflow_form';
-
-  // Embed the workspace publish form into the workflow forms if we're executing
-  // the 'publish' transition.
-  if ($is_simple_transition_form || $is_workflow_form) {
-    /** @var \Drupal\entity_workflow\Form\EntityWorkflowFormInterface $form_object */
-    $form_object = $form_state->getFormObject();
-
-    if (($is_simple_transition_form && $form_object->getTransition()->id() !== 'publish')
-      || ($is_workflow_form && !isset($form['transition']['#options']['publish']))
-    ) {
-      return;
-    }
-
-    /** @var \Drupal\workspaces\WorkspaceInterface $workspace */
-    $workspace = $form_object->getEntity();
-
-    $form['publish_form'] = [
-      '#type' => 'container',
-      '#weight' => 5,
-      '#tree' => TRUE,
-      '#states' => $is_workflow_form
-        ? ['visible' => [':input[name="transition"]' => ['value' => 'publish']]]
-        : [],
-    ];
-    $form['publish_form']['subform'] = [
-      '#tree' => TRUE,
-    ];
-
-    $publish_form_object = _entity_workflow_workspace_get_workspace_publish_form_object($workspace);
-    $subform_state = SubformState::createForSubform($form['publish_form']['subform'], $form, $form_state, $publish_form_object);
-    $subform_state->addBuildInfo('args', [$workspace]);
-    \Drupal::formBuilder()->prepareForm($publish_form_object->getFormId(), $form['publish_form']['subform'], $subform_state);
-
-    // Keep track of the validation handlers that we need to run. We ignore
-    // submit handlers because workspaces are published by a transition event.
-    // @see \Drupal\entity_workflow_workspace\EventSubscriber\EntityWorkflowWorkspaceEventSubscriber::onInitiateTransaction()
-    $subform_validate = $form['publish_form']['subform']['#validate'];
-    $subform_validate[0] = [$publish_form_object, 'validateForm'];
-
-    // Keep only the relevant elements from the workspace publish form.
-    $children = Element::children($form['publish_form']['subform']);
-    $children = array_diff($children, $form_state->getCleanValueKeys());
-    $form['publish_form']['subform'] = array_intersect_key($form['publish_form']['subform'], array_flip($children));
-
-    $form['publish_form']['subform']['#subform_validate'] = $subform_validate;
-
-    $form['#validate'][] = 'entity_workflow_workspace_form_validate';
-  }
+#[LegacyHook]
+function entity_workflow_workspace_form_alter(&$form, FormStateInterface $form_state, $form_id): void
+{
+    \Drupal::service(EntityWorkflowWorkspaceHooks::class)->formAlter($form, $form_state, $form_id);
 }
 
 /**
diff --git a/modules/entity_workflow_workspace/entity_workflow_workspace.services.yml b/modules/entity_workflow_workspace/entity_workflow_workspace.services.yml
index 279aaa6..9cf6f3e 100644
--- a/modules/entity_workflow_workspace/entity_workflow_workspace.services.yml
+++ b/modules/entity_workflow_workspace/entity_workflow_workspace.services.yml
@@ -7,3 +7,7 @@ services:
     class: Drupal\entity_workflow_workspace\Routing\RouteSubscriber
     tags:
       - { name: event_subscriber }
+
+  Drupal\entity_workflow_workspace\Hook\EntityWorkflowWorkspaceHooks:
+    class: Drupal\entity_workflow_workspace\Hook\EntityWorkflowWorkspaceHooks
+    autowire: true
diff --git a/modules/entity_workflow_workspace/src/Hook/EntityWorkflowWorkspaceHooks.php b/modules/entity_workflow_workspace/src/Hook/EntityWorkflowWorkspaceHooks.php
new file mode 100644
index 0000000..69147c8
--- /dev/null
+++ b/modules/entity_workflow_workspace/src/Hook/EntityWorkflowWorkspaceHooks.php
@@ -0,0 +1,186 @@
+<?php
+
+namespace Drupal\entity_workflow_workspace\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubformState;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\entity_workflow_workspace\EntityWorkflowWorkspaceListBuilder;
+use Drupal\workflows\Entity\Workflow;
+use Drupal\workflows\WorkflowInterface;
+use Drupal\workspaces\Form\WorkspacePublishForm;
+use Drupal\workspaces\WorkspaceInterface;
+use Drupal\wse\Form\WseWorkspacePublishForm;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for entity_workflow_workspace.
+ */
+class EntityWorkflowWorkspaceHooks
+{
+    use StringTranslationTrait;
+    /**
+     * Implements hook_entity_workflow_type_alter().
+     */
+    #[Hook('entity_workflow_type_alter')]
+    public function entityWorkflowTypeAlter(array &$configuration, $plugin_id)
+    {
+        if ($plugin_id !== 'entity_workflow_workspace') {
+            return;
+        }
+        // Alter in the access callback to all transitions to make things easier, but
+        // only if it hasn't been set.
+        foreach ($configuration['transitions'] as &$transition_info) {
+            if (empty($transition_info['access_callback'])) {
+                $transition_info['access_callback'] = 'entity_workflow_workspace_transition_access';
+            }
+        }
+    }
+    /**
+     * Implements hook_ENTITY_TYPE_access().
+     */
+    #[Hook('workspace_access')]
+    public function workspaceAccess(\Drupal\Core\Entity\EntityInterface $entity, $operation, \Drupal\Core\Session\AccountInterface $account)
+    {
+        // Don't alter access for standard ops (like view, create, edit, delete)
+        // where this module doesn't define any state transition permissions; see
+        // \Drupal\entity_workflow_workspace\Plugin\WorkflowType\EntityWorkflowWorkspace::getPermissions().
+        $workspace_workflow = \Drupal\workflows\Entity\Workflow::load('workspace')->getTypePlugin();
+        if (!in_array($operation, array_keys($workspace_workflow->getConfiguration()['transitions']), TRUE)) {
+            return \Drupal\Core\Access\AccessResult::neutral();
+        }
+        // Be sure to enforce transition permissions.
+        if (!$account->hasPermission('use workspace transition ' . $operation) && !$account->hasPermission('use all workspace transitions')) {
+            return \Drupal\Core\Access\AccessResult::forbidden();
+        }
+        /** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */
+        $workspace_association = \Drupal::service('workspaces.association');
+        $tracked_entities = $workspace_association->getTrackedEntities($entity->id());
+        // Don't allow any transition (except new drafts and unpublishing) for empty
+        // workspaces.
+        if (!in_array($operation, [
+            'new_draft',
+            'unpublish',
+        ], TRUE) && empty($tracked_entities)) {
+            return \Drupal\Core\Access\AccessResult::forbidden();
+        }
+        // Publish op only allowed if all entities in the workspace are in approved
+        // state.
+        if ($operation === 'publish') {
+            /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
+            $entity_workflow_info = \Drupal::service('entity_workflow.info');
+            foreach ($tracked_entities as $entity_type_id => $entities) {
+                if (!$entity_workflow_info->isEntityTypeSupported($entity_type_id)) {
+                    continue;
+                }
+                if (entity_workflow_has_not_entity_state('content', $entity->id(), $entity_type_id, [
+                ], 'approved')) {
+                    // Just return, we don't need to process more.
+                    return \Drupal\Core\Access\AccessResult::forbidden();
+                }
+            }
+        }
+        return \Drupal\Core\Access\AccessResult::allowedIfHasPermission($account, 'use workspace transition ' . $operation);
+    }
+    /**
+     * Implements hook_entity_type_alter().
+     *
+     * Swaps the workspace list builder so we can customize their operations and the
+     * toolbar switcher.
+     */
+    #[Hook('entity_type_alter')]
+    public function entityTypeAlter(array &$entity_types)
+    {
+        $entity_types['workspace']->setListBuilderClass(\Drupal\entity_workflow_workspace\EntityWorkflowWorkspaceListBuilder::class);
+        /** @var \Drupal\workspaces\WorkspaceInformationInterface $workspace_info */
+        $workspace_info = \Drupal::service('workspaces.information');
+        foreach ($entity_types as $entity_type) {
+            if ($workspace_info->isEntityTypeSupported($entity_type)) {
+                $entity_type->addConstraint('LockedWorkspace');
+            }
+        }
+    }
+    /**
+     * Implements hook_ENTITY_TYPE_view_alter() for the 'workspace' entity type.
+     */
+    #[Hook('workspace_view_alter')]
+    public function workspaceViewAlter(array &$build, \Drupal\Core\Entity\EntityInterface $workspace, \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display)
+    {
+        if ($build['#view_mode'] == 'full') {
+            $workflow = \Drupal::entityTypeManager()->getStorage('workflow')->load('workspace');
+            $state = entity_workflow_get_entity_state($workspace, $workflow->id());
+            $build['state'] = [
+                '#type' => 'item',
+                '#title' => $this->t('State'),
+                '#markup' => $state ? $workflow->getTypePlugin()->getState($state)->label() : $this->t('N/A'),
+                '#wrapper_attributes' => [
+                    'class' => [
+                        'container-inline',
+                    ],
+                    'style' => 'font-size: 1.2em',
+                ],
+                '#weight' => -10,
+            ];
+        }
+    }
+    /**
+     * Implements hook_form_alter().
+     */
+    #[Hook('form_alter')]
+    public function formAlter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id): void
+    {
+        $is_simple_transition_form = $form_id === 'entity_workflow_simple_transition_form';
+        $is_workflow_form = $form_id === 'entity_workflow_form';
+        // Embed the workspace publish form into the workflow forms if we're executing
+        // the 'publish' transition.
+        if ($is_simple_transition_form || $is_workflow_form) {
+            /** @var \Drupal\entity_workflow\Form\EntityWorkflowFormInterface $form_object */
+            $form_object = $form_state->getFormObject();
+            if ($is_simple_transition_form && $form_object->getTransition()->id() !== 'publish' || $is_workflow_form && !isset($form['transition']['#options']['publish'])) {
+                return;
+            }
+            /** @var \Drupal\workspaces\WorkspaceInterface $workspace */
+            $workspace = $form_object->getEntity();
+            $form['publish_form'] = [
+                '#type' => 'container',
+                '#weight' => 5,
+                '#tree' => TRUE,
+                '#states' => $is_workflow_form ? [
+                    'visible' => [
+                        ':input[name="transition"]' => [
+                            'value' => 'publish',
+                        ],
+                    ],
+                ] : [
+                ],
+            ];
+            $form['publish_form']['subform'] = [
+                '#tree' => TRUE,
+            ];
+            $publish_form_object = _entity_workflow_workspace_get_workspace_publish_form_object($workspace);
+            $subform_state = \Drupal\Core\Form\SubformState::createForSubform($form['publish_form']['subform'], $form, $form_state, $publish_form_object);
+            $subform_state->addBuildInfo('args', [
+                $workspace,
+            ]);
+            \Drupal::formBuilder()->prepareForm($publish_form_object->getFormId(), $form['publish_form']['subform'], $subform_state);
+            // Keep track of the validation handlers that we need to run. We ignore
+            // submit handlers because workspaces are published by a transition event.
+            // @see \Drupal\entity_workflow_workspace\EventSubscriber\EntityWorkflowWorkspaceEventSubscriber::onInitiateTransaction()
+            $subform_validate = $form['publish_form']['subform']['#validate'];
+            $subform_validate[0] = [
+                $publish_form_object,
+                'validateForm',
+            ];
+            // Keep only the relevant elements from the workspace publish form.
+            $children = \Drupal\Core\Render\Element::children($form['publish_form']['subform']);
+            $children = array_diff($children, $form_state->getCleanValueKeys());
+            $form['publish_form']['subform'] = array_intersect_key($form['publish_form']['subform'], array_flip($children));
+            $form['publish_form']['subform']['#subform_validate'] = $subform_validate;
+            $form['#validate'][] = 'entity_workflow_workspace_form_validate';
+        }
+    }
+}
diff --git a/modules/entity_workflow_workspace/src/Plugin/Validation/Constraint/LockedWorkspaceConstraintValidator.php b/modules/entity_workflow_workspace/src/Plugin/Validation/Constraint/LockedWorkspaceConstraintValidator.php
index 176a6a7..27675b8 100644
--- a/modules/entity_workflow_workspace/src/Plugin/Validation/Constraint/LockedWorkspaceConstraintValidator.php
+++ b/modules/entity_workflow_workspace/src/Plugin/Validation/Constraint/LockedWorkspaceConstraintValidator.php
@@ -29,7 +29,7 @@ class LockedWorkspaceConstraintValidator extends ConstraintValidator implements
   /**
    * {@inheritdoc}
    */
-  public function validate($entity, Constraint $constraint) {
+  public function validate(mixed $entity, Constraint $constraint): void {
     /** @var \Drupal\Core\Entity\EntityInterface $entity */
     if (isset($entity) && !$entity->isNew()) {
       $active_workspace = $this->workspaceManager->getActiveWorkspace();
diff --git a/src/Hook/EntityWorkflowHooks.php b/src/Hook/EntityWorkflowHooks.php
new file mode 100644
index 0000000..6d20999
--- /dev/null
+++ b/src/Hook/EntityWorkflowHooks.php
@@ -0,0 +1,363 @@
+<?php
+
+namespace Drupal\entity_workflow\Hook;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Url;
+use Drupal\entity_workflow\Event\EntityWorkflowEvents;
+use Drupal\entity_workflow\Event\InitiateTransitionEvent;
+use Drupal\workflows\WorkflowInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for entity_workflow.
+ */
+class EntityWorkflowHooks
+{
+    use StringTranslationTrait;
+    // -----------------------------------------------------------------------
+    // Drupal core hooks
+    /**
+     * Implements hook_field_info_alter().
+     */
+    #[Hook('field_info_alter')]
+    public function fieldInfoAlter(&$info)
+    {
+        // Add temporary BC field type for sites that are using the core patch.
+        // @see https://www.drupal.org/project/drupal/issues/2835545
+        if (!isset($info['workflow_state']) && isset($info['entity_workflow_state'])) {
+            $info['workflow_state'] = $info['entity_workflow_state'];
+            $info['workflow_state']['id'] = 'workflow_state';
+        }
+    }
+    /**
+     * Implements hook_entity_base_field_info().
+     */
+    #[Hook('entity_base_field_info')]
+    public function entityBaseFieldInfo(\Drupal\Core\Entity\EntityTypeInterface $entity_type)
+    {
+        if ($workflows = \Drupal::service('entity_workflow.info')->getWorkflowsInfoForEntityType($entity_type->id())) {
+            $fields = [
+            ];
+            foreach ($workflows as $workflow_id => $workflow_label) {
+                $field_name = entity_workflow_get_field_name($workflow_id);
+                $fields[$field_name] = \Drupal\Core\Field\BaseFieldDefinition::create('entity_workflow_state')->setLabel($this->t('Entity workflow: @label', [
+                    '@label' => $workflow_label,
+                ]))->setDescription($this->t('The workflow state of this entity for the @label workflow.', [
+                    '@label' => $workflow_label,
+                ]))->setTranslatable(TRUE)->setRevisionable(TRUE)->setSetting('workflow', $workflow_id)->setDisplayOptions('view', [
+                    'region' => 'hidden',
+                ]);
+            }
+            return $fields;
+        }
+    }
+    /**
+     * Implements hook_entity_bundle_field_info().
+     */
+    #[Hook('entity_bundle_field_info')]
+    public function entityBundleFieldInfo(\Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle, array $base_field_definitions)
+    {
+        if ($workflows = \Drupal::service('entity_workflow.info')->getWorkflowsInfoForEntityType($entity_type->id())) {
+            foreach ($workflows as $workflow_id => $workflow_label) {
+                $field_name = entity_workflow_get_field_name($workflow_id);
+                if (isset($base_field_definitions[$field_name])) {
+                    // Add the target bundle to the workflow state field. Since each bundle
+                    // can be attached to a different workflow, adding this information to
+                    // the field definition allows the associated workflow to be derived
+                    // where a field definition is present.
+                    $base_field_definitions[$field_name]->setTargetBundle($bundle);
+                    return [
+                        $field_name => $base_field_definitions[$field_name],
+                    ];
+                }
+            }
+        }
+    }
+    /**
+     * Implements hook_entity_bundle_info_alter().
+     */
+    #[Hook('entity_bundle_info_alter')]
+    public function entityBundleInfoAlter(&$bundles)
+    {
+        /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
+        $entity_workflow_info = \Drupal::service('entity_workflow.info');
+        foreach ($entity_workflow_info->getWorkflowEntities() as $workflow_id => $workflow) {
+            /** @var \Drupal\entity_workflow\WorkflowType\EntityWorkflowTypeInterface $workflow_plugin */
+            $workflow_plugin = $workflow->getTypePlugin();
+            foreach ($workflow_plugin->getEntityTypes() as $entity_type_id) {
+                if (isset($bundles[$entity_type_id])) {
+                    foreach (array_keys($bundles[$entity_type_id]) as $bundle_id) {
+                        if ($workflow_plugin->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
+                            $bundles[$entity_type_id][$bundle_id]['entity_workflows'][$workflow_id] = $workflow->label();
+                        }
+                    }
+                }
+            }
+        }
+    }
+    /**
+     * Implements hook_ENTITY_TYPE_insert() for the 'workflow' entity type.
+     *
+     * Installs the workflow state base field when a new workflow is created.
+     */
+    #[Hook('workflow_insert')]
+    public function workflowInsert(\Drupal\workflows\WorkflowInterface $workflow)
+    {
+        /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
+        $entity_workflow_info = \Drupal::service('entity_workflow.info');
+        if ($entity_workflow_info->isEntityWorkflow($workflow)) {
+            /** @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_manager */
+            $entity_definition_manager = \Drupal::service('entity.definition_update_manager');
+            $storage_definition = \Drupal\Core\Field\BaseFieldDefinition::create('entity_workflow_state')->setLabel($this->t('Entity workflow: @label', [
+                '@label' => $workflow->label(),
+            ]))->setDescription($this->t('The workflow state of this entity for the @label workflow.', [
+                '@label' => $workflow->label(),
+            ]))->setTranslatable(TRUE)->setRevisionable(TRUE);
+            foreach ($workflow->getTypePlugin()->getEntityTypes() as $entity_type_id) {
+                $field_name = entity_workflow_get_field_name($workflow->id());
+                $entity_definition_manager->installFieldStorageDefinition($field_name, $entity_type_id, 'entity_workflow', $storage_definition);
+            }
+            // When a workflow is added, the router needs to be rebuilt to add the
+            // corresponding tabs and local actions.
+            \Drupal::service('router.builder')->setRebuildNeeded();
+            // The bundle info cache also needs to be cleared in order to take the new
+            // workflow into account when field definitions are rebuilt.
+            \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+        }
+    }
+    /**
+     * Implements hook_ENTITY_TYPE_update() for the 'workflow' entity type.
+     */
+    #[Hook('workflow_update')]
+    public function workflowUpdate(\Drupal\workflows\WorkflowInterface $workflow)
+    {
+        /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
+        $entity_workflow_info = \Drupal::service('entity_workflow.info');
+        if ($entity_workflow_info->isEntityWorkflow($workflow)) {
+            // When a workflow is updated, the router needs to be rebuilt to account for
+            // possible changes to menu local tasks and local actions.
+            \Drupal::service('router.builder')->setRebuildNeeded();
+        }
+    }
+    /**
+     * Implements hook_ENTITY_TYPE_delete() for the 'workflow' entity type.
+     *
+     * Uninstalls the workflow_state base field when a workflow is deleted.
+     */
+    #[Hook('workflow_delete')]
+    public function workflowDelete(\Drupal\workflows\WorkflowInterface $workflow)
+    {
+        /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
+        $entity_workflow_info = \Drupal::service('entity_workflow.info');
+        if ($entity_workflow_info->isEntityWorkflow($workflow)) {
+            /** @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_manager */
+            $entity_definition_manager = \Drupal::service('entity.definition_update_manager');
+            foreach ($workflow->getTypePlugin()->getEntityTypes() as $entity_type_id) {
+                $field_name = entity_workflow_get_field_name($workflow->id());
+                if ($storage_definition = $entity_definition_manager->getFieldStorageDefinition($field_name, $entity_type_id)) {
+                    $entity_definition_manager->uninstallFieldStorageDefinition($storage_definition);
+                }
+            }
+            // When a workflow is deleted, the router needs to be rebuilt to add the
+            // corresponding tabs and local actions.
+            \Drupal::service('router.builder')->setRebuildNeeded();
+        }
+    }
+    /**
+     * Implements hook_entity_delete().
+     */
+    #[Hook('entity_delete')]
+    public function entityDelete(\Drupal\Core\Entity\EntityInterface $entity)
+    {
+        $workflow_transition_log_storage = \Drupal::entityTypeManager()->getStorage('workflow_transition_log');
+        // Remove transition logs when a workspace is deleted.
+        if ($entity->getEntityTypeId() === 'workspace') {
+            $result = $workflow_transition_log_storage->getQuery()->condition('workspace_id', $entity->id())->accessCheck(FALSE)->execute();
+            $logs = $workflow_transition_log_storage->loadMultiple($result);
+            $workflow_transition_log_storage->delete($logs);
+        }
+        if (!\Drupal::service('entity_workflow.info')->isEntityTypeSupported($entity->getEntityTypeId())) {
+            return;
+        }
+        // Remove transition logs when a supported entity is deleted.
+        $logs = entity_workflow_get_history($entity->getEntityTypeId(), [
+            $entity->id(),
+        ]);
+        $workflow_transition_log_storage->delete($logs);
+    }
+    /**
+     * Implements hook_entity_revision_delete().
+     */
+    #[Hook('entity_revision_delete')]
+    public function entityRevisionDelete(\Drupal\Core\Entity\EntityInterface $entity)
+    {
+        /** @var \Drupal\Core\Entity\RevisionableInterface $entity */
+        if (!\Drupal::service('entity_workflow.info')->isEntityTypeSupported($entity->getEntityTypeId())) {
+            return;
+        }
+        // Remove transition logs when a supported entity revision is deleted.
+        $workflow_transition_log_storage = \Drupal::entityTypeManager()->getStorage('workflow_transition_log');
+        $result = $workflow_transition_log_storage->getQuery()->condition('entity_revision_id', $entity->getRevisionId())->accessCheck(FALSE)->execute();
+        $logs = $workflow_transition_log_storage->loadMultiple($result);
+        $workflow_transition_log_storage->delete($logs);
+    }
+    /**
+     * Implements hook_entity_presave().
+     */
+    #[Hook('entity_presave')]
+    public function entityPresave(\Drupal\Core\Entity\EntityInterface $entity)
+    {
+        /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+        // Ensure that only changing the workflow state of an entity does not create
+        // a new revision.
+        if (isset($entity->_entityWorkflowEnforceNoNewRevision)) {
+            $entity->setNewRevision(FALSE);
+        }
+    }
+    /**
+     * Implements hook_ENTITY_TYPE_view_alter() for the 'workspace' entity type.
+     */
+    #[Hook('workspace_view_alter')]
+    public function workspaceViewAlter(array &$build, \Drupal\Core\Entity\EntityInterface $workspace, \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display)
+    {
+        if ($build['#view_mode'] == 'full') {
+            /** @var \Drupal\entity_workflow\EntityWorkflowInfo $entity_workflow_info */
+            $entity_workflow_info = \Drupal::service('entity_workflow.info');
+            $header =& $build['changes']['list']['#header'];
+            // Ensure that we take into account only the revisions that are displayed
+            // on the page.
+            $tracked_entities = [
+            ];
+            foreach (\Drupal\Core\Render\Element::children($build['changes']['list']) as $key) {
+                [$entity_type_id, $entity_id] = explode(':', $key, 2);
+                $revision_id = $build['changes']['list'][$key]['#entity']->getRevisionId();
+                $tracked_entities[$entity_type_id][$revision_id] = $entity_id;
+            }
+            // Add entity workflow state information to the workspace manage page.
+            foreach ($tracked_entities as $entity_type_id => $entity_ids) {
+                if (!$entity_workflow_info->isEntityTypeSupported($entity_type_id)) {
+                    continue;
+                }
+                // Gather the latest transition logs for every entity workflow of this
+                // entity type.
+                foreach (array_keys($entity_workflow_info->getWorkflowsInfoForEntityType($entity_type_id)) as $workflow_id) {
+                    $latest_transition_history[$workflow_id] = entity_workflow_get_history($entity_type_id, $entity_ids, $workflow_id, $workspace->id(), TRUE);
+                }
+                foreach ($entity_ids as $entity_id) {
+                    $render =& $build['changes']['list'][$entity_type_id . ':' . $entity_id];
+                    /** @var \Drupal\Core\Entity\ContentEntityInterface $tracked_entity */
+                    $tracked_entity = $render['#entity'];
+                    foreach ($entity_workflow_info->getWorkflowsInfoForEntityType($entity_type_id) as $workflow_id => $workflow_label) {
+                        $workflow_render_key = $workflow_id . '_workflow';
+                        // Insert a new header column before 'Operations';
+                        $workflow_header = [
+                            $workflow_render_key => $this->t('@label workflow', [
+                                '@label' => $workflow_label,
+                            ]),
+                        ];
+                        $pos = (int) array_search('operations', array_keys($header)) ?: count($header);
+                        $header = array_merge(array_slice($header, 0, $pos), $workflow_header, array_slice($header, $pos));
+                        // Link to the workflow state form if the user has access to it.
+                        $workflow_field = entity_workflow_get_field($tracked_entity, $workflow_id);
+                        $label = $workflow_field->getStateLabel();
+                        // Look up the route name to make sure it exists.
+                        try {
+                            $url = \Drupal\Core\Url::fromRoute("entity.{$entity_type_id}.workflow", [
+                                $entity_type_id => $tracked_entity->id(),
+                                'workflow' => $workflow_id,
+                            ], [
+                                'query' => [
+                                    'destination' => \Drupal::request()->getRequestUri(),
+                                ],
+                            ]);
+                        } catch (\Exception $exception) {
+                            $url = NULL;
+                        }
+                        // Entities given the state by default might not have a uid or
+                        // timestamp so be cautious.
+                        if (isset($latest_transition_history[$workflow_id][$tracked_entity->id()])) {
+                            $transition_log = $latest_transition_history[$workflow_id][$tracked_entity->id()];
+                            $state_summary = [
+                                '#theme' => 'entity_workflow_state_summary',
+                                '#state' => $label,
+                                '#url' => $url,
+                                '#url_access' => $url && $url->access(),
+                                '#user' => [
+                                    '#theme' => 'username',
+                                    '#account' => $transition_log->uid->entity,
+                                ],
+                                '#date' => $transition_log->getCreatedTime(),
+                            ];
+                        } else {
+                            $state_summary = [
+                                '#markup' => $label,
+                            ];
+                        }
+                        // Insert a new column before 'Operations';
+                        $pos = (int) array_search('operations', array_keys($render)) ?: count($header);
+                        $workflow_data = [
+                            $workflow_render_key => $state_summary,
+                        ];
+                        $render = array_merge(array_slice($render, 0, $pos), $workflow_data, array_slice($render, $pos));
+                    }
+                }
+            }
+        }
+    }
+    /**
+     * Implements hook_theme().
+     */
+    #[Hook('theme')]
+    public function theme()
+    {
+        return [
+            'entity_workflow_state_summary' => [
+                'variables' => [
+                    'state' => NULL,
+                    'url' => NULL,
+                    'url_access' => NULL,
+                    'user' => NULL,
+                    'date' => NULL,
+                ],
+                'template' => 'entity-workflow-state-summary',
+            ],
+        ];
+    }
+    /**
+     * Implements hook_views_data_alter().
+     */
+    #[Hook('views_data_alter')]
+    public function viewsDataAlter(&$data)
+    {
+        // Use our custom filter for the source entity ID of transition logs, which
+        // can be either numeric or string.
+        $data['workflow_transition_log']['entity_id']['argument']['id'] = 'workflow_transition_log_source_entity_id';
+        $data['workflow_transition_log']['entity_id_string']['argument']['id'] = 'workflow_transition_log_source_entity_id';
+    }
+    /**
+     * Implements hook_field_widget_info_alter().
+     */
+    #[Hook('field_widget_info_alter')]
+    public function fieldWidgetInfoAlter(array &$info)
+    {
+        if (isset($info['options_select'])) {
+            $info['options_select']['field_types'][] = 'entity_workflow_state';
+        }
+    }
+    /**
+     * Implements hook_field_formatter_info_alter().
+     */
+    #[Hook('field_formatter_info_alter')]
+    public function fieldFormatterInfoAlter(array &$info)
+    {
+        if (isset($info['list_default'])) {
+            $info['list_default']['field_types'][] = 'entity_workflow_state';
+        }
+    }
+}
diff --git a/src/Plugin/Validation/Constraint/EntityWorkflowStateConstraintValidator.php b/src/Plugin/Validation/Constraint/EntityWorkflowStateConstraintValidator.php
index 1299bdf..6bcecb1 100644
--- a/src/Plugin/Validation/Constraint/EntityWorkflowStateConstraintValidator.php
+++ b/src/Plugin/Validation/Constraint/EntityWorkflowStateConstraintValidator.php
@@ -15,7 +15,7 @@ class EntityWorkflowStateConstraintValidator extends ConstraintValidator {
   /**
    * {@inheritdoc}
    */
-  public function validate($value, Constraint $constraint) {
+  public function validate(mixed $value, Constraint $constraint): void {
     if (!$value->getEntity()->isNew() && !$value->isValid()) {
       $this->context->addViolation($constraint->message, ['@state' => $value->value]);
     }
diff --git a/tests/modules/entity_workflow_test/entity_workflow_test.module b/tests/modules/entity_workflow_test/entity_workflow_test.module
index 0c03994..9d2764a 100644
--- a/tests/modules/entity_workflow_test/entity_workflow_test.module
+++ b/tests/modules/entity_workflow_test/entity_workflow_test.module
@@ -1,35 +1,31 @@
 <?php
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\entity_workflow_test\Hook\EntityWorkflowTestHooks;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\workflows\WorkflowInterface;
 
 /**
  * Implements hook_entity_workflow_bulk_workflow_entities().
  */
-function entity_workflow_test_entity_workflow_bulk_workflow_entities(WorkflowInterface $workflow, EntityInterface $entity) {
-  /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
-  $entity_type_manager = \Drupal::service('entity_type.manager');
-
-  $entities = [];
-  foreach ($entity_type_manager->getStorage($entity->getEntityTypeId())->loadByProperties() as $entity_id => $loaded_entity) {
-    $entities[$entity->getEntityTypeId() . '--' . $entity_id] = $loaded_entity;
-  }
-
-  return $entities;
+#[LegacyHook]
+function entity_workflow_test_entity_workflow_bulk_workflow_entities(WorkflowInterface $workflow, EntityInterface $entity)
+{
+    return \Drupal::service(EntityWorkflowTestHooks::class)->entityWorkflowBulkWorkflowEntities($workflow, $entity);
 }
 
 /**
  * Implements hook_entity_workflow_has_bulk_workflow_alter().
  */
+#[LegacyHook]
 function entity_workflow_test_entity_workflow_has_bulk_workflow_alter(&$access, WorkflowInterface $workflow, EntityInterface $entity) {
-  $access = TRUE;
+  \Drupal::service(EntityWorkflowTestHooks::class)->entityWorkflowHasBulkWorkflowAlter($access, $workflow, $entity);
 }
 
 /**
  * Implements hook_entity_workflow_type_alter().
  */
+#[LegacyHook]
 function entity_workflow_test_entity_workflow_type_alter(array &$configuration, $plugin_id) {
-  if ($plugin_id === 'entity_workflow_test_content') {
-    $configuration['default_transition_log'] = t('The entity has been imported into a workspace.');
-  }
+  \Drupal::service(EntityWorkflowTestHooks::class)->entityWorkflowTypeAlter($configuration, $plugin_id);
 }
diff --git a/tests/modules/entity_workflow_test/entity_workflow_test.services.yml b/tests/modules/entity_workflow_test/entity_workflow_test.services.yml
new file mode 100644
index 0000000..aed8365
--- /dev/null
+++ b/tests/modules/entity_workflow_test/entity_workflow_test.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\entity_workflow_test\Hook\EntityWorkflowTestHooks:
+    class: Drupal\entity_workflow_test\Hook\EntityWorkflowTestHooks
+    autowire: true
diff --git a/tests/modules/entity_workflow_test/src/Hook/EntityWorkflowTestHooks.php b/tests/modules/entity_workflow_test/src/Hook/EntityWorkflowTestHooks.php
new file mode 100644
index 0000000..3c5d1a0
--- /dev/null
+++ b/tests/modules/entity_workflow_test/src/Hook/EntityWorkflowTestHooks.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\entity_workflow_test\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\workflows\WorkflowInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for entity_workflow_test.
+ */
+class EntityWorkflowTestHooks
+{
+    use StringTranslationTrait;
+    /**
+     * Implements hook_entity_workflow_bulk_workflow_entities().
+     */
+    #[Hook('entity_workflow_bulk_workflow_entities')]
+    public function entityWorkflowBulkWorkflowEntities(\Drupal\workflows\WorkflowInterface $workflow, \Drupal\Core\Entity\EntityInterface $entity)
+    {
+        /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+        $entity_type_manager = \Drupal::service('entity_type.manager');
+        $entities = [
+        ];
+        foreach ($entity_type_manager->getStorage($entity->getEntityTypeId())->loadByProperties() as $entity_id => $loaded_entity) {
+            $entities[$entity->getEntityTypeId() . '--' . $entity_id] = $loaded_entity;
+        }
+        return $entities;
+    }
+    /**
+     * Implements hook_entity_workflow_has_bulk_workflow_alter().
+     */
+    #[Hook('entity_workflow_has_bulk_workflow_alter')]
+    public function entityWorkflowHasBulkWorkflowAlter(&$access, \Drupal\workflows\WorkflowInterface $workflow, \Drupal\Core\Entity\EntityInterface $entity)
+    {
+        $access = TRUE;
+    }
+    /**
+     * Implements hook_entity_workflow_type_alter().
+     */
+    #[Hook('entity_workflow_type_alter')]
+    public function entityWorkflowTypeAlter(array &$configuration, $plugin_id)
+    {
+        if ($plugin_id === 'entity_workflow_test_content') {
+            $configuration['default_transition_log'] = $this->t('The entity has been imported into a workspace.');
+        }
+    }
+}
