diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py
index a7cb75e0afddeb0094c57b55fb515547cb29ac91..9ff1e0251ce0fb6d896c849eaa4a8889bf30f15d 100644
--- a/opentech/apply/activity/messaging.py
+++ b/opentech/apply/activity/messaging.py
@@ -4,6 +4,7 @@ import requests
 from django.db import models
 from django.conf import settings
 from django.contrib import messages
+from django.contrib.auth import get_user_model
 from django.template.loader import render_to_string
 
 from .models import INTERNAL, PUBLIC
@@ -11,8 +12,12 @@ from .options import MESSAGES
 from .tasks import send_mail
 
 
+User = get_user_model()
+
+
 def link_to(target, request):
-    return request.scheme + '://' + request.get_host() + target.get_absolute_url()
+    if target:
+        return request.scheme + '://' + request.get_host() + target.get_absolute_url()
 
 
 neat_related = {
@@ -70,11 +75,38 @@ class AdapterBase:
     def recipients(self, message_type, **kwargs):
         raise NotImplementedError()
 
+    def batch_recipients(self, message_type, submissions, **kwargs):
+        # Default batch recipients is to send a message to each of the recipients that would
+        # receive a message under normal conditions
+        return [
+            {
+                'recipients': self.recipients(message_type, submission=submission, **kwargs),
+                'submissions': [submission]
+            }
+            for submission in submissions
+        ]
+
+    def process_batch(self, message_type, events, request, user, submissions, related=None, **kwargs):
+        events_by_submission = {
+            event.submission.id: event
+            for event in events
+        }
+        for recipient in self.batch_recipients(message_type, submissions, **kwargs):
+            recipients = recipient['recipients']
+            submissions = recipient['submissions']
+            events = [events_by_submission[submission.id] for submission in submissions]
+            self.process_send(message_type, recipients, events, request, user, submissions=submissions, submission=None, related=related, **kwargs)
+
     def process(self, message_type, event, request, user, submission, related=None, **kwargs):
+        recipients = self.recipients(message_type, **kwargs)
+        self.process_send(message_type, recipients, [event], request, user, submission, related=related, **kwargs)
+
+    def process_send(self, message_type, recipients, events, request, user, submission, submissions=None, related=None, **kwargs):
         kwargs = {
             'request': request,
             'user': user,
             'submission': submission,
+            'submissions': submissions,
             'related': related,
             **kwargs,
         }
@@ -85,30 +117,40 @@ class AdapterBase:
         if not message:
             return
 
-        for recipient in self.recipients(message_type, **kwargs):
-            message_log = self.create_log(message, recipient, event)
+        for recipient in recipients:
+            message_logs = self.create_logs(message, recipient, *events)
+
             if settings.SEND_MESSAGES or self.always_send:
-                status = self.send_message(message, recipient=recipient, message_log=message_log, **kwargs)
+                status = self.send_message(message, recipient=recipient, logs=message_logs, **kwargs)
             else:
                 status = 'Message not sent as SEND_MESSAGES==FALSE'
 
-            message_log.update_status(status)
+            message_logs.update_status = status
 
             if not settings.SEND_MESSAGES:
                 if recipient:
                     message = '{} [to: {}]: {}'.format(self.adapter_type, recipient, message)
                 else:
                     message = '{}: {}'.format(self.adapter_type, message)
-                messages.add_message(request, messages.INFO, message)
+                messages.add_message(request, messages.DEBUG, message)
 
-    def create_log(self, message, recipient, event):
+    def create_logs(self, message, recipient, *events):
         from .models import Message
-        return Message.objects.create(
-            type=self.adapter_type,
-            content=message,
-            recipient=recipient or '',
-            event=event,
+        messages = Message.objects.bulk_create(
+            Message(
+                **self.log_kwargs(message, recipient, event)
+            )
+            for event in events
         )
+        return Message.objects.filter(id__in=[message.id for message in messages])
+
+    def log_kwargs(self, message, recipient, event):
+        return {
+            'type': self.adapter_type,
+            'content': message,
+            'recipient': recipient or '',
+            'event': event,
+        }
 
     def send_message(self, message, **kwargs):
         # Process the message, should return the result of the send
@@ -128,6 +170,7 @@ class ActivityAdapter(AdapterBase):
         MESSAGES.DETERMINATION_OUTCOME: 'Sent a determination. Outcome: {determination.clean_outcome}',
         MESSAGES.INVITED_TO_PROPOSAL: 'Invited to submit a proposal',
         MESSAGES.REVIEWERS_UPDATED: 'reviewers_updated',
+        MESSAGES.BATCH_REVIEWERS_UPDATED: 'batch_reviewers_updated',
         MESSAGES.NEW_REVIEW: 'Submitted a review',
         MESSAGES.OPENED_SEALED: 'Opened the submission while still sealed',
         MESSAGES.SCREENING: 'Screening status from {old_status} to {submission.screening_status}'
@@ -145,7 +188,7 @@ class ActivityAdapter(AdapterBase):
             return {'visibility': INTERNAL}
         return {}
 
-    def reviewers_updated(self, added, removed, **kwargs):
+    def reviewers_updated(self, added=list(), removed=list(), **kwargs):
         message = ['Reviewers updated.']
         if added:
             message.append('Added:')
@@ -157,6 +200,9 @@ class ActivityAdapter(AdapterBase):
 
         return ' '.join(message)
 
+    def batch_reviewers_updated(self, added, **kwargs):
+        return 'Batch ' + self.reviewers_updated(added, **kwargs)
+
     def handle_transition(self, old_phase, submission, **kwargs):
         base_message = 'Progressed from {old_display} to {new_display}'
 
@@ -184,7 +230,7 @@ class ActivityAdapter(AdapterBase):
 
         return staff_message
 
-    def send_message(self, message, user, submission, **kwargs):
+    def send_message(self, message, user, submission, submissions, **kwargs):
         from .models import Activity, PUBLIC
         visibility = kwargs.get('visibility', PUBLIC)
 
@@ -195,6 +241,12 @@ class ActivityAdapter(AdapterBase):
         else:
             related_object = None
 
+        try:
+            # If this was a batch action we want to pull out the submission
+            submission = submissions[0]
+        except TypeError:
+            pass
+
         Activity.actions.create(
             user=user,
             submission=submission,
@@ -214,6 +266,7 @@ class SlackAdapter(AdapterBase):
         MESSAGES.EDIT: '{user} has edited <{link}|{submission.title}>',
         MESSAGES.APPLICANT_EDIT: '{user} has edited <{link}|{submission.title}>',
         MESSAGES.REVIEWERS_UPDATED: '{user} has updated the reviewers on <{link}|{submission.title}>',
+        MESSAGES.BATCH_REVIEWERS_UPDATED: 'handle_batch_reviewers',
         MESSAGES.TRANSITION: '{user} has updated the status of <{link}|{submission.title}>: {old_phase.display_name} → {submission.phase}',
         MESSAGES.DETERMINATION_OUTCOME: 'A determination for <{link}|{submission.title}> was sent by email. Outcome: {determination.clean_outcome}',
         MESSAGES.PROPOSAL_SUBMITTED: 'A proposal has been submitted for review: <{link}|{submission.title}>',
@@ -230,13 +283,45 @@ class SlackAdapter(AdapterBase):
 
     def extra_kwargs(self, message_type, **kwargs):
         submission = kwargs['submission']
+        submissions = kwargs['submissions']
         request = kwargs['request']
         link = link_to(submission, request)
-        return {'link': link}
+        links = {
+            submission.id: link_to(submission, request)
+            for submission in submissions
+        }
+        return {
+            'link': link,
+            'links': links,
+        }
 
     def recipients(self, message_type, submission, **kwargs):
         return [self.slack_id(submission.lead)]
 
+    def batch_recipients(self, message_type, submissions, **kwargs):
+        # We group the messages by lead
+        leads = User.objects.filter(id__in=submissions.values('lead'))
+        return [
+            {
+                'recipients': [self.slack_id(lead)],
+                'submissions': submissions.filter(lead=lead),
+            } for lead in leads
+        ]
+
+    def handle_batch_reviewers(self, submissions, links, user, added, **kwargs):
+        submissions_text = ', '.join(
+            f'<{links[submission.id]}|{submission.title}>'
+            for submission in submissions
+        )
+        reviewers_text = ', '.join([str(user) for user in added])
+        return (
+            '{user} has batch added {reviewers_text} as reviewers on: {submissions_text}'.format(
+                user=user,
+                submissions_text=submissions_text,
+                reviewers_text=reviewers_text,
+            )
+        )
+
     def notify_reviewers(self, submission, **kwargs):
         reviewers_to_notify = []
         for reviewer in submission.reviewers.all():
@@ -317,13 +402,17 @@ class EmailAdapter(AdapterBase):
         MESSAGES.READY_FOR_REVIEW: 'messages/email/ready_to_review.html',
     }
 
-    def extra_kwargs(self, message_type, submission, **kwargs):
-        if message_type == MESSAGES.READY_FOR_REVIEW:
-            subject = 'Application ready to review: {submission.title}'.format(submission=submission)
-        else:
-            subject = submission.page.specific.subject or 'Your application to Open Technology Fund: {submission.title}'.format(submission=submission)
+    def get_subject(self, message_type, submission):
+        if submission:
+            if message_type == MESSAGES.READY_FOR_REVIEW:
+                subject = 'Application ready to review: {submission.title}'.format(submission=submission)
+            else:
+                subject = submission.page.specific.subject or 'Your application to Open Technology Fund: {submission.title}'.format(submission=submission)
+            return subject
+
+    def extra_kwargs(self, message_type, submission, submissions, **kwargs):
         return {
-            'subject': subject,
+            'subject': self.get_subject(message_type, submission),
         }
 
     def notify_comment(self, **kwargs):
@@ -352,37 +441,76 @@ class EmailAdapter(AdapterBase):
     def render_message(self, template, **kwargs):
         return render_to_string(template, kwargs)
 
-    def send_message(self, message, submission, subject, recipient, **kwargs):
+    def send_message(self, message, submission, subject, recipient, logs, **kwargs):
         try:
             send_mail(
                 subject,
                 message,
                 submission.page.specific.from_address,
                 [recipient],
-                log=kwargs['message_log']
+                logs=logs
             )
         except Exception as e:
             return 'Error: ' + str(e)
 
 
+class DjangoMessagesAdapter(AdapterBase):
+    adapter_type = 'Django'
+    always_send = True
+
+    messages = {
+        MESSAGES.BATCH_REVIEWERS_UPDATED: 'batch_reviewers_updated',
+    }
+
+    def batch_reviewers_updated(self, added, submissions, **kwargs):
+        return (
+            'Batch reviewers added: ' +
+            ', '.join([str(user) for user in added]) +
+            ' to ' +
+            ', '.join(['"{}"'.format(submission.title) for submission in submissions])
+        )
+
+    def recipients(self, *args, **kwargs):
+        return [None]
+
+    def batch_recipients(self, message_type, submissions, *args, **kwargs):
+        return [{
+            'recipients': [None],
+            'submissions': submissions,
+        }]
+
+    def send_message(self, message, request, **kwargs):
+        messages.add_message(request, messages.INFO, message)
+
+
 class MessengerBackend:
     def __init__(self, *adpaters):
         self.adapters = adpaters
 
-    def __call__(self, message_type, request, user, submission, related=None, **kwargs):
-        return self.send(message_type, request=request, user=user, submission=submission, related=related, **kwargs)
+    def __call__(self, *args, related=None, **kwargs):
+        return self.send(*args, related=related, **kwargs)
 
-    def send(self, message_type, request, user, submission, related, **kwargs):
+    def send(self, message_type, request, user, related, submission=None, submissions=list(), **kwargs):
         from .models import Event
-        event = Event.objects.create(type=message_type.name, by=user, submission=submission)
-        for adapter in self.adapters:
-            adapter.process(message_type, event, request=request, user=user, submission=submission, related=related, **kwargs)
+        if submission:
+            event = Event.objects.create(type=message_type.name, by=user, submission=submission)
+            for adapter in self.adapters:
+                adapter.process(message_type, event, request=request, user=user, submission=submission, related=related, **kwargs)
+
+        elif submissions:
+            events = Event.objects.bulk_create(
+                Event(type=message_type.name, by=user, submission=submission)
+                for submission in submissions
+            )
+            for adapter in self.adapters:
+                adapter.process_batch(message_type, events, request=request, user=user, submissions=submissions, related=related, **kwargs)
 
 
 adapters = [
     ActivityAdapter(),
     SlackAdapter(),
     EmailAdapter(),
+    DjangoMessagesAdapter(),
 ]
 
 
diff --git a/opentech/apply/activity/migrations/0014_add_batch_reviewer_message.py b/opentech/apply/activity/migrations/0014_add_batch_reviewer_message.py
new file mode 100644
index 0000000000000000000000000000000000000000..261fc13324a2ad246101a82ddc10b9568855dc5f
--- /dev/null
+++ b/opentech/apply/activity/migrations/0014_add_batch_reviewer_message.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0.9 on 2019-01-31 23:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('activity', '0013_add_new_event_type_screening'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='event',
+            name='type',
+            field=models.CharField(choices=[('UPDATE_LEAD', 'Update Lead'), ('EDIT', 'Edit'), ('APPLICANT_EDIT', 'Applicant Edit'), ('NEW_SUBMISSION', 'New Submission'), ('SCREENING', 'Screening'), ('TRANSITION', 'Transition'), ('DETERMINATION_OUTCOME', 'Determination Outcome'), ('INVITED_TO_PROPOSAL', 'Invited To Proposal'), ('REVIEWERS_UPDATED', 'Reviewers Updated'), ('BATCH_REVIEWERS_UPDATED', 'Batch Reviewers Updated'), ('READY_FOR_REVIEW', 'Ready For Review'), ('NEW_REVIEW', 'New Review'), ('COMMENT', 'Comment'), ('PROPOSAL_SUBMITTED', 'Proposal Submitted'), ('OPENED_SEALED', 'Opened Sealed Submission')], max_length=50),
+        ),
+    ]
diff --git a/opentech/apply/activity/models.py b/opentech/apply/activity/models.py
index 6366f0f98ca9aaf61b063efe22b4fd2c8fcd5ea2..4712437346508a82cdb629f273f222fb41caa843 100644
--- a/opentech/apply/activity/models.py
+++ b/opentech/apply/activity/models.py
@@ -134,6 +134,18 @@ class Event(models.Model):
         return ' '.join([self.get_type_display(), 'by:', str(self.by), 'on:', self.submission.title])
 
 
+class MessagesQueryset(models.QuerySet):
+    def update_status(self, status):
+        return self.update(
+            status=Case(
+                When(status='', then=Value(status)),
+                default=Concat('status', Value('<br />' + status))
+            )
+        )
+
+    update_status.queryset_only = True
+
+
 class Message(models.Model):
     """Model to track content of messages sent from an event"""
 
@@ -144,6 +156,8 @@ class Message(models.Model):
     status = models.TextField()
     external_id = models.CharField(max_length=75, null=True, blank=True)  # Stores the id of the object from an external system
 
+    objects = MessagesQueryset.as_manager()
+
     def update_status(self, status):
         if status:
             self.status = Case(
diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py
index 46d744e035fa0af24a76b6c63996edf6e3523e6e..35aa1e64454ab3a0918c60c986792a4f4dc4dec0 100644
--- a/opentech/apply/activity/options.py
+++ b/opentech/apply/activity/options.py
@@ -11,6 +11,7 @@ class MESSAGES(Enum):
     DETERMINATION_OUTCOME = 'Determination Outcome'
     INVITED_TO_PROPOSAL = 'Invited To Proposal'
     REVIEWERS_UPDATED = 'Reviewers Updated'
+    BATCH_REVIEWERS_UPDATED = 'Batch Reviewers Updated'
     READY_FOR_REVIEW = 'Ready For Review'
     NEW_REVIEW = 'New Review'
     COMMENT = 'Comment'
diff --git a/opentech/apply/activity/tasks.py b/opentech/apply/activity/tasks.py
index 73ff71b8343d368b91776ef80fc0b59cdb6a1dd0..d0d920c3e8aec1c95b52c45ed62a3afde57b4a37 100644
--- a/opentech/apply/activity/tasks.py
+++ b/opentech/apply/activity/tasks.py
@@ -8,7 +8,7 @@ app = Celery('tasks')
 app.config_from_object(settings, namespace='CELERY', force=True)
 
 
-def send_mail(subject, message, from_address, recipients, log=None):
+def send_mail(subject, message, from_address, recipients, logs=None):
     # Convenience method to wrap the tasks and handle the callback
     send_mail_task.apply_async(
         kwargs={
@@ -17,7 +17,7 @@ def send_mail(subject, message, from_address, recipients, log=None):
             'from_email': from_address,
             'to': recipients,
         },
-        link=update_message_status.s(log.id),
+        link=update_message_status.s(log.values_list('id', flat=True)),
     )
 
 
@@ -42,8 +42,8 @@ def send_mail_task(**kwargs):
 
 
 @app.task
-def update_message_status(response, message_id):
+def update_message_status(response, message_ids):
     from .models import Message
-    message = Message.objects.get(id=message_id)
+    message = Message.objects.filter(id__in=message_ids)
     message.external_id = response['id']
     message.update_status(response['status'])
diff --git a/opentech/apply/activity/tests/test_tasks.py b/opentech/apply/activity/tests/test_tasks.py
index b6aede89d7e74c4ab4886d7b956c5196e26e62b6..f468f396c0e0e2f0415359a9a912beecc5097b31 100644
--- a/opentech/apply/activity/tests/test_tasks.py
+++ b/opentech/apply/activity/tests/test_tasks.py
@@ -16,5 +16,5 @@ class TestSendEmail(TestCase):
             'from_email': 'from_email',
             'to': 'to',
         }
-        send_mail(*kwargs, log=MessageFactory())
+        send_mail(*kwargs, logs=[MessageFactory()])
         email_mock.assert_called_once_with(**kwargs)
diff --git a/opentech/apply/funds/forms.py b/opentech/apply/funds/forms.py
index 0b1df3d3b4a79c45b485b0cff5600b80a4a3961a..5e4e7de599fbd619d93ab181ae4f9a0e22ba6eb3 100644
--- a/opentech/apply/funds/forms.py
+++ b/opentech/apply/funds/forms.py
@@ -66,29 +66,29 @@ class UpdateReviewersForm(forms.ModelForm):
         model = ApplicationSubmission
         fields: list = []
 
-    def can_alter_reviewers(self, user):
-        return self.instance.stage.has_external_review and user == self.instance.lead
-
     def __init__(self, *args, **kwargs):
         self.user = kwargs.pop('user')
         super().__init__(*args, **kwargs)
         reviewers = self.instance.reviewers.all()
-        self.submitted_reviewers = User.objects.filter(id__in=self.instance.reviews.values('author'))
-
-        staff_field = self.fields['staff_reviewers']
-        staff_field.queryset = staff_field.queryset.exclude(id__in=self.submitted_reviewers)
-        staff_field.initial = reviewers
+        submitted_reviewers = User.objects.filter(id__in=self.instance.reviews.values('author'))
 
-        if self.can_alter_reviewers(self.user):
-            review_field = self.fields['reviewer_reviewers']
-            review_field.queryset = review_field.queryset.exclude(id__in=self.submitted_reviewers)
-            review_field.initial = reviewers
+        self.prepare_field('staff_reviewers', reviewers, submitted_reviewers)
+        if self.can_alter_external_reviewers(self.instance, self.user):
+            self.prepare_field('reviewer_reviewers', reviewers, submitted_reviewers)
         else:
             self.fields.pop('reviewer_reviewers')
 
+    def prepare_field(self, field_name, initial, excluded):
+        field = self.fields[field_name]
+        field.queryset = field.queryset.exclude(id__in=excluded)
+        field.initial = initial
+
+    def can_alter_external_reviewers(self, instance, user):
+        return instance.stage.has_external_review and (user == instance.lead or user.is_superuser)
+
     def save(self, *args, **kwargs):
         instance = super().save(*args, **kwargs)
-        if self.can_alter_reviewers(self.user):
+        if self.can_alter_external_reviewers(self.instance, self.user):
             reviewers = self.cleaned_data.get('reviewer_reviewers')
         else:
             reviewers = instance.reviewers_not_reviewed
@@ -99,3 +99,15 @@ class UpdateReviewersForm(forms.ModelForm):
             self.submitted_reviewers
         )
         return instance
