diff --git a/opentech/apply/activity/__init__.py b/opentech/apply/activity/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/opentech/apply/activity/apps.py b/opentech/apply/activity/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..34cfcc035c5ba7b644b8d40ef0f61bd953171b31
--- /dev/null
+++ b/opentech/apply/activity/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class ActivityConfig(AppConfig):
+    name = 'activity'
diff --git a/opentech/apply/activity/forms.py b/opentech/apply/activity/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..1931ffde3a71848d774ef2b3251679a640e5eea1
--- /dev/null
+++ b/opentech/apply/activity/forms.py
@@ -0,0 +1,9 @@
+from django import forms
+
+from .models import Activity
+
+
+class CommentForm(forms.ModelForm):
+    class Meta:
+        model = Activity
+        fields = ('message',)
diff --git a/opentech/apply/activity/migrations/0001_initial.py b/opentech/apply/activity/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..b5e7d8c3be47f7ad5afd532ae439e69b16fb976d
--- /dev/null
+++ b/opentech/apply/activity/migrations/0001_initial.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.8 on 2018-02-28 11:03
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('funds', '0025_update_with_file_blocks'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Activity',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('timestamp', models.DateTimeField(auto_now_add=True)),
+                ('message', models.TextField()),
+                ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='funds.ApplicationSubmission', related_name='activities')),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['-timestamp'],
+            },
+        ),
+    ]
diff --git a/opentech/apply/activity/migrations/0002_activity_type.py b/opentech/apply/activity/migrations/0002_activity_type.py
new file mode 100644
index 0000000000000000000000000000000000000000..bc7ca42a580b691ee57c12663478579882542ebf
--- /dev/null
+++ b/opentech/apply/activity/migrations/0002_activity_type.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.8 on 2018-03-01 15:49
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('activity', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='activity',
+            name='type',
+            field=models.CharField(choices=[('comment', 'Comment'), ('action', 'Action')], default='comment', max_length=30),
+            preserve_default=False,
+        ),
+    ]
diff --git a/opentech/apply/activity/migrations/__init__.py b/opentech/apply/activity/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/opentech/apply/activity/models.py b/opentech/apply/activity/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae0acc1145982ecaa2a44d76c5c9a88153b17a2b
--- /dev/null
+++ b/opentech/apply/activity/models.py
@@ -0,0 +1,45 @@
+from django.conf import settings
+from django.db import models
+
+COMMENT = 'comment'
+ACTION = 'action'
+
+ACTIVITY_TYPES = {
+    COMMENT: 'Comment',
+    ACTION: 'Action',
+}
+
+
+class ActivityBaseManager(models.Manager):
+    def create(self, **kwargs):
+        kwargs.update(type=self.type)
+        return super().create(**kwargs)
+
+    def get_queryset(self):
+        return super().get_queryset().filter(type=self.type)
+
+
+class CommentManger(ActivityBaseManager):
+    type = COMMENT
+
+
+class ActionManager(ActivityBaseManager):
+    type = ACTION
+
+
+class Activity(models.Model):
+    timestamp = models.DateTimeField(auto_now_add=True)
+    type = models.CharField(choices=ACTIVITY_TYPES.items(), max_length=30)
+    user = models.ForeignKey(settings.AUTH_USER_MODEL)
+    submission = models.ForeignKey('funds.ApplicationSubmission', related_name='activities')
+    message = models.TextField()
+
+    objects = models.Manager()
+    comments = CommentManger()
+    actions = ActionManager()
+
+    class Meta:
+        ordering = ['-timestamp']
+
+    def __str__(self):
+        return '{}: for "{}"'.format(self.get_type_display(), self.submission)
diff --git a/opentech/apply/activity/templates/activity/include/action_list.html b/opentech/apply/activity/templates/activity/include/action_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..ebd8bbaa11232a7312f180a2ec98681b9ec50958
--- /dev/null
+++ b/opentech/apply/activity/templates/activity/include/action_list.html
@@ -0,0 +1,3 @@
+{% for action in actions %}
+    {% include "activity/include/listing_base.html" with activity=action %}
+{% endfor %}
diff --git a/opentech/apply/activity/templates/activity/include/all_activity_list.html b/opentech/apply/activity/templates/activity/include/all_activity_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..312a0b3294fa6438dbb81bb3da4973e7151f152f
--- /dev/null
+++ b/opentech/apply/activity/templates/activity/include/all_activity_list.html
@@ -0,0 +1,3 @@
+{% for activity in all_activity %}
+    {% include "activity/include/listing_base.html" with activity=activity %}
+{% endfor %}
diff --git a/opentech/apply/activity/templates/activity/include/comment_form.html b/opentech/apply/activity/templates/activity/include/comment_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..c20300aec2bd4315b05c7d672885b34c9c9e602d
--- /dev/null
+++ b/opentech/apply/activity/templates/activity/include/comment_form.html
@@ -0,0 +1,5 @@
+<form method="post" id="comment-form">
+    {% csrf_token %}
+    {{ comment_form }}
+    <input id="comment-form-submit" name="form-submitted" type="submit" form="comment-form" value="Comment">
+</form>
diff --git a/opentech/apply/activity/templates/activity/include/comment_list.html b/opentech/apply/activity/templates/activity/include/comment_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..cfd58fa44049d4fd9544e96d4aa58ddb506e2de1
--- /dev/null
+++ b/opentech/apply/activity/templates/activity/include/comment_list.html
@@ -0,0 +1,3 @@
+{% for comment in comments %}
+    {% include "activity/include/listing_base.html" with activity=comment %}
+{% endfor %}
diff --git a/opentech/apply/activity/templates/activity/include/listing_base.html b/opentech/apply/activity/templates/activity/include/listing_base.html
new file mode 100644
index 0000000000000000000000000000000000000000..8d44760d741a3ec26f32fc3f51b9dc0ba634d1dc
--- /dev/null
+++ b/opentech/apply/activity/templates/activity/include/listing_base.html
@@ -0,0 +1,5 @@
+<div>
+    <p>{{ activity.timestamp }}</p>
+    <p>{{ activity.user }}</p>
+    <p>{{ activity.message }}</p>
+</div>
diff --git a/opentech/apply/activity/views.py b/opentech/apply/activity/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..c680bda9d570f291388d6425936e9c4182aa7fc9
--- /dev/null
+++ b/opentech/apply/activity/views.py
@@ -0,0 +1,63 @@
+from django.views.generic import CreateView, View
+
+from .forms import CommentForm
+from .models import Activity, COMMENT
+
+
+ACTIVITY_LIMIT = 50
+
+
+class AllActivityContextMixin:
+    def get_context_data(self, **kwargs):
+        extra = {
+            'actions': Activity.actions.filter(submission__in=self.object_list)[:ACTIVITY_LIMIT],
+            'comments': Activity.comments.filter(submission__in=self.object_list[:ACTIVITY_LIMIT]),
+            'all_activity': Activity.objects.filter(submission__in=self.object_list)[:ACTIVITY_LIMIT],
+        }
+        return super().get_context_data(**extra, **kwargs)
+
+
+class ActivityContextMixin:
+    def get_context_data(self, **kwargs):
+        extra = {
+            'actions': Activity.actions.filter(submission=self.object),
+            'comments': Activity.comments.filter(submission=self.object),
+        }
+
+        return super().get_context_data(**extra, **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_context_data(self, **kwargs):
+        # Use the previous context but override the validated form
+        form = kwargs.pop('form')
+        kwargs.update(self.kwargs['context'])
+        kwargs.update(**{self.context_name: form})
+        return super().get_context_data(**kwargs)
+
+    @classmethod
+    def contribute_form(cls, submission):
+        return cls.context_name, cls.form_class(instance=submission)
+
+
+class CommentFormView(DelegatedViewMixin, CreateView):
+    form_class = CommentForm
+    context_name = 'comment_form'
+
+    def form_valid(self, form):
+        form.instance.user = self.request.user
+        form.instance.submission = self.kwargs['submission']
+        form.instance.type = COMMENT
+        return super().form_valid(form)
+
+    def get_success_url(self):
+        return self.object.submission.get_absolute_url()
+
+    @classmethod
+    def contribute_form(cls, submission):
+        # We dont want to pass the submission as the instance
+        return super().contribute_form(None)
diff --git a/opentech/apply/funds/admin_forms.py b/opentech/apply/funds/admin_forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..643ab0d9012941d53390c04c154df894918814d5
--- /dev/null
+++ b/opentech/apply/funds/admin_forms.py
@@ -0,0 +1,34 @@
+from wagtail.wagtailadmin.forms import WagtailAdminPageForm
+
+
+class WorkflowFormAdminForm(WagtailAdminPageForm):
+    def clean(self):
+        cleaned_data = super().clean()
+        model = self._meta.model
+
+        workflow = model.workflow_class_from_name(cleaned_data['workflow_name'])
+        application_forms = self.formsets['forms']
+
+        self.validate_stages_equal_forms(workflow, application_forms)
+
+        return cleaned_data
+
+    def validate_stages_equal_forms(self, workflow, application_forms):
+        if application_forms.is_valid():
+            valid_forms = [form for form in application_forms if not form.cleaned_data['DELETE']]
+            number_of_forms = len(valid_forms)
+            plural_form = 's' if number_of_forms > 1 else ''
+
+            number_of_stages = len(workflow.stage_classes)
+            plural_stage = 's' if number_of_stages > 1 else ''
+
+            if number_of_forms != number_of_stages:
+                self.add_error(
+                    None,
+                    'Number of forms does not match number of stages: '
+                    f'{number_of_stages} stage{plural_stage} and {number_of_forms} '
+                    f'form{plural_form} provided',
+                )
+
+                for form in valid_forms[number_of_stages:]:
+                    form.add_error('form', 'Exceeds required number of forms for stage, please remove.')
diff --git a/opentech/apply/funds/forms.py b/opentech/apply/funds/forms.py
index 643ab0d9012941d53390c04c154df894918814d5..0de2d45500fde063fcfaf1c43aafa2be0ee3bf2b 100644
--- a/opentech/apply/funds/forms.py
+++ b/opentech/apply/funds/forms.py
@@ -1,34 +1,36 @@
-from wagtail.wagtailadmin.forms import WagtailAdminPageForm
+from django import forms
 
+from .models import ApplicationSubmission
 
-class WorkflowFormAdminForm(WagtailAdminPageForm):
-    def clean(self):
-        cleaned_data = super().clean()
-        model = self._meta.model
 
-        workflow = model.workflow_class_from_name(cleaned_data['workflow_name'])
-        application_forms = self.formsets['forms']
+class ProgressSubmissionForm(forms.ModelForm):
+    action = forms.ChoiceField()
 
-        self.validate_stages_equal_forms(workflow, application_forms)
+    class Meta:
+        model = ApplicationSubmission
+        fields: list = []
 
-        return cleaned_data
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        choices = [(action, action) for action in self.instance.phase.action_names]
+        action_field = self.fields['action']
+        action_field.choices = choices
+        action_field.label = f'Current status: {self.instance.phase.name}'
+        self.should_show = bool(choices)
 
-    def validate_stages_equal_forms(self, workflow, application_forms):
-        if application_forms.is_valid():
-            valid_forms = [form for form in application_forms if not form.cleaned_data['DELETE']]
-            number_of_forms = len(valid_forms)
-            plural_form = 's' if number_of_forms > 1 else ''
+    def save(self, *args, **kwargs):
+        new_phase = self.instance.workflow.process(self.instance.phase, self.cleaned_data['action'])
+        self.instance.status = str(new_phase)
+        return super().save(*args, **kwargs)
 
-            number_of_stages = len(workflow.stage_classes)
-            plural_stage = 's' if number_of_stages > 1 else ''
 
-            if number_of_forms != number_of_stages:
-                self.add_error(
-                    None,
-                    'Number of forms does not match number of stages: '
-                    f'{number_of_stages} stage{plural_stage} and {number_of_forms} '
-                    f'form{plural_form} provided',
-                )
+class UpdateSubmissionLeadForm(forms.ModelForm):
+    class Meta:
+        model = ApplicationSubmission
+        fields = ('lead',)
 
-                for form in valid_forms[number_of_stages:]:
-                    form.add_error('form', 'Exceeds required number of forms for stage, please remove.')
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        lead_field = self.fields['lead']
+        lead_field.label = f'Update lead from { self.instance.lead } to'
+        lead_field.queryset = lead_field.queryset.exclude(id=self.instance.lead.id)
diff --git a/opentech/apply/funds/migrations/0026_add_leads_to_submission_and_lab.py b/opentech/apply/funds/migrations/0026_add_leads_to_submission_and_lab.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d68ac6179b435e8d6a2187c814cb6e72bed0d53
--- /dev/null
+++ b/opentech/apply/funds/migrations/0026_add_leads_to_submission_and_lab.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.8 on 2018-03-02 17:08
+from __future__ import unicode_literals
+
+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', '0025_update_with_file_blocks'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='applicationsubmission',
+            name='lead',
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='submission_lead', to=settings.AUTH_USER_MODEL),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='labtype',
+            name='lead',
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='lab_lead', to=settings.AUTH_USER_MODEL),
+            preserve_default=False,
+        ),
+    ]
diff --git a/opentech/apply/funds/models.py b/opentech/apply/funds/models.py
index 5e67acccff3944518b1a0f3685e38413e800f771..c36510831779e8db5128b3da927b7a8b58fbf82e 100644
--- a/opentech/apply/funds/models.py
+++ b/opentech/apply/funds/models.py
@@ -38,7 +38,7 @@ from opentech.apply.users.groups import STAFF_GROUP_NAME
 
 from .blocks import CustomFormFieldsBlock, MustIncludeFieldBlock, REQUIRED_BLOCK_NAMES
 from .edit_handlers import FilteredFieldPanel, ReadOnlyPanel, ReadOnlyInlinePanel
