Index: resources/sql/patches/20131227.heraldobject.sql
===================================================================
--- /dev/null
+++ resources/sql/patches/20131227.heraldobject.sql
@@ -0,0 +1,6 @@
+ALTER TABLE {$NAMESPACE}_herald.herald_rule
+  ADD triggerObjectPHID VARCHAR(64) COLLATE utf8_bin;
+
+ALTER TABLE {$NAMESPACE}_herald.herald_rule
+  ADD KEY `key_trigger` (triggerObjectPHID);
+
Index: src/applications/herald/config/HeraldRuleTypeConfig.php
===================================================================
--- src/applications/herald/config/HeraldRuleTypeConfig.php
+++ src/applications/herald/config/HeraldRuleTypeConfig.php
@@ -3,12 +3,14 @@
 final class HeraldRuleTypeConfig {
 
   const RULE_TYPE_GLOBAL = 'global';
+  const RULE_TYPE_OBJECT = 'object';
   const RULE_TYPE_PERSONAL = 'personal';
 
   public static function getRuleTypeMap() {
     $map = array(
-      self::RULE_TYPE_GLOBAL     => pht('Global'),
-      self::RULE_TYPE_PERSONAL   => pht('Personal'),
+      self::RULE_TYPE_PERSONAL => pht('Personal'),
+      self::RULE_TYPE_OBJECT => pht('Object'),
+      self::RULE_TYPE_GLOBAL => pht('Global'),
     );
     return $map;
   }
Index: src/applications/herald/controller/HeraldDisableController.php
===================================================================
--- src/applications/herald/controller/HeraldDisableController.php
+++ src/applications/herald/controller/HeraldDisableController.php
@@ -28,7 +28,7 @@
       return new Aphront404Response();
     }
 
-    if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) {
+    if ($rule->isGlobalRule()) {
       $this->requireApplicationCapability(
         HeraldCapabilityManageGlobalRules::CAPABILITY);
     }
Index: src/applications/herald/controller/HeraldNewController.php
===================================================================
--- src/applications/herald/controller/HeraldNewController.php
+++ src/applications/herald/controller/HeraldNewController.php
@@ -147,13 +147,18 @@
   private function renderRuleTypeControl(array $rule_type_map, $e_rule) {
     $request = $this->getRequest();
 
-    // Reorder array to put "personal" first.
+    // Reorder array to put less powerful rules first.
     $rule_type_map = array_select_keys(
       $rule_type_map,
       array(
         HeraldRuleTypeConfig::RULE_TYPE_PERSONAL,
+        HeraldRuleTypeConfig::RULE_TYPE_OBJECT,
+        HeraldRuleTypeConfig::RULE_TYPE_GLOBAL,
       )) + $rule_type_map;
 
+    // TODO: Enable this.
+    unset($rule_type_map[HeraldRuleTypeConfig::RULE_TYPE_OBJECT]);
+
     list($can_global, $global_link) = $this->explainApplicationCapability(
       HeraldCapabilityManageGlobalRules::CAPABILITY,
       pht('You have permission to create and manage global rules.'),
Index: src/applications/herald/controller/HeraldRuleController.php
===================================================================
--- src/applications/herald/controller/HeraldRuleController.php
+++ src/applications/herald/controller/HeraldRuleController.php
@@ -49,7 +49,7 @@
       $cancel_uri = $this->getApplicationURI();
     }
 
-    if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) {
+    if ($rule->isGlobalRule()) {
       $this->requireApplicationCapability(
         HeraldCapabilityManageGlobalRules::CAPABILITY);
     }
@@ -561,7 +561,17 @@
       ->withContentTypes(array($rule->getContentType()))
       ->execute();
 
-    if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
+    if ($rule->isObjectRule()) {
+      // Object rules may depend on other rules for the same object.
+      $all_rules += id(new HeraldRuleQuery())
+        ->setViewer($viewer)
+        ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT))
+        ->withContentTypes(array($rule->getContentType()))
+        ->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID()))
+        ->execute();
+    }
+
+    if ($rule->isPersonalRule()) {
       // Personal rules may depend upon your other personal rules.
       $all_rules += id(new HeraldRuleQuery())
         ->setViewer($viewer)
Index: src/applications/herald/engine/HeraldEngine.php
===================================================================
--- src/applications/herald/engine/HeraldEngine.php
+++ src/applications/herald/engine/HeraldEngine.php
@@ -256,6 +256,11 @@
         "Rule failed automatically because it is a personal rule and its ".
         "owner can not see the object.");
       $result = false;
+    } else if (!$this->canRuleApplyToObject($rule, $object)) {
+      $reason = pht(
+        "Rule failed automatically because it is an object rule which is ".
+        "not relevant for this object.");
+      $result = false;
     } else {
       foreach ($conditions as $condition) {
         $match = $this->doesConditionMatch($rule, $condition, $object);
@@ -381,8 +386,8 @@
     HeraldRule $rule,
     HeraldAdapter $adapter) {
 
-    // Authorship is irrelevant for global rules.
-    if ($rule->isGlobalRule()) {
+    // Authorship is irrelevant for global rules and object rules.
+    if ($rule->isGlobalRule() || $rule->isObjectRule()) {
       return true;
     }
 
@@ -405,4 +410,25 @@
       PhabricatorPolicyCapability::CAN_VIEW);
   }
 
+  private function canRuleApplyToObject(
+    HeraldRule $rule,
+    HeraldAdapter $adapter) {
+
+    // Rules which are not object rules can apply to anything.
+    if (!$rule->isObjectRule()) {
+      return true;
+    }
+
+    $trigger_phid = $rule->getTriggerObjectPHID();
+    $object_phid = $adapter->getPHID();
+
+    if ($trigger_phid == $object_phid) {
+      return true;
+    }
+
+    // TODO: We should also handle projects.
+
+    return false;
+  }
+
 }
