diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -778,6 +778,7 @@
     'HeraldCondition' => 'applications/herald/storage/HeraldCondition.php',
     'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php',
     'HeraldController' => 'applications/herald/controller/HeraldController.php',
+    'HeraldCustomAction' => 'applications/herald/extension/HeraldCustomAction.php',
     'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php',
     'HeraldDifferentialRevisionAdapter' => 'applications/herald/adapter/HeraldDifferentialRevisionAdapter.php',
     'HeraldDisableController' => 'applications/herald/controller/HeraldDisableController.php',
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
@@ -100,6 +100,22 @@
   private $isNewObject;
   private $customFields = false;
 
+  private static $customActions = null;
+
+  public function __construct() {
+    if (self::$customActions === null) {
+      $symbols = id(new PhutilSymbolLoader())
+        ->setAncestorClass('HeraldCustomAction')
+        ->selectAndLoadSymbols();
+      
+      self::$customActions = array();
+      foreach ($symbols as $symbol) {
+        $name = $symbol['name'];
+        self::$customActions[] = new $name();
+      }
+    }
+  }
+
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
@@ -143,7 +159,21 @@
     }
   }
 
-  abstract public function applyHeraldEffects(array $effects);
+  public abstract function applyHeraldEffects(array $effects);
+
+  protected function handleCustomHeraldEffect(HeraldEffect $effect) {
+    foreach (self::$customActions as $customAction) {
+      $result = $customAction->applyEffect(
+        $this->getObjectForCustomActions(),
+        $effect);
+
+      if ($result !== null) {
+        return $result;
+      }
+    }
+
+    return null;
+  }
 
   public function isAvailableToUser(PhabricatorUser $viewer) {
     $applications = id(new PhabricatorApplicationQuery())
@@ -196,6 +226,10 @@
     return 1000;
   }
 
+  protected function getObjectForCustomActions() {
+    return null;
+  }
+
 
 /* -(  Fields  )------------------------------------------------------------- */
 
@@ -643,13 +677,23 @@
 
 /* -(  Actions  )------------------------------------------------------------ */
 
-  abstract public function getActions($rule_type);
+  public function getActions($rule_type) {
+    $arrays = array();
+    foreach (self::$customActions as $customAction) {
+      $result = array();
+      foreach ($customAction->getActions($this, $rule_type) as $key => $action) {
+        $result[] = 'custom:'.$key;
+      }
+      $arrays = array_merge($arrays, $result);
+    }
+    return $arrays;
+  }
 
   public function getActionNameMap($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
       case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
-        return array(
+        $standard = array(
           self::ACTION_NOTHING      => pht('Do nothing'),
           self::ACTION_ADD_CC       => pht('Add emails to CC'),
           self::ACTION_REMOVE_CC    => pht('Remove emails from CC'),
@@ -663,8 +707,9 @@
           self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'),
           self::ACTION_BLOCK => pht('Block change with message'),
         );
+        break;
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
-        return array(
+        $standard = array(
           self::ACTION_NOTHING      => pht('Do nothing'),
           self::ACTION_ADD_CC       => pht('Add me to CC'),
           self::ACTION_REMOVE_CC    => pht('Remove me from CC'),
@@ -677,9 +722,20 @@
           self::ACTION_ADD_BLOCKING_REVIEWERS =>
             pht('Add me as a blocking reviewer'),
         );
+        break;
       default:
         throw new Exception("Unknown rule type '{$rule_type}'!");
     }
+
+    foreach (self::$customActions as $customAction) {
+      $result = array();
+      foreach ($customAction->getActions($this, $rule_type) as $key => $action) {
+        $result['custom:'.$key] = $action['name'];
+      }
+      $standard = array_merge($standard, $result);
+    }
+
+    return $standard;
   }
 
   public function willSaveAction(
@@ -811,7 +867,7 @@
     }
   }
 
