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
@@ -4572,7 +4572,10 @@
     'PonderFooterView' => 'applications/ponder/view/PonderFooterView.php',
     'PonderModerateCapability' => 'applications/ponder/capability/PonderModerateCapability.php',
     'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php',
+    'PonderQuestionAnswerTransaction' => 'applications/ponder/xaction/PonderQuestionAnswerTransaction.php',
+    'PonderQuestionAnswerWikiTransaction' => 'applications/ponder/xaction/PonderQuestionAnswerWikiTransaction.php',
     'PonderQuestionCommentController' => 'applications/ponder/controller/PonderQuestionCommentController.php',
+    'PonderQuestionContentTransaction' => 'applications/ponder/xaction/PonderQuestionContentTransaction.php',
     'PonderQuestionCreateMailReceiver' => 'applications/ponder/mail/PonderQuestionCreateMailReceiver.php',
     'PonderQuestionEditController' => 'applications/ponder/controller/PonderQuestionEditController.php',
     'PonderQuestionEditor' => 'applications/ponder/editor/PonderQuestionEditor.php',
@@ -4586,9 +4589,12 @@
     'PonderQuestionSearchEngine' => 'applications/ponder/query/PonderQuestionSearchEngine.php',
     'PonderQuestionStatus' => 'applications/ponder/constants/PonderQuestionStatus.php',
     'PonderQuestionStatusController' => 'applications/ponder/controller/PonderQuestionStatusController.php',
+    'PonderQuestionStatusTransaction' => 'applications/ponder/xaction/PonderQuestionStatusTransaction.php',
+    'PonderQuestionTitleTransaction' => 'applications/ponder/xaction/PonderQuestionTitleTransaction.php',
     'PonderQuestionTransaction' => 'applications/ponder/storage/PonderQuestionTransaction.php',
     'PonderQuestionTransactionComment' => 'applications/ponder/storage/PonderQuestionTransactionComment.php',
     'PonderQuestionTransactionQuery' => 'applications/ponder/query/PonderQuestionTransactionQuery.php',
+    'PonderQuestionTransactionType' => 'applications/ponder/xaction/PonderQuestionTransactionType.php',
     'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php',
     'PonderRemarkupRule' => 'applications/ponder/remarkup/PonderRemarkupRule.php',
     'PonderSchemaSpec' => 'applications/ponder/storage/PonderSchemaSpec.php',
@@ -10126,7 +10132,10 @@
       'PhabricatorSpacesInterface',
       'PhabricatorFulltextInterface',
     ),
+    'PonderQuestionAnswerTransaction' => 'PonderQuestionTransactionType',
+    'PonderQuestionAnswerWikiTransaction' => 'PonderQuestionTransactionType',
     'PonderQuestionCommentController' => 'PonderController',
+    'PonderQuestionContentTransaction' => 'PonderQuestionTransactionType',
     'PonderQuestionCreateMailReceiver' => 'PhabricatorMailReceiver',
     'PonderQuestionEditController' => 'PonderController',
     'PonderQuestionEditor' => 'PonderEditor',
@@ -10140,9 +10149,12 @@
     'PonderQuestionSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PonderQuestionStatus' => 'PonderConstants',
     'PonderQuestionStatusController' => 'PonderController',
-    'PonderQuestionTransaction' => 'PhabricatorApplicationTransaction',
+    'PonderQuestionStatusTransaction' => 'PonderQuestionTransactionType',
+    'PonderQuestionTitleTransaction' => 'PonderQuestionTransactionType',
+    'PonderQuestionTransaction' => 'PhabricatorModularTransaction',
     'PonderQuestionTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PonderQuestionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PonderQuestionTransactionType' => 'PhabricatorModularTransactionType',
     'PonderQuestionViewController' => 'PonderController',
     'PonderRemarkupRule' => 'PhabricatorObjectRemarkupRule',
     'PonderSchemaSpec' => 'PhabricatorConfigSchemaSpec',
