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
@@ -114,6 +114,7 @@
     'AlmanacServiceEditor' => 'applications/almanac/editor/AlmanacServiceEditor.php',
     'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php',
     'AlmanacServiceNameNgrams' => 'applications/almanac/storage/AlmanacServiceNameNgrams.php',
+    'AlmanacServiceNameTransaction' => 'applications/almanac/xaction/AlmanacServiceNameTransaction.php',
     'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php',
     'AlmanacServicePropertyEditEngine' => 'applications/almanac/editor/AlmanacServicePropertyEditEngine.php',
     'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php',
@@ -121,11 +122,13 @@
     'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php',
     'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php',
     'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php',
+    'AlmanacServiceTransactionType' => 'applications/almanac/xaction/AlmanacServiceTransactionType.php',
     'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php',
     'AlmanacServiceTypeDatasource' => 'applications/almanac/typeahead/AlmanacServiceTypeDatasource.php',
     'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php',
     'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php',
     'AlmanacTransaction' => 'applications/almanac/storage/AlmanacTransaction.php',
+    'AlmanacTransactionType' => 'applications/almanac/xaction/AlmanacTransactionType.php',
     'AphlictDropdownDataQuery' => 'applications/aphlict/query/AphlictDropdownDataQuery.php',
     'Aphront304Response' => 'aphront/response/Aphront304Response.php',
     'Aphront400Response' => 'aphront/response/Aphront400Response.php',
@@ -5316,18 +5319,21 @@
     'AlmanacServiceEditor' => 'AlmanacEditor',
     'AlmanacServiceListController' => 'AlmanacServiceController',
     'AlmanacServiceNameNgrams' => 'PhabricatorSearchNgrams',
+    'AlmanacServiceNameTransaction' => 'AlmanacServiceTransactionType',
     'AlmanacServicePHIDType' => 'PhabricatorPHIDType',
     'AlmanacServicePropertyEditEngine' => 'AlmanacPropertyEditEngine',
     'AlmanacServiceQuery' => 'AlmanacQuery',
     'AlmanacServiceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
     'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine',
-    'AlmanacServiceTransaction' => 'AlmanacTransaction',
+    'AlmanacServiceTransaction' => 'PhabricatorModularTransaction',
     'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'AlmanacServiceTransactionType' => 'AlmanacTransactionType',
     'AlmanacServiceType' => 'Phobject',
     'AlmanacServiceTypeDatasource' => 'PhabricatorTypeaheadDatasource',
     'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase',
     'AlmanacServiceViewController' => 'AlmanacServiceController',
     'AlmanacTransaction' => 'PhabricatorApplicationTransaction',
+    'AlmanacTransactionType' => 'PhabricatorModularTransactionType',
     'AphlictDropdownDataQuery' => 'Phobject',
     'Aphront304Response' => 'AphrontResponse',
     'Aphront400Response' => 'AphrontResponse',
diff --git a/src/applications/almanac/editor/AlmanacServiceEditEngine.php b/src/applications/almanac/editor/AlmanacServiceEditEngine.php
--- a/src/applications/almanac/editor/AlmanacServiceEditEngine.php
+++ b/src/applications/almanac/editor/AlmanacServiceEditEngine.php
@@ -98,7 +98,7 @@
         ->setKey('name')
         ->setLabel(pht('Name'))
         ->setDescription(pht('Name of the service.'))
-        ->setTransactionType(AlmanacServiceTransaction::TYPE_NAME)
+        ->setTransactionType(AlmanacServiceNameTransaction::TRANSACTIONTYPE)
         ->setIsRequired(true)
         ->setValue($object->getName()),
     );
diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php
--- a/src/applications/almanac/editor/AlmanacServiceEditor.php
+++ b/src/applications/almanac/editor/AlmanacServiceEditor.php
@@ -7,152 +7,23 @@
     return pht('Almanac Service');
   }
 
+  public function getCreateObjectTitle($author, $object) {
+    return pht('%s created this service.', $author);
+  }
+
+  public function getCreateObjectTitleForFeed($author, $object) {
+    return pht('%s created %s.', $author, $object);
+  }
+
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
-    $types[] = AlmanacServiceTransaction::TYPE_NAME;
-
     $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
     $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
 
     return $types;
   }
 