+
+
+class BatchUpdateReviewersForm(forms.Form):
+    staff_reviewers = forms.ModelMultipleChoiceField(
+        queryset=User.objects.staff(),
+        widget=Select2MultiCheckboxesWidget(attrs={'data-placeholder': 'Staff'}),
+    )
+    submission_ids = forms.CharField(widget=forms.HiddenInput())
+
+    def clean_submission_ids(self):
+        value = self.cleaned_data['submission_ids']
+        return [int(submission) for submission in value.split(',')]
diff --git a/opentech/apply/funds/tables.py b/opentech/apply/funds/tables.py
index 369024b2b94bb10d39ab0e48897eb2a30553110d..b49943f90a7c98996142e0df56f073873d95516e 100644
--- a/opentech/apply/funds/tables.py
+++ b/opentech/apply/funds/tables.py
@@ -65,8 +65,25 @@ class SubmissionsTable(tables.Table):
         return qs, True
 
 
-class AdminSubmissionsTable(SubmissionsTable):
-    """Adds admin only columns to the submissions table"""
+class LabeledCheckboxColumn(tables.CheckBoxColumn):
+    def wrap_with_label(self, checkbox, for_value):
+        return format_html(
+            '<label for="{}">{}</label>',
+            for_value,
+            checkbox,
+        )
+
+    @property
+    def header(self):
+        checkbox = super().header
+        return self.wrap_with_label(checkbox, 'selectall')
+
+    def render(self, value, record, bound_column):
+        checkbox = super().render(value=value, record=record, bound_column=bound_column)
+        return self.wrap_with_label(checkbox, value)
+
+
+class BaseAdminSubmissionsTable(SubmissionsTable):
     lead = tables.Column(order_by=('lead.full_name',))
     reviews_stats = tables.TemplateColumn(template_name='funds/tables/column_reviews.html', verbose_name=mark_safe("Reviews\n<span>Assgn.\tComp.</span>"), orderable=False)
     screening_status = tables.Column(verbose_name="Screening")