diff --git a/src/applications/ponder/controller/PonderAnswerSaveController.php b/src/applications/ponder/controller/PonderAnswerSaveController.php
--- a/src/applications/ponder/controller/PonderAnswerSaveController.php
+++ b/src/applications/ponder/controller/PonderAnswerSaveController.php
@@ -38,7 +38,7 @@
 
     $xactions = array();
     $xactions[] = id(new PonderQuestionTransaction())
-      ->setTransactionType(PonderQuestionTransaction::TYPE_ANSWERS)
+      ->setTransactionType(PonderQuestionAnswerTransaction::TRANSACTIONTYPE)
       ->setNewValue(
         array(
           '+' => array(
diff --git a/src/applications/ponder/controller/PonderQuestionEditController.php b/src/applications/ponder/controller/PonderQuestionEditController.php
--- a/src/applications/ponder/controller/PonderQuestionEditController.php
+++ b/src/applications/ponder/controller/PonderQuestionEditController.php
@@ -63,20 +63,23 @@
         $xactions = array();
 
         $xactions[] = id(clone $template)
-          ->setTransactionType(PonderQuestionTransaction::TYPE_TITLE)
+          ->setTransactionType(PonderQuestionTitleTransaction::TRANSACTIONTYPE)
           ->setNewValue($v_title);
 
         $xactions[] = id(clone $template)
-          ->setTransactionType(PonderQuestionTransaction::TYPE_CONTENT)
+          ->setTransactionType(
+            PonderQuestionContentTransaction::TRANSACTIONTYPE)
           ->setNewValue($v_content);
 
         $xactions[] = id(clone $template)
-          ->setTransactionType(PonderQuestionTransaction::TYPE_ANSWERWIKI)
+          ->setTransactionType(
+            PonderQuestionAnswerWikiTransaction::TRANSACTIONTYPE)
           ->setNewValue($v_wiki);
 
         if (!$is_new) {
           $xactions[] = id(clone $template)
-            ->setTransactionType(PonderQuestionTransaction::TYPE_STATUS)
+            ->setTransactionType(
+              PonderQuestionStatusTransaction::TRANSACTIONTYPE)
             ->setNewValue($v_status);
         }
 
diff --git a/src/applications/ponder/controller/PonderQuestionStatusController.php b/src/applications/ponder/controller/PonderQuestionStatusController.php
--- a/src/applications/ponder/controller/PonderQuestionStatusController.php
+++ b/src/applications/ponder/controller/PonderQuestionStatusController.php
@@ -28,7 +28,7 @@
 
       $xactions = array();
       $xactions[] = id(new PonderQuestionTransaction())
-        ->setTransactionType(PonderQuestionTransaction::TYPE_STATUS)
+        ->setTransactionType(PonderQuestionStatusTransaction::TRANSACTIONTYPE)
         ->setNewValue($v_status);
 
       $editor = id(new PonderQuestionEditor())
diff --git a/src/applications/ponder/editor/PonderQuestionEditor.php b/src/applications/ponder/editor/PonderQuestionEditor.php
--- a/src/applications/ponder/editor/PonderQuestionEditor.php
+++ b/src/applications/ponder/editor/PonderQuestionEditor.php
@@ -32,7 +32,7 @@
 
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
-        case PonderQuestionTransaction::TYPE_ANSWERS:
+        case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
           return true;
       }
     }
@@ -46,7 +46,7 @@
 
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
-        case PonderQuestionTransaction::TYPE_ANSWERS:
+        case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
           $new_value = $xaction->getNewValue();
           $new = idx($new_value, '+', array());
           foreach ($new as $new_answer) {
@@ -70,117 +70,9 @@
     $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
     $types[] = PhabricatorTransactions::TYPE_SPACE;
 
-    $types[] = PonderQuestionTransaction::TYPE_TITLE;
-    $types[] = PonderQuestionTransaction::TYPE_CONTENT;
-    $types[] = PonderQuestionTransaction::TYPE_ANSWERS;
-    $types[] = PonderQuestionTransaction::TYPE_STATUS;
-    $types[] = PonderQuestionTransaction::TYPE_ANSWERWIKI;
-
     return $types;
   }
 
-  protected function getCustomTransactionOldValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PonderQuestionTransaction::TYPE_TITLE:
-        return $object->getTitle();
-      case PonderQuestionTransaction::TYPE_CONTENT:
-        return $object->getContent();
-      case PonderQuestionTransaction::TYPE_ANSWERS:
-        return mpull($object->getAnswers(), 'getPHID');
-      case PonderQuestionTransaction::TYPE_STATUS:
-        return $object->getStatus();
-      case PonderQuestionTransaction::TYPE_ANSWERWIKI:
-        return $object->getAnswerWiki();
-    }
-  }
-
-  protected function getCustomTransactionNewValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PonderQuestionTransaction::TYPE_TITLE:
-      case PonderQuestionTransaction::TYPE_CONTENT:
-      case PonderQuestionTransaction::TYPE_STATUS:
-      case PonderQuestionTransaction::TYPE_ANSWERWIKI:
-        return $xaction->getNewValue();
-      case PonderQuestionTransaction::TYPE_ANSWERS:
-        $raw_new_value = $xaction->getNewValue();
-        $new_value = array();
-        foreach ($raw_new_value as $key => $answers) {
-          $phids = array();
-          foreach ($answers as $answer) {
-            $obj = idx($answer, 'answer');
-            if (!$answer) {
-              continue;
-            }
-            $phids[] = $obj->getPHID();
-          }
-          $new_value[$key] = $phids;
-        }
-        $xaction->setNewValue($new_value);
-        return $this->getPHIDTransactionNewValue($xaction);
-    }
-  }
-
-  protected function applyCustomInternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PonderQuestionTransaction::TYPE_TITLE:
-        $object->setTitle($xaction->getNewValue());
-        break;
-      case PonderQuestionTransaction::TYPE_CONTENT:
-        $object->setContent($xaction->getNewValue());
-        break;
-      case PonderQuestionTransaction::TYPE_STATUS:
-        $object->setStatus($xaction->getNewValue());
-        break;
-      case PonderQuestionTransaction::TYPE_ANSWERWIKI:
-        $object->setAnswerWiki($xaction->getNewValue());
-        break;
-      case PonderQuestionTransaction::TYPE_ANSWERS:
-        $old = $xaction->getOldValue();
-        $new = $xaction->getNewValue();
-
-        $add = array_diff_key($new, $old);
-        $rem = array_diff_key($old, $new);
-
-        $count = $object->getAnswerCount();
-        $count += count($add);
-        $count -= count($rem);
-
-        $object->setAnswerCount($count);
-        break;
-    }
-  }
-
-  protected function applyCustomExternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-    return;
-  }
-
-  protected function mergeTransactions(
-    PhabricatorApplicationTransaction $u,
-    PhabricatorApplicationTransaction $v) {
-
-    $type = $u->getTransactionType();
-    switch ($type) {
-      case PonderQuestionTransaction::TYPE_TITLE:
-      case PonderQuestionTransaction::TYPE_CONTENT:
-      case PonderQuestionTransaction::TYPE_STATUS:
-      case PonderQuestionTransaction::TYPE_ANSWERWIKI:
-        return $v;
-    }
-
-    return parent::mergeTransactions($u, $v);
-  }
-
   protected function supportsSearch() {
     return true;
   }
