diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index 66d1e2a..d80882a 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -2274,19 +2274,19 @@ function menu_get_local_actions() {
   $links = menu_local_tasks();
   $router_item = menu_get_item();
   // @todo Consider storing the results of hook_local_actions() in a static.
-  foreach (Drupal::moduleHandler()->invokeAll('local_actions') as $route_info) {
-    if (in_array($router_item['route_name'], $route_info['appears_on'])) {
-      $route_path = _menu_router_translate_route($route_info['route_name']);
-      $action_router_item = menu_get_item($route_path);
-      $links['actions'][$route_path] = array(
-        '#theme' => 'menu_local_action',
-        '#link' => array(
-          'title' => $route_info['title'],
-          'href' => $route_path,
-        ),
-        '#access' => $action_router_item['access'],
-      );
-    }
+  $manager = Drupal::service('plugin.manager.system.menu_local_action');
+  $local_actions = $manager->getActionsForRoute($router_item['route_name']);
+  foreach ($local_actions as $plugin) {
+    $route_path = $manager->getPath($plugin);
+    $action_router_item = menu_get_item($route_path);
+    $links['actions'][$route_path] = array(
+      '#theme' => 'menu_local_action',
+      '#link' => array(
+        'title' => $manager->getTitle($plugin),
+        'href' => $route_path,
+      ),
+      '#access' => $action_router_item['access'],
+    );
   }
   return $links['actions'];
 }
diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module
index 5367d64..41e7c62 100644
--- a/core/modules/config/tests/config_test/config_test.module
+++ b/core/modules/config/tests/config_test/config_test.module
@@ -58,21 +58,6 @@ function config_test_menu() {
 }
 
 /**
- * Implements hook_local_actions()
- */
-function config_test_local_actions() {
-  return array(
-    array(
-      'route_name' => 'config_test_entity_add',
-      'title' => t('Add test configuration'),
-      'appears_on' => array(
-        'config_test_list_page',
-      ),
-    ),
-  );
-}
-
-/**
  * Loads a ConfigTest object.
  *
  * @param string $id
diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Menu/AddConfigTestEntityLocalAction.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Menu/AddConfigTestEntityLocalAction.php
new file mode 100644
index 0000000..a4ce744
--- /dev/null
+++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Menu/AddConfigTestEntityLocalAction.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_test\Plugin\Menu\AddConfigTestEntityLocalAction.
+ */
+
+namespace Drupal\config_test\Plugin\Menu;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\system\Plugin\MenuLocalActionBase;
+use Drupal\system\Annotation\MenuLocalActionPlugin;
+
+
+/**
+ * @MenuLocalActionPlugin(
+ *   id = "config_test_entity_add_local_action",
+ *   route_name = "config_test_entity_add",
+ *   title = @Translation("Add test configuration"),
+ *   appears_on = {"config_test_list_page"}
+ * )
+ */
+class AddConfigTestEntityLocalAction extends MenuLocalActionBase {
+
+}
\ No newline at end of file
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index 717d156..4469eb2 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -146,21 +146,6 @@ function filter_menu() {
 }
 
 /**
- * Implements hook_local_actions().
- */
-function filter_local_actions() {
-  return array(
-    array(
-      'route_name' => 'filter_format_add',
-      'title' => t('Add text format'),
-      'appears_on' => array(
-        'filter_admin_overview',
-      ),
-    ),
-  );
-}
-
-/**
  * Loads a text format object from the database.
  *
  * @param $format_id
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/Menu/AddFilterFormatLocalAction.php b/core/modules/filter/lib/Drupal/filter/Plugin/Menu/AddFilterFormatLocalAction.php
new file mode 100644
index 0000000..201fee1
--- /dev/null
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/Menu/AddFilterFormatLocalAction.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter\Plugin\Menu\AddFilterFormatLocalAction.
+ */
+
+namespace Drupal\filter\Plugin\Menu;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\system\Plugin\MenuLocalActionBase;
+use Drupal\system\Annotation\MenuLocalActionPlugin;
+
+
+/**
+ * @MenuLocalActionPlugin(
+ *   id = "filter_format_add_local_action",
+ *   route_name = "filter_format_add",
+ *   title = @Translation("Add text format"),
+ *   appears_on = {"filter_admin_overview"}
+ * )
+ */
+class AddFilterFormatLocalAction extends MenuLocalActionBase {
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Annotation/MenuLocalActionPlugin.php b/core/modules/system/lib/Drupal/system/Annotation/MenuLocalActionPlugin.php
new file mode 100644
index 0000000..bb6b760
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Annotation/MenuLocalActionPlugin.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Annotation\MenuLocalActionPlugin.
+ */
+
+namespace Drupal\system\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a MenuLocalActionPlugin type annotation object.
+ *
+ * @Annotation
+ */
+class MenuLocalActionPlugin extends Plugin {
+
+  /**
+   * The ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The static title for the local action.
+   *
+   * @var string
+   */
+  public $title;
+
+  /**
+   * The route name.
+   *
+   * @var string
+   */
+  public $route_name;
+
+  /**
+   * Array of route names this appears on.
+   *
+   * @var array
+   */
+  public $appears_on;
+}
diff --git a/core/modules/system/lib/Drupal/system/Plugin/MenuLocalActionBase.php b/core/modules/system/lib/Drupal/system/Plugin/MenuLocalActionBase.php
new file mode 100644
index 0000000..55d9bfa
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Plugin/MenuLocalActionBase.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Plugin\MenuLocalActionBase.
+ */
+
+namespace Drupal\system\Plugin;
+
+use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginBase;
+use Drupal\system\Plugin\MenuLocalActionInterface;
+use Drupal\Core\StringTranslation\Translator\TranslatorInterface;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides defaults and base methods for menu local action plugins.
+ *
+ * @todo This class needs more documetation and/or @see references.
+ */
+abstract class MenuLocalActionBase extends ContainerFactoryPluginBase implements MenuLocalActionInterface {
+  /**
+   * String translation object.
+   *
+   * @var \Drupal\Core\StringTranslation\Translator\TranslatorInterface
+   */
+  protected $t;
+
+  /**
+   * URL generator object.
+   *
+   * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
+   */
+  protected $generator;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static(
+      $container->get('string_translation'),
+      $container->get('url_generator'),
+      $configuration,
+      $plugin_id,
+      $plugin_definition
+    );
+  }
+
+  public function __construct(TranslatorInterface $string_translation,  UrlGeneratorInterface $generator, array $configuration, $plugin_id, array $plugin_definition) {
+    // This is available for subclasses that need to translate
+    // a dynamic title.
+    $this->t = $string_translation;
+    $this->generator = $generator;
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteName() {
+    return $this->pluginDefinition['route_name'];
+  }
+
+  /**
+   * Return the title to be shown for this action.
+   */
+  public function getTitle() {
+    // Subclasses may pull in the request or specific attributes as parameters.
+    return $this->pluginDefinition['title'];
+  }
+
+  /**
+   * Retun the path corresponding to the route.
+   */
+  public function getPath() {
+    // Subclasses may set a request into the generator or
+    // use any desired method to generate the path.
+    return $this->generator->generate($this->getRouteName());
+  }
+}
diff --git a/core/modules/system/lib/Drupal/system/Plugin/MenuLocalActionInterface.php b/core/modules/system/lib/Drupal/system/Plugin/MenuLocalActionInterface.php
new file mode 100644
index 0000000..8eb24d2
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Plugin/MenuLocalActionInterface.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Plugin\MenuLocalActionInterface.
+ */
+
+namespace Drupal\system\Plugin;
+
+/**
+ * Defines an interface for menu local actions.
+ */
+interface MenuLocalActionInterface {
+
+  /**
+   * Get the route name from the settings.
+   *
+   * @return string
+   *   The name of the route this actions links to.
+   */
+  public function getRouteName();
+}
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Type/MenuLocalActionManager.php b/core/modules/system/lib/Drupal/system/Plugin/Type/MenuLocalActionManager.php
new file mode 100644
index 0000000..d33c0d4
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Plugin/Type/MenuLocalActionManager.php
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Plugin\Type\MenuLocalActionManager.
+ */
+
+namespace Drupal\system\Plugin\Type;
+
+use Drupal\Core\Controller\ControllerResolver;
+use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
+use Drupal\Core\Plugin\Discovery\AlterDecorator;
+use Drupal\Core\Plugin\Discovery\CacheDecorator;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\system\Plugin\MenuLocalActionInterface;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Manages discovery and instantiation of Plugin UI plugins.
+ *
+ * @todo This class needs @see references and/or more documentation.
+ */
+class MenuLocalActionManager extends DefaultPluginManager {
+
+  /**
+   * A controller resolver object.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolver
+   */
+  protected $controllerResolver;
+
+  /**
+   * A request object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * The plugin instances.
+   *
+   * @var array
+   */
+  protected $instances = array();
+
+  /**
+   * Constructs a \Drupal\system\Plugin\Type\MenuLocalActionManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations,
+   * @param \Drupal\Core\Controller\ControllerResolver $controller_resolver
+   *   An object to use in introspecting route methods.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object to use for building titles and paths for plugin instances.
+   */
+  public function __construct(\Traversable $namespaces, ControllerResolver $controller_resolver, Request $request) {
+    $annotation_namespaces = array('Drupal\system\Annotation' => $namespaces['Drupal\system']);
+    parent::__construct('Menu', $namespaces, $annotation_namespaces, 'Drupal\system\Annotation\MenuLocalActionPlugin');
+    $this->controllerResolver = $controller_resolver;
+    $this->request = $request;
+  }
+
+  public function getTitle(MenuLocalActionInterface $local_action) {
+    $controller = array($local_action, 'getTitle');
+    $arguments = $this->controllerResolver->getArguments($this->request, $controller);
+
+    if (is_callable($controller)) {
+      return call_user_func_array($controller, $arguments);
+    }
+    // @todo What to do if it is not callable.
+    return '';
+  }
+
+  public function getPath(MenuLocalActionInterface $local_action) {
+    $controller = array($local_action, 'getPath');
+    $arguments = $this->controllerResolver->getArguments($this->request, $controller);
+    if (is_callable($controller)) {
+      return trim(call_user_func_array($controller, $arguments), '/');
+    }
+    // @todo What to do if it is not callable.
+    return '';
+  }
+
+  /**
+   * Find all local actions that appear on a named route.
+   *
+   * @param string $route_name
+   *
+   * @return array
+   *   Objects implementing MenuLocalActionInterface that appear on the route path.
+   */
+  public function getActionsForRoute($route_name) {
+
+    if (!isset($this->instances[$route_name])) {
+      $this->instances[$route_name] = array();
+      // @todo - optimize this lookup by compiling or caching.
+      foreach ($this->getDefinitions() as $plugin_id => $action_info) {
+        if (in_array($route_name, $action_info['appears_on'])) {
+          $plugin = $this->createInstance($plugin_id);
+          $this->instances[$route_name][$plugin_id] = $plugin;
+        }
+      }
+    }
+    return $this->instances[$route_name];
+  }
+}
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 16bb0de..1f7b25f 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -872,31 +872,6 @@ function hook_menu() {
 }
 
 /**
- * Define route-based local actions.
- *
- * Instead of using MENU_LOCAL_ACTION in hook_menu(), implement
- * hook_local_actions().
- *
- * @return array
- *   An associative array containing the following keys:
- *   - route_name: The machine name of the local action route.
- *   - title: The title of the local action.
- *   - appears_on: An array of route names for this action to be display on.
- */
-function hook_local_actions() {
-  return array(
-    array(
-      'route_name' => 'mymodule.route.action',
-      'title' => t('Perform local action'),
-      'appears_on' => array(
-        'mymodule.other_route',
-        'mymodule.other_other_route',
-      ),
-    ),
-  );
-}
-
-/**
  * Alter the data being saved to the {menu_router} table after hook_menu is invoked.
  *
  * This hook is invoked by menu_router_build(). The menu definitions are passed
diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml
index 6aefa00..a68b788 100644
--- a/core/modules/system/system.services.yml
+++ b/core/modules/system/system.services.yml
@@ -6,6 +6,9 @@ services:
   plugin.manager.system.plugin_ui:
     class: Drupal\system\Plugin\Type\PluginUIManager
     arguments: ['@container.namespaces']
+  plugin.manager.system.menu_local_action:
+    class: Drupal\system\Plugin\Type\MenuLocalActionManager
+    arguments: ['@container.namespaces', '@controller_resolver', '@request']
   system.manager:
     class: Drupal\system\SystemManager
     arguments: ['@module_handler', '@database']
diff --git a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Plugin/Menu/MenuTestLocalAction.php b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Plugin/Menu/MenuTestLocalAction.php
new file mode 100644
index 0000000..92b6117
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Plugin/Menu/MenuTestLocalAction.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\menu_test\Plugin\Menu\MenuTestLocalAction.
+ */
+
+namespace Drupal\menu_test\Plugin\Menu;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\system\Plugin\MenuLocalActionBase;
+use Drupal\system\Annotation\MenuLocalActionPlugin;
+
+
+/**
+ * @MenuLocalActionPlugin(
+ *   id = "menu_test_local_action3",
+ *   route_name = "menu_test_local_action3",
+ *   title = @Translation("My routing action"),
+ *   appears_on = {"menu_test_local_action1"}
+ * )
+ */
+class MenuTestLocalAction extends MenuLocalActionBase {
+
+}
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index b5c0c47..fd8f4ff 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -452,21 +452,6 @@ function menu_test_local_action_dynamic_title($arg) {
 }
 
 /**
- * Implements hook_local_actions().
- */
-function menu_test_local_actions() {
-  return array(
-    array(
-      'route_name' => 'menu_test_local_action3',
-      'title' => t('My routing action'),
-      'appears_on' => array(
-        'menu_test_local_action1',
-      ),
-    ),
-  );
-}
-
-/**
  * Implements hook_menu_local_tasks().
  *
  * If the menu_test.settings configuration 'tasks.add' has been set, adds
diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Plugin/Menu/AddViewLocalAction.php b/core/modules/views_ui/lib/Drupal/views_ui/Plugin/Menu/AddViewLocalAction.php
new file mode 100644
index 0000000..777085a
--- /dev/null
+++ b/core/modules/views_ui/lib/Drupal/views_ui/Plugin/Menu/AddViewLocalAction.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\views_ui\Plugin\Menu\AddViewLocalAction.
+ */
+
+namespace Drupal\views_ui\Plugin\Menu;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\system\Plugin\MenuLocalActionBase;
+use Drupal\system\Annotation\MenuLocalActionPlugin;
+
+
+/**
+ * @MenuLocalActionPlugin(
+ *   id = "views_add_local_action",
+ *   route_name = "views_ui.add",
+ *   title = @Translation("Add new view"),
+ *   appears_on = {"views_ui.list"}
+ * )
+ */
+class AddViewLocalAction extends MenuLocalActionBase {
+
+}
diff --git a/core/modules/views_ui/views_ui.module b/core/modules/views_ui/views_ui.module
index 4ce0595..d317d90 100644
--- a/core/modules/views_ui/views_ui.module
+++ b/core/modules/views_ui/views_ui.module
@@ -115,21 +115,6 @@ function views_ui_entity_info(&$entity_info) {
 }
 
 /**
- * Implements hook_local_actions().
- */
-function views_ui_local_actions() {
-  return array(
-    array(
-      'route_name' => 'views_ui.add',
-      'title' => t('Add new view'),
-      'appears_on' => array(
-        'views_ui.list',
-      ),
-    ),
-  );
-}
-
-/**
  * Implements hook_theme().
  */
 function views_ui_theme() {