@@ -79,8 +96,17 @@ class AdminSubmissionsTable(SubmissionsTable):
         return format_html('<span>{}</span>', value)
 
 
-class SummarySubmissionsTable(AdminSubmissionsTable):
-    class Meta(AdminSubmissionsTable.Meta):
+class AdminSubmissionsTable(BaseAdminSubmissionsTable):
+    """Adds admin only columns to the submissions table"""
+    selected = LabeledCheckboxColumn(accessor=A('pk'), attrs={'input': {'class': 'js-batch-select'}, 'th__input': {'class': 'js-batch-select-all'}})
+
+    class Meta(BaseAdminSubmissionsTable.Meta):
+        fields = ('selected', *BaseAdminSubmissionsTable.Meta.fields)
+        sequence = fields
+
+
+class SummarySubmissionsTable(BaseAdminSubmissionsTable):
+    class Meta(BaseAdminSubmissionsTable.Meta):
         orderable = False
 
 
diff --git a/opentech/apply/funds/templates/funds/base_submissions_table.html b/opentech/apply/funds/templates/funds/base_submissions_table.html
index d9214f80da196c86ca5211367455a9e292fd1bac..322a4dca51c1d0a1b9c8aaf5e03531a308549429 100644
--- a/opentech/apply/funds/templates/funds/base_submissions_table.html
+++ b/opentech/apply/funds/templates/funds/base_submissions_table.html
@@ -3,6 +3,7 @@
 {% load render_table from django_tables2 %}
 
 {% block extra_css %}