@@ -190,7 +82,7 @@
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
-      case PonderQuestionTransaction::TYPE_ANSWERS:
+      case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
         return false;
     }
 
@@ -202,7 +94,7 @@
     array $xactions) {
       foreach ($xactions as $xaction) {
         switch ($xaction->getTransactionType()) {
-          case PonderQuestionTransaction::TYPE_ANSWERS:
+          case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
             return false;
         }
       }
@@ -221,7 +113,7 @@
     array $xactions) {
       foreach ($xactions as $xaction) {
         switch ($xaction->getTransactionType()) {
-          case PonderQuestionTransaction::TYPE_ANSWERS:
+          case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
             return false;
         }
       }
@@ -269,7 +161,7 @@
       $old = $xaction->getOldValue();
       $new = $xaction->getNewValue();
       // If the user just asked the question, add the question text.
-      if ($type == PonderQuestionTransaction::TYPE_CONTENT) {
+      if ($type == PonderQuestionContentTransaction::TRANSACTIONTYPE) {
         if ($old === null) {
           $body->addRawSection($new);
         }
diff --git a/src/applications/ponder/storage/PonderQuestionTransaction.php b/src/applications/ponder/storage/PonderQuestionTransaction.php
--- a/src/applications/ponder/storage/PonderQuestionTransaction.php
+++ b/src/applications/ponder/storage/PonderQuestionTransaction.php
@@ -1,13 +1,7 @@
 <?php
 
 final class PonderQuestionTransaction
-  extends PhabricatorApplicationTransaction {
-
-  const TYPE_TITLE = 'ponder.question:question';
-  const TYPE_CONTENT = 'ponder.question:content';
-  const TYPE_ANSWERS = 'ponder.question:answer';
-  const TYPE_STATUS = 'ponder.question:status';
-  const TYPE_ANSWERWIKI = 'ponder.question:wiki';
+  extends PhabricatorModularTransaction {
 
   const MAILTAG_DETAILS = 'question:details';
   const MAILTAG_COMMENT = 'question:comment';
@@ -30,87 +24,8 @@
     return new PonderQuestionTransactionComment();
   }
 
-  public function getRequiredHandlePHIDs() {
-    $phids = parent::getRequiredHandlePHIDs();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_ANSWERS:
-        $phids[] = $this->getNewAnswerPHID();
-        $phids[] = $this->getObjectPHID();
-        break;
-    }
-
-    return $phids;
-  }
-
-  public function getRemarkupBlocks() {
-    $blocks = parent::getRemarkupBlocks();
-    switch ($this->getTransactionType()) {
-      case self::TYPE_CONTENT:
-        $blocks[] = $this->getNewValue();
-        break;
-    }
-    return $blocks;
-  }
-
-  public function getTitle() {
-    $author_phid = $this->getAuthorPHID();
-    $object_phid = $this->getObjectPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
-        if ($old === null) {
-          return pht(
-            '%s asked this question.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s edited the question title from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        }
-      case self::TYPE_CONTENT:
-        return pht(
-          '%s edited the question description.',
-          $this->renderHandleLink($author_phid));
-      case self::TYPE_ANSWERWIKI:
-        return pht(
-          '%s edited the question answer wiki.',
-          $this->renderHandleLink($author_phid));
-      case self::TYPE_ANSWERS:
-        $answer_handle = $this->getHandle($this->getNewAnswerPHID());
-        $question_handle = $this->getHandle($object_phid);
-
-        return pht(
-          '%s answered %s',
-          $this->renderHandleLink($author_phid),
-          $this->renderHandleLink($object_phid));
-      case self::TYPE_STATUS:
-        switch ($new) {
-          case PonderQuestionStatus::STATUS_OPEN:
-            return pht(
-              '%s reopened this question.',
-              $this->renderHandleLink($author_phid));
-          case PonderQuestionStatus::STATUS_CLOSED_RESOLVED:
-            return pht(
-              '%s closed this question as resolved.',
-              $this->renderHandleLink($author_phid));
-          case PonderQuestionStatus::STATUS_CLOSED_OBSOLETE:
-            return pht(
-              '%s closed this question as obsolete.',
-              $this->renderHandleLink($author_phid));
-          case PonderQuestionStatus::STATUS_CLOSED_INVALID:
-            return pht(
-              '%s closed this question as invalid.',
-              $this->renderHandleLink($author_phid));
-        }
-    }
-
-    return parent::getTitle();
+  public function getBaseTransactionClass() {
+    return 'PonderQuestionTransactionType';
   }
 
   public function getMailTags() {
@@ -120,13 +35,13 @@
       case PhabricatorTransactions::TYPE_COMMENT:
         $tags[] = self::MAILTAG_COMMENT;
         break;
-      case self::TYPE_TITLE:
-      case self::TYPE_CONTENT:
-      case self::TYPE_STATUS:
-      case self::TYPE_ANSWERWIKI:
+      case PonderQuestionTitleTransaction::TRANSACTIONTYPE:
+      case PonderQuestionContentTransaction::TRANSACTIONTYPE:
+      case PonderQuestionStatusTransaction::TRANSACTIONTYPE:
+      case PonderQuestionAnswerWikiTransaction::TRANSACTIONTYPE:
         $tags[] = self::MAILTAG_DETAILS;
         break;
-      case self::TYPE_ANSWERS:
+      case PonderQuestionAnswerTransaction::TRANSACTIONTYPE:
         $tags[] = self::MAILTAG_ANSWERS;
         break;
       default:
@@ -136,182 +51,4 @@
     return $tags;
   }
 
-  public function getIcon() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
-      case self::TYPE_CONTENT:
-      case self::TYPE_ANSWERWIKI:
-        return 'fa-pencil';
-      case self::TYPE_STATUS:
-        return PonderQuestionStatus::getQuestionStatusIcon($new);
-      case self::TYPE_ANSWERS:
-        return 'fa-plus';
-    }
-
-    return parent::getIcon();
-  }
-
-  public function getColor() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
-      case self::TYPE_CONTENT:
-      case self::TYPE_ANSWERWIKI:
-        return PhabricatorTransactions::COLOR_BLUE;
-      case self::TYPE_ANSWERS:
-        return PhabricatorTransactions::COLOR_GREEN;
-      case self::TYPE_STATUS:
-        return PonderQuestionStatus::getQuestionStatusTagColor($new);
-    }
-  }
-
-  public function hasChangeDetails() {
-    switch ($this->getTransactionType()) {
-      case self::TYPE_CONTENT:
-      case self::TYPE_ANSWERWIKI:
-        return true;
-    }
-    return parent::hasChangeDetails();
-  }
-
-  public function renderChangeDetails(PhabricatorUser $viewer) {
-    return $this->renderTextCorpusChangeDetails(
-      $viewer,
-      $this->getOldValue(),
-      $this->getNewValue());
-  }
-
-  public function getActionStrength() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
-        if ($old === null) {
-          return 3;
-        }
-        break;
-      case self::TYPE_ANSWERS:
-        return 2;
-    }
-
-    return parent::getActionStrength();
-  }
-
-  public function getActionName() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
-        if ($old === null) {
-          return pht('Asked');
-        }
-        break;
-      case self::TYPE_ANSWERS:
-        return pht('Answered');
-    }
-
-    return parent::getActionName();
-  }
-
-  public function getTitleForFeed() {
-    $author_phid = $this->getAuthorPHID();
-    $object_phid = $this->getObjectPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_TITLE:
-        if ($old === null) {
-          return pht(
-            '%s asked a question: %s',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($object_phid));
-        } else {
-          return pht(
-            '%s edited the title of %s (was "%s")',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($object_phid),
-            $old);
-        }
-      case self::TYPE_CONTENT:
-        return pht(
-          '%s edited the description of %s',
-          $this->renderHandleLink($author_phid),
-          $this->renderHandleLink($object_phid));
-      case self::TYPE_ANSWERWIKI:
-        return pht(
-          '%s edited the answer wiki for %s',
-          $this->renderHandleLink($author_phid),
-          $this->renderHandleLink($object_phid));
-      case self::TYPE_ANSWERS:
-        $answer_handle = $this->getHandle($this->getNewAnswerPHID());
-        $question_handle = $this->getHandle($object_phid);
-        return pht(
-          '%s answered %s',
-          $this->renderHandleLink($author_phid),
-          $answer_handle->renderLink($question_handle->getFullName()));
-      case self::TYPE_STATUS:
-        switch ($new) {
-          case PonderQuestionStatus::STATUS_OPEN:
-            return pht(
-              '%s reopened %s.',
-              $this->renderHandleLink($author_phid),
-              $this->renderHandleLink($object_phid));
-          case PonderQuestionStatus::STATUS_CLOSED_RESOLVED:
-            return pht(
-              '%s closed %s as resolved.',
-              $this->renderHandleLink($author_phid),
-              $this->renderHandleLink($object_phid));
-          case PonderQuestionStatus::STATUS_CLOSED_INVALID:
-            return pht(
-              '%s closed %s as invalid.',
-              $this->renderHandleLink($author_phid),
-              $this->renderHandleLink($object_phid));
-          case PonderQuestionStatus::STATUS_CLOSED_OBSOLETE:
-            return pht(
-              '%s closed %s as obsolete.',
-              $this->renderHandleLink($author_phid),
-              $this->renderHandleLink($object_phid));
-        }
-    }
-
-    return parent::getTitleForFeed();
-  }
-
-  public function getRemarkupBodyForFeed(PhabricatorFeedStory $story) {
-    $text = null;
-    switch ($this->getTransactionType()) {
-      case self::TYPE_CONTENT:
-        $text = $this->getNewValue();
-        break;
-    }
-    return $text;
-  }
-
-  /**
-   * Currently the application only supports adding answers one at a time.
-   * This data is stored as a list of phids. Use this function to get the
-   * new phid.
-   */
-  private function getNewAnswerPHID() {
-    $new = $this->getNewValue();
-    $old = $this->getOldValue();
-    $add = array_diff($new, $old);
-
-    if (count($add) != 1) {
-      throw new Exception(
-        pht('There should be only one answer added at a time.'));
-    }
-
-    return reset($add);
-  }
-
 }
