diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -106,15 +106,28 @@
 
   public function getCustomActions() {
     if ($this->customActions === null) {
-      $this->customActions = id(new PhutilSymbolLoader())
+      $custom_actions = id(new PhutilSymbolLoader())
         ->setAncestorClass('HeraldCustomAction')
         ->loadObjects();
 
-      foreach ($this->customActions as $key => $object) {
+      foreach ($custom_actions as $key => $object) {
         if (!$object->appliesToAdapter($this)) {
-          unset($this->customActions[$key]);
+          unset($custom_actions[$key]);
         }
       }
+
+      $this->customActions = array();
+      foreach ($custom_actions as $action) {
+        $key = $action->getActionKey();
+
+        if (array_key_exists($key, $this->customActions)) {
+          throw new Exception(
+            'More than one Herald custom action implementation '.
+            'handles the action key: \''.$key.'\'.');
+        }
+
+        $this->customActions[$key] = $action;
+      }
     }
 
     return $this->customActions;
@@ -163,20 +176,16 @@
     }
   }
 
-  public abstract function applyHeraldEffects(array $effects);
+  abstract public function applyHeraldEffects(array $effects);
 
   protected function handleCustomHeraldEffect(HeraldEffect $effect) {
-    foreach ($this->getCustomActions() as $custom_action) {
-      if ($effect->getAction() == $custom_action->getActionKey()) {
-        $result = $custom_action->applyEffect(
-          $this,
-          $this->getObject(),
-          $effect);
-
-        if ($result !== null) {
-          return $result;
-        }
-      }
+    $custom_action = idx($this->getCustomActions(), $effect->getAction());
+
+    if ($custom_action !== null) {
+      return $custom_action->applyEffect(
+        $this,
+        $this->getObject(),
+        $effect);
     }
 
     return null;
@@ -688,16 +697,21 @@
 
 /* -(  Actions  )------------------------------------------------------------ */
 
-  public function getActions($rule_type) {
+  public function getCustomActionsForRuleType($rule_type) {
     $results = array();
     foreach ($this->getCustomActions() as $custom_action) {
       if ($custom_action->appliesToRuleType($rule_type)) {
-        $results[] = $custom_action->getActionKey();
+        $results[] = $custom_action;
       }
     }
     return $results;
   }
 
+  public function getActions($rule_type) {
+    $custom_actions = $this->getCustomActionsForRuleType($rule_type);
+    return mpull($custom_actions, 'getActionKey');
+  }
+
   public function getActionNameMap($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
@@ -737,12 +751,8 @@
         throw new Exception("Unknown rule type '{$rule_type}'!");
     }
 
-    foreach ($this->getCustomActions() as $custom_action) {
-      if ($custom_action->appliesToRuleType($rule_type)) {
-        $standard[$custom_action->getActionKey()] =
-          $custom_action->getActionName();
-      }
-    }
+    $custom_actions = $this->getCustomActionsForRuleType($rule_type);
+    $standard += mpull($custom_actions, 'getActionName', 'getActionKey');
 
     return $standard;
   }
@@ -922,12 +932,9 @@
       }
     }
 
-    foreach ($this->getCustomActions() as $custom_action) {
-      if ($custom_action->appliesToRuleType($rule_type)) {
-        if ($action === $custom_action->getActionKey()) {
-          return $custom_action->getActionType();
-        }
-      }
+    $custom_action = idx($this->getCustomActions(), $action);
+    if ($custom_action !== null) {
+      return $custom_action->getActionType();
     }
 
     throw new Exception("Unknown or invalid action '".$action."'.");