diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py
index 4ee98bb81eebc2fb1032f8276cac7e6f0340d78c..36c6f74c604ae7be5ef477fe39594a796bef050a 100644
--- a/opentech/apply/activity/messaging.py
+++ b/opentech/apply/activity/messaging.py
@@ -44,6 +44,7 @@ neat_related = {
     MESSAGES.UPDATE_LEAD: 'old_lead',
     MESSAGES.NEW_REVIEW: 'review',
     MESSAGES.TRANSITION: 'old_phase',
+    MESSAGES.BATCH_TRANSITION: 'transitions',
     MESSAGES.APPLICANT_EDIT: 'revision',
     MESSAGES.EDIT: 'revision',
     MESSAGES.COMMENT: 'comment',
@@ -182,6 +183,7 @@ class ActivityAdapter(AdapterBase):
     always_send = True
     messages = {
         MESSAGES.TRANSITION: 'handle_transition',
+        MESSAGES.BATCH_TRANSITION: 'handle_batch_transition',
         MESSAGES.NEW_SUBMISSION: 'Submitted {submission.title} for {submission.page.title}',
         MESSAGES.EDIT: 'Edited',
         MESSAGES.APPLICANT_EDIT: 'Edited',
@@ -249,6 +251,12 @@ class ActivityAdapter(AdapterBase):
 
         return staff_message
 
+    def handle_batch_transition(self, transitions, submissions, **kwargs):
+        kwargs.pop('submission')
+        for submission in submissions:
+            old_phase = transitions[submission.phase]
+            return self.handle_transition(old_phase=old_phase, submission=submission, **kwargs)
+
     def send_message(self, message, user, submission, submissions, **kwargs):
         from .models import Activity, PUBLIC
         visibility = kwargs.get('visibility', PUBLIC)
diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py
index 35aa1e64454ab3a0918c60c986792a4f4dc4dec0..f7b71357666a5226533f85fb507408a85269c38e 100644
--- a/opentech/apply/activity/options.py
+++ b/opentech/apply/activity/options.py
@@ -8,6 +8,7 @@ class MESSAGES(Enum):
     NEW_SUBMISSION = 'New Submission'
     SCREENING = 'Screening'
     TRANSITION = 'Transition'
+    BATCH_TRANSITION = 'Batch Transition'
     DETERMINATION_OUTCOME = 'Determination Outcome'
     INVITED_TO_PROPOSAL = 'Invited To Proposal'
     REVIEWERS_UPDATED = 'Reviewers Updated'
diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py
index 6f406b0594d7fe62ff6b9d1d80ad169749145fd1..8754ecc5e5531b9c458e3eabb05d7d434fa54d5e 100644
--- a/opentech/apply/funds/views.py
+++ b/opentech/apply/funds/views.py
@@ -136,20 +136,37 @@ class BatchProgressSubmissionView(DelegatedViewMixin, FormView):
         transitions = form.cleaned_data.get('action')
 
         failed = []
+        phase_changes = {}
         for submission in submissions:
             valid_actions = {action for action, _ in submission.get_actions_for_user(self.request.user)}
-            transition = (valid_actions & set(transitions)).pop()
             try:
-                submission.perform_transition(transition, self.request.user, request=self.request)
-            except PermissionDenied:
+                transition = (valid_actions & set(transitions)).pop()
+                submission.perform_transition(
+                    transition,
+                    self.request.user,
+                    request=self.request,
+                    notify=False,
+                )
+            except (PermissionDenied, KeyError):
                 failed.append(submission)
+            else:
+                phase_changes[submission.phase] = transitions[transition]
 
         if failed:
             messages.warning(
                 self.request,
-                _('You do no have permission to do that to: ') +
+                _('Failed to update: ') +
                 ', '.join(str(submission) for submission in failed)
             )
+
+        messenger(
+            MESSAGES.BATCH_TRANSITION,
+            user=self.request.user,
+            request=self.request,
+            submissions=submissions.exclude(id__in=[submission.id for submission in failed]),
+            related=phase_changes,
+        )
+
         return super().form_valid(form)
 
 
diff --git a/opentech/apply/funds/workflow.py b/opentech/apply/funds/workflow.py
index 23deb6fd30a1ccb5d5f7aa4e3fe5764ee4a13d2e..b71ebe9a13026e266c8e2600486b9b87cc6d4dfb 100644
--- a/opentech/apply/funds/workflow.py
+++ b/opentech/apply/funds/workflow.py
@@ -735,14 +735,13 @@ def get_determination_transitions():
 
 def get_actions_mapping():
     # Maps action names to the phase they originate from
-    transitions = defaultdict(lambda: {'display': '', 'phases': []})
+    transitions = defaultdict(lambda: {'display': '', 'transitions': {}})
     for phase_name, phase in PHASES:
         for transition_name, transition in phase.transitions.items():
             transition_display = transition['display']
             transition_key = slugify(transition_display)
-            transitions[transition_key].setdefault('transitions', []).append(transition_name)
+            transitions[transition_key]['transitions'][transition_name] = phase
             transitions[transition_key]['display'] = transition_display
-            transitions[transition_key]['phases'].append(phase_name)
 
     return transitions