-from .forms import WorkflowFormAdminForm
+from .admin_forms import WorkflowFormAdminForm
 from .workflow import SingleStage, DoubleStage
 
 
@@ -382,11 +382,17 @@ class LabType(EmailForm, WorkflowStreamForm, SubmittableStreamForm):  # type: ig
     class Meta:
         verbose_name = _("Lab")
 
+    lead = models.ForeignKey(settings.AUTH_USER_MODEL, limit_choices_to={'groups__name': STAFF_GROUP_NAME}, related_name='lab_lead')
+
     parent_page_types = ['apply_home.ApplyHomePage']
     subpage_types = []  # type: ignore
 
+    content_panels = WorkflowStreamForm.content_panels + [
+        FieldPanel('lead')
+    ]
+
     edit_handler = TabbedInterface([
-        ObjectList(WorkflowStreamForm.content_panels, heading='Content'),
+        ObjectList(content_panels, heading='Content'),
         EmailForm.email_tab,
         ObjectList(WorkflowStreamForm.promote_panels, heading='Promote'),
     ])
@@ -434,6 +440,7 @@ class ApplicationSubmission(WorkflowHelpers, AbstractFormSubmission):
     form_fields = StreamField(CustomFormFieldsBlock())
     page = models.ForeignKey('wagtailcore.Page', on_delete=models.PROTECT)
     round = models.ForeignKey('wagtailcore.Page', on_delete=models.PROTECT, related_name='submissions', null=True)