+<link rel="stylesheet" href="{% static 'css/apply/fancybox.css' %}">
 {{ filter.form.media.css }}
 {% endblock %}
 
@@ -16,10 +17,13 @@
 
 {% block extra_js %}
     {{ filter.form.media.js }}
+    <script src="//cdnjs.cloudflare.com/ajax/libs/fancybox/3.4.1/jquery.fancybox.min.js"></script>
+    <script src="{% static 'js/apply/fancybox-global.js' %}"></script>
     <script src="{% static 'js/apply/all-submissions-table.js' %}"></script>
     <script src="https://cdn.jsdelivr.net/npm/symbol-es6@0.1.2/symbol-es6.min.js"></script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/url-search-params/1.1.0/url-search-params.js"></script>
     <script src="{% static 'js/apply/submission-filters.js' %}"></script>
     <script src="{% static 'js/apply/submission-tooltips.js' %}"></script>
     <script src="{% static 'js/apply/tabs.js' %}"></script>
+    <script src="{% static 'js/apply/batch-actions.js' %}"></script>
 {% endblock %}
diff --git a/opentech/apply/funds/templates/funds/includes/batch_update_reviewer_form.html b/opentech/apply/funds/templates/funds/includes/batch_update_reviewer_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..d74095ba0a497d3c206669684cd33608c9003813
--- /dev/null
+++ b/opentech/apply/funds/templates/funds/includes/batch_update_reviewer_form.html
@@ -0,0 +1,9 @@
+<div class="modal modal--secondary" id="batch-update-reviewers">
+    <h4 class="modal__header-bar">Add Reviewers</h4>
+    <div class="modal__list-item modal__list-item--meta" aria-live="polite">
+        <span class="js-batch-title-count"></span>
+        <a href="#" class="modal__hide-link js-toggle-batch-list">Show</a>
+    </div>
+    <div class="modal__list js-batch-titles is-closed" aria-live="polite"></div>
+    {% include 'funds/includes/delegated_form_base.html' with form=batch_reviewer_form value='Update'%}
+</div>
diff --git a/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html b/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html
index e4cff0f2d2f2147b94009bcebc63f27aefbe9f7f..7edcad1832cbf156157176e6ead6b98116ed6392 100644
--- a/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html
+++ b/opentech/apply/funds/templates/funds/includes/table_filter_and_search.html
@@ -1,14 +1,30 @@
 <div class="wrapper wrapper--table-actions">
