diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py
index d38841979716887ec80ee92c46a5ea6067299075..4ee98bb81eebc2fb1032f8276cac7e6f0340d78c 100644
--- a/opentech/apply/activity/messaging.py
+++ b/opentech/apply/activity/messaging.py
@@ -1,5 +1,6 @@
 import json
 import requests
+from collections import defaultdict
 
 from django.db import models
 from django.conf import settings
@@ -20,6 +21,24 @@ def link_to(target, request):
         return request.scheme + '://' + request.get_host() + target.get_absolute_url()
 
 
+def group_reviewers(reviewers):
+    groups = defaultdict(list)
+    for reviewer in reviewers:
+        groups[reviewer.role].append(reviewer.reviewer)
+    return groups
+
+
+def reviewers_message(reviewers):
+    messages = []
+    for role, reviewers in group_reviewers(reviewers).items():
+        message = ', '.join(str(reviewer) for reviewer in reviewers)
+        if role:
+            message += ' as ' + str(role)
+        message += '.'
+        messages.append(message)
+    return messages
+
+
 neat_related = {
     MESSAGES.DETERMINATION_OUTCOME: 'determination',
     MESSAGES.UPDATE_LEAD: 'old_lead',
@@ -192,11 +211,11 @@ class ActivityAdapter(AdapterBase):
         message = ['Reviewers updated.']
         if added:
             message.append('Added:')
-            message.append(', '.join([str(user) for user in added]) + '.')
+            message.extend(reviewers_message(added))
 
         if removed:
             message.append('Removed:')
-            message.append(', '.join([str(user) for user in removed]) + '.')
+            message.extend(reviewers_message(removed))
 
         return ' '.join(message)
 
@@ -265,7 +284,7 @@ class SlackAdapter(AdapterBase):
         MESSAGES.COMMENT: 'A new {comment.visibility} comment has been posted on <{link}|{submission.title}> by {user}',
         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.REVIEWERS_UPDATED: 'reviewers_updated',
         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}',
@@ -308,6 +327,19 @@ class SlackAdapter(AdapterBase):
             } for lead in leads
         ]
 
+    def reviewers_updated(self, submission, link, user, added=list(), removed=list(), **kwargs):
+        message = [f'{user} has updated the reviewers on <{link}|{submission.title}>.']
+
+        if added:
+            message.append('Added:')
+            message.extend(reviewers_message(added))
+
+        if removed:
+            message.append('Removed:')
+            message.extend(reviewers_message(removed))
+
+        return ' '.join(message)
+
     def handle_batch_reviewers(self, submissions, links, user, added, **kwargs):
         submissions_text = ', '.join(
             f'<{links[submission.id]}|{submission.title}>'
diff --git a/opentech/apply/activity/tests/test_messaging.py b/opentech/apply/activity/tests/test_messaging.py
index 4b9838aaf062e2106768f668114736621d0c2d4a..e2c8b6318234ba997d35a0d04d96a0b259e23e23 100644
--- a/opentech/apply/activity/tests/test_messaging.py
+++ b/opentech/apply/activity/tests/test_messaging.py
@@ -10,7 +10,11 @@ from django.test import TestCase, override_settings
 from django.contrib.messages import get_messages
 
 from opentech.apply.utils.testing import make_request
-from opentech.apply.funds.tests.factories import ApplicationSubmissionFactory
+from opentech.apply.funds.tests.factories import (
+    ApplicationSubmissionFactory,
+    AssignedReviewersFactory,
+    AssignedWithRoleReviewersFactory,
+)
 from opentech.apply.review.tests.factories import ReviewFactory
 from opentech.apply.users.tests.factories import ReviewerFactory, UserFactory
 
@@ -202,26 +206,44 @@ class TestActivityAdapter(TestCase):
         self.assertEqual(activity.submission, submission)
 
     def test_reviewers_message_no_removed(self):
-        message = self.adapter.reviewers_updated([1], [])
+        assigned_reviewer = AssignedReviewersFactory()
+
+        message = self.adapter.reviewers_updated([assigned_reviewer], [])
 
         self.assertTrue('Added' in message)
         self.assertFalse('Removed' in message)
-        self.assertTrue('1' in message)
+        self.assertTrue(str(assigned_reviewer.reviewer) in message)
 
     def test_reviewers_message_no_added(self):
-        message = self.adapter.reviewers_updated([], [1])
+        assigned_reviewer = AssignedReviewersFactory()
+        message = self.adapter.reviewers_updated([], [assigned_reviewer])
 
         self.assertFalse('Added' in message)
         self.assertTrue('Removed' in message)
-        self.assertTrue('1' in message)
+        self.assertTrue(str(assigned_reviewer.reviewer) in message)
 
     def test_reviewers_message_both(self):
-        message = self.adapter.reviewers_updated([1], [2])
+        added, removed = AssignedReviewersFactory.create_batch(2)
+        message = self.adapter.reviewers_updated([added], [removed])
 
         self.assertTrue('Added' in message)
         self.assertTrue('Removed' in message)
-        self.assertTrue('1' in message)
-        self.assertTrue('2' in message)
+        self.assertTrue(str(added.reviewer) in message)
+        self.assertTrue(str(removed.reviewer) in message)
+
+    def test_reviewers_with_role(self):
+        with_role = AssignedWithRoleReviewersFactory()
+        message = self.adapter.reviewers_updated([with_role], [])
+        self.assertTrue(str(with_role.reviewer) in message)
+        self.assertTrue(str(with_role.role) in message)
+
+    def test_reviewers_with_and_without_role(self):
+        with_role = AssignedWithRoleReviewersFactory()
+        without_role = AssignedReviewersFactory()
+        message = self.adapter.reviewers_updated([with_role, without_role], [])
+        self.assertTrue(str(with_role.reviewer) in message)
+        self.assertTrue(str(with_role.role) in message)
+        self.assertTrue(str(without_role.reviewer) in message)
 
     def test_internal_transition_kwarg_for_invisible_transition(self):
         submission = ApplicationSubmissionFactory(status='post_review_discussion')
@@ -275,11 +297,15 @@ class TestActivityAdapter(TestCase):
         activity = Activity.objects.first()
         self.assertEqual(activity.related_object, None)
 
-    def test_review_saved_on_activtiy(self):
-        submission = ApplicationSubmissionFactory()
-        user = UserFactory()
-        review = ReviewFactory(submission=submission)
-        self.adapter.send_message('a message', user=user, submission=submission, submissions=[], related=review)
+    def test_review_saved_on_activity(self):
+        review = ReviewFactory()
+        self.adapter.send_message(
+            'a message',
+            user=review.author,
+            submission=review.submission,
+            submissions=[],
+            related=review,
+        )
         activity = Activity.objects.first()
         self.assertEqual(activity.related_object, review)
 
diff --git a/opentech/apply/funds/admin.py b/opentech/apply/funds/admin.py
index 689f73027216344b55a4b82eed731de5b7b55dfb..9f1cbc8464a1bd34f8f87b42682fdbbd928da97c 100644
--- a/opentech/apply/funds/admin.py
+++ b/opentech/apply/funds/admin.py
@@ -1,7 +1,7 @@
 from wagtail.contrib.modeladmin.helpers import PermissionHelper
 from wagtail.contrib.modeladmin.options import ModelAdmin, ModelAdminGroup
 
-from opentech.apply.funds.models import ScreeningStatus
+from opentech.apply.funds.models import ReviewerRole, ScreeningStatus
 from opentech.apply.review.admin import ReviewFormAdmin
 from opentech.apply.utils.admin import ListRelatedMixin
 from .admin_helpers import (
@@ -74,6 +74,12 @@ class LabAdmin(ModelAdmin):
     menu_label = 'Labs'
 
 
+class ReviewerRoleAdmin(ModelAdmin):
+    model = ReviewerRole
+    menu_icon = 'group'
+    menu_label = 'Reviewer Roles'
+
+
 class NoDeletePermission(PermissionHelper):
     def user_can_delete_obj(self, user, obj):
         return False
@@ -106,4 +112,5 @@ class ApplyAdminGroup(ModelAdminGroup):
         ReviewFormAdmin,
         CategoryAdmin,
         ScreeningStatusAdmin,
+        ReviewerRoleAdmin,
     )
diff --git a/opentech/apply/funds/forms.py b/opentech/apply/funds/forms.py
index a75cda24e32c1b113f30aa31bb7bc1643640b6c4..0a71b92e8892a8f0e27251e79c5abb814e7595d5 100644
--- a/opentech/apply/funds/forms.py
+++ b/opentech/apply/funds/forms.py
@@ -1,9 +1,11 @@
 from django import forms
+from django.utils.text import slugify
+from django.utils.translation import ugettext_lazy as _
 
 from opentech.apply.users.models import User
 
-from .models import ApplicationSubmission
-from .widgets import Select2MultiCheckboxesWidget
+from .models import ApplicationSubmission, AssignedReviewers, ReviewerRole
+from .widgets import Select2MultiCheckboxesWidget, Select2IconWidget
 
 
 class ProgressSubmissionForm(forms.ModelForm):
@@ -50,13 +52,8 @@ class UpdateSubmissionLeadForm(forms.ModelForm):
 
 
 class UpdateReviewersForm(forms.ModelForm):
-    staff_reviewers = forms.ModelMultipleChoiceField(
-        queryset=User.objects.staff(),
-        widget=Select2MultiCheckboxesWidget(attrs={'data-placeholder': 'Staff'}),
-        required=False,
-    )
     reviewer_reviewers = forms.ModelMultipleChoiceField(
-        queryset=User.objects.reviewers().exclude(id__in=User.objects.staff()),
+        queryset=User.objects.reviewers().only('pk', 'full_name'),
         widget=Select2MultiCheckboxesWidget(attrs={'data-placeholder': 'Reviewers'}),
         label='Reviewers',
         required=False,
@@ -69,12 +66,47 @@ class UpdateReviewersForm(forms.ModelForm):
     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'))
 
-        self.prepare_field('staff_reviewers', reviewers, self.submitted_reviewers)
+        assigned_roles = {
+            assigned.role: assigned.reviewer
+            for assigned in self.instance.assigned.filter(
+                role__isnull=False
+            )
+        }
+
+        staff_reviewers = User.objects.staff().only('full_name', 'pk')
+
+        self.role_fields = {}
+        for role in ReviewerRole.objects.all().order_by('order'):
+            field_name = 'role_reviewer_' + slugify(str(role))
+            self.role_fields[field_name] = role
+
+            self.fields[field_name] = forms.ModelChoiceField(
+                queryset=staff_reviewers,
+                widget=Select2IconWidget(attrs={
+                    'data-placeholder': 'Select a reviewer', 'icon': role.icon
+                }),
+                required=False,
+                label=f'{role.name} Reviewer',
+            )
+            # Pre-populate form field
+            self.fields[field_name].initial = assigned_roles.get(role)
+
+        self.submitted_reviewers = User.objects.filter(
+            id__in=self.instance.reviews.values('author'),
+        )
+
         if self.can_alter_external_reviewers(self.instance, self.user):
-            self.prepare_field('reviewer_reviewers', reviewers, self.submitted_reviewers)
+
+            reviewers = self.instance.reviewers.all().only('pk')
+            self.prepare_field(
+                'reviewer_reviewers',
+                initial=reviewers,
+                excluded=self.submitted_reviewers
+            )
+
+            # Move the non-role reviewers field to the end of the field list
+            self.fields.move_to_end('reviewer_reviewers')
         else:
             self.fields.pop('reviewer_reviewers')
 
@@ -86,18 +118,77 @@ class UpdateReviewersForm(forms.ModelForm):
     def can_alter_external_reviewers(self, instance, user):
         return instance.stage.has_external_review and (user == instance.lead or user.is_superuser)
 
+    def clean(self):
+        cleaned_data = super().clean()
+        role_reviewers = [
+            user
+            for field, user in self.cleaned_data.items()
+            if field != 'reviewer_reviewers'
+        ]
+
+        # If any of the users match and are set to multiple roles, throw an error
+        if len(role_reviewers) != len(set(role_reviewers)) and any(role_reviewers):
+            self.add_error(None, _('Users cannot be assigned to multiple roles.'))
+
+        return cleaned_data
+
     def save(self, *args, **kwargs):
         instance = super().save(*args, **kwargs)
+        """
+        1. Update role reviewers
+        2. Update non-role reviewers
+            2a. Remove those not on form
+            2b. Add in any new non-role reviewers selected
+        3. Add in anyone who has already reviewed but who is not selected as a reviewer on the form
+        """
+
+        # 1. Update role reviewers
+        assigned_roles = {
+            role: self.cleaned_data[field]
+            for field, role in self.role_fields.items()
+        }
+        for role, reviewer in assigned_roles.items():
+            if reviewer:
+                AssignedReviewers.objects.update_or_create(
+                    submission=instance,
+                    role=role,
+                    defaults={'reviewer': reviewer},
+                )
+
+        # 2. Update non-role reviewers
+        # 2a. Remove those not on form
         if self.can_alter_external_reviewers(self.instance, self.user):
             reviewers = self.cleaned_data.get('reviewer_reviewers')
-        else:
-            reviewers = instance.reviewers_not_reviewed
-
-        instance.reviewers.set(
-            self.cleaned_data['staff_reviewers'] |
-            reviewers |
-            self.submitted_reviewers
+            assigned_reviewers = instance.assigned.without_roles()
+            assigned_reviewers.exclude(
+                reviewer__in=reviewers | self.submitted_reviewers
+            ).delete()
+
+            remaining_reviewers = assigned_reviewers.values_list('reviewer_id', flat=True)
+
+            # 2b. Add in any new non-role reviewers selected
+            AssignedReviewers.objects.bulk_create(
+                AssignedReviewers(
+                    submission=instance,
+                    role=None,
+                    reviewer=reviewer
+                ) for reviewer in reviewers
+                if reviewer.id not in remaining_reviewers
+            )
+
+        # 3. Add in anyone who has already reviewed but who is not selected as a reviewer on the form
+        orphaned_reviews = instance.reviews.exclude(
+            author__in=instance.assigned.values('reviewer')
+        ).select_related('author')
+
+        AssignedReviewers.objects.bulk_create(
+            AssignedReviewers(
+                submission=instance,
+                role=None,
+                reviewer=review.author
+            ) for review in orphaned_reviews
         )
+
         return instance
 
 
diff --git a/opentech/apply/funds/migrations/0051_reviewerrole.py b/opentech/apply/funds/migrations/0051_reviewerrole.py
new file mode 100644
index 0000000000000000000000000000000000000000..57049c521b645d74dc8264e879ab0890649e1692
--- /dev/null
+++ b/opentech/apply/funds/migrations/0051_reviewerrole.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.0.10 on 2019-02-07 15:46
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('images', '0003_customimage_drupal_id'),
+        ('funds', '0050_roundsandlabs'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ReviewerRole',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=128)),
+                ('order', models.IntegerField(blank=True, help_text='The order this role should appear in the Update Reviewers form.', null=True)),
+                ('icon', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage')),
+            ],
+        ),
+    ]
diff --git a/opentech/apply/funds/migrations/0052_assigned_reviewers_pre.py b/opentech/apply/funds/migrations/0052_assigned_reviewers_pre.py
new file mode 100644
index 0000000000000000000000000000000000000000..66f76fe41c3552f3885114bac4ed5c17a501fd42
--- /dev/null
+++ b/opentech/apply/funds/migrations/0052_assigned_reviewers_pre.py
@@ -0,0 +1,43 @@
+# Generated by Django 2.0.10 on 2019-02-07 15:52
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('funds', '0051_reviewerrole'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='AssignedReviewers',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('reviewer', models.ForeignKey(limit_choices_to={'groups__name__in': ['Staff', 'Reviewer']}, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+                ('role', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='funds.ReviewerRole')),
+            ],
+        ),
+        migrations.AlterField(
+            model_name='applicationsubmission',
+            name='reviewers',
+            field=models.ManyToManyField(blank=True, limit_choices_to={'groups__name__in': ['Staff', 'Reviewer']}, related_name='submissions_reviewer_OLD', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='assignedreviewers',
+            name='submission',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assigned', to='funds.ApplicationSubmission'),
+        ),
+        migrations.AddField(
+            model_name='applicationsubmission',
+            name='reviewers_new',
+            field=models.ManyToManyField(blank=True, related_name='submissions_reviewer', through='funds.AssignedReviewers', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterUniqueTogether(
+            name='assignedreviewers',
+            unique_together={('submission', 'role')},
+        ),
+    ]
diff --git a/opentech/apply/funds/migrations/0053_move_reviewer_data.py b/opentech/apply/funds/migrations/0053_move_reviewer_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e64748220abe5d0a8899c44dc76a071c2dc391a
--- /dev/null
+++ b/opentech/apply/funds/migrations/0053_move_reviewer_data.py
@@ -0,0 +1,29 @@
+# Generated by Django 2.0.10 on 2019-02-07 15:54
+
+from django.db import migrations
+
+
+def move_reviewer_data(apps, schema_editor):
+    # We need to move data to the new `reviewers_new` field which will be renamed to `reviewers` in the next migration
+    # This data migration is necessary because you cannot add a through model to an existing M2M field
+    ApplicationSubmission = apps.get_model('funds', 'ApplicationSubmission')
+    AssignedReviewers = apps.get_model('funds', 'AssignedReviewers')
+    for submission in ApplicationSubmission.objects.all():
+        AssignedReviewers.objects.bulk_create(
+            AssignedReviewers(
+                submission=submission,
+                reviewer=reviewer,
+                role=None,
+            ) for reviewer in submission.reviewers.all()
+        )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('funds', '0052_assigned_reviewers_pre'),
+    ]
+
+    operations = [
+        migrations.RunPython(move_reviewer_data, reverse_code=migrations.RunPython.noop),
+    ]
diff --git a/opentech/apply/funds/migrations/0054_remove_applicationsubmission_reviewers.py b/opentech/apply/funds/migrations/0054_remove_applicationsubmission_reviewers.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9806dcc8b8501c61fb7e8b4e298a33f31649522
--- /dev/null
+++ b/opentech/apply/funds/migrations/0054_remove_applicationsubmission_reviewers.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.0.10 on 2019-02-07 16:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('funds', '0053_move_reviewer_data'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='applicationsubmission',
+            name='reviewers',
+        ),
+    ]
diff --git a/opentech/apply/funds/migrations/0055_reviewers_rename.py b/opentech/apply/funds/migrations/0055_reviewers_rename.py
new file mode 100644
index 0000000000000000000000000000000000000000..66ed97192288a211849479fd2209282a654276c9
--- /dev/null
+++ b/opentech/apply/funds/migrations/0055_reviewers_rename.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0.10 on 2019-02-07 16:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('funds', '0054_remove_applicationsubmission_reviewers'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='applicationsubmission',
+            old_name='reviewers_new',
+            new_name='reviewers',
+        ),
+    ]
diff --git a/opentech/apply/funds/models/__init__.py b/opentech/apply/funds/models/__init__.py
index 101a4b48011e5d342fcf8164a75a073020d4de32..4730a3a44167279ff536202439cdaa88cd6c12c0 100644
--- a/opentech/apply/funds/models/__init__.py
+++ b/opentech/apply/funds/models/__init__.py
@@ -2,11 +2,12 @@ from django.utils.translation import ugettext_lazy as _
 
 from .applications import ApplicationBase, RoundBase, LabBase, RoundsAndLabs  # NOQA
 from .forms import ApplicationForm
+from .reviewer_role import ReviewerRole
 from .screening import ScreeningStatus
-from .submissions import ApplicationSubmission, ApplicationRevision
+from .submissions import ApplicationSubmission, AssignedReviewers, ApplicationRevision
 
 
-__all__ = ['ApplicationSubmission', 'ApplicationRevision', 'ApplicationForm', 'ScreeningStatus']
+__all__ = ['ApplicationSubmission', 'AssignedReviewers', 'ApplicationRevision', 'ApplicationForm', 'ScreeningStatus', 'ReviewerRole']
 
 
 class FundType(ApplicationBase):
diff --git a/opentech/apply/funds/models/reviewer_role.py b/opentech/apply/funds/models/reviewer_role.py
new file mode 100644
index 0000000000000000000000000000000000000000..d06477a4e053bae5acf14c2bb3555181a1403f4d
--- /dev/null
+++ b/opentech/apply/funds/models/reviewer_role.py
@@ -0,0 +1,28 @@
+from django.db import models
+from wagtail.admin.edit_handlers import FieldPanel
+from wagtail.images.edit_handlers import ImageChooserPanel
+
+
+class ReviewerRole(models.Model):
+    name = models.CharField(max_length=128)
+    icon = models.ForeignKey(
+        'images.CustomImage',
+        null=True,
+        blank=True,
+        related_name='+',
+        on_delete=models.SET_NULL
+    )
+    order = models.IntegerField(
+        help_text='The order this role should appear in the Update Reviewers form.',
+        null=True,
+        blank=True,
+    )
+
+    panels = [
+        FieldPanel('name'),
+        ImageChooserPanel('icon'),
+        FieldPanel('order'),
+    ]
+
+    def __str__(self):
+        return self.name
diff --git a/opentech/apply/funds/models/submissions.py b/opentech/apply/funds/models/submissions.py
index 397b2ec9f532e500785eeb5ea459277673369b45..0e1f7d50cb36be9533e8b4d7f37f82b8ce40f806 100644
--- a/opentech/apply/funds/models/submissions.py
+++ b/opentech/apply/funds/models/submissions.py
@@ -304,8 +304,8 @@ class ApplicationSubmission(
     reviewers = models.ManyToManyField(
         settings.AUTH_USER_MODEL,
         related_name='submissions_reviewer',
-        limit_choices_to=LIMIT_TO_STAFF_AND_REVIEWERS,
         blank=True,
+        through='AssignedReviewers',
     )
     user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
     search_data = models.TextField()
@@ -525,7 +525,11 @@ class ApplicationSubmission(
 
         if creating:
             self.process_file_data(files)
-            self.reviewers.set(self.get_from_parent('reviewers').all())
+            for reviewer in self.get_from_parent('reviewers').all():
+                AssignedReviewers.objects.create(
+                    reviewer=reviewer,
+                    submission=self
+                )
             first_revision = ApplicationRevision.objects.create(
                 submission=self,
                 form_data=self.form_data,
@@ -674,3 +678,51 @@ class ApplicationRevision(AccessFormData, models.Model):
             'to': self.id,
             'from': previous_revision.id,
         })
+
+
+class AssignedReviewersQuerySet(models.QuerySet):
+    def with_roles(self):
+        return self.filter(role__isnull=False)
+
+    def without_roles(self):
+        return self.filter(role__isnull=True)
+
+
+class AssignedReviewers(models.Model):
+    reviewer = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        on_delete=models.CASCADE,
+        limit_choices_to=LIMIT_TO_STAFF_AND_REVIEWERS,
+    )
+    submission = models.ForeignKey(
+        ApplicationSubmission,
+        related_name='assigned',
+        on_delete=models.CASCADE
+    )
+    role = models.ForeignKey(
+        'funds.ReviewerRole',
+        related_name='+',
+        on_delete=models.SET_NULL,
+        null=True,
+    )
+
+    objects = AssignedReviewersQuerySet.as_manager()
+
+    class Meta:
+        unique_together = ('submission', 'role')
+
+    def __str__(self):
+        return f'{self.reviewer} as {self.role}'
+
+    def __eq__(self, other):
+        if not isinstance(other, models.Model):
+            return False
+        if self._meta.concrete_model != other._meta.concrete_model:
+            return False
+        my_pk = self.pk
+        if my_pk is None:
+            return self is other
+        return all([
+            self.reviewer_id == other.reviewer_id,
+            self.role_id == other.role_id,
+        ])
diff --git a/opentech/apply/funds/templates/funds/includes/delegated_form_base.html b/opentech/apply/funds/templates/funds/includes/delegated_form_base.html
index 794d7fc93f08f054d0549c2f2ee6f484f1623b63..c6f0746dc01080d2b61ae9874c63155cb935f8c0 100644
--- a/opentech/apply/funds/templates/funds/includes/delegated_form_base.html
+++ b/opentech/apply/funds/templates/funds/includes/delegated_form_base.html
@@ -1,7 +1,10 @@
+{% load util_tags %}
 <form class="form {{extra_classes}}" method="post" id="{{ form.name }}">
     {% csrf_token %}
     <div class="form__item">
         {{ form }}
     </div>
+
     <input class="button button--primary button--top-space" id="{{ form.name }}-submit" name="{{ form_prefix }}{{ form.name }}" type="submit" form="{{ form.name }}" value="{{ value }}">
+
 </form>
diff --git a/opentech/apply/funds/templates/funds/widgets/icon_select2.html b/opentech/apply/funds/templates/funds/widgets/icon_select2.html
new file mode 100644
index 0000000000000000000000000000000000000000..0449cc92a9ae92152ecc7b820adbc22209fffb9f
--- /dev/null
+++ b/opentech/apply/funds/templates/funds/widgets/icon_select2.html
@@ -0,0 +1,3 @@
+{% load wagtailimages_tags %}
+{% if widget.attrs.icon %}<div>{% image widget.attrs.icon max-20x20 %}</div>{% endif %}
+{% include 'django/forms/widgets/select.html' %}
diff --git a/opentech/apply/funds/tests/factories/models.py b/opentech/apply/funds/tests/factories/models.py
index 6cc1e8165379c40dc229242e941bcb90c123e21d..713b3dc1b046fa2e1f0d93c80f7d5e9244aee248 100644
--- a/opentech/apply/funds/tests/factories/models.py
+++ b/opentech/apply/funds/tests/factories/models.py
@@ -6,9 +6,11 @@ import wagtail_factories
 from opentech.apply.funds.models import (
     ApplicationSubmission,
     ApplicationRevision,
+    AssignedReviewers,
     FundType,
     LabType,
     RequestForPartners,
+    ReviewerRole,
     Round,
     ScreeningStatus,
     SealedRound,
@@ -37,6 +39,8 @@ __all__ = [
     'ApplicationFormFactory',
     'ApplicationRevisionFactory',
     'ApplicationSubmissionFactory',
+    'AssignedReviewersFactory',
+    'AssignedWithRoleReviewersFactory',
     'InvitedToProposalFactory',
     'RoundFactory',
     'RoundBaseFormFactory',
@@ -47,6 +51,7 @@ __all__ = [
     'ScreeningStatusFactory',
     'SealedRoundFactory',
     'SealedSubmissionFactory',
+    'ReviewerRoleFactory',
     'TodayRoundFactory',
     'workflow_for_stages',
 ]
@@ -244,7 +249,34 @@ class ApplicationSubmissionFactory(factory.DjangoModelFactory):
     @factory.post_generation
     def reviewers(self, create, reviewers, **kwargs):
         if create and reviewers:
-            self.reviewers.set(reviewers)
+            AssignedReviewers.objects.bulk_create(
+                AssignedReviewers(
+                    reviewer=reviewer,
+                    submission=self,
+                    role=None
+                ) for reviewer in reviewers
+            )
+
+
+class ReviewerRoleFactory(factory.DjangoModelFactory):
+    class Meta:
+        model = ReviewerRole
+
+    name = factory.Faker('word')
+    order = factory.Sequence(lambda n: n)
+
+
+class AssignedReviewersFactory(factory.DjangoModelFactory):
+    class Meta:
+        model = AssignedReviewers
+
+    submission = factory.SubFactory(ApplicationSubmissionFactory)
+    role = None
+    reviewer = factory.SubFactory(StaffFactory)
+
+
+class AssignedWithRoleReviewersFactory(AssignedReviewersFactory):
+    role = factory.SubFactory(ReviewerRoleFactory)
 
 
 class InvitedToProposalFactory(ApplicationSubmissionFactory):
diff --git a/opentech/apply/funds/tests/test_forms.py b/opentech/apply/funds/tests/test_forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf85c793df9bda67dae919d8d8a312f242936fbe
--- /dev/null
+++ b/opentech/apply/funds/tests/test_forms.py
@@ -0,0 +1,115 @@
+from django.test import TestCase
+from opentech.apply.funds.tests.factories import (
+    ApplicationSubmissionFactory,
+    AssignedWithRoleReviewersFactory,
+    AssignedReviewersFactory,
+    InvitedToProposalFactory,
+    ReviewerRoleFactory,
+)
+from opentech.apply.review.tests.factories import ReviewFactory
+from opentech.apply.users.tests.factories import (
+    ReviewerFactory,
+    StaffFactory,
+)
+
+
+from opentech.apply.funds.forms import UpdateReviewersForm
+
+
+class TestReviewerFormQueries(TestCase):
+    def test_queries_init_and_render(self):
+        user = StaffFactory()
+
+        ReviewerRoleFactory.create_batch(3)
+
+        StaffFactory.create_batch(3)
+        ReviewerFactory.create_batch(3)
+
+        submission = InvitedToProposalFactory(lead=user, workflow_stages=2)
+
+        # Reviewers
+        # Assigned Reviewers
+        # Roles
+        with self.assertNumQueries(3):
+            form = UpdateReviewersForm(user=user, instance=submission)
+
+        # 3 x Staff - 1 per Role
+        # 1 x reviewers queryset
+        # 1 x submission reviewers
+        with self.assertNumQueries(5):
+            form.as_p()
+
+    def test_queries_roles_swap(self):
+        user = StaffFactory()
+        submission = ApplicationSubmissionFactory()
+
+        staff = StaffFactory.create_batch(4)
+        roles = ReviewerRoleFactory.create_batch(2)
+
+        form = UpdateReviewersForm(user=user, instance=submission)
+
+        AssignedWithRoleReviewersFactory(role=roles[0], submission=submission, reviewer=staff[0])
+        AssignedWithRoleReviewersFactory(role=roles[1], submission=submission, reviewer=staff[1])
+
+        data = {}
+        for field, user in zip(form.fields, staff):
+            if field.startswith('role'):
+                data[field] = user.id
+            else:
+                data[field] = None
+
+        form = UpdateReviewersForm(data, user=user, instance=submission)
+
+        self.assertTrue(form.is_valid())
+
+        # 1 - Submission
+        # 8 - 1 per role (2 savepoint, 1 get, 1 update)
+        # 1 - check - orphaned
+        with self.assertNumQueries(10):
+            form.save()
+
+    def test_queries_reviewers_swap(self):
+        user = StaffFactory()
+        submission = InvitedToProposalFactory(lead=user)
+
+        reviewers = ReviewerFactory.create_batch(4)
+
+        AssignedReviewersFactory(submission=submission, reviewer=reviewers[0])
+        AssignedReviewersFactory(submission=submission, reviewer=reviewers[1])
+
+        data = {'reviewer_reviewers': [reviewer.id for reviewer in reviewers[2:]]}
+
+        form = UpdateReviewersForm(data, user=user, instance=submission)
+
+        self.assertTrue(form.is_valid())
+
+        # 1 - Submission
+        # 1 - Delete old
+        # 1 - Cache existing
+        # 1 - Add new
+        # 1 - check - orphaned
+        with self.assertNumQueries(5):
+            form.save()
+
+    def test_queries_existing_reviews(self):
+        user = StaffFactory()
+        submission = InvitedToProposalFactory(lead=user)
+
+        reviewers = ReviewerFactory.create_batch(4)
+
+        ReviewFactory(submission=submission, author=reviewers[0])
+        ReviewFactory(submission=submission, author=reviewers[1])
+
+        data = {'reviewer_reviewers': [reviewer.id for reviewer in reviewers[2:]]}
+
+        form = UpdateReviewersForm(data, user=user, instance=submission)
+
+        self.assertTrue(form.is_valid())
+
+        # 1 - Submission
+        # 1 - Delete old
+        # 1 - Cache existing
+        # 1 - Add new
+        # 1 - check - orphaned
+        with self.assertNumQueries(5):
+            form.save()
diff --git a/opentech/apply/funds/tests/test_views.py b/opentech/apply/funds/tests/test_views.py
index 44b1f6f3d6450f995316d3b1d082c6d09aa63177..2eee0123a4632af75ee2309c5faebb6c45e5f2d4 100644
--- a/opentech/apply/funds/tests/test_views.py
+++ b/opentech/apply/funds/tests/test_views.py
@@ -1,17 +1,23 @@
 from datetime import datetime, timedelta
 import json
 
+from django.test import TestCase
+
 from opentech.apply.activity.models import Activity, INTERNAL
 from opentech.apply.determinations.tests.factories import DeterminationFactory
 from opentech.apply.funds.tests.factories import (
     ApplicationSubmissionFactory,
     ApplicationRevisionFactory,
+    AssignedWithRoleReviewersFactory,
+    AssignedReviewersFactory,
     InvitedToProposalFactory,
     LabSubmissionFactory,
+    ReviewerRoleFactory,
     ScreeningStatusFactory,
     SealedRoundFactory,
     SealedSubmissionFactory,
 )
+from opentech.apply.review.tests.factories import ReviewFactory
 from opentech.apply.stream_forms.testing.factories import flatten_for_form
 from opentech.apply.users.tests.factories import (
     ReviewerFactory,
@@ -200,36 +206,37 @@ class TestReviewersUpdateView(BaseSubmissionViewTestCase):
         super().setUpTestData()
         cls.staff = StaffFactory.create_batch(4)
         cls.reviewers = ReviewerFactory.create_batch(4)
+        cls.roles = ReviewerRoleFactory.create_batch(2)
 
-    def post_form(self, submission, staff=list(), reviewers=list()):
-        return self.post_page(submission, {
+    def post_form(self, submission, reviewer_roles=list(), reviewers=list()):
+        data = {
             'form-submitted-reviewer_form': '',
-            'staff_reviewers': [s.id for s in staff],
             'reviewer_reviewers': [r.id for r in reviewers]
-        })
+        }
+        data.update(
+            **{
+                f'role_reviewer_{str(role)}': reviewer.id
+                for role, reviewer in zip(self.roles, reviewer_roles)
+            }
+        )
+        return self.post_page(submission, data)
 
     def test_lead_can_add_staff_single(self):
         submission = ApplicationSubmissionFactory(lead=self.user)
 
-        self.post_form(submission, self.staff)
-
-        self.assertCountEqual(submission.reviewers.all(), self.staff)
+        self.post_form(submission, reviewer_roles=[self.staff[0]])
 
-    def test_lead_can_remove_staff_single(self):
-        submission = ApplicationSubmissionFactory(lead=self.user, reviewers=self.staff)
-        self.assertCountEqual(submission.reviewers.all(), self.staff)
-
-        self.post_form(submission, [])
-
-        self.assertCountEqual(submission.reviewers.all(), [])
+        self.assertCountEqual(submission.reviewers.all(), [self.staff[0]])
 
-    def test_lead_can_remove_some_staff(self):
-        submission = ApplicationSubmissionFactory(lead=self.user, reviewers=self.staff)
-        self.assertCountEqual(submission.reviewers.all(), self.staff)
+    def test_lead_can_change_staff_single(self):
+        submission = ApplicationSubmissionFactory(lead=self.user)
+        AssignedWithRoleReviewersFactory(role=self.roles[0], submission=submission, reviewer=self.staff[0])
+        self.assertCountEqual(submission.reviewers.all(), [self.staff[0]])
 
-        self.post_form(submission, self.staff[0:2])
+        self.post_form(submission, reviewer_roles=[self.staff[1]])
 
-        self.assertCountEqual(submission.reviewers.all(), self.staff[0:2])
+        self.assertCountEqual(submission.reviewers.all(), [self.staff[1]])
+        self.assertEqual(submission.assigned.with_roles().first().reviewer, self.staff[1])
 
     def test_lead_cant_add_reviewers_single(self):
         submission = ApplicationSubmissionFactory(lead=self.user)
@@ -238,43 +245,28 @@ class TestReviewersUpdateView(BaseSubmissionViewTestCase):
 
         self.assertCountEqual(submission.reviewers.all(), [])
 
-    def test_lead_can_add_staff_and_reviewers_for_proposal(self):
+    def test_lead_can_add_reviewers_for_proposal(self):
         submission = InvitedToProposalFactory(lead=self.user)
 
-        self.post_form(submission, self.staff, self.reviewers)
+        self.post_form(submission, reviewers=self.reviewers)
 
-        self.assertCountEqual(submission.reviewers.all(), self.staff + self.reviewers)
+        self.assertCountEqual(submission.reviewers.all(), self.reviewers)
 
-    def test_lead_can_remove_staff_and_reviewers_for_proposal(self):
-        submission = InvitedToProposalFactory(lead=self.user, reviewers=self.staff + self.reviewers)
-        self.assertCountEqual(submission.reviewers.all(), self.staff + self.reviewers)
+    def test_lead_can_remove_reviewers_for_proposal(self):
+        submission = InvitedToProposalFactory(lead=self.user, reviewers=self.reviewers)
+        self.assertCountEqual(submission.reviewers.all(), self.reviewers)
 
         self.post_form(submission)
 
         self.assertCountEqual(submission.reviewers.all(), [])
 
-    def test_lead_can_remove_some_staff_and_reviewers_for_proposal(self):
-        submission = InvitedToProposalFactory(lead=self.user, reviewers=self.staff + self.reviewers)
-        self.assertCountEqual(submission.reviewers.all(), self.staff + self.reviewers)
-
-        self.post_form(submission, self.staff[0:2], self.reviewers[0:2])
-
-        self.assertCountEqual(submission.reviewers.all(), self.staff[0:2] + self.reviewers[0:2])
-
-    def test_staff_can_add_staff_single(self):
-        submission = ApplicationSubmissionFactory()
-
-        self.post_form(submission, self.staff)
-
-        self.assertCountEqual(submission.reviewers.all(), self.staff)
-
-    def test_staff_can_remove_staff_single(self):
-        submission = ApplicationSubmissionFactory(reviewers=self.staff)
-        self.assertCountEqual(submission.reviewers.all(), self.staff)
+    def test_lead_can_remove_some_reviewers_for_proposal(self):
+        submission = InvitedToProposalFactory(lead=self.user, reviewers=self.reviewers)
+        self.assertCountEqual(submission.reviewers.all(), self.reviewers)
 
-        self.post_form(submission, [])
+        self.post_form(submission, reviewers=self.reviewers[0:2])
 
-        self.assertCountEqual(submission.reviewers.all(), [])
+        self.assertCountEqual(submission.reviewers.all(), self.reviewers[0:2])
 
     def test_staff_cant_add_reviewers_proposal(self):
         submission = ApplicationSubmissionFactory()
@@ -291,6 +283,77 @@ class TestReviewersUpdateView(BaseSubmissionViewTestCase):
 
         self.assertCountEqual(submission.reviewers.all(), self.reviewers)
 
+    def test_lead_can_change_role_reviewer_and_review_remains(self):
+        submission = ApplicationSubmissionFactory()
+        AssignedWithRoleReviewersFactory(role=self.roles[0], submission=submission, reviewer=self.staff[0])
+
+        # Add a review from that staff reviewer
+        ReviewFactory(submission=submission, author=self.staff[0])
+
+        # Assign a different reviewer to the same role
+        self.post_form(submission, reviewer_roles=[self.staff[1]])
+
+        # Make sure that the ex-role-reviewer is still assigned record
+        self.assertCountEqual(submission.reviewers.all(), self.staff[0:2])
+
+    def test_can_remove_external_reviewer_and_review_remains(self):
+        submission = ApplicationSubmissionFactory(lead=self.user)
+        reviewer = self.reviewers[0]
+        AssignedReviewersFactory(submission=submission, reviewer=reviewer)
+        ReviewFactory(submission=submission, author=reviewer)
+
+        # Assign a different reviewer to the same role
+        self.post_form(submission, reviewers=[])
+
+        self.assertCountEqual(submission.reviewers.all(), [reviewer])
+
+
+class TestPostSaveOnReview(TestCase):
+    def test_external_reviewer_exists_after_review(self):
+        reviewer = ReviewerFactory()
+        submission = ApplicationSubmissionFactory()
+
+        # Add a review from a new reviewer who isn't assigned
+        ReviewFactory(submission=submission, author=reviewer)
+
+        self.assertCountEqual(submission.reviewers.all(), [reviewer])
+
+    def test_assigned_external_reviewer_exists_after_review(self):
+        reviewer = ReviewerFactory()
+        submission = ApplicationSubmissionFactory()
+
+        # Add a review from a new reviewer who is assigned
+        AssignedReviewersFactory(submission=submission, reviewer=reviewer)
+        ReviewFactory(submission=submission, author=reviewer)
+
+        self.assertCountEqual(submission.reviewers.all(), [reviewer])
+
+    def test_staff_reviewer_exists_after_submitting_review(self):
+        staff = StaffFactory()
+        submission = ApplicationSubmissionFactory()
+
+        ReviewFactory(submission=submission, author=staff)
+
+        self.assertCountEqual(submission.reviewers.all(), [staff])
+
+    def test_assigned_staff_reviewer_exists_after_review(self):
+        staff = StaffFactory()
+        submission = ApplicationSubmissionFactory()
+
+        AssignedReviewersFactory(submission=submission, reviewer=staff)
+        ReviewFactory(submission=submission, author=staff)
+
+        self.assertCountEqual(submission.reviewers.all(), [staff])
+
+    def test_role_reviewer_exists_after_review(self):
+        staff = StaffFactory()
+        submission = ApplicationSubmissionFactory()
+
+        AssignedWithRoleReviewersFactory(submission=submission, reviewer=staff)
+        ReviewFactory(submission=submission, author=staff)
+
+        self.assertCountEqual(submission.reviewers.all(), [staff])
+
 
 class TestApplicantSubmissionView(BaseSubmissionViewTestCase):
     user_factory = UserFactory
diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py
index 44145ee6f97929c79886b7a4f27bdb12ee400ba3..214436f296f078add7c38b0c598bfef010394c5f 100644
--- a/opentech/apply/funds/views.py
+++ b/opentech/apply/funds/views.py
@@ -38,7 +38,13 @@ from .forms import (
     UpdateReviewersForm,
     UpdateSubmissionLeadForm,
 )
-from .models import ApplicationSubmission, ApplicationRevision, RoundsAndLabs, RoundBase, LabBase
+from .models import (
+    ApplicationSubmission,
+    ApplicationRevision,
+    RoundsAndLabs,
+    RoundBase,
+    LabBase
+)
 from .tables import (
     AdminSubmissionsTable,
     ReviewerSubmissionsTable,
@@ -291,10 +297,13 @@ class UpdateReviewersView(DelegatedViewMixin, UpdateView):
     context_name = 'reviewer_form'
 
     def form_valid(self, form):
-        old_reviewers = set(self.get_object().reviewers.all())
+        old_reviewers = set(
+            copy(reviewer)
+            for reviewer in form.instance.assigned.all()
+        )
         response = super().form_valid(form)
-        new_reviewers = set(form.instance.reviewers.all())
 
+        new_reviewers = set(form.instance.assigned.all())
         added = new_reviewers - old_reviewers
         removed = old_reviewers - new_reviewers
 
diff --git a/opentech/apply/funds/widgets.py b/opentech/apply/funds/widgets.py
index d7ac9d879f2ea91801d78b718bd2e9874071f787..dbaf8ba228462006ed3bfa4080dfa8d9a69b03e9 100644
--- a/opentech/apply/funds/widgets.py
+++ b/opentech/apply/funds/widgets.py
@@ -1,6 +1,6 @@
 from django.contrib.staticfiles.templatetags.staticfiles import static
 
-from django_select2.forms import Select2MultipleWidget
+from django_select2.forms import Select2Widget, Select2MultipleWidget
 
 
 class Select2MultiCheckboxesWidget(Select2MultipleWidget):
@@ -20,3 +20,13 @@ class Select2MultiCheckboxesWidget(Select2MultipleWidget):
         attrs = super().build_attrs(*args, **kwargs)
         attrs['class'] = attrs['class'].replace('django-select2', 'django-select2-checkboxes')
         return attrs
+
+
+class Select2IconWidget(Select2Widget):
+    template_name = 'funds/widgets/icon_select2.html'
+
+    def __init__(self, *args, **kwargs):
+        attrs = kwargs.get('attrs', {})
+        attrs.setdefault('icon', '')
+        kwargs['attrs'] = attrs
+        super().__init__(*args, **kwargs)
diff --git a/opentech/apply/review/models.py b/opentech/apply/review/models.py
index 75cfa7c1fad1fd8419eab33dbf4d45997db14aba..e4103ae65d9b6325e091ed26f4e9c9036ad34769 100644
--- a/opentech/apply/review/models.py
+++ b/opentech/apply/review/models.py
@@ -160,8 +160,12 @@ class Review(ReviewFormFieldsMixin, BaseStreamForm, AccessFormData, models.Model
 
 @receiver(post_save, sender=Review)
 def update_submission_reviewers_list(sender, **kwargs):
+    from opentech.apply.funds.models import AssignedReviewers
+
     review = kwargs.get('instance')
 
     # Make sure the reviewer is in the reviewers list on the submission
-    if not review.submission.reviewers.filter(id=review.author.id).exists():
-        review.submission.reviewers.add(review.author)
+    AssignedReviewers.objects.get_or_create(
+        submission=review.submission,
+        reviewer=review.author,
+    )
diff --git a/opentech/static_src/src/javascript/apply/fancybox-global.js b/opentech/static_src/src/javascript/apply/fancybox-global.js
index 616c78f49aa0bb3ea7e7990687c7bb680babe55e..2e61578bcfbe2a5cb28124a8c0405c98bbbfcdff 100644
--- a/opentech/static_src/src/javascript/apply/fancybox-global.js
+++ b/opentech/static_src/src/javascript/apply/fancybox-global.js
@@ -18,4 +18,16 @@
         $('.django-select2-checkboxes').select2('close');
     });
 
+    $(document).ready(
+        $('.modal').each((idx, element) => {
+            var modal = $(element);
+            var error = modal.has('.errorlist');
+            if (error.length) {
+                const modalID = modal.attr('id');
+                const buttonTrigger = $(`[data-src="#${modalID}"]`);
+                buttonTrigger[0].click();
+            }
+        })
+    );
+
 })(jQuery);