+    lead = models.ForeignKey(settings.AUTH_USER_MODEL, limit_choices_to={'groups__name': STAFF_GROUP_NAME}, related_name='submission_lead')
     user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
     search_data = models.TextField()
 
@@ -515,6 +522,12 @@ class ApplicationSubmission(WorkflowHelpers, AbstractFormSubmission):
                 self.workflow_name = self.page.workflow_name
             self.status = str(self.workflow.first())
 
+            try:
+                self.lead = self.round.specific.lead
+            except AttributeError:
+                # Its a lab
+                self.lead = self.page.specific.lead
+
         # add a denormed version of the answer for searching
         self.search_data = ' '.join(self.prepare_search_values())
 
@@ -558,6 +571,9 @@ class ApplicationSubmission(WorkflowHelpers, AbstractFormSubmission):
 
         return form_data
 
+    def get_absolute_url(self):
+        return reverse('funds:submission', args=(self.id,))
+
     def __getattr__(self, item):
         # fall back to values defined on the data
         if item in REQUIRED_BLOCK_NAMES:
@@ -565,4 +581,7 @@ class ApplicationSubmission(WorkflowHelpers, AbstractFormSubmission):
         raise AttributeError('{} has no attribute "{}"'.format(repr(self), item))
 
     def __str__(self):