-    <button class="button button--filters button--contains-icons js-toggle-filters">Filters</button>
+    <div class="actions-bar">
+        {# Left #}
+        <div class="actions-bar__inner actions-bar__inner--left">
+            <p class="actions-bar__total"><span class="js-total-actions">0</span> Selected</p>
+            <form action="" class="js-batch-update-status">
+                <button class="button button--action button--change-status" type="submit">Change status</button>
+            </form>
+
+            <button data-fancybox data-src="#batch-update-reviewers" class="button button--action button--reviewers js-batch-update-reviewers" type="submit">Reviewers</button>
+        </div>
+
+        {# Right #}
+        <div class="actions-bar__inner actions-bar__inner--right">
+            <button class="button button--filters button--contains-icons button--action js-toggle-filters">Filters</button>
+
+            {% if use_search|default:False %}
+            <form method="get" role="search" class="form form--search-desktop js-search-form">
+                <button class="button button--search" type="submit" aria-label="Search">
+                    <svg class="icon icon--magnifying-glass icon--search"><use xlink:href="#magnifying-glass"></use></svg>
+                </button>
+                <input class="input input--search input--secondary js-search-input" type="text" placeholder="Search submissions" name="query"{% if search_term %} value="{{ search_term }}"{% endif %} aria-label="Search input">
+            </form>
+            {% endif %}
+        </div>
+    </div>
 
-    {% if use_search|default:False %}
-    <form method="get" role="search" class="form form--search js-search-form">
-        <button class="button button--search" type="submit" aria-label="Search">
-            <svg class="icon icon--magnifying-glass icon--search"><use xlink:href="#magnifying-glass"></use></svg>
-        </button>
-        <input class="input input--search input--secondary js-search-input" type="text" placeholder="Search submissions" name="query"{% if search_term %} value="{{ search_term }}"{% endif %} aria-label="Search input">
-    </form>
-    {% endif %}
 </div>
 
 <div class="filters">
@@ -27,3 +43,5 @@
         </ul>
     </form>
 </div>
+
+{% include "funds/includes/batch_update_reviewer_form.html" %}
diff --git a/opentech/apply/funds/templates/funds/tables/table.html b/opentech/apply/funds/templates/funds/tables/table.html
index cb3095fc7f961f67e78ed0eadc73308fbf4e2f24..66599d9371b55d206e44642fe6df8ed0f9000a23 100644
--- a/opentech/apply/funds/templates/funds/tables/table.html
+++ b/opentech/apply/funds/templates/funds/tables/table.html
@@ -5,7 +5,9 @@
     <tr {{ row.attrs.as_html }}>
         {% for column, cell in row.items %}
             <td {{ column.attrs.td.as_html }}>
-                <span class="mobile-label {{ column.attrs.td.class }}">{{ column.header }}: </span>
+                {% if column.name != "selected" %}
+                    <span class="mobile-label {{ column.attrs.td.class }}">{{ column.header }}: </span>
+                {% endif %}
                 {% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}
             </td>
         {% endfor %}
@@ -54,6 +56,7 @@
                     <tr class="submission-meta__row">
                     {% for column in row.table.columns %}
                         {% if forloop.first %}
+                        {% elif forloop.counter == 2 %}
                             <th>Linked {{ row.record.previous.stage }}</th>
                         {% else %}
                             <th class="th th--{{ column.header|lower }}">{{ column.header }}</th>
@@ -63,7 +66,13 @@
 
                     {# mutate the row to render the data for the child row #}
                     {% with row=row|row_from_record:row.record.previous %}
-                        {{ block.super }}
+                        <tr {{ row.attrs.as_html }}>
+                            {% for column, cell in row.items %}
+                                {% if column.name != "selected" %}
+                                    <td {{ column.attrs.td.as_html }}>{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}</td>
+                                    {% endif %}
+                            {% endfor %}
+                        </tr>
                     {% endwith %}
                 </table>
             </td>
diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py
index 78df28b2b8b3a3c0aef24fef6a7a6821b3b3a462..8b498fa78a8d686da67037ddf9676c8562b21363 100644
--- a/opentech/apply/funds/views.py
+++ b/opentech/apply/funds/views.py
@@ -10,7 +10,7 @@ from django.urls import reverse_lazy
 from django.utils.decorators import method_decorator
 from django.utils.text import mark_safe
 from django.utils.translation import ugettext_lazy as _
-from django.views.generic import DetailView, ListView, UpdateView
+from django.views.generic import DetailView, FormView, ListView, UpdateView
 
 from django_filters.views import FilterView
 from django_tables2.views import SingleTableMixin
@@ -27,10 +27,17 @@ from opentech.apply.activity.messaging import messenger, MESSAGES
 from opentech.apply.determinations.views import DeterminationCreateOrUpdateView
 from opentech.apply.review.views import ReviewContextMixin
 from opentech.apply.users.decorators import staff_required
-from opentech.apply.utils.views import DelegateableView, ViewDispatcher
+from opentech.apply.users.models import User
+from opentech.apply.utils.views import DelegateableListView, DelegateableView, ViewDispatcher
 
 from .differ import compare
-from .forms import ProgressSubmissionForm, ScreeningSubmissionForm, UpdateReviewersForm, UpdateSubmissionLeadForm
+from .forms import (
+    BatchUpdateReviewersForm,
+    ProgressSubmissionForm,
+    ScreeningSubmissionForm,
+    UpdateReviewersForm,
+    UpdateSubmissionLeadForm,
+)
 from .models import ApplicationSubmission, ApplicationRevision, RoundsAndLabs, RoundBase, LabBase
 from .tables import (
     AdminSubmissionsTable,
@@ -69,15 +76,48 @@ class BaseAdminSubmissionsTable(SingleTableMixin, FilterView):
         return self.filterset_class._meta.model.objects.current().for_table(self.request.user)
 
     def get_context_data(self, **kwargs):
-        kwargs = super().get_context_data(**kwargs)
-
         search_term = self.request.GET.get('query')
-        kwargs.update(
+
+        return super().get_context_data(
             search_term=search_term,
             filter_action=self.filter_action,
+            **kwargs,
         )
 
-        return super().get_context_data(**kwargs)
+
+@method_decorator(staff_required, name='dispatch')
+class BatchUpdateReviewersView(DelegatedViewMixin, FormView):
+    form_class = BatchUpdateReviewersForm
+    context_name = 'batch_reviewer_form'
+
+    def form_invalid(self, form):
+        messages.error(self.request, mark_safe(_('Sorry something went wrong') + form.errors.as_ul()))
+        return super().form_invalid(form)
+
+    def form_valid(self, form):
+        """
+        Loop through all submissions selected on the page,
+        Add any reviewers that were selected,  only if they are not
+        currently saved to that submission.
+        Send out a message of updates.
+        """
+        reviewers = User.objects.filter(id__in=form.cleaned_data['staff_reviewers'])
+
+        submission_ids = form.cleaned_data['submission_ids']
+        submissions = ApplicationSubmission.objects.filter(id__in=submission_ids)
+
+        for submission in submissions:
+            submission.reviewers.add(*reviewers)
+
+        messenger(
+            MESSAGES.BATCH_REVIEWERS_UPDATED,
+            request=self.request,
+            user=self.request.user,
+            submissions=submissions,
+            added=reviewers,
+        )
+
+        return super().form_valid(form)
 
 
 class SubmissionOverviewView(AllActivityContextMixin, BaseAdminSubmissionsTable):
@@ -107,12 +147,18 @@ class SubmissionOverviewView(AllActivityContextMixin, BaseAdminSubmissionsTable)
         )
 
 
-class SubmissionListView(AllActivityContextMixin, BaseAdminSubmissionsTable):
+class SubmissionListView(AllActivityContextMixin, BaseAdminSubmissionsTable, DelegateableListView):
     template_name = 'funds/submissions.html'
+    form_views = [
+        BatchUpdateReviewersView
+    ]
 
 
-class SubmissionsByRound(AllActivityContextMixin, BaseAdminSubmissionsTable):
+class SubmissionsByRound(AllActivityContextMixin, BaseAdminSubmissionsTable, DelegateableListView):
     template_name = 'funds/submissions_by_round.html'
+    form_views = [
+        BatchUpdateReviewersView
+    ]
 
     excluded_fields = ('round', 'lead', 'fund')
 
@@ -238,10 +284,11 @@ class UpdateReviewersView(DelegatedViewMixin, UpdateView):
             added=added,
             removed=removed,
         )
+
         return response
 
 
-class AdminSubmissionDetailView(ReviewContextMixin, ActivityContextMixin, DelegateableView):
+class AdminSubmissionDetailView(ReviewContextMixin, ActivityContextMixin, DelegateableView, DetailView):
     template_name_suffix = '_admin_detail'
     model = ApplicationSubmission
     form_views = [
diff --git a/opentech/apply/utils/views.py b/opentech/apply/utils/views.py
index 87932814703c400b72c81e554d1bc1695c9e806e..3e83f17d44a38942df0dc9f1133d0c3141c7cff2 100644
--- a/opentech/apply/utils/views.py
+++ b/opentech/apply/utils/views.py
@@ -1,7 +1,9 @@
 from django.contrib.auth.decorators import login_required
+from django.forms.models import ModelForm
 from django.utils.decorators import method_decorator
 from django.views import defaults
-from django.views.generic import DetailView, View
+from django.views.generic import View
+from django.views.generic.base import ContextMixin
 from django.views.generic.detail import SingleObjectTemplateResponseMixin
 from django.views.generic.edit import ModelFormMixin, ProcessFormView
 
@@ -35,12 +37,20 @@ class ViewDispatcher(View):
         return view.as_view()(request, *args, **kwargs)
 
 
-class DelegateableView(DetailView):
-    """A view which passes its context to child form views to allow them to post to the same URL """
+class DelegatableBase(ContextMixin):
+    """
+    A view which passes its context to child form views to allow them to post to the same URL
+    `DelegateableViews` objects should contain form views that inherit from `DelegatedViewMixin`
+    and `FormView`
+    """
     form_prefix = 'form-submitted-'
 
+    def get_form_args(self):
+        return (None, None)
+
     def get_context_data(self, **kwargs):
-        forms = dict(form_view.contribute_form(self.object, self.request.user) for form_view in self.form_views)
+        forms = dict(form_view.contribute_form(*self.get_form_args()) for form_view in self.form_views)
+
         return super().get_context_data(
             form_prefix=self.form_prefix,
             **forms,
@@ -48,13 +58,9 @@ class DelegateableView(DetailView):
         )
 
     def post(self, request, *args, **kwargs):
-        self.object = self.get_object()
-
-        kwargs['submission'] = self.object
-
         # Information to pretend we originate from this view
-        kwargs['template_names'] = self.get_template_names()
         kwargs['context'] = self.get_context_data()
+        kwargs['template_names'] = self.get_template_names()
 
         for form_view in self.form_views:
             if self.form_prefix + form_view.context_name in request.POST:
@@ -64,14 +70,34 @@ class DelegateableView(DetailView):
         return self.get(request, *args, **kwargs)
 
 
+class DelegateableView(DelegatableBase):
+    def get_form_args(self):
+        return self.object, self.request.user
+
+    def post(self, request, *args, **kwargs):
+        self.object = self.get_object()
+
+        kwargs['submission'] = self.object
+
+        return super().post(request, *args, **kwargs)
+
+
+class DelegateableListView(DelegatableBase):
+    def post(self, request, *args, **kwargs):
+        self.object_list = self.get_queryset()
+        return super().post(request, *args, **kwargs)
+
+
 class DelegatedViewMixin(View):
     """For use on create views accepting forms from another view"""
+
     def get_template_names(self):
         return self.kwargs['template_names']
 
     def get_form_kwargs(self):
         kwargs = super().get_form_kwargs()
-        kwargs['user'] = self.request.user
+        if self.is_model_form():
+            kwargs['user'] = self.request.user
         return kwargs
 
     def get_form(self, *args, **kwargs):
@@ -86,12 +112,22 @@ class DelegatedViewMixin(View):
         kwargs.update(**{self.context_name: form})
         return super().get_context_data(**kwargs)
 
+    @classmethod
+    def is_model_form(cls):
+        return issubclass(cls.form_class, ModelForm)
+
     @classmethod
     def contribute_form(cls, submission, user):
-        form = cls.form_class(instance=submission, user=user)
+        if cls.is_model_form():
+            form = cls.form_class(instance=submission, user=user)
+        else:
+            form = cls.form_class()  # This is for the batch update, we don't pass in the user or a single submission
         form.name = cls.context_name
         return cls.context_name, form
 
+    def get_success_url(self):
+        return self.request.path
+
 
 class CreateOrUpdateView(SingleObjectTemplateResponseMixin, ModelFormMixin, ProcessFormView):
 
diff --git a/opentech/settings/base.py b/opentech/settings/base.py
index fb39394186581b10c4c316deb34e0a6de93ce67c..ca17c3ff66873d59355387e1ff0c228ca0b8195e 100644
--- a/opentech/settings/base.py
+++ b/opentech/settings/base.py
@@ -446,6 +446,11 @@ HIJACK_DECORATOR = 'opentech.apply.users.decorators.superuser_decorator'
 
 # Messaging Settings
 SEND_MESSAGES = env.get('SEND_MESSAGES', 'false').lower() == 'true'
+
+if not SEND_MESSAGES:
+    from django.contrib.messages import constants as message_constants
+    MESSAGE_LEVEL = message_constants.DEBUG
+
 SLACK_DESTINATION_URL = env.get('SLACK_DESTINATION_URL', None)
 SLACK_DESTINATION_ROOM = env.get('SLACK_DESTINATION_ROOM', None)
 
diff --git a/opentech/static_src/src/images/add-person.svg b/opentech/static_src/src/images/add-person.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4d2cd548b3494d17e1ba5a881de76ddd8fb753e6
--- /dev/null
+++ b/opentech/static_src/src/images/add-person.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewbox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#0d7db0" d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
diff --git a/opentech/static_src/src/images/arrow-split.svg b/opentech/static_src/src/images/arrow-split.svg
new file mode 100644
index 0000000000000000000000000000000000000000..27ba4b500c1fd7fd6b54c479e9769ca4b4b82ef3
--- /dev/null
+++ b/opentech/static_src/src/images/arrow-split.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewbox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#0d7db0" d="M14 4l2.29 2.29-2.88 2.88 1.42 1.42 2.88-2.88L20 10V4zm-4 0H4v6l2.29-2.29 4.71 4.7V20h2v-8.41l-5.29-5.3z"/></svg>
diff --git a/opentech/static_src/src/javascript/apply/batch-actions.js b/opentech/static_src/src/javascript/apply/batch-actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..6d2af16f8d6269e0b28086a1440e76f8f18a2fab
--- /dev/null
+++ b/opentech/static_src/src/javascript/apply/batch-actions.js
@@ -0,0 +1,108 @@
+(function ($) {
+
+    'use strict';
+
+    const $body = $('body');
+    const $checkbox = $('.js-batch-select');
+    const $allCheckboxInput = $('.js-batch-select-all');
+    const $batchReviewersButton = $('.js-batch-update-reviewers');
+    const $batchTitlesList = $('.js-batch-titles');
+    const $batchTitleCount = $('.js-batch-title-count');
+    const $hiddenIDlist = $('#id_submission_ids');
+    const $toggleBatchList = $('.js-toggle-batch-list');
+    const activeClass = 'batch-actions-enabled';
+    const closedClass = 'is-closed';
+
+    $(window).on('load', function () {
+        toggleBatchActions();
+        updateCount();
+    });
+
+    $allCheckboxInput.change(function () {
+        if ($(this).is(':checked')) {
+            $checkbox.each(function () {
+                this.checked = true;
+            });
+        }
+        else {
+            $checkbox.each(function () {
+                this.checked = false;
+            });
+        }
+
+        toggleBatchActions();
+        updateCount();
+    });
+
+    $checkbox.change(function () {
+        // see how many checkboxes are :checked
+        toggleBatchActions();
+
+        // updates selected checbox count
+        updateCount();
+
+        // reset the check all input
+        if (!$(this).is(':checked') && $allCheckboxInput.is(':checked')) {
+            resetCheckAllInput();
+        }
+    });
+
+    // append selected project titles to batch update reviewer modal
+    $batchReviewersButton.click(function () {
+        $batchTitlesList.html('');
+        $batchTitleCount.html('');
+        $batchTitlesList.addClass(closedClass);
+        $toggleBatchList.html('Show');
+
+        let selectedIDs = [];
+
+        $checkbox.each(function () {
+            if ($(this).is(':checked')) {
+                const href = $(this).parents('tr').find('.js-title').find('a').attr('href');
+                const title = $(this).parents('tr').find('.js-title').data('tooltip');
+
+                $batchTitlesList.append(`
+                    <a href="${href}" class="modal__list-item" target="_blank" rel="noopener noreferrer" title="${title}">
+                        ${title}
+                        <svg class="modal__open-link-icon"><use xlink:href="#open-in-new-tab"></use></svg>
+                    </a>
+                `);
+                selectedIDs.push($(this).parents('tr').data('record-id'));
+            }
+        });
+
+        $batchTitleCount.append(`${selectedIDs.length} submissions selected`);
+        $hiddenIDlist.val(selectedIDs.join(','));
+    });
+
+    // show/hide the list of actions
+    $toggleBatchList.click(e => {
+        e.preventDefault();
+
+        if ($('.js-batch-titles').hasClass(closedClass)) {
+            $toggleBatchList.html('Hide');
+        }
+        else {
+            $toggleBatchList.html('Show');
+        }
+
+        $batchTitlesList.toggleClass(closedClass);
+    });
+
+    function toggleBatchActions() {
+        if ($('.js-batch-select:checked').length) {
+            $body.addClass(activeClass);
+        }
+        else {
+            $body.removeClass(activeClass);
+        }
+    }
+
+    function updateCount() {
+        $('.js-total-actions').html($('.js-batch-select:checked').length);
+    }
+
+    function resetCheckAllInput() {
+        $allCheckboxInput.prop('checked', false);
+    }
+})(jQuery);
diff --git a/opentech/static_src/src/sass/apply/abstracts/_mixins.scss b/opentech/static_src/src/sass/apply/abstracts/_mixins.scss
index b4dd94038021114927a8d1fa8ba106e9728cc191..ed0308e137cac385d8899c56d96e7b2f0d8f30b4 100644
--- a/opentech/static_src/src/sass/apply/abstracts/_mixins.scss
+++ b/opentech/static_src/src/sass/apply/abstracts/_mixins.scss
@@ -221,3 +221,23 @@
         height: calc(100vh -  #{$listing-header-height});
     }
 }
+
+@mixin checkbox-without-label {
+    input[type='checkbox'] {
+        margin: 0 auto;
+        display: block;
+        width: 20px;
+        height: 20px;
+        border: 1px solid $color--mid-grey;
+        -webkit-appearance: none; // sass-lint:disable-line no-vendor-prefixes
+        -moz-appearance: none; // sass-lint:disable-line no-vendor-prefixes
+        appearance: none;
+        background-color: $color--white;
+
+        &:checked {
+            background: url('./../../images/tick.svg') $color--dark-blue center no-repeat;
+            background-size: 12px;
+            border: 1px solid $color--dark-blue;
+        }
+    }
+}
diff --git a/opentech/static_src/src/sass/apply/components/_actions-bar.scss b/opentech/static_src/src/sass/apply/components/_actions-bar.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e60aea02d6b353dda695cd0b54b49a9ae2330f2f
--- /dev/null
+++ b/opentech/static_src/src/sass/apply/components/_actions-bar.scss
@@ -0,0 +1,54 @@
+.actions-bar {
+    margin: 20px 0;
+    width: 100%;
+
+    @include media-query(tablet-landscape) {
+        display: flex;
+        justify-content: space-between;
+    }
+
+    &__inner {
+        & > * {
+            margin-bottom: 20px;
+        }
+
+        @include media-query(tablet-landscape) {
+            display: flex;
+            align-items: center;
+
+            & > * {
+                margin: 0 0 0 20px;
+
+                &:first-child {
+                    margin-left: 0;
+                }
+            }
+        }
+
+        &--left {
+            display: none;
+
+            @include media-query(tablet-landscape) {
+                display: flex;
+                opacity: 0;
+                pointer-events: none;
+                transition: opacity $quick-transition;
+
+                .batch-actions-enabled & {
+                    opacity: 1;
+                    pointer-events: all;
+                }
+            }
+        }
+    }
+
+    &__total {
+        background-color: $color--light-blue;
+        color: $color--white;
+        padding: 6px 16px;
+        border-radius: 30px;
+        min-width: 120px;
+        text-align: center;
+        font-weight: $weight--semibold;
+    }
+}
diff --git a/opentech/static_src/src/sass/apply/components/_all-submissions-table.scss b/opentech/static_src/src/sass/apply/components/_all-submissions-table.scss
index 81df77c7fe8888b7d61db64eda57aaaaa8f5850f..f7e6b5fe2420ffff8aecb5754eed5e70002a72cf 100644
--- a/opentech/static_src/src/sass/apply/components/_all-submissions-table.scss
+++ b/opentech/static_src/src/sass/apply/components/_all-submissions-table.scss
@@ -24,7 +24,7 @@
             &.title {
                 @include media-query($table-breakpoint) {
                     width: 130px;
-                    padding-left: 20px;
+                    padding-left: 10px;
                 }
 
                 @include media-query(desktop) {
@@ -37,6 +37,14 @@
                     width: 150px;
                 }
             }
+
+            &.selected {
+                @include checkbox-without-label;
+
+                @include media-query($table-breakpoint) {
+                    width: 60px;
+                }
+            }
         }
 
         tr {
@@ -57,13 +65,12 @@
                 @include media-query($table-breakpoint) {
                     display: flex;
                     align-items: center;
-                    padding-top: 20px;
+                    padding-top: 10px;
                     padding-left: 10px;
                 }
 
                 @include media-query(desktop) {
                     display: table-cell;
-                    padding-left: 20px;
                 }
 
                 &.has-tooltip {
@@ -142,6 +149,16 @@
                 }
             }
 
+            // batch action checkboxes
+            &.selected {
+                @include checkbox-without-label;
+                display: none;
+
+                @include media-query($table-breakpoint) {
+                    display: table-cell;
+                }
+            }
+
             // arrow to toggle project info - added via js
             @include media-query($table-breakpoint) {
                 .arrow {
diff --git a/opentech/static_src/src/sass/apply/components/_button.scss b/opentech/static_src/src/sass/apply/components/_button.scss
index 5ebcce8979c9328a65d199918924f6676d478454..1b35fecaee3e34a6f7c3126a87de812b780706dc 100644
--- a/opentech/static_src/src/sass/apply/components/_button.scss
+++ b/opentech/static_src/src/sass/apply/components/_button.scss
@@ -80,25 +80,10 @@
         width: 100%;
 
         @include media-query(tablet-landscape) {
-            background: none;
-            padding: 0 10px;
-            border: 0;
-            align-items: center;
-            justify-content: flex-start;
-            max-width: initial;
-            width: auto;
-
             &::before {
                 content: '';
                 background-image: url('./../../images/filters.svg');
-                background-color: transparent;
-                background-position: left center;
                 transform: rotate(90deg);
-                background-size: 20px;
-                width: 20px;
-                height: 20px;
-                display: inline-block;
-                margin-right: 10px;
             }
         }
     }
@@ -221,4 +206,54 @@
             fill: $color--white;
         }
     }
+
+    &--action {
+        display: flex;
+        font-weight: $weight--normal;
+        color: $color--default;
+        transition: none;
+
+        @include media-query(tablet-landscape) {
+            background: none;
+            padding: 0;
+            border: 0;
+            align-items: center;
+            justify-content: flex-start;
+            width: auto;
+            transition: opacity $transition;
+            opacity: .7;
+            font-weight: $weight--semibold;
+
+            &:hover {
+                opacity: 1;
+            }
+
+            &::before {
+                content: '';
+                background-image: url('./../../images/filters.svg');
+                background-color: transparent;
+                background-position: left center;
+                background-size: 20px;
+                width: 20px;
+                height: 20px;
+                display: inline-block;
+                margin-right: 10px;
+            }
+        }
+    }
+
+    &--change-status {
+        display: none;
+
+        &::before {
+            background-image: url('./../../images/arrow-split.svg');
+            transform: rotate(90deg);
+        }
+    }
+
+    &--reviewers {
+        &::before {
+            background-image: url('./../../images/add-person.svg');
+        }
+    }
 }