-  protected function getCustomTransactionOldValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-    switch ($xaction->getTransactionType()) {
-      case AlmanacServiceTransaction::TYPE_NAME:
-        return $object->getName();
-    }
-
-    return parent::getCustomTransactionOldValue($object, $xaction);
-  }
-
-  protected function getCustomTransactionNewValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case AlmanacServiceTransaction::TYPE_NAME:
-        return $xaction->getNewValue();
-    }
-
-    return parent::getCustomTransactionNewValue($object, $xaction);
-  }
-
-  protected function applyCustomInternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case AlmanacServiceTransaction::TYPE_NAME:
-        $object->setName($xaction->getNewValue());
-        return;
-    }
-
-    return parent::applyCustomInternalTransaction($object, $xaction);
-  }
-
-  protected function applyCustomExternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case AlmanacServiceTransaction::TYPE_NAME:
-        return;
-    }
-
-    return parent::applyCustomExternalTransaction($object, $xaction);
-  }
-
-  protected function validateTransaction(
-    PhabricatorLiskDAO $object,
-    $type,
-    array $xactions) {
-
-    $errors = parent::validateTransaction($object, $type, $xactions);
-
-    switch ($type) {
-      case AlmanacServiceTransaction::TYPE_NAME:
-        $missing = $this->validateIsEmptyTextField(
-          $object->getName(),
-          $xactions);
-
-        if ($missing) {
-          $error = new PhabricatorApplicationTransactionValidationError(
-            $type,
-            pht('Required'),
-            pht('Service name is required.'),
-            nonempty(last($xactions), null));
-
-          $error->setIsMissingFieldError(true);
-          $errors[] = $error;
-        } else {
-          foreach ($xactions as $xaction) {
-            $message = null;
-
-            $name = $xaction->getNewValue();
-
-            try {
-              AlmanacNames::validateName($name);
-            } catch (Exception $ex) {
-              $message = $ex->getMessage();
-            }
-
-            if ($message !== null) {
-              $error = new PhabricatorApplicationTransactionValidationError(
-                $type,
-                pht('Invalid'),
-                $message,
-                $xaction);
-              $errors[] = $error;
-              continue;
-            }
-
-            $other = id(new AlmanacServiceQuery())
-              ->setViewer(PhabricatorUser::getOmnipotentUser())
-              ->withNames(array($name))
-              ->executeOne();
-            if ($other && ($other->getID() != $object->getID())) {
-              $error = new PhabricatorApplicationTransactionValidationError(
-                $type,
-                pht('Not Unique'),
-                pht('Almanac services must have unique names.'),
-                last($xactions));
-              $errors[] = $error;
-              continue;
-            }
-
-            if ($name === $object->getName()) {
-              continue;
-            }
-
-            $namespace = AlmanacNamespace::loadRestrictedNamespace(
-              $this->getActor(),
-              $name);
-            if ($namespace) {
-              $error = new PhabricatorApplicationTransactionValidationError(
-                $type,
-                pht('Restricted'),
-                pht(
-                  'You do not have permission to create Almanac services '.
-                  'within the "%s" namespace.',
-                  $namespace->getName()),
-                $xaction);
-              $errors[] = $error;
-              continue;
-            }
-          }
-        }
-
-        break;
-    }
-
-    return $errors;
-  }
-
-
   protected function validateAllTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
diff --git a/src/applications/almanac/storage/AlmanacServiceTransaction.php b/src/applications/almanac/storage/AlmanacServiceTransaction.php
--- a/src/applications/almanac/storage/AlmanacServiceTransaction.php
+++ b/src/applications/almanac/storage/AlmanacServiceTransaction.php
@@ -1,37 +1,22 @@
 <?php
 
 final class AlmanacServiceTransaction
-  extends AlmanacTransaction {
+  extends PhabricatorModularTransaction {
 
-  const TYPE_NAME = 'almanac:service:name';
+  public function getApplicationName() {
+    return 'almanac';
+  }
+
+  public function getApplicationTransactionCommentObject() {
+    return null;
+  }
 
   public function getApplicationTransactionType() {
     return AlmanacServicePHIDType::TYPECONST;
   }
 
-  public function getTitle() {
-    $author_phid = $this->getAuthorPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_NAME:
-        if ($old === null) {
-          return pht(
-            '%s created this service.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s renamed this service from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        }
-        break;
-    }
-
-    return parent::getTitle();
+  public function getBaseTransactionClass() {
+    return 'AlmanacServiceTransactionType';
   }
 
 }
diff --git a/src/applications/almanac/xaction/AlmanacServiceNameTransaction.php b/src/applications/almanac/xaction/AlmanacServiceNameTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/almanac/xaction/AlmanacServiceNameTransaction.php
@@ -0,0 +1,87 @@
+<?php
+
+final class AlmanacServiceNameTransaction
+  extends AlmanacServiceTransactionType {
+
+  const TRANSACTIONTYPE = 'almanac:service:name';
+
+  public function generateOldValue($object) {
+    return $object->getName();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setName($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s renamed this service from %s to %s.',
+      $this->renderAuthorLink(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s renamed %s from %s to %s.',
+      $this->renderAuthorLink(),
+      $this->renderObject(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
+      $errors[] = $this->newRequiredError(
+        pht('Almanac services must have a name.'));
+    }
+
+    foreach ($xactions as $xaction) {
+      $name = $xaction->getNewValue();
+
+      $message = null;
+      try {
+        AlmanacNames::validateName($name);
+      } catch (Exception $ex) {
+        $message = $ex->getMessage();
+      }
+
+      if ($message !== null) {
+        $errors[] = $this->newInvalidError($message, $xaction);
+        continue;
+      }
+
+      if ($name === $object->getName()) {
+        continue;
+      }
+
+      $other = id(new AlmanacServiceQuery())
+        ->setViewer(PhabricatorUser::getOmnipotentUser())
+        ->withNames(array($name))
+        ->executeOne();
+      if ($other && ($other->getID() != $object->getID())) {
+        $errors[] = $this->newInvalidError(
+          pht('Almanac services must have unique names.'),
+          $xaction);
+        continue;
+      }
+
+      $namespace = AlmanacNamespace::loadRestrictedNamespace(
+        $this->getActor(),
+        $name);
+      if ($namespace) {
+        $errors[] = $this->newInvalidError(
+          pht(
+            'You do not have permission to create Almanac services '.
+            'within the "%s" namespace.',
+            $namespace->getName()),
+          $xaction);
+        continue;
+      }
+    }
+
+    return $errors;
+  }
+}
diff --git a/src/applications/almanac/xaction/AlmanacServiceTransactionType.php b/src/applications/almanac/xaction/AlmanacServiceTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/almanac/xaction/AlmanacServiceTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class AlmanacServiceTransactionType
+  extends AlmanacTransactionType {}
diff --git a/src/applications/almanac/xaction/AlmanacTransactionType.php b/src/applications/almanac/xaction/AlmanacTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/almanac/xaction/AlmanacTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class AlmanacTransactionType
+  extends PhabricatorModularTransactionType {}