-  public static function getValueTypeForAction($action, $rule_type) {
+  public function getValueTypeForAction($action, $rule_type) {
     $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
 
     if ($is_personal) {
@@ -829,8 +885,6 @@
           return self::VALUE_FLAG_COLOR;
         case self::ACTION_ADD_PROJECTS:
           return self::VALUE_PROJECT;
-        default:
-          throw new Exception("Unknown or invalid action '{$action}'.");
       }
     } else {
       switch ($action) {
@@ -854,10 +908,18 @@
           return self::VALUE_BUILD_PLAN;
         case self::ACTION_BLOCK:
           return self::VALUE_TEXT;
-        default:
-          throw new Exception("Unknown or invalid action '{$action}'.");
       }
     }
+
+    foreach (self::$customActions as $customAction) {
+      foreach ($customAction->getActions($this, $rule_type) as $key => $act) {
+        if ('custom:'.$key === $action) {
+          return $act['type'];
+        }
+      }
+    }
+
+    throw new Exception("Unknown or invalid action '".$action."'.");
   }
 
 
diff --git a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
--- a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
+++ b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
@@ -47,6 +47,10 @@
       "and run build plans.");
   }
 
+  protected function getObjectForCustomActions() {
+    return $this->revision;
+  }
+
   public function supportsRuleType($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
@@ -366,25 +370,29 @@
   public function getActions($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
-        return array(
-          self::ACTION_ADD_CC,
-          self::ACTION_REMOVE_CC,
-          self::ACTION_EMAIL,
-          self::ACTION_ADD_REVIEWERS,
-          self::ACTION_ADD_BLOCKING_REVIEWERS,
-          self::ACTION_APPLY_BUILD_PLANS,
-          self::ACTION_NOTHING,
-        );
+        return array_merge(
+          array(
+            self::ACTION_ADD_CC,
+            self::ACTION_REMOVE_CC,
+            self::ACTION_EMAIL,
+            self::ACTION_ADD_REVIEWERS,
+            self::ACTION_ADD_BLOCKING_REVIEWERS,
+            self::ACTION_APPLY_BUILD_PLANS,
+            self::ACTION_NOTHING,
+          ),
+          parent::getActions($rule_type));
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
-        return array(
-          self::ACTION_ADD_CC,
-          self::ACTION_REMOVE_CC,
-          self::ACTION_EMAIL,
-          self::ACTION_FLAG,
-          self::ACTION_ADD_REVIEWERS,
-          self::ACTION_ADD_BLOCKING_REVIEWERS,
-          self::ACTION_NOTHING,
-        );
+        return array_merge(
+          array(
+            self::ACTION_ADD_CC,
+            self::ACTION_REMOVE_CC,
+            self::ACTION_EMAIL,
+            self::ACTION_FLAG,
+            self::ACTION_ADD_REVIEWERS,
+            self::ACTION_ADD_BLOCKING_REVIEWERS,
+            self::ACTION_NOTHING,
+          ),
+          parent::getActions($rule_type));
     }
   }
 
@@ -502,7 +510,13 @@
             pht('Applied build plans.'));
           break;
         default:
-          throw new Exception("No rules to handle action '{$action}'.");
+          $custom_result = parent::handleCustomHeraldEffect($effect);
+          if ($custom_result === null) {
+            throw new Exception("No rules to handle action '{$action}'.");
+          }
+
+          $result[] = $custom_result;
+          break;
       }
     }
     return $result;
diff --git a/src/applications/herald/controller/HeraldTranscriptController.php b/src/applications/herald/controller/HeraldTranscriptController.php
--- a/src/applications/herald/controller/HeraldTranscriptController.php
+++ b/src/applications/herald/controller/HeraldTranscriptController.php
@@ -310,13 +310,15 @@
           $target = $target;
           break;
         default:
-          if ($target) {
+          if (is_array($target) && $target) {
             foreach ($target as $k => $phid) {
               if (isset($handles[$phid])) {
                 $target[$k] = $handles[$phid]->getName();
               }
             }
             $target = implode("\n", $target);
+          } elseif (is_string($target)) {
+            $target = $target;
           } else {
             $target = '<empty>';
           }
diff --git a/src/applications/herald/extension/HeraldCustomAction.php b/src/applications/herald/extension/HeraldCustomAction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/herald/extension/HeraldCustomAction.php
@@ -0,0 +1,11 @@
+<?php
+
+abstract class HeraldCustomAction {
+
+  public abstract function getActions(HeraldAdapter $adapter, $rule_type);
+
+  public abstract function applyEffect($object, HeraldEffect $effect);
+
+}
+
+