diff --git a/opentech/static_src/src/sass/apply/components/_form.scss b/opentech/static_src/src/sass/apply/components/_form.scss
index c7708add8d06a46a2ac14c522915aa821a09bc82..bb1fad78da89d37102766f16e9bd0e48fa2702d1 100644
--- a/opentech/static_src/src/sass/apply/components/_form.scss
+++ b/opentech/static_src/src/sass/apply/components/_form.scss
@@ -17,11 +17,10 @@
         }
     }
 
-    &--search {
+    &--search-desktop {
         position: relative;
         max-width: 300px;
         margin-top: $mobile-gutter;
-        width: 100%;
 
         @include media-query(tablet-landscape) {
             max-width: 280px;
diff --git a/opentech/static_src/src/sass/apply/components/_modal.scss b/opentech/static_src/src/sass/apply/components/_modal.scss
index beeb6c0ae4d8e7246db72fb32ba9a05671b1268d..d486cca48b00a9e2f839ecf737089c43b4cef4bd 100644
--- a/opentech/static_src/src/sass/apply/components/_modal.scss
+++ b/opentech/static_src/src/sass/apply/components/_modal.scss
@@ -1,10 +1,72 @@
 .modal {
+    $root: &;
     display: none;
     width: calc(100% - 40px);
     padding: 20px;
 
+    &--secondary {
+        padding: 0;
+    }
+
     @include media-query(small-tablet) {
         width: 580px;
         padding: 30px;
     }
+
+    &__header-bar {
+        color: $color--white;
+        background-color: $color--dark-blue;
+        margin: -24px -24px 0;
+        padding: 15px;
+        text-align: center;
+    }
+
+    &__list {
+        max-height: 200px;
+        overflow: scroll;
+        margin: 0 -24px 20px;
+        padding: 0;
+        border-bottom: 2px solid $color--light-mid-grey;
+        box-shadow: inset 0 -10px 20px -10px $color--mid-grey;
+        transition: max-height $transition;
+
+        &.is-closed {
+            max-height: 0;
+            border-bottom: 0;
+        }
+    }
+
+    &__list-item {
+        display: block;
+        font-size: map-get($font-sizes, zeta);
+        padding: 12px 28px;
+        border-bottom: 2px solid $color--light-mid-grey;
+        margin: 0;
+        color: $color--default;
+
+        &--meta {
+            color: $color--dark-blue;
+            font-weight: $weight--semibold;
+            display: flex;
+            justify-content: space-between;
+            margin: 0 -24px;
+        }
+    }
+
+    &__hide-link {
+        text-decoration: underline;
+    }
+
+    &__open-link-icon {
+        width: 20px;
+        height: 20px;
+        fill: $color--dark-grey;
+        opacity: 0;
+        transition: opacity $quick-transition;
+        pointer-events: none;
+
+        #{$root}__list-item:hover & {
+            opacity: 1;
+        }
+    }
 }
diff --git a/opentech/static_src/src/sass/apply/fancybox.scss b/opentech/static_src/src/sass/apply/fancybox.scss
index 440a4fc308a538505f29399ac7eb603b85581d2b..e01a47923d6f75200525d969ec325368fda585f0 100644
--- a/opentech/static_src/src/sass/apply/fancybox.scss
+++ b/opentech/static_src/src/sass/apply/fancybox.scss
@@ -325,6 +325,10 @@ body.fancybox-iosfix {
 	position: relative;
     overflow: visible;
     shape-rendering: geometricPrecision;
+
+	.modal--secondary & {
+		display: none;
+	}
 }
 
 .fancybox-button svg path {
@@ -424,6 +428,13 @@ body.fancybox-iosfix {
 	transition: background-color .25s;
 	box-sizing: border-box;
 	z-index: 2;
+
+	.modal--secondary & {
+		color: white;
+		font-size: 40px;
+		margin-top: 12px;
+    	margin-right: 8px;
+	}
 }
 
 .fancybox-close-small:focus {
diff --git a/opentech/static_src/src/sass/apply/main.scss b/opentech/static_src/src/sass/apply/main.scss
index 4423f6ed552181b4708da92017bba897de5f1aa7..386c5f11111e127b697b8f9782045cb4176366b7 100644
--- a/opentech/static_src/src/sass/apply/main.scss
+++ b/opentech/static_src/src/sass/apply/main.scss
@@ -12,6 +12,7 @@
 @import 'components/all-rounds-table';
 @import 'components/admin-bar';
 @import 'components/activity-feed';
+@import 'components/actions-bar';
 @import 'components/comment';
 @import 'components/button';
 @import 'components/editor';
diff --git a/opentech/templates/includes/sprites.html b/opentech/templates/includes/sprites.html
index f97fe2b51ba63be09caed92a276cc3481e976769..42bf4cb3d50f9fe3d9a3ac8da8fd2091430dcb80 100644
--- a/opentech/templates/includes/sprites.html
+++ b/opentech/templates/includes/sprites.html
@@ -300,4 +300,8 @@
     <symbol id="exclamation-point" viewbox="0 0 24 24">
         <path fill="none" d="M0 0h24v24H0V0z"/><path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
     </symbol>
+
+    <symbol id="open-in-new-tab" viewbox="0 0 24 24">
+        <path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
+    </symbol>
 </svg>