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
@@ -4621,6 +4621,8 @@
     'PhrictionDocumentQuery' => 'applications/phriction/query/PhrictionDocumentQuery.php',
     'PhrictionDocumentStatus' => 'applications/phriction/constants/PhrictionDocumentStatus.php',
     'PhrictionDocumentTitleHeraldField' => 'applications/phriction/herald/PhrictionDocumentTitleHeraldField.php',
+    'PhrictionDocumentTitleTransaction' => 'applications/phriction/xaction/PhrictionDocumentTitleTransaction.php',
+    'PhrictionDocumentTransactionType' => 'applications/phriction/xaction/PhrictionDocumentTransactionType.php',
     'PhrictionEditConduitAPIMethod' => 'applications/phriction/conduit/PhrictionEditConduitAPIMethod.php',
     'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php',
     'PhrictionHistoryConduitAPIMethod' => 'applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php',
@@ -10257,6 +10259,8 @@
     'PhrictionDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhrictionDocumentStatus' => 'PhrictionConstants',
     'PhrictionDocumentTitleHeraldField' => 'PhrictionDocumentHeraldField',
+    'PhrictionDocumentTitleTransaction' => 'PhrictionDocumentTransactionType',
+    'PhrictionDocumentTransactionType' => 'PhabricatorModularTransactionType',
     'PhrictionEditConduitAPIMethod' => 'PhrictionConduitAPIMethod',
     'PhrictionEditController' => 'PhrictionController',
     'PhrictionHistoryConduitAPIMethod' => 'PhrictionConduitAPIMethod',
@@ -10270,7 +10274,7 @@
     'PhrictionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhrictionSearchEngine' => 'PhabricatorApplicationSearchEngine',
-    'PhrictionTransaction' => 'PhabricatorApplicationTransaction',
+    'PhrictionTransaction' => 'PhabricatorModularTransaction',
     'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
diff --git a/src/applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php
--- a/src/applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php
+++ b/src/applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php
@@ -47,7 +47,7 @@
 
     $xactions = array();
     $xactions[] = id(new PhrictionTransaction())