Index: src/applications/herald/query/HeraldRuleQuery.php
===================================================================
--- src/applications/herald/query/HeraldRuleQuery.php
+++ src/applications/herald/query/HeraldRuleQuery.php
@@ -9,6 +9,7 @@
   private $ruleTypes;
   private $contentTypes;
   private $disabled;
+  private $triggerObjectPHIDs;
 
   private $needConditionsAndActions;
   private $needAppliedToPHIDs;
@@ -49,6 +50,11 @@
     return $this;
   }
 
+  public function withTriggerObjectPHIDs(array $phids) {
+    $this->triggerObjectPHIDs = $phids;
+    return $this;
+  }
+
   public function needConditionsAndActions($need) {
     $this->needConditionsAndActions = $need;
     return $this;
@@ -137,6 +143,35 @@
       }
     }
 
+    $object_phids = array();
+    foreach ($rules as $rule) {
+      if ($rule->isObjectRule()) {
+        $object_phids[] = $rule->getTriggerObjectPHID();
+      }
+    }
+
+    if ($object_phids) {
+      $objects = id(new PhabricatorObjectQuery())
+        ->setParentQuery($this)
+        ->setViewer($this->getViewer())
+        ->withPHIDs($object_phids)
+        ->execute();
+      $objects = mpull($objects, null, 'getPHID');
+    } else {
+      $objects = array();
+    }
+
+    foreach ($rules as $key => $rule) {
+      if ($rule->isObjectRule()) {
+        $object = idx($objects, $rule->getTriggerObjectPHID());
+        if (!$object) {
+          unset($rules[$key]);
+          continue;
+        }
+        $rule->attachTriggerObject($object);
+      }
+    }
+
     return $rules;
   }
 
@@ -185,6 +220,13 @@
         (int)$this->disabled);
     }
 
+    if ($this->triggerObjectPHIDs) {
+      $where[] = qsprintf(
+        $conn_r,
+        'rule.triggerObjectPHID IN (%Ls)',
+        $this->triggerObjectPHIDs);
+    }
+
     $where[] = $this->buildPagingClause($conn_r);
 
     return $this->formatWhereClause($where);
@@ -192,9 +234,9 @@
 
   private function validateRuleAuthors(array $rules) {
 
-    // "Global" rules always have valid authors.
+    // "Global" and "Object" rules always have valid authors.
     foreach ($rules as $key => $rule) {
-      if ($rule->isGlobalRule()) {
+      if ($rule->isGlobalRule() || $rule->isObjectRule()) {
         $rule->attachValidAuthor(true);
         unset($rules[$key]);
         continue;
Index: src/applications/herald/storage/HeraldRule.php
===================================================================
--- src/applications/herald/storage/HeraldRule.php
+++ src/applications/herald/storage/HeraldRule.php
@@ -15,8 +15,9 @@
   protected $repetitionPolicy;
   protected $ruleType;
   protected $isDisabled = 0;
+  protected $triggerObjectPHID;
 
-  protected $configVersion = 20;
+  protected $configVersion = 22;
 
   // phids for which this rule has been applied
   private $ruleApplied = self::ATTACHABLE;
@@ -24,6 +25,7 @@
   private $author = self::ATTACHABLE;
   private $conditions;
   private $actions;
+  private $triggerObject = self::ATTACHABLE;
 
   public function getConfiguration() {
     return array(
@@ -146,9 +148,7 @@
   }
 
   public function delete() {
-
-// TODO:
-//    $this->openTransaction();
+    $this->openTransaction();
       queryfx(
         $this->establishConnection('w'),
         'DELETE FROM %T WHERE ruleID = %d',
@@ -159,8 +159,10 @@
         'DELETE FROM %T WHERE ruleID = %d',
         id(new HeraldAction())->getTableName(),
         $this->getID());
-      parent::delete();
-//    $this->saveTransaction();
+      $result = parent::delete();
+    $this->saveTransaction();
+
+    return $result;
   }
 
   public function hasValidAuthor() {
@@ -189,6 +191,19 @@
     return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
   }
 
+  public function isObjectRule() {
+    return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT);
+  }
+
+  public function attachTriggerObject($trigger_object) {
+    $this->triggerObject = $trigger_object;
+    return $this;
+  }
+
+  public function getTriggerObject() {
+    return $this->assertAttached($this->triggerObject);
+  }
+
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
@@ -211,6 +226,8 @@
           $global = HeraldCapabilityManageGlobalRules::CAPABILITY;
           return $herald->getPolicy($global);
       }
+    } else if ($this->isObjectRule()) {
+      return $this->getTriggerObject()->getPolicy($capability);
     } else {
       return PhabricatorPolicies::POLICY_NOONE;
     }
@@ -227,6 +244,8 @@
   public function describeAutomaticCapability($capability) {
     if ($this->isPersonalRule()) {
       return pht("A personal rule's owner can always view and edit it.");
+    } else if ($this->isObjectRule()) {
+      return pht("Object rules inherit the policies of their objects.");
     }
 
     return null;
Index: src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
===================================================================
--- src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
+++ src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
@@ -1852,6 +1852,10 @@
         'type' => 'sql',
         'name' => $this->getPatchPath('20131224.harbormanual.sql'),
       ),
+      '20131227.heraldobject.sql' => array(
+        'type' => 'sql',
+        'name' => $this->getPatchPath('20131227.heraldobject.sql'),
+      ),
     );
   }
 }