-        return str(super().__str__())
+        return f'{self.title} from {self.full_name} for {self.page.title}'
+
+    def __repr__(self):
+        return f'<{self.__class__.__name__}: {str(self.form_data)}>'
diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html
index fd5c00db4c56bff923bbd76dbfc07f4ae3e9032f..d6a2e2805a0d886ee8e88541807e7b2cf971073e 100644
--- a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html
+++ b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html
@@ -9,9 +9,9 @@
             <span>{{ object.stage }}</span>
             <span>{{ object.page }}</span>
             <span>{{ object.round }}</span>
-            <span>Lead: {{ object.round.specific.lead }}</span>
+            <span>Lead: {{ object.lead }}</span>
         </h5>
-        {% include "funds/includes/status_bar.html" with workflow=object.workflow status=object.status %}
+        {% include "funds/includes/status_bar.html" with workflow=object.workflow status=object.phase %}
     </div>
 </div>
 
@@ -50,6 +50,8 @@
         </div>
 
         <aside class="sidebar">
+            {% include "funds/includes/progress_form.html" %}
+            {% include "funds/includes/update_lead_form.html" %}
             {% if other_submissions %}
                 <div class="sidebar__inner">
                 <h6 class="heading heading--light-grey heading--small heading--uppercase">Past Submissions</h6>
@@ -59,6 +61,9 @@
                 {% endfor %}
                 </div>
             {% endif %}
+            {% include "activity/include/comment_form.html" %}
+            {% include "activity/include/comment_list.html" %}
+            {% include "activity/include/action_list.html" %}
         </aside>
     </div>
 </div>