-      ->setTransactionType(PhrictionTransaction::TYPE_TITLE)
+      ->setTransactionType(PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
       ->setNewValue($request->getValue('title'));
     $xactions[] = id(new PhrictionTransaction())
       ->setTransactionType(PhrictionTransaction::TYPE_CONTENT)
diff --git a/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php
--- a/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php
+++ b/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php
@@ -42,7 +42,7 @@
 
     $xactions = array();
     $xactions[] = id(new PhrictionTransaction())
-      ->setTransactionType(PhrictionTransaction::TYPE_TITLE)
+      ->setTransactionType(PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
       ->setNewValue($request->getValue('title'));
     $xactions[] = id(new PhrictionTransaction())
       ->setTransactionType(PhrictionTransaction::TYPE_CONTENT)
diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php
--- a/src/applications/phriction/controller/PhrictionEditController.php
+++ b/src/applications/phriction/controller/PhrictionEditController.php
@@ -133,7 +133,7 @@
 
       $xactions = array();
       $xactions[] = id(new PhrictionTransaction())
-        ->setTransactionType(PhrictionTransaction::TYPE_TITLE)
+        ->setTransactionType(PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
         ->setNewValue($title);
       $xactions[] = id(new PhrictionTransaction())
         ->setTransactionType(PhrictionTransaction::TYPE_CONTENT)
@@ -174,7 +174,8 @@
       } catch (PhabricatorApplicationTransactionValidationException $ex) {
         $validation_exception = $ex;
         $e_title = nonempty(
-          $ex->getShortMessage(PhrictionTransaction::TYPE_TITLE),
+          $ex->getShortMessage(
+            PhrictionDocumentTitleTransaction::TRANSACTIONTYPE),
           true);
         $e_content = nonempty(
           $ex->getShortMessage(PhrictionTransaction::TYPE_CONTENT),
diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php
--- a/src/applications/phriction/editor/PhrictionTransactionEditor.php
+++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php
@@ -29,7 +29,7 @@
     return $this;
   }
 
-  private function getOldContent() {
+  public function getOldContent() {
     return $this->oldContent;
   }
 
@@ -38,7 +38,7 @@
     return $this;
   }
 
-  private function getNewContent() {
+  public function getNewContent() {
     return $this->newContent;
   }
 
@@ -80,7 +80,6 @@
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
-    $types[] = PhrictionTransaction::TYPE_TITLE;
     $types[] = PhrictionTransaction::TYPE_CONTENT;
     $types[] = PhrictionTransaction::TYPE_DELETE;
     $types[] = PhrictionTransaction::TYPE_MOVE_TO;
@@ -99,11 +98,6 @@
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
-      case PhrictionTransaction::TYPE_TITLE:
-        if ($this->getIsNewObject()) {
-          return null;
-        }
-        return $this->getOldContent()->getTitle();
       case PhrictionTransaction::TYPE_CONTENT:
         if ($this->getIsNewObject()) {
           return null;
@@ -121,7 +115,6 @@
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
-      case PhrictionTransaction::TYPE_TITLE:
       case PhrictionTransaction::TYPE_CONTENT:
       case PhrictionTransaction::TYPE_DELETE:
         return $xaction->getNewValue();
@@ -154,7 +147,7 @@
 
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
-      case PhrictionTransaction::TYPE_TITLE:
+      case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
       case PhrictionTransaction::TYPE_CONTENT:
       case PhrictionTransaction::TYPE_DELETE:
       case PhrictionTransaction::TYPE_MOVE_TO:
@@ -178,7 +171,6 @@
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
-      case PhrictionTransaction::TYPE_TITLE:
       case PhrictionTransaction::TYPE_CONTENT:
       case PhrictionTransaction::TYPE_MOVE_TO:
         $object->setStatus(PhrictionDocumentStatus::STATUS_EXISTS);
@@ -232,9 +224,6 @@
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
-      case PhrictionTransaction::TYPE_TITLE:
-        $this->getNewContent()->setTitle($xaction->getNewValue());
-        break;
       case PhrictionTransaction::TYPE_CONTENT:
         $this->getNewContent()->setContent($xaction->getNewValue());
         break;
@@ -270,7 +259,7 @@
     $save_content = false;
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
-        case PhrictionTransaction::TYPE_TITLE:
+        case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
         case PhrictionTransaction::TYPE_CONTENT:
         case PhrictionTransaction::TYPE_DELETE:
         case PhrictionTransaction::TYPE_MOVE_AWAY:
@@ -312,7 +301,8 @@
               $slug);
             $stub_xactions = array();
             $stub_xactions[] = id(new PhrictionTransaction())
-              ->setTransactionType(PhrictionTransaction::TYPE_TITLE)
+              ->setTransactionType(
+                PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
               ->setNewValue(PhabricatorSlug::getDefaultTitle($slug))
               ->setMetadataValue('stub:create:phid', $object->getPHID());
             $stub_xactions[] = id(new PhrictionTransaction())
@@ -477,30 +467,6 @@
 
     foreach ($xactions as $xaction) {
       switch ($type) {
-        case PhrictionTransaction::TYPE_TITLE:
-          $title = $object->getContent()->getTitle();
-          $missing = $this->validateIsEmptyTextField(
-            $title,
-            $xactions);
-
-          if ($missing) {
-            $error = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Required'),
-              pht('Document title is required.'),
-              nonempty(last($xactions), null));
-
-            $error->setIsMissingFieldError(true);
-            $errors[] = $error;
-          } else if ($this->getProcessContentVersionError()) {
-            $error = $this->validateContentVersion($object, $type, $xaction);
-            if ($error) {
-              $this->setProcessContentVersionError(false);
-              $errors[] = $error;
-            }
-          }
-          break;
-
         case PhrictionTransaction::TYPE_CONTENT:
           if ($xaction->getMetadataValue('stub:create:phid')) {
             continue;
diff --git a/src/applications/phriction/storage/PhrictionTransaction.php b/src/applications/phriction/storage/PhrictionTransaction.php
--- a/src/applications/phriction/storage/PhrictionTransaction.php
+++ b/src/applications/phriction/storage/PhrictionTransaction.php
@@ -1,9 +1,8 @@
 <?php
 
 final class PhrictionTransaction
-  extends PhabricatorApplicationTransaction {
+  extends PhabricatorModularTransaction {
 
-  const TYPE_TITLE = 'title';
   const TYPE_CONTENT = 'content';
   const TYPE_DELETE  = 'delete';
   const TYPE_MOVE_TO = 'move-to';
@@ -27,6 +26,10 @@
     return new PhrictionTransactionComment();
   }
 
+  public function getBaseTransactionClass() {
+    return 'PhrictionDocumentTransactionType';
+  }
+
   public function getRequiredHandlePHIDs() {
     $phids = parent::getRequiredHandlePHIDs();
     $new = $this->getNewValue();
@@ -35,14 +38,13 @@
       case self::TYPE_MOVE_AWAY:
         $phids[] = $new['phid'];
         break;
-      case self::TYPE_TITLE:
+      case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
         if ($this->getMetadataValue('stub:create:phid')) {
           $phids[] = $this->getMetadataValue('stub:create:phid');
         }
         break;
     }
 
-
     return $phids;
   }
 
@@ -77,7 +79,7 @@
       case self::TYPE_MOVE_TO:
       case self::TYPE_MOVE_AWAY:
         return true;
-      case self::TYPE_TITLE:
+      case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
         return $this->getMetadataValue('stub:create:phid', false);
     }
     return parent::shouldHideForMail($xactions);
@@ -88,7 +90,7 @@
       case self::TYPE_MOVE_TO:
       case self::TYPE_MOVE_AWAY:
         return true;
-      case self::TYPE_TITLE:
+      case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
         return $this->getMetadataValue('stub:create:phid', false);
     }
     return parent::shouldHideForFeed();
@@ -96,8 +98,6 @@
 
   public function getActionStrength() {
     switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
-        return 1.4;
       case self::TYPE_CONTENT:
         return 1.3;
       case self::TYPE_DELETE:
@@ -115,29 +115,14 @@
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
-        if ($old === null) {
-          if ($this->getMetadataValue('stub:create:phid')) {
-            return pht('Stubbed');
-          } else {
-            return pht('Created');
-          }
-        }
-
-        return pht('Retitled');
-
       case self::TYPE_CONTENT:
         return pht('Edited');
-
       case self::TYPE_DELETE:
         return pht('Deleted');
-
       case self::TYPE_MOVE_TO:
         return pht('Moved');
-
       case self::TYPE_MOVE_AWAY:
         return pht('Moved Away');
-
     }
 
     return parent::getActionName();
@@ -148,7 +133,6 @@
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
       case self::TYPE_CONTENT:
         return 'fa-pencil';
       case self::TYPE_DELETE:
@@ -169,26 +153,6 @@
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
-        if ($old === null) {
-          if ($this->getMetadataValue('stub:create:phid')) {
-            return pht(
-              '%s stubbed out this document when creating %s.',
-              $this->renderHandleLink($author_phid),
-              $this->renderHandleLink(
-                $this->getMetadataValue('stub:create:phid')));
-          } else {
-            return pht(
-              '%s created this document.',
-              $this->renderHandleLink($author_phid));
-          }
-        }
-        return pht(
-          '%s changed the title from "%s" to "%s".',
-          $this->renderHandleLink($author_phid),
-          $old,
-          $new);
-
       case self::TYPE_CONTENT:
         return pht(
           '%s edited the document content.',
@@ -224,20 +188,6 @@
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
-        if ($old === null) {
-          return pht(
-            '%s created %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($object_phid));
-        }
-
-        return pht(
-          '%s renamed %s from "%s" to "%s".',
-          $this->renderHandleLink($author_phid),
-          $this->renderHandleLink($object_phid),
-          $old,
-          $new);
 
       case self::TYPE_CONTENT:
         return pht(
@@ -273,7 +223,7 @@
   public function getMailTags() {
     $tags = array();
     switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
+      case PhrictionDocumentTitleTransaction::TRANSACTIONTYPE:
         $tags[] = self::MAILTAG_TITLE;
         break;
       case self::TYPE_CONTENT:
diff --git a/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php
@@ -0,0 +1,94 @@
+<?php
+
+final class PhrictionDocumentTitleTransaction
+  extends PhrictionDocumentTransactionType {
+
+  const TRANSACTIONTYPE = 'title';
+
+  public function generateOldValue($object) {
+    if ($this->isNewObject()) {
+      return null;
+    }
+    return $this->getEditor()->getOldContent()->getTitle();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setStatus(PhrictionDocumentStatus::STATUS_EXISTS);
+    $this->getEditor()->getNewContent()->setTitle($value);
+  }
+
+  public function getActionStrength() {
+    return 1.4;
+  }
+
+  public function getActionName() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if ($old === null) {
+      if ($this->getMetadataValue('stub:create:phid')) {
+        return pht('Stubbed');
+      } else {
+        return pht('Created');
+      }
+    }
+    return pht('Retitled');
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if ($old === null) {
+      if ($this->getMetadataValue('stub:create:phid')) {
+        return pht(
+          '%s stubbed out this document when creating %s.',
+          $this->renderAuthor(),
+          $this->renderHandleLink(
+            $this->getMetadataValue('stub:create:phid')));
+      } else {
+        return pht(
+          '%s created this document.',
+          $this->renderAuthor());
+      }
+    }
+
+    return pht(
+      '%s changed the title from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function getTitleForFeed() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if ($old === null) {
+      return pht(
+        '%s created %s.',
+        $this->renderAuthor(),
+        $this->renderObject());
+    }
+
+    return pht(
+      '%s renamed %s from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderObject(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    $title = $object->getContent()->getTitle();
+    if ($this->isEmptyTextTransaction($title, $xactions)) {
+      $errors[] = $this->newRequiredError(
+        pht('Documents must have a title.'));
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/phriction/xaction/PhrictionDocumentTransactionType.php b/src/applications/phriction/xaction/PhrictionDocumentTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phriction/xaction/PhrictionDocumentTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhrictionDocumentTransactionType
+  extends PhabricatorModularTransactionType {}
diff --git a/src/applications/transactions/storage/PhabricatorModularTransaction.php b/src/applications/transactions/storage/PhabricatorModularTransaction.php
--- a/src/applications/transactions/storage/PhabricatorModularTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorModularTransaction.php
@@ -160,7 +160,7 @@
     return parent::attachViewer($viewer);
   }
 
-  final public function hasChangeDetails() {
+  /* final */ public function hasChangeDetails() {
     if ($this->getTransactionImplementation()->hasChangeDetailView()) {
       return true;
     }
@@ -168,7 +168,7 @@
     return parent::hasChangeDetails();
   }
 
-  final public function renderChangeDetails(PhabricatorUser $viewer) {
+  /* final */ public function renderChangeDetails(PhabricatorUser $viewer) {
     $impl = $this->getTransactionImplementation();
     $impl->setViewer($viewer);
     $view = $impl->newChangeDetailView();