diff --git a/extension/reviewbotext/forms.py b/extension/reviewbotext/forms.py
index 7e95867ca55df7349b12e7ae6764d380825ec81d..e19014fda87683433b6943b6792b2ba1530b5d31 100644
--- a/extension/reviewbotext/forms.py
+++ b/extension/reviewbotext/forms.py
@@ -27,6 +27,7 @@ class ReviewBotConfigForm(IntegrationConfigForm):
 
     COMMENT_ON_UNMODIFIED_CODE_DEFAULT = False
     OPEN_ISSUES_DEFAULT = True
+    MANUALLY_RUN_ONLY_DEFAULT = False
     MAX_COMMENTS_DEFAULT = 30
 
     #: When to run this configuration.
@@ -51,6 +52,14 @@ class ReviewBotConfigForm(IntegrationConfigForm):
         required=False,
         initial=OPEN_ISSUES_DEFAULT)
 
+    #: If this tool should need to be run manually.
+    manually_run_only = forms.BooleanField(
+        label=_('Only run this tool manually'),
+        required=False,
+        help_text=_('Wait to run this tool until manually started. This adds a'
+                    ' button to new review requests to manually run a tool.'),
+        initial=MANUALLY_RUN_ONLY_DEFAULT)
+
     #: Maximum number of comments to make.
     max_comments = forms.IntegerField(
         label=_('Maximum Comments'),
@@ -139,6 +148,7 @@ class ReviewBotConfigForm(IntegrationConfigForm):
                 'fields': ('comment_on_unmodified_code',
                            'open_issues',
                            'max_comments',
-                           'tool_options'),
+                           'tool_options',
+                           'manually_run_only',),
             }),
         )
diff --git a/extension/reviewbotext/integration.py b/extension/reviewbotext/integration.py
index fe6dd4cd2e484af658cf8e8d98ed8a372fe0bffa..d20dd0bf222d115b60bf47af4889e58be0e53679 100644
--- a/extension/reviewbotext/integration.py
+++ b/extension/reviewbotext/integration.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 import json
 import logging
+from datetime import datetime
 
 from django.utils.functional import cached_property
 from django.utils.translation import ugettext_lazy as _
@@ -9,7 +10,8 @@ from djblets.extensions.hooks import SignalHook
 from reviewboard.admin.server import get_server_url
 from reviewboard.integrations import Integration
 from reviewboard.reviews.models import StatusUpdate
-from reviewboard.reviews.signals import review_request_published
+from reviewboard.reviews.signals import (review_request_published,
+                                         status_update_request_run)
 
 from reviewbotext.forms import ReviewBotConfigForm
 from reviewbotext.models import Tool
@@ -24,12 +26,16 @@ class ReviewBotIntegration(Integration):
 
     name = 'Review Bot'
     description = _('Performs automated analysis and review on code changes.')
+    DESCRIPTION_WAITING = _('waiting to run')
+    DESCRIPTION_STARTING = _('starting...')
     config_form_cls = ReviewBotConfigForm
 
     def initialize(self):
         """Initialize the integration hooks."""
         SignalHook(self, review_request_published,
                    self._on_review_request_published)
+        SignalHook(self, status_update_request_run,
+                   self._on_status_update_request_run)
 
     @cached_property
     def icon_static_urls(self):
@@ -43,6 +49,71 @@ class ReviewBotIntegration(Integration):
             '2x': extension.get_static_url('images/reviewbot@2x.png'),
         }
 
+    def _get_matching_configs(self, review_request):
+        """Return the matching configurations for a review request.
+
+        Args:
+            review_request (reviewboard.reviews.models.ReviewRequest):
+                The review request to get the configurations for.
+
+        Yields:
+            tuple:
+            A 3-tuple of the following:
+
+            * The tool model to use (:py:class:`reviewbotext.models.Tool`).
+            * The tool options (:py:class:`dict`).
+            * The review settings (:py:class:`dict`).
+
+            The tool options dictionary contains the matching configuration's
+            settings specific to the tool.
+
+            The review settings dictionary contains settings in common with
+            all integration configurations of tools.
+        """
+        matching_configs = [
+            config
+            for config in self.get_configs(review_request.local_site)
+            if config.match_conditions(form_cls=self.config_form_cls,
+                                       review_request=review_request)
+        ]
+
+        for config in matching_configs:
+            tool_id = config.settings.get('tool')
+
+            try:
+                tool = Tool.objects.get(pk=tool_id)
+            except Tool.DoesNotExist:
+                logging.error('Skipping Review Bot integration config %s (%d) '
+                              'because Tool with pk=%d does not exist.',
+                              config.name, config.pk, tool_id)
+
+            # Make sure tools have suitable defaults when missing settings.
+            review_settings = {
+                'max_comments': config.settings.get(
+                    'max_comments',
+                    ReviewBotConfigForm.MAX_COMMENTS_DEFAULT),
+                'manually_run': config.settings.get(
+                    'manually_run_only',
+                    ReviewBotConfigForm.MANUALLY_RUN_ONLY_DEFAULT),
+                'comment_unmodified': config.settings.get(
+                    'comment_on_unmodified_code',
+                    ReviewBotConfigForm.COMMENT_ON_UNMODIFIED_CODE_DEFAULT),
+                'open_issues': config.settings.get(
+                    'open_issues',
+                    ReviewBotConfigForm.OPEN_ISSUES_DEFAULT),
+            }
+
+            try:
+                tool_options = json.loads(
+                    config.settings.get('tool_options', '{}'))
+            except Exception as e:
+                logging.exception('Failed to parse tool_options for Review '
+                                  'Bot integration config %s (%d): %s',
+                                  config.name, config.pk, e)
+                tool_options = {}
+
+            yield tool, tool_options, review_settings
+
     def _on_review_request_published(self, sender, review_request, **kwargs):
         """Handle when a review request is published.
 
@@ -56,6 +127,10 @@ class ReviewBotIntegration(Integration):
             **kwargs (dict):
                 Additional keyword arguments.
         """