diff --git a/opentech/apply/funds/templates/funds/includes/progress_form.html b/opentech/apply/funds/templates/funds/includes/progress_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..8d4b54481406eb93bf14c6ba74062824fbea05c7
--- /dev/null
+++ b/opentech/apply/funds/templates/funds/includes/progress_form.html
@@ -0,0 +1,7 @@
+{% if progress_form.should_show %}
+<form method="post" id="progress-form">
+    {% csrf_token %}
+    {{ progress_form }}
+    <input id="progress-form-submit" name="form-submitted" type="submit" form="progress-form" value="Progress">
+</form>
+{% endif %}
diff --git a/opentech/apply/funds/templates/funds/includes/status_bar.html b/opentech/apply/funds/templates/funds/includes/status_bar.html
index d99ed4c6819bdd39b36cc41f6cb3fa29f41c9bde..e537515025ce4cea59a39f9c96c8e192d1455c3d 100644
--- a/opentech/apply/funds/templates/funds/includes/status_bar.html
+++ b/opentech/apply/funds/templates/funds/includes/status_bar.html
@@ -1,17 +1,27 @@
 <div class="status-bar">
-    {# '.status-bar__item--is-complete' needs to be added to each 'complete' step #}
-    {# '.status-bar__item--is-current' needs to be added to the current step #}
-    {% for phase in workflow %}
-        <div class="status-bar__item {% if phase == status %}status-bar__item--is-current{% endif %}">
-            <span class="status-bar__tooltip" data-title="{{ phase.name }}" aria-label="{{ phase.name }}"></span>
+    {% for phase in status.stage %}
+        <div class="status-bar__item
+                    {% if phase == status %}
+                        status-bar__item--is-current
+                    {% elif phase.step < status.step %}
+                        status-bar__item--is-complete
+                    {% endif %}">
+            <span class="status-bar__tooltip"
+                {% if phase == status %}
+                    {# We want to display the status explicitly in case phase is a MultiStep (displays "Outcome" for name) #}
+                    data-title="{{ status.name }}" aria-label="{{ status.name }}"
+                {% else %}
+                    data-title="{{ phase.name }}" aria-label="{{ phase.name }}"
+                {% endif %}
+            ></span>
             <svg class="status-bar__icon"><use xlink:href="#tick-alt"></use></svg>
         </div>
     {% endfor %}
 </div>
 <div class="status-bar--mobile">
-    {% for phase in workflow %}
+    {% for phase in status.stage %}
         {% if phase == status %}
-            <h6 class="status-bar__subheading">{{ phase.name }}</h6>
+            <h6 class="status-bar__subheading">{{ status.name }}</h6>
         {% endif %}
     {% endfor %}
 </div>
diff --git a/opentech/apply/funds/templates/funds/includes/update_lead_form.html b/opentech/apply/funds/templates/funds/includes/update_lead_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..b6ad97907a33f678a11a2560a5927cfc96755aaf
--- /dev/null
+++ b/opentech/apply/funds/templates/funds/includes/update_lead_form.html
@@ -0,0 +1,5 @@
+<form method="post" id="update-lead-form">
+    {% csrf_token %}
+    {{ lead_form }}
+    <input id="update-form-submit" name="form-submitted" type="submit" form="update-lead-form" value="Update">
+</form>
diff --git a/opentech/apply/funds/templates/funds/submissions.html b/opentech/apply/funds/templates/funds/submissions.html
index 3428cc9a7d1cc291ff9c14e714f62d167a7d14c8..f039d269e2cac92256acb8b8ece3c1a753a740aa 100644
--- a/opentech/apply/funds/templates/funds/submissions.html
+++ b/opentech/apply/funds/templates/funds/submissions.html
@@ -39,6 +39,10 @@
     {% render_table table %}
 </div>
 
+{% include "activity/include/comment_list.html" %}
+{% include "activity/include/action_list.html" %}
+{% include "activity/include/all_activity_list.html" %}
+
 {% endblock %}
 
 {% block extra_js %}
diff --git a/opentech/apply/funds/tests/factories/models.py b/opentech/apply/funds/tests/factories/models.py
index 11253d317fef1b31999f806812a880d135b9652b..87d4ffcf551dea830299f5e1412c56edf4e3fd73 100644
--- a/opentech/apply/funds/tests/factories/models.py
+++ b/opentech/apply/funds/tests/factories/models.py
@@ -121,6 +121,7 @@ class LabFactory(wagtail_factories.PageFactory):
 
     # Will need to update how the stages are identified as Fund Page changes
     workflow_name = factory.LazyAttribute(lambda o: list(FundType.WORKFLOWS.keys())[o.workflow_stages - 1])
+    lead = factory.SubFactory(UserFactory, groups__name=STAFF_GROUP_NAME)
 
 
 class LabFormFactory(AbstractRelatedFormFactory):
diff --git a/opentech/apply/funds/tests/test_models.py b/opentech/apply/funds/tests/test_models.py
index 4f2e4517aa95ea00c6c56ef75b9149bc6ccae8d3..348486c45843031a19bce56e317cda2bfd73dd75 100644
--- a/opentech/apply/funds/tests/test_models.py
+++ b/opentech/apply/funds/tests/test_models.py
@@ -202,7 +202,7 @@ class TestFormSubmission(TestCase):
 
         self.round_page = RoundFactory(parent=fund)
         RoundFormFactory(round=self.round_page, form=form)
-        self.lab_page = LabFactory()
+        self.lab_page = LabFactory(lead=self.round_page.lead)
         LabFormFactory(lab=self.lab_page, form=form)
 
     def submit_form(self, page=None, email=None, name=None, user=AnonymousUser()):
diff --git a/opentech/apply/funds/tests/test_workflow.py b/opentech/apply/funds/tests/test_workflow.py
index ad1898c37ed551200ad44aa9a6449716b6df4cd0..7fd8a08ed07478d85b1d314010143294d10c1605 100644
--- a/opentech/apply/funds/tests/test_workflow.py
+++ b/opentech/apply/funds/tests/test_workflow.py
@@ -58,6 +58,28 @@ class TestStageCreation(SimpleTestCase):
         self.assertEqual(stage.name, name)
         self.assertEqual(stage.form, form)
 
+    def test_can_create_with_multi_phase_step(self):
+        first_phase, second_phase = Phase(name='first'), Phase(name='second')
+        change_first = ChangePhaseAction(first_phase, 'first')
+        change_second = ChangePhaseAction(second_phase, 'second')
+
+        class PhaseSwitch(Phase):
+            actions = [change_first, change_second]
+
+        class MultiPhaseStep(Stage):
+            name = 'stage'
+            phases = [
+                PhaseSwitch(),
+                [first_phase, second_phase],
+            ]
+
+        stage = MultiPhaseStep(None)
+        self.assertEqual(stage.steps, 2)
+
+        current_phase = stage.phases[0]
+        self.assertEqual(current_phase.process(change_first.name), stage.phases[1])  # type: ignore
+        self.assertEqual(current_phase.process(change_second.name), stage.phases[2])  # type: ignore
+
     def test_can_get_next_phase(self):
         stage = StageFactory.build(num_phases=2)
         self.assertEqual(stage.next(stage.phases[0]), stage.phases[1])
diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py
index 6068b7cca4cdf15a0530f7c3cca1a9791ad2134e..cc28bdbff046fb84a8c23c3e8e1a9cc7a9e83d17 100644
--- a/opentech/apply/funds/views.py
+++ b/opentech/apply/funds/views.py
@@ -1,16 +1,25 @@
 from django import forms
 from django.template.response import TemplateResponse
-from django.views.generic import DetailView
+from django.views.generic import DetailView, UpdateView
 
 from django_filters.views import FilterView
 from django_tables2.views import SingleTableMixin
 
+from opentech.apply.activity.views import (
+    AllActivityContextMixin,
+    ActivityContextMixin,
+    CommentFormView,
+    DelegatedViewMixin,
+)
+from opentech.apply.activity.models import Activity
+
+from .forms import ProgressSubmissionForm, UpdateSubmissionLeadForm
 from .models import ApplicationSubmission
 from .tables import SubmissionsTable, SubmissionFilter, SubmissionFilterAndSearch
 from .workflow import SingleStage, DoubleStage
 
 
-class SubmissionListView(SingleTableMixin, FilterView):
+class SubmissionListView(AllActivityContextMixin, SingleTableMixin, FilterView):
     template_name = 'funds/submissions.html'
     table_class = SubmissionsTable
 
@@ -40,15 +49,71 @@ class SubmissionSearchView(SingleTableMixin, FilterView):
         )
 
 
-class SubmissionDetailView(DetailView):
+class ProgressSubmissionView(DelegatedViewMixin, UpdateView):
+    model = ApplicationSubmission
+    form_class = ProgressSubmissionForm
+    context_name = 'progress_form'
+
+    def form_valid(self, form):
+        old_phase = form.instance.phase.name
+        response = super().form_valid(form)
+        new_phase = form.instance.phase.name
+        Activity.actions.create(
+            user=self.request.user,
+            submission=self.kwargs['submission'],
+            message=f'Progressed from {old_phase} to {new_phase}'
+        )
+        return response
+
+
+class UpdateLeadView(DelegatedViewMixin, UpdateView):
+    model = ApplicationSubmission
+    form_class = UpdateSubmissionLeadForm
+    context_name = 'lead_form'
+
+    def form_valid(self, form):
+        # Fetch the old lead from the database
+        old_lead = self.get_object().lead
+        response = super().form_valid(form)
+        new_lead = form.instance.lead
+        Activity.actions.create(
+            user=self.request.user,
+            submission=self.kwargs['submission'],
+            message=f'Lead changed from {old_lead} to {new_lead}'
+        )
+        return response
+
+
+class SubmissionDetailView(ActivityContextMixin, DetailView):
     model = ApplicationSubmission
+    form_views = {
+        'progress': ProgressSubmissionView,
+        'comment': CommentFormView,
+        'update': UpdateLeadView,
+    }
 
     def get_context_data(self, **kwargs):
+        forms = dict(form_view.contribute_form(self.object) for form_view in self.form_views.values())
         return super().get_context_data(
             other_submissions=self.model.objects.filter(user=self.object.user).exclude(id=self.object.id),
-            **kwargs
+            **forms,
+            **kwargs,
         )
 
+    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()
+
+        form_submitted = request.POST['form-submitted'].lower()
+        view = self.form_views[form_submitted].as_view()
+
+        return view(request, *args, **kwargs)
+
 
 workflows = [SingleStage, DoubleStage]
 
diff --git a/opentech/apply/funds/workflow.py b/opentech/apply/funds/workflow.py
index b356dd158876933ec3ab865c374fea8839389a40..1f621ec609ed66a582b416f3ec84f94fcc3b7e8d 100644
--- a/opentech/apply/funds/workflow.py
+++ b/opentech/apply/funds/workflow.py
@@ -1,6 +1,7 @@
+from collections import defaultdict
 import copy
 
-from typing import Iterable, Iterator, List, Sequence, Type, Union
+from typing import Dict, Iterable, Iterator, List, Sequence, Type, Union
 
 from django.forms import Form
 from django.utils.text import slugify
@@ -9,17 +10,26 @@ from django.utils.text import slugify
 This file defines classes which allow you to compose workflows based on the following structure:
 
 Workflow -> Stage -> Phase -> Action
+
+These classes are designed such that they can be mapped to a wagtail streamfield to allow admins
+to build/adjust workflows as required.
+
+Current limitations:
+* Changing the name of a phase will mean that any object which references it cannot progress. [will
+be fixed when streamfield, may require intermediate fix prior to launch]
+* Outcomes are a special case of phase and perhaps should be handled separately. [will look at when
+progressing stages]
 """
 
 
-def phase_name(stage: 'Stage', phase: Union['Phase', str], occurrence: int) -> str:
+def phase_name(stage: 'Stage', phase: Union['Phase', str], step: int) -> str:
     # Build the identifiable name for a phase
     if not isinstance(phase, str):
         phase_name = phase._internal
     else:
         phase_name = phase
 
-    return '__'.join([stage.name, phase_name, str(occurrence)])
+    return '__'.join([stage.name, phase_name, str(step)])
 
 
 class Workflow(Iterable):
@@ -113,27 +123,43 @@ class Stage(Iterable):
         # TODO: consider removing form from stage as the stage is generic and
         # shouldn't care about forms.
         self.form = form
+        self.steps = len(self.phases)
         # Make the phases new instances to prevent errors with mutability
-        existing_phases: set = set()
-        new_phases: list = list()
-        for phase in self.phases:
-            phase.stage = self
-            while str(phase) in existing_phases:
-                phase.occurrence += 1
-            existing_phases.add(str(phase))
-            new_phases.append(copy.copy(phase))
-        self.phases = new_phases
-
-    def __iter__(self) -> Iterator['Phase']:
-        yield from self.phases
+        self.phases = self.copy_phases(self.phases)
+
+    def copy_phases(self, phases: List['Phase']) -> List['Phase']:
+        new_phases = list()
+        for step, phase in enumerate(self.phases):
+            try:
+                new_phases.append(self.copy_phase(phase, step))
+            except AttributeError:
+                # We have a step with multiple equivalent phases
+                for sub_phase in phase:
+                    new_phases.append(self.copy_phase(sub_phase, step))
+        return new_phases
+
+    def copy_phase(self, phase: 'Phase', step: int) -> 'Phase':
+        phase.stage = self
+        phase.step = step
+        return copy.copy(phase)
+
+    def __iter__(self) -> 'PhaseIterator':
+        return PhaseIterator(self.phases, self.steps)
 
     def __str__(self) -> str:
         return self.name
 
     def get_phase(self, phase_name: str) -> 'Phase':
         for phase in self.phases:
-            if str(phase) == phase_name:
+            if phase == phase_name:
                 return phase
+
+        # We don't have the exact name
+        for phase in self.phases:
+            if phase._internal == phase_name:
+                # Grab the first phase to match the name
+                return phase
+
         return None
 
     def first(self) -> 'Phase':
@@ -152,6 +178,45 @@ class Stage(Iterable):
         return None
 
 
+class PhaseIterator(Iterator):
+    class Step:
+        """Allow handling phases which are equivalent e.g. outcomes (accepted/rejected)
+        Delegates to the underlying phases except where naming is concerned
+        """
+        def __init__(self, phases: List['Phase']) -> None:
+            self.phases = phases
+
+        @property
+        def step(self) -> int:
+            return self.phases[0].step
+
+        @property
+        def name(self) -> str:
+            # Hardcode a name for multi-phased step - always outcome at the moment
+            if len(self.phases) > 1:
+                return 'Outcome'
+            return self.phases[0].name
+
+        def __eq__(self, other: object) -> bool:
+            return any(phase == other for phase in self.phases)
+
+    def __init__(self, phases: List['Phase'], steps: int) -> None:
+        self.current = 0
+        self.phases: Dict[int, List['Phase']] = defaultdict(list)
+        for phase in phases:
+            self.phases[phase.step].append(phase)
+        self.steps = steps
+
+    def __iter__(self) -> 'PhaseIterator':
+        return self
+
+    def __next__(self) -> 'Step':
+        self.current += 1
+        if self.current > self.steps:
+            raise StopIteration
+        return self.Step(self.phases[self.current - 1])
+
+
 class Phase:
     """
     Holds the Actions which a user can perform at each stage. A Phase with no actions is
@@ -173,12 +238,12 @@ class Phase:
         self._internal = slugify(self.name)
         self.stage: Union['Stage', None] = None
         self._actions = {action.name: action for action in self.actions}
-        self.occurrence: int = 0
+        self.step: int = 0
 
     def __eq__(self, other: Union[object, str]) -> bool:
         if isinstance(other, str):
             return str(self) == other
-        to_match = ['name', 'occurrence']
+        to_match = ['name', 'step']
         return all(getattr(self, attr) == getattr(other, attr) for attr in to_match)
 
     @property
@@ -186,7 +251,7 @@ class Phase:
         return list(self._actions.keys())
 
     def __str__(self) -> str:
-        return phase_name(self.stage, self, self.occurrence)
+        return phase_name(self.stage, self, self.step)
 
     def __getitem__(self, value: str) -> 'Action':
         return self._actions[value]
@@ -218,7 +283,7 @@ class ChangePhaseAction(Action):
 
     def process(self, phase: 'Phase') -> Union['Phase', None]:
         if isinstance(self.target_phase, str):
-            return phase.stage.get_phase(phase_name(phase.stage, self.target_phase, 0))
+            return phase.stage.get_phase(self.target_phase)
         return self.target_phase
 
 
@@ -275,8 +340,7 @@ class RequestStage(Stage):
         DiscussionWithNextPhase(),
         ReviewPhase(),
         DiscussionPhase(),
-        accepted,
-        rejected,
+        [accepted, rejected]
     ]
 
 
@@ -285,8 +349,7 @@ class ConceptStage(Stage):
     phases = [
         DiscussionWithNextPhase(),
         ReviewPhase(),
-        DiscussionWithProgressionPhase(),
-        rejected,
+        [DiscussionWithProgressionPhase(), rejected]
     ]
 
 
@@ -298,8 +361,7 @@ class ProposalStage(Stage):
         DiscussionWithNextPhase(),
         ReviewPhase('AC Review', public_name='In AC review'),
         DiscussionPhase(public_name='In AC review'),
-        accepted,
-        rejected,
+        [accepted, rejected]
     ]
 
 
diff --git a/opentech/settings/base.py b/opentech/settings/base.py
index 3e9b3885934f75879296b689ce3bfd3dd62a1b74..ba29eb1f5f9f281f3d817f25605b5205e051eba6 100644
--- a/opentech/settings/base.py
+++ b/opentech/settings/base.py
@@ -14,6 +14,7 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
 INSTALLED_APPS = [
     'opentech.images',
 
+    'opentech.apply.activity',
     'opentech.apply.categories',
     'opentech.apply.funds',
     'opentech.apply.dashboard',
diff --git a/requirements.txt b/requirements.txt
index 9a8e1221bbcdd67d36852ee7c27ba38e1461334c..110437cac5a56fb9110ca0a2dfdbd7f639d69cbd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,7 +14,7 @@ uwsgidecorators==1.1.0
 mypy==0.550
 factory_boy==2.9.2
 # wagtail_factories - waiting on merge and release form master branch
-git+git://github.com/todd-dembrey/wagtail-factories.git#egg=wagtail_factories
+git+git://github.com/todd-dembrey/wagtail-factories.git@a3131766ac21f0d1df18e5f19bdf2dfadd073479#egg=wagtail_factories
 
 flake8