diff --git a/resources/sql/autopatches/20190226.harbor.01.planprops.sql b/resources/sql/autopatches/20190226.harbor.01.planprops.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20190226.harbor.01.planprops.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildplan
+  ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190226.harbor.02.planvalue.sql b/resources/sql/autopatches/20190226.harbor.02.planvalue.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20190226.harbor.02.planvalue.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildplan
+  SET properties = '{}' WHERE properties = '';
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
@@ -1328,6 +1328,9 @@
     'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
     'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
     'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php',
+    'HarbormasterBuildPlanBehavior' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php',
+    'HarbormasterBuildPlanBehaviorOption' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php',
+    'HarbormasterBuildPlanBehaviorTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php',
     'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php',
     'HarbormasterBuildPlanDefaultEditCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultEditCapability.php',
     'HarbormasterBuildPlanDefaultViewCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultViewCapability.php',
@@ -1424,6 +1427,7 @@
     'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php',
     'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
     'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php',
+    'HarbormasterPlanBehaviorController' => 'applications/harbormaster/controller/HarbormasterPlanBehaviorController.php',
     'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php',
     'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php',
     'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php',
@@ -6942,6 +6946,9 @@
       'PhabricatorConduitResultInterface',
       'PhabricatorProjectInterface',
     ),
+    'HarbormasterBuildPlanBehavior' => 'Phobject',
+    'HarbormasterBuildPlanBehaviorOption' => 'Phobject',
+    'HarbormasterBuildPlanBehaviorTransaction' => 'HarbormasterBuildPlanTransactionType',
     'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource',
     'HarbormasterBuildPlanDefaultEditCapability' => 'PhabricatorPolicyCapability',
     'HarbormasterBuildPlanDefaultViewCapability' => 'PhabricatorPolicyCapability',
@@ -7057,6 +7064,7 @@
     'HarbormasterMessageType' => 'Phobject',
     'HarbormasterObject' => 'HarbormasterDAO',
     'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup',
+    'HarbormasterPlanBehaviorController' => 'HarbormasterPlanController',
     'HarbormasterPlanController' => 'HarbormasterController',
     'HarbormasterPlanDisableController' => 'HarbormasterPlanController',
     'HarbormasterPlanEditController' => 'HarbormasterPlanController',
diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
--- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
+++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
@@ -83,6 +83,8 @@
             => 'HarbormasterPlanEditController',
           'order/(?:(?P<id>\d+)/)?' => 'HarbormasterPlanOrderController',
           'disable/(?P<id>\d+)/' => 'HarbormasterPlanDisableController',
+          'behavior/(?P<id>\d+)/(?P<behaviorKey>[^/]+)/' =>
+             'HarbormasterPlanBehaviorController',
           'run/(?P<id>\d+)/' => 'HarbormasterPlanRunController',
           '(?P<id>\d+)/' => 'HarbormasterPlanViewController',
         ),
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php b/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php
@@ -0,0 +1,92 @@
+<?php
+
+final class HarbormasterPlanBehaviorController
+  extends HarbormasterPlanController {
+
+  public function handleRequest(AphrontRequest $request) {
+    $viewer = $this->getViewer();
+
+    $plan = id(new HarbormasterBuildPlanQuery())
+      ->setViewer($viewer)
+      ->withIDs(array($request->getURIData('id')))
+      ->requireCapabilities(
+        array(
+          PhabricatorPolicyCapability::CAN_VIEW,
+          PhabricatorPolicyCapability::CAN_EDIT,
+        ))
+      ->executeOne();
+    if (!$plan) {
+      return new Aphront404Response();
+    }
+
+    $behavior_key = $request->getURIData('behaviorKey');
+    $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+
+    $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+    $behavior = idx($behaviors, $behavior_key);
+    if (!$behavior) {
+      return new Aphront404Response();
+    }
+
+    $plan_uri = $plan->getURI();
+
+    $v_option = $behavior->getPlanOption($plan)->getKey();
+    if ($request->isFormPost()) {
+      $v_option = $request->getStr('option');
+
+      $xactions = array();
+
+      $xactions[] = id(new HarbormasterBuildPlanTransaction())
+        ->setTransactionType(
+          HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE)
+        ->setMetadataValue($metadata_key, $behavior_key)
+        ->setNewValue($v_option);
+
+      $editor = id(new HarbormasterBuildPlanEditor())
+        ->setActor($viewer)
+        ->setContinueOnNoEffect(true)
+        ->setContinueOnMissingFields(true)
+        ->setContentSourceFromRequest($request);
+
+      $editor->applyTransactions($plan, $xactions);
+
+      return id(new AphrontRedirectResponse())->setURI($plan_uri);
+    }
+
+    $select_control = id(new AphrontFormRadioButtonControl())
+      ->setName('option')
+      ->setValue($v_option)
+      ->setLabel(pht('Option'));
+
+    foreach ($behavior->getOptions() as $option) {
+      $icon = id(new PHUIIconView())
+        ->setIcon($option->getIcon());
+
+      $select_control->addButton(
+        $option->getKey(),
+        array(
+          $icon,
+          ' ',
+          $option->getName(),
+        ),
+        $option->getDescription());
+    }
+
+    $form = id(new AphrontFormView())
+      ->setViewer($viewer)
+      ->appendInstructions(
+        pht(
+          'Choose a build plan behavior for "%s".',
+          phutil_tag('strong', array(), $behavior->getName())))
+      ->appendRemarkupInstructions($behavior->getEditInstructions())
+      ->appendControl($select_control);
+
+    return $this->newDialog()
+      ->setTitle(pht('Edit Behavior: %s', $behavior->getName()))
+      ->appendForm($form)
+      ->setWidth(AphrontDialogView::WIDTH_FORM)
+      ->addSubmitButton(pht('Save Changes'))
+      ->addCancelButton($plan_uri);
+  }
+
+}
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
--- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
@@ -61,6 +61,7 @@
     }
 
     $builds_view = $this->newBuildsView($plan);
+    $options_view = $this->newOptionsView($plan);
 
     $timeline = $this->buildTransactionTimeline(
       $plan,
@@ -74,6 +75,7 @@
         array(
           $error,
           $step_list,
+          $options_view,
           $builds_view,
           $timeline,
         ));
@@ -484,4 +486,75 @@
       ->appendChild($list);
   }
 
+
+  private function newOptionsView(HarbormasterBuildPlan $plan) {
+    $viewer = $this->getViewer();
+
+    $can_edit = PhabricatorPolicyFilter::hasCapability(
+      $viewer,
+      $plan,
+      PhabricatorPolicyCapability::CAN_EDIT);
+
+    $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+
+    $rows = array();
+    foreach ($behaviors as $behavior) {
+      $option = $behavior->getPlanOption($plan);
+
+      $icon = $option->getIcon();
+      $icon = id(new PHUIIconView())->setIcon($icon);
+
+      $edit_uri = new PhutilURI(
+        $this->getApplicationURI(
+          urisprintf(
+            'plan/behavior/%d/%s/',
+            $plan->getID(),
+            $behavior->getKey())));
+
+      $edit_button = id(new PHUIButtonView())
+        ->setTag('a')
+        ->setColor(PHUIButtonView::GREY)
+        ->setSize(PHUIButtonView::SMALL)
+        ->setDisabled(!$can_edit)
+        ->setWorkflow(true)
+        ->setText(pht('Edit'))
+        ->setHref($edit_uri);
+
+      $rows[] = array(
+        $icon,
+        $behavior->getName(),
+        $option->getName(),
+        $option->getDescription(),
+        $edit_button,
+      );
+    }
+
+    $table = id(new AphrontTableView($rows))
+      ->setHeaders(
+        array(
+          null,
+          pht('Name'),
+          pht('Behavior'),
+          pht('Details'),
+          null,
+        ))
+      ->setColumnClasses(
+        array(
+          null,
+          'pri',
+          null,
+          'wide',
+          null,
+        ));
+
+
+    $header = id(new PHUIHeaderView())
+      ->setHeader(pht('Plan Behaviors'));
+
+    return id(new PHUIObjectBoxView())
+      ->setHeader($header)
+      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+      ->setTable($table);
+  }
+
 }
diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php
--- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php
+++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php
@@ -77,7 +77,7 @@
   }
 
   protected function buildCustomEditFields($object) {
-    return array(
+    $fields = array(
       id(new PhabricatorTextEditField())
         ->setKey('name')
         ->setLabel(pht('Name'))
@@ -89,6 +89,36 @@
         ->setConduitTypeDescription(pht('New plan name.'))
         ->setValue($object->getName()),
     );
+
+
+    $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+
+    $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+    foreach ($behaviors as $behavior) {
+      $key = $behavior->getKey();
+
+      // Get the raw key off the object so that we don't reset stuff to
+      // default values by mistake if a behavior goes missing somehow.
+      $storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
+        $key);
+      $behavior_option = $object->getPlanProperty($storage_key);
+
+      if (!strlen($behavior_option)) {
+        $behavior_option = $behavior->getPlanOption($object)->getKey();
+      }
+
+      $fields[] = id(new PhabricatorSelectEditField())
+        ->setIsFormField(false)
+        ->setKey(sprintf('behavior.%s', $behavior->getKey()))
+        ->setMetadataValue($metadata_key, $behavior->getKey())
+        ->setLabel(pht('Behavior: %s', $behavior->getName()))
+        ->setTransactionType(
+          HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE)
+        ->setValue($behavior_option)
+        ->setOptions($behavior->getOptionMap());
+    }
+
+    return $fields;
   }
 
 }
diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php
@@ -0,0 +1,348 @@
+<?php
+
+final class HarbormasterBuildPlanBehavior
+  extends Phobject {
+
+  private $key;
+  private $name;
+  private $options;
+  private $defaultKey;
+  private $editInstructions;
+
+  public function setKey($key) {
+    $this->key = $key;
+    return $this;
+  }
+
+  public function getKey() {
+    return $this->key;
+  }
+
+  public function setName($name) {
+    $this->name = $name;
+    return $this;
+  }
+
+  public function getName() {
+    return $this->name;
+  }
+
+  public function setEditInstructions($edit_instructions) {
+    $this->editInstructions = $edit_instructions;
+    return $this;
+  }
+
+  public function getEditInstructions() {
+    return $this->editInstructions;
+  }
+
+  public function getOptionMap() {
+    return mpull($this->options, 'getName', 'getKey');
+  }
+
+  public function setOptions(array $options) {
+    assert_instances_of($options, 'HarbormasterBuildPlanBehaviorOption');
+
+    $key_map = array();
+    $default = null;
+
+    foreach ($options as $option) {
+      $key = $option->getKey();
+
+      if (isset($key_map[$key])) {
+        throw new Exception(
+          pht(
+            'Multiple behavior options (for behavior "%s") have the same '.
+            'key ("%s"). Each option must have a unique key.',
+            $this->getKey(),
+            $key));
+      }
+      $key_map[$key] = true;
+
+      if ($option->getIsDefault()) {
+        if ($default === null) {
+          $default = $key;
+        } else {
+          throw new Exception(
+            pht(
+              'Multiple behavior options (for behavior "%s") are marked as '.
+              'default options ("%s" and "%s"). Exactly one option must be '.
+              'marked as the default option.',
+              $this->getKey(),
+              $default,
+              $key));
+        }
+      }
+    }
+
+    if ($default === null) {
+      throw new Exception(
+        pht(
+          'No behavior option is marked as the default option (for '.
+          'behavior "%s"). Exactly one option must be marked as the '.
+          'default option.',
+          $this->getKey()));
+    }
+
+    $this->options = mpull($options, null, 'getKey');
+    $this->defaultKey = $default;
+
+    return $this;
+  }
+
+  public function getOptions() {
+    return $this->options;
+  }
+
+  public function getPlanOption(HarbormasterBuildPlan $plan) {
+    $behavior_key = $this->getKey();
+    $storage_key = self::getStorageKeyForBehaviorKey($behavior_key);
+
+    $plan_value = $plan->getPlanProperty($storage_key);
+    if (isset($this->options[$plan_value])) {
+      return $this->options[$plan_value];
+    }
+
+    return idx($this->options, $this->defaultKey);
+  }
+
+  public static function getTransactionMetadataKey() {
+    return 'behavior-key';
+  }
+
+  public static function getStorageKeyForBehaviorKey($behavior_key) {
+    return sprintf('behavior.%s', $behavior_key);
+  }
+
+  public static function newPlanBehaviors() {
+    $draft_options = array(
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('always')
+        ->setIcon('fa-check-circle-o green')
+        ->setName(pht('Always'))
+        ->setIsDefault(true)
+        ->setDescription(
+          pht(
+            'Revisions are not sent for review until the build completes, '.
+            'and are returned to the author for updates if the build fails.')),
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('building')
+        ->setIcon('fa-pause-circle-o yellow')
+        ->setName(pht('If Building'))
+        ->setDescription(
+          pht(
+            'Revisions are not sent for review until the build completes, '.
+            'but they will be sent for review even if it fails.')),
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('never')
+        ->setIcon('fa-circle-o red')
+        ->setName(pht('Never'))
+        ->setDescription(
+          pht(
+            'Revisions are sent for review regardless of the status of the '.
+            'build.')),
+    );
+
+    $land_options = array(
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('always')
+        ->setIcon('fa-check-circle-o green')
+        ->setName(pht('Always'))
+        ->setIsDefault(true)
+        ->setDescription(
+          pht(
+            '"arc land" warns if the build is still running or has '.
+            'failed.')),
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('building')
+        ->setIcon('fa-pause-circle-o yellow')
+        ->setName(pht('If Building'))
+        ->setDescription(
+          pht(
+            '"arc land" warns if the build is still running, but ignores '.
+            'the build if it has failed.')),
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('complete')
+        ->setIcon('fa-dot-circle-o yellow')
+        ->setName(pht('If Complete'))
+        ->setDescription(
+          pht(
+            '"arc land" warns if the build has failed, but ignores the '.
+            'build if it is still running.')),
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('never')
+        ->setIcon('fa-circle-o red')
+        ->setName(pht('Never'))
+        ->setDescription(
+          pht(
+            '"arc land" never warns that the build is still running or '.
+            'has failed.')),
+    );
+
+    $aggregate_options = array(
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('always')
+        ->setIcon('fa-check-circle-o green')
+        ->setName(pht('Always'))
+        ->setIsDefault(true)
+        ->setDescription(
+          pht(
+            'The buildable waits for the build, and fails if the '.
+            'build fails.')),
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('building')
+        ->setIcon('fa-pause-circle-o yellow')
+        ->setName(pht('If Building'))
+        ->setDescription(
+          pht(
+            'The buildable waits for the build, but does not fail '.
+            'if the build fails.')),
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('never')
+        ->setIcon('fa-circle-o red')
+        ->setName(pht('Never'))
+        ->setDescription(
+          pht(
+            'The buildable does not wait for the build.')),
+    );
+
+    $restart_options = array(
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('always')
+        ->setIcon('fa-repeat green')
+        ->setName(pht('Always'))
+        ->setIsDefault(true)
+        ->setDescription(
+          pht('The build may be restarted.')),
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('never')
+        ->setIcon('fa-times red')
+        ->setName(pht('Never'))
+        ->setDescription(
+          pht('The build may not be restarted.')),
+    );
+
+    $run_options = array(
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('edit')
+        ->setIcon('fa-pencil green')
+        ->setName(pht('If Editable'))
+        ->setIsDefault(true)
+        ->setDescription(
+          pht('Only users who can edit the plan can run it manually.')),
+      id(new HarbormasterBuildPlanBehaviorOption())
+        ->setKey('view')
+        ->setIcon('fa-exclamation-triangle yellow')
+        ->setName(pht('If Viewable'))
+        ->setDescription(
+          pht(
+            'Any user who can view the plan can run it manually.')),
+    );
+
+    $behaviors = array(
+      id(new self())
+        ->setKey('hold-drafts')
+        ->setName(pht('Hold Drafts'))
+        ->setEditInstructions(
+          pht(
+            'When users create revisions in Differential, the default '.
+            'behavior is to hold them in the "Draft" state until all builds '.
+            'pass. Once builds pass, the revisions promote and are sent for '.
+            'review, which notifies reviewers.'.
+            "\n\n".
+            'The general intent of this workflow is to make sure reviewers '.
+            'are only spending time on review once changes survive automated '.
+            'tests. If a change does not pass tests, it usually is not '.
+            'really ready for review.'.
+            "\n\n".
+            'If you want to promote revisions out of "Draft" before builds '.
+            'pass, or promote revisions even when builds fail, you can '.
+            'change the promotion behavior. This may be useful if you have '.
+            'very long-running builds, or some builds which are not very '.
+            'important.'.
+            "\n\n".
+            'Users may always use "Request Review" to promote a "Draft" '.
+            'revision, even if builds have failed or are still in progress.'))
+        ->setOptions($draft_options),
+      id(new self())
+        ->setKey('arc-land')
+        ->setName(pht('Warn When Landing'))
+        ->setEditInstructions(
+          pht(
+            'When a user attempts to `arc land` a revision and that revision '.
+            'has ongoing or failed builds, the default behavior of `arc` is '.
+            'to warn them about those builds and give them a chance to '.
+            'reconsider: they may want to wait for ongoing builds to '.
+            'complete, or fix failed builds before landing the change.'.
+            "\n\n".
+            'If you do not want to warn users about this build, you can '.
+            'change the warning behavior. This may be useful if the build '.
+            'takes a long time to run (so you do not expect users to wait '.
+            'for it) or the outcome is not important.'.
+            "\n\n".
+            'This warning is only advisory. Users may always elect to ignore '.
+            'this warning and continue, even if builds have failed.'))
+        ->setOptions($land_options),
+      id(new self())
+        ->setKey('buildable')
+        ->setEditInstructions(
+          pht(
+            'The overall state of a buildable (like a commit or revision) is '.
+            'normally the aggregation of the individual states of all builds '.
+            'that have run against it.'.
+            "\n\n".
+            'Buildables are "building" until all builds pass (which changes '.
+            'them to "pass"), or any build fails (which changes them to '.
+            '"fail").'.
+            "\n\n".
+            'You can change this behavior if you do not want to wait for this '.
+            'build, or do not care if it fails.'))
+        ->setName(pht('Affects Buildable'))
+        ->setOptions($aggregate_options),
+      id(new self())
+        ->setKey('restartable')
+        ->setEditInstructions(
+          pht(
+            'Usually, builds may be restarted. This may be useful if you '.
+            'suspect a build has failed for environmental or circumstantial '.
+            'reasons unrelated to the actual code, and want to give it '.
+            'another chance at glory.'.
+            "\n\n".
+            'If you want to prevent a build from being restarted, you can '.
+            'change the behavior here. This may be useful to prevent '.
+            'accidents where a build with a dangerous side effect (like '.
+            'deployment) is restarted improperly.'))
+        ->setName(pht('Restartable'))
+        ->setOptions($restart_options),
+      id(new self())
+        ->setKey('runnable')
+        ->setEditInstructions(
+          pht(
+            'To run a build manually, you normally must have permission to '.
+            'edit the related build plan. If you would prefer that anyone who '.
+            'can see the build plan be able to run and restart the build, you '.
+            'can change the behavior here.'.
+            "\n\n".
+            'Note that this affects both {nav Run Plan Manually} and '.
+            '{nav Restart Build}, since the two actions are largely '.
+            'equivalent.'.
+            "\n\n".
+            'WARNING: This may be unsafe, particularly if the build has '.
+            'side effects like deployment.'.
+            "\n\n".
+            'If you weaken this policy, an attacker with control of an '.
+            'account that has "Can View" permission but not "Can Edit" '.
+            'permission can manually run this build against any old version '.
+            'of the code, including versions with known security issues.'.
+            "\n\n".
+            'If running the build has a side effect like deploying code, '.
+            'they can force deployment of a vulnerable version and then '.
+            'escalate into an attack against the deployed service.'))
+        ->setName(pht('Runnable'))
+        ->setOptions($run_options),
+    );
+
+    return mpull($behaviors, null, 'getKey');
+  }
+
+}
diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php
@@ -0,0 +1,57 @@
+<?php
+
+final class HarbormasterBuildPlanBehaviorOption
+  extends Phobject {
+
+  private $name;
+  private $key;
+  private $icon;
+  private $description;
+  private $isDefault;
+
+  public function setName($name) {
+    $this->name = $name;
+    return $this;
+  }
+
+  public function getName() {
+    return $this->name;
+  }
+
+  public function setKey($key) {
+    $this->key = $key;
+    return $this;
+  }
+
+  public function getKey() {
+    return $this->key;
+  }
+
+  public function setDescription($description) {
+    $this->description = $description;
+    return $this;
+  }
+
+  public function getDescription() {
+    return $this->description;
+  }
+
+  public function setIsDefault($is_default) {
+    $this->isDefault = $is_default;
+    return $this;
+  }
+
+  public function getIsDefault() {
+    return $this->isDefault;
+  }
+
+  public function setIcon($icon) {
+    $this->icon = $icon;
+    return $this;
+  }
+
+  public function getIcon() {
+    return $this->icon;
+  }
+
+}
diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
--- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
+++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
@@ -17,6 +17,7 @@
   protected $planAutoKey;
   protected $viewPolicy;
   protected $editPolicy;
+  protected $properties = array();
 
   const STATUS_ACTIVE   = 'active';
   const STATUS_DISABLED = 'disabled';
@@ -45,6 +46,9 @@
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
+      self::CONFIG_SERIALIZATION => array(
+        'properties' => self::SERIALIZATION_JSON,
+      ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'name' => 'sort128',
         'planStatus' => 'text32',
@@ -94,6 +98,15 @@
     return pht('Plan %d', $this->getID());
   }
 
+  public function getPlanProperty($key, $default = null) {
+    return idx($this->properties, $key, $default);
+  }
+
+  public function setPlanProperty($key, $value) {
+    $this->properties[$key] = $value;
+    return $this;
+  }
+
 
 /* -(  Autoplans  )---------------------------------------------------------- */
 
diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php
@@ -0,0 +1,127 @@
+<?php
+
+final class HarbormasterBuildPlanBehaviorTransaction
+  extends HarbormasterBuildPlanTransactionType {
+
+  const TRANSACTIONTYPE = 'behavior';
+
+  public function generateOldValue($object) {
+    $behavior = $this->getBehavior();
+    return $behavior->getPlanOption($object)->getKey();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $key = $this->getStorageKey();
+    return $object->setPlanProperty($key, $value);
+  }
+
+  public function getTitle() {
+    $old_value = $this->getOldValue();
+    $new_value = $this->getNewValue();
+
+    $behavior = $this->getBehavior();
+    if ($behavior) {
+      $behavior_name = $behavior->getName();
+
+      $options = $behavior->getOptions();
+      if (isset($options[$old_value])) {
+        $old_value = $options[$old_value]->getName();
+      }
+
+      if (isset($options[$new_value])) {
+        $new_value = $options[$new_value]->getName();
+      }
+    } else {
+      $behavior_name = $this->getBehaviorKey();
+    }
+
+    return pht(
+      '%s changed the %s behavior for this plan from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderValue($behavior_name),
+      $this->renderValue($old_value),
+      $this->renderValue($new_value));
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+    $behaviors = mpull($behaviors, null, 'getKey');
+
+    foreach ($xactions as $xaction) {
+      $key = $this->getBehaviorKeyForTransaction($xaction);
+
+      if (!isset($behaviors[$key])) {
+        $errors[] = $this->newInvalidError(
+          pht(
+            'No behavior with key "%s" exists. Valid keys are: %s.',
+            $key,
+            implode(', ', array_keys($behaviors))),
+          $xaction);
+        continue;
+      }
+
+      $behavior = $behaviors[$key];
+      $options = $behavior->getOptions();
+
+      $storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
+        $key);
+      $old = $object->getPlanProperty($storage_key);
+      $new = $xaction->getNewValue();
+
+      if ($old === $new) {
+        continue;
+      }
+
+      if (!isset($options[$new])) {
+        $errors[] = $this->newInvalidError(
+          pht(
+            'Value "%s" is not a valid option for behavior "%s". Valid '.
+            'options are: %s.',
+            $new,
+            $key,
+            implode(', ', array_keys($options))),
+          $xaction);
+        continue;
+      }
+    }
+
+    return $errors;
+  }
+
+  public function getTransactionTypeForConduit($xaction) {
+    return 'behavior';
+  }
+
+  public function getFieldValuesForConduit($xaction, $data) {
+    return array(
+      'key' => $this->getBehaviorKeyForTransaction($xaction),
+      'old' => $xaction->getOldValue(),
+      'new' => $xaction->getNewValue(),
+    );
+  }
+
+  private function getBehaviorKeyForTransaction(
+    PhabricatorApplicationTransaction $xaction) {
+    $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+    return $xaction->getMetadataValue($metadata_key);
+  }
+
+  private function getBehaviorKey() {
+    $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+    return $this->getMetadataValue($metadata_key);
+  }
+
+  private function getBehavior() {
+    $behavior_key = $this->getBehaviorKey();
+    $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+    return idx($behaviors, $behavior_key);
+  }
+
+  private function getStorageKey() {
+    return HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
+      $this->getBehaviorKey());
+  }
+
+}