+        from reviewbotext.extension import ReviewBotExtension
+
+        extension = ReviewBotExtension.instance
+
         review_request_id = review_request.get_display_id()
         diffset = review_request.get_latest_diffset()
 
@@ -74,16 +149,9 @@ class ReviewBotIntegration(Integration):
                 'added' not in fields_changed['diff']):
                 return
 
-        from reviewbotext.extension import ReviewBotExtension
-        extension = ReviewBotExtension.instance
-
-        matching_configs = [
-            config
-            for config in self.get_configs(review_request.local_site)
-            if config.match_conditions(form_cls=self.config_form_cls,
-                                       review_request=review_request)
-        ]
+        matching_configs = self._get_matching_configs(review_request)
 
+        # If there aren't any matching configurations, we have nothing to do.
         if not matching_configs:
             return
 
@@ -94,46 +162,89 @@ class ReviewBotIntegration(Integration):
         session = extension.login_user()
         user = extension.user
 
-        for config in matching_configs:
-            tool_id = config.settings.get('tool')
+        for tool, tool_options, review_settings in matching_configs:
+            if review_settings['manually_run']:
+                status_update = StatusUpdate.objects.create(
+                    service_id='reviewbot.%s' % tool.name,
+                    summary=tool.name,
+                    description=ReviewBotIntegration.DESCRIPTION_WAITING,
+                    review_request=review_request,
+                    change_description=changedesc,
+                    state=StatusUpdate.NEEDS_RUN,
+                    timeout=tool.timeout,
+                    user=user)
+            else:
+                status_update = StatusUpdate.objects.create(
+                    service_id='reviewbot.%s' % tool.name,
+                    summary=tool.name,
+                    description=ReviewBotIntegration.DESCRIPTION_STARTING,
+                    review_request=review_request,
+                    change_description=changedesc,
+                    state=StatusUpdate.PENDING,
+                    timeout=tool.timeout,
+                    user=user)
+
+                repository = review_request.repository
+                queue = '%s.%s' % (tool.entry_point, tool.version)
+
+                if tool.working_directory_required:
+                    queue = '%s.%s' % (queue, repository.name)
+
+                extension.celery.send_task(
+                    'reviewbot.tasks.RunTool',
+                    kwargs={
+                        'server_url': server_url,
+                        'session': session,
+                        'username': user.username,
+                        'review_request_id': review_request_id,
+                        'diff_revision': diffset.revision,
+                        'status_update_id': status_update.pk,
+                        'review_settings': review_settings,
+                        'tool_options': tool_options,
+                        'repository_name': repository.name,
+                        'base_commit_id': diffset.base_commit_id,
+                    },
+                    queue=queue)
+
+    def _on_status_update_request_run(self, sender, status_update, **kwargs):
+        """Handle when a request to run the tools is made.
 
-            try:
-                tool = Tool.objects.get(pk=tool_id)
-            except Tool.DoesNotExist:
-                logging.error('Skipping Review Bot integration config %s (%d) '
-                              'because Tool with pk=%d does not exist.',
-                              config.name, config.pk, tool_id)
+        Args:
+            sender (object):
+                The sender of the signal.
 
-            review_settings = {
-                'max_comments': config.settings.get(
-                    'max_comments',
-                    ReviewBotConfigForm.MAX_COMMENTS_DEFAULT),
-                'comment_unmodified': config.settings.get(
-                    'comment_on_unmodified_code',
-                    ReviewBotConfigForm.COMMENT_ON_UNMODIFIED_CODE_DEFAULT),
-                'open_issues': config.settings.get(
-                    'open_issues',
-                    ReviewBotConfigForm.OPEN_ISSUES_DEFAULT),
-            }
+            status_update (reviewboard.reviews.models.StatusUpdate):
+                The review request which was published.
 
-            try:
-                tool_options = json.loads(
-                    config.settings.get('tool_options', '{}'))
-            except Exception as e:
-                logging.exception('Failed to parse tool_options for Review '
-                                  'Bot integration config %s (%d): %s',
-                                  config.name, config.pk, e)
-                tool_options = {}
+            **kwargs (dict):
+                Additional keyword arguments.
+        """
+        from reviewbotext.extension import ReviewBotExtension
+
+        extension = ReviewBotExtension.instance
+
+        review_request = status_update.review_request
+        review_request_id = review_request.get_display_id()
+        diffset = review_request.get_latest_diffset()
+
+        matching_configs = self._get_matching_configs(review_request)
+
+        # If there aren't any matching configurations, we have nothing to do.
+        if not matching_configs:
+            return
+
+        server_url = get_server_url(local_site=review_request.local_site)
+
+        # TODO: This creates a new session entry. We should figure out a better
+        # way for Review Bot workers to authenticate to the server.
+        session = extension.login_user()
+        user = extension.user
 
-            status_update = StatusUpdate.objects.create(
-                service_id='reviewbot.%s' % tool.name,
-                summary=tool.name,
-                description='starting...',
-                review_request=review_request,
-                change_description=changedesc,
-                state=StatusUpdate.PENDING,
-                timeout=tool.timeout,
-                user=user)
+        for tool, tool_options, review_settings in matching_configs:
+            status_update.description = \
+                ReviewBotIntegration.DESCRIPTION_STARTING
+            status_update.state = StatusUpdate.PENDING
+            status_update.timestamp = datetime.now()
 
             repository = review_request.repository
             queue = '%s.%s' % (tool.entry_point, tool.version)