diff --git a/src/applications/ponder/xaction/PonderQuestionAnswerTransaction.php b/src/applications/ponder/xaction/PonderQuestionAnswerTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/ponder/xaction/PonderQuestionAnswerTransaction.php
@@ -0,0 +1,28 @@
+<?php
+
+final class PonderQuestionAnswerTransaction
+  extends PonderQuestionTransactionType {
+
+  const TRANSACTIONTYPE = 'ponder.question:answer';
+
+  public function generateOldValue($object) {
+    return $object->getAnswers();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $count = $object->getAnswerCount();
+    $count++;
+    $object->setAnswerCount($count);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s added an answer.',
+      $this->renderAuthor());
+  }
+
+  public function getIcon() {
+    return 'fa-plus';
+  }
+
+}
diff --git a/src/applications/ponder/xaction/PonderQuestionAnswerWikiTransaction.php b/src/applications/ponder/xaction/PonderQuestionAnswerWikiTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/ponder/xaction/PonderQuestionAnswerWikiTransaction.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PonderQuestionAnswerWikiTransaction
+  extends PonderQuestionTransactionType {
+
+  const TRANSACTIONTYPE = 'ponder.question:wiki';
+
+  public function generateOldValue($object) {
+    return $object->getAnswerWiki();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setAnswerWiki($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s updated the answer wiki.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s updated the answer wiki for %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function hasChangeDetailView() {
+    return true;
+  }
+
+  public function getMailDiffSectionHeader() {
+    return pht('CHANGES TO ANSWER WIKI');
+  }
+
+  public function newChangeDetailView() {
+    $viewer = $this->getViewer();
+
+    return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+      ->setViewer($viewer)
+      ->setOldText($this->getOldValue())
+      ->setNewText($this->getNewValue());
+  }
+
+  public function newRemarkupChanges() {
+    $changes = array();
+
+    $changes[] = $this->newRemarkupChange()
+      ->setOldValue($this->getOldValue())
+      ->setNewValue($this->getNewValue());
+
+    return $changes;
+  }
+
+}
diff --git a/src/applications/ponder/xaction/PonderQuestionContentTransaction.php b/src/applications/ponder/xaction/PonderQuestionContentTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/ponder/xaction/PonderQuestionContentTransaction.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PonderQuestionContentTransaction
+  extends PonderQuestionTransactionType {
+
+  const TRANSACTIONTYPE = 'ponder.question:content';
+
+  public function generateOldValue($object) {
+    return $object->getContent();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setContent($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s updated the question details.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s updated the question details for %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function hasChangeDetailView() {
+    return true;
+  }
+
+  public function getMailDiffSectionHeader() {
+    return pht('CHANGES TO QUESTION DETAILS');
+  }
+
+  public function newChangeDetailView() {
+    $viewer = $this->getViewer();
+
+    return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+      ->setViewer($viewer)
+      ->setOldText($this->getOldValue())
+      ->setNewText($this->getNewValue());
+  }
+
+  public function newRemarkupChanges() {
+    $changes = array();
+
+    $changes[] = $this->newRemarkupChange()
+      ->setOldValue($this->getOldValue())
+      ->setNewValue($this->getNewValue());
+
+    return $changes;
+  }
+
+}
diff --git a/src/applications/ponder/xaction/PonderQuestionStatusTransaction.php b/src/applications/ponder/xaction/PonderQuestionStatusTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/ponder/xaction/PonderQuestionStatusTransaction.php
@@ -0,0 +1,74 @@
+<?php
+
+final class PonderQuestionStatusTransaction
+  extends PonderQuestionTransactionType {
+
+  const TRANSACTIONTYPE = 'ponder.question:status';
+
+  public function generateOldValue($object) {
+    return $object->getStatus();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setStatus($value);
+  }
+
+  public function getTitle() {
+    $new = $this->getNewValue();
+    switch ($new) {
+      case PonderQuestionStatus::STATUS_OPEN:
+        return pht(
+          '%s reopened this question.',
+          $this->renderAuthor());
+      case PonderQuestionStatus::STATUS_CLOSED_RESOLVED:
+        return pht(
+          '%s closed this question as resolved.',
+          $this->renderAuthor());
+      case PonderQuestionStatus::STATUS_CLOSED_OBSOLETE:
+        return pht(
+          '%s closed this question as obsolete.',
+          $this->renderAuthor());
+      case PonderQuestionStatus::STATUS_CLOSED_INVALID:
+        return pht(
+          '%s closed this question as invalid.',
+          $this->renderAuthor());
+    }
+  }
+
+  public function getTitleForFeed() {
+    $new = $this->getNewValue();
+    switch ($new) {
+      case PonderQuestionStatus::STATUS_OPEN:
+        return pht(
+          '%s reopened %s.',
+          $this->renderAuthor(),
+          $this->renderObject());
+      case PonderQuestionStatus::STATUS_CLOSED_RESOLVED:
+        return pht(
+          '%s closed %s as resolved.',
+          $this->renderAuthor(),
+          $this->renderObject());
+      case PonderQuestionStatus::STATUS_CLOSED_INVALID:
+        return pht(
+          '%s closed %s as invalid.',
+          $this->renderAuthor(),
+          $this->renderObject());
+      case PonderQuestionStatus::STATUS_CLOSED_OBSOLETE:
+        return pht(
+          '%s closed %s as obsolete.',
+          $this->renderAuthor(),
+          $this->renderObject());
+    }
+  }
+
+  public function getIcon() {
+    $new = $this->getNewValue();
+    return PonderQuestionStatus::getQuestionStatusIcon($new);
+  }
+
+  public function getColor() {
+    $new = $this->getNewValue();
+    return PonderQuestionStatus::getQuestionStatusTagColor($new);
+  }
+
+}
diff --git a/src/applications/ponder/xaction/PonderQuestionTitleTransaction.php b/src/applications/ponder/xaction/PonderQuestionTitleTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/ponder/xaction/PonderQuestionTitleTransaction.php
@@ -0,0 +1,55 @@
+<?php
+
+final class PonderQuestionTitleTransaction
+  extends PonderQuestionTransactionType {
+
+  const TRANSACTIONTYPE = 'ponder.question:question';
+
+  public function generateOldValue($object) {
+    return $object->getTitle();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setTitle($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s updated the question from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s updated %s from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderObject(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    if ($this->isEmptyTextTransaction($object->getTitle(), $xactions)) {
+      $errors[] = $this->newRequiredError(
+        pht('Questions must have a title.'));
+    }
+
+    $max_length = $object->getColumnMaximumByteLength('title');
+    foreach ($xactions as $xaction) {
+      $new_value = $xaction->getNewValue();
+      $new_length = strlen($new_value);
+      if ($new_length > $max_length) {
+        $errors[] = $this->newInvalidError(
+          pht('The title can be no longer than %s characters.',
+          new PhutilNumber($max_length)));
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/ponder/xaction/PonderQuestionTransactionType.php b/src/applications/ponder/xaction/PonderQuestionTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/ponder/xaction/PonderQuestionTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PonderQuestionTransactionType
+  extends PhabricatorModularTransactionType {}