diff --git a/opentech/apply/templates/apply/demo_workflow.html b/opentech/apply/templates/apply/demo_workflow.html
new file mode 100644
index 0000000000000000000000000000000000000000..0187115e51dd9930bb31df9825de4903e4f4d906
--- /dev/null
+++ b/opentech/apply/templates/apply/demo_workflow.html
@@ -0,0 +1,60 @@
+<head>
+    <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
+    <link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">
+    <link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
+</head>
+<body>
+    <main class="wrapper">
+        <nav>
+            <section class="container">
+                <a class="button button-clear" href="{% url 'workflow_demo' 1 %}">Single Stage</a>
+                <a class="button button-clear" href="{% url 'workflow_demo' 2 %}">Double Stage</a>
+            </section>
+        </nav>
+        <section class="container">
+            <h1>Demo of interacting with the workflow</h1>
+        </section>
+        <section class="container">
+            <h2>{{ workflow}}</h2>
+            <h3>{{ phase.stage }}</h3>
+        {% if form %}
+            <form method="post">
+            {% csrf_token %}
+                {{ form }}
+                <input id="current" type="hidden" name="current" value="{{ phase }}" />
+                <button id="submit" name="submit">Submit</button>
+            </form>
+        {% else %}
+            <h4>OTF: {{ phase.name }}</h4>
+            <h4>Public: {{ phase.public_name }}</h4>
+            <form method="post">
+            {% csrf_token %}
+            <input id="current" type="hidden" name="current" value="{{ phase }}" />
+            {% for action in phase.action_names %}
+                <button id="action" name="action" value="{{ action }}">{{ action }}</button>
+            {% empty %}
+                <h4>There are no actions</h4>
+            {% endfor %}
+            </form>
+        </section>
+        <section class="container">
+            <h3>Logs</h3>
+            <ul>
+            {% for log in logs %}
+                <li>{{ log }}</li>
+            {% endfor %}
+            </ul>
+        </section>
+        <section class="container">
+            <h3>Submission</h3>
+            {% for key, value in data.items %}
+                <h4>{{ key }}</h4>
+                {{ value }}
+            {% endfor %}
+        {% endif %}
+        </section>
+        <section class="container">
+            <a class="button" href="">Reset</a>
+        </section>
+    </main>
+</body>
diff --git a/opentech/apply/tests.py b/opentech/apply/tests.py
deleted file mode 100644
index a79ca8be565f44aacce95bad20c1ee34d175ed20..0000000000000000000000000000000000000000
--- a/opentech/apply/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# from django.test import TestCase
-
-# Create your tests here.
diff --git a/opentech/apply/tests/__init__.py b/opentech/apply/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/opentech/apply/tests/factories.py b/opentech/apply/tests/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4f2cbd4066d2c7d9e8c754f95b5ad8dddf8910c
--- /dev/null
+++ b/opentech/apply/tests/factories.py
@@ -0,0 +1,109 @@
+from django.forms import Form
+import factory
+
+from opentech.apply.workflow import Action, Phase, Stage, Workflow
+
+
+class ListSubFactory(factory.SubFactory):
+    def __init__(self, *args, count=0, **kwargs):
+        self.count = count
+        super().__init__(*args, **kwargs)
+
+    def evaluate(self, *args, **kwargs):
+        if isinstance(self.count, factory.declarations.BaseDeclaration):
+            self.evaluated_count = self.count.evaluate(*args, **kwargs)
+        else:
+            self.evaluated_count = self.count
+
+        return super().evaluate(*args, **kwargs)
+
+    def generate(self, step, params):
+        subfactory = self.get_factory()
+        force_sequence = step.sequence if self.FORCE_SEQUENCE else None
+        return [
+            step.recurse(subfactory, params, force_sequence=force_sequence)
+            for _ in range(self.evaluated_count)
+        ]
+
+
+class ActionFactory(factory.Factory):
+    class Meta:
+        model = Action
+
+    name = factory.Faker('word')
+
+
+class PhaseFactory(factory.Factory):
+    class Meta:
+        model = Phase
+
+    class Params:
+        num_actions = factory.Faker('random_int', min=1, max=5)
+
+    name = factory.Faker('word')
+    actions = ListSubFactory(ActionFactory, count=factory.SelfAttribute('num_actions'))
+    stage = factory.PostGeneration(
+        lambda obj, create, extracted, **kwargs: StageFactory.build(phases=[obj])
+    )
+
+    @classmethod
+    def _create(cls, model_class, *args, **kwargs):
+        actions = kwargs.pop('actions')
+        new_class = type(model_class.__name__, (model_class,), {'actions': actions})
+        return new_class(*args, **kwargs)
+
+    @classmethod
+    def _build(cls, model_class, *args, **kwargs):
+        # defer to create because parent uses build
+        return cls._create(model_class, *args, **kwargs)
+
+
+class StageFactory(factory.Factory):
+    class Meta:
+        model = Stage
+        inline_args = ('form',)
+
+    class Params:
+        num_phases = factory.Faker('random_int', min=1, max=3)
+
+    name = factory.Faker('word')
+    form = factory.LazyFunction(Form)
+    phases = ListSubFactory(PhaseFactory, count=factory.SelfAttribute('num_phases'))
+
+    @classmethod
+    def _create(cls, model_class, *args, **kwargs):
+        # Returns a new class
+        phases = kwargs.pop('phases')
+        name = kwargs.pop('name')
+        return type(model_class.__name__, (model_class,), {'phases': phases, 'name': name})
+
+    @classmethod
+    def _build(cls, model_class, *args, **kwargs):
+        # returns an instance of the stage class
+        phases = kwargs.pop('phases')
+        name = kwargs.pop('name')
+        new_class = type(model_class.__name__, (model_class,), {'phases': phases, 'name': name})
+        return new_class(*args, **kwargs)
+
+
+class WorkflowFactory(factory.Factory):
+    class Meta:
+        model = Workflow
+        rename = {'stages': 'stage_classes'}
+
+    class Params:
+        num_stages = factory.Faker('random_int', min=1, max=3)
+
+    name = factory.Faker('word')
+    stages = ListSubFactory(StageFactory, count=factory.SelfAttribute('num_stages'))
+
+    @factory.LazyAttribute
+    def forms(self):
+        return [Form() for _ in range(self.num_stages)]
+
+    @classmethod
+    def _create(cls, model_class, *args, **kwargs):
+        name = kwargs.pop('name')
+        stages = kwargs.pop('stage_classes')
+        new_class = type(model_class.__name__, (model_class,), {'name': name, 'stage_classes': stages})
+        return new_class(*args, **kwargs)
diff --git a/opentech/apply/tests/test_workflow.py b/opentech/apply/tests/test_workflow.py
new file mode 100644
index 0000000000000000000000000000000000000000..4b6cbce3c5ccbe52df7acfdda9c6eb16553358bc
--- /dev/null
+++ b/opentech/apply/tests/test_workflow.py
@@ -0,0 +1,129 @@
+from django.test import SimpleTestCase
+from django.forms import Form
+
+from opentech.apply.workflow import (
+    Action,
+    ChangePhaseAction,
+    NextPhaseAction,
+    Phase,
+    Stage,
+    Workflow,
+)
+
+from .factories import ActionFactory, PhaseFactory, StageFactory, WorkflowFactory
+
+
+class TestWorkflowCreation(SimpleTestCase):
+    def test_can_create_workflow(self):
+        stage = StageFactory()
+
+        class NewWorkflow(Workflow):
+            name = 'single_stage'
+            stage_classes = [stage]
+        workflow = NewWorkflow([Form()])
+        self.assertEqual(workflow.name, NewWorkflow.name)
+        self.assertEqual(len(workflow.stages), 1)
+
+    def test_returns_first_phase_if_no_arg(self):
+        workflow = WorkflowFactory(num_stages=1, stages__num_phases=1)
+        self.assertEqual(workflow.next(), workflow.stages[0].phases[0])
+
+    def test_can_get_the_current_phase(self):
+        workflow = WorkflowFactory(num_stages=1, stages__num_phases=2)
+        phase = workflow.stages[0].phases[0]
+        self.assertEqual(workflow.current(str(phase)), phase)
+
+    def test_returns_next_stage(self):
+        workflow = WorkflowFactory(num_stages=2, stages__num_phases=1)
+        self.assertEqual(workflow.next_stage(workflow.stages[0]), workflow.stages[1])
+
+    def test_returns_none_if_no_next(self):
+        workflow = WorkflowFactory(num_stages=1, stages__num_phases=1)
+        self.assertEqual(workflow.next(workflow.stages[0].phases[0]), None)
+
+    def test_returns_next_phase(self):
+        workflow = WorkflowFactory(num_stages=2, stages__num_phases=1)
+        self.assertEqual(workflow.next(workflow.stages[0].phases[0]), workflow.stages[1].phases[0])
+
+    def test_returns_next_phase_shared_name(self):
+        workflow = WorkflowFactory(num_stages=1, stages__num_phases=3, stages__phases__name='the_same')
+        self.assertEqual(workflow.next(workflow.stages[0].phases[0]), workflow.stages[0].phases[1])
+
+
+class TestStageCreation(SimpleTestCase):
+    def test_can_create_stage(self):
+        name = 'the_stage'
+        form = Form()
+        stage = Stage(form, name=name)
+        self.assertEqual(stage.name, name)
+        self.assertEqual(stage.form, form)
+
+    def test_can_get_next_phase(self):
+        stage = StageFactory.build(num_phases=2)
+        self.assertEqual(stage.next(stage.phases[0]), stage.phases[1])
+
+    def test_get_none_if_no_next_phase(self):
+        stage = StageFactory.build(num_phases=1)
+        self.assertEqual(stage.next(stage.phases[0]), None)
+
+
+class TestPhaseCreation(SimpleTestCase):
+    def test_can_create_phase(self):
+        name = 'the_phase'
+        phase = Phase(name)
+        self.assertEqual(phase.name, name)
+
+    def test_can_get_action_from_phase(self):
+        actions = ActionFactory.create_batch(3)
+        action = actions[1]
+        phase = PhaseFactory(actions=actions)
+        self.assertEqual(phase[action.name], action)
+
+    def test_uses_name_if_no_public(self):
+        phase = Phase('Phase Name')
+        self.assertEqual(phase.public_name, phase.name)
+
+    def test_uses_public_if_provided(self):
+        public_name = 'Public Name'
+        phase = Phase('Phase Name', public_name=public_name)
+        self.assertEqual(phase.public_name, public_name)
+        self.assertNotEqual(phase.public_name, phase.name)
+
+    def test_uses_public_if_provided_on_class(self):
+        class NewPhase(Phase):
+            public_name = 'Public Name'
+        phase = NewPhase('Phase Name')
+        self.assertEqual(phase.public_name, NewPhase.public_name)
+        self.assertNotEqual(phase.public_name, phase.name)
+
+
+class TestActions(SimpleTestCase):
+    def test_can_create_action(self):
+        name = 'action stations'
+        action = Action(name)
+        self.assertEqual(action.name, name)
+
+    def test_calling_processes_the_action(self):
+        action = ActionFactory()
+        with self.assertRaises(NotImplementedError):
+            action.process('')
+
+
+class TestCustomActions(SimpleTestCase):
+    def test_next_phase_action_returns_none_if_no_next(self):
+        action = NextPhaseAction('the next!')
+        phase = PhaseFactory(actions=[action])
+        self.assertEqual(phase.process(action.name), None)
+
+    def test_next_phase_action_returns_next_phase(self):
+        action = NextPhaseAction('the next!')
+        stage = StageFactory.build(num_phases=2, phases__actions=[action])
+        self.assertEqual(stage.phases[0].process(action.name), stage.phases[1])
+
+    def test_change_phase_will_skip_phase(self):
+        target_phase = PhaseFactory()
+        action = ChangePhaseAction(target_phase.name, 'skip!')
+        other_phases = PhaseFactory.create_batch(2, actions=[action])
+        stage = StageFactory.build(phases=[*other_phases, target_phase])
+        self.assertEqual(stage.phases[0].process(action.name), stage.phases[2])
+        self.assertEqual(stage.phases[1].process(action.name), stage.phases[2])
diff --git a/opentech/apply/urls.py b/opentech/apply/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d1bd77962f3ed7588235332bf0f14ffedc8e163
--- /dev/null
+++ b/opentech/apply/urls.py
@@ -0,0 +1,7 @@
+from django.conf.urls import url
+
+from .views import demo_workflow
+
+urlpatterns = [
+    url(r'^demo/(?P<wf_id>[1-2])/$', demo_workflow, name="workflow_demo")
+]
diff --git a/opentech/apply/views.py b/opentech/apply/views.py
index fd0e0449559b2e00e226cc9f96df7caed44172aa..49fa8d1b708e88cf8359ec41e664e59fa5e6fd1b 100644
--- a/opentech/apply/views.py
+++ b/opentech/apply/views.py
@@ -1,3 +1,62 @@
-# from django.shortcuts import render
+from django import forms
+from django.template.response import TemplateResponse
 
-# Create your views here.
+from .workflow import SingleStage, DoubleStage
+
+
+workflows = [SingleStage, DoubleStage]
+
+
+class BasicSubmissionForm(forms.Form):
+    who_are_you = forms.CharField()
+
+
+def demo_workflow(request, wf_id):
+    logs = request.session.get('logs', list())
+    submission = request.session.get('submission', dict())
+
+    wf = int(wf_id)
+    workflow_class = workflows[wf - 1]
+    workflow = workflow_class([BasicSubmissionForm] * wf)
+
+    current_phase = request.POST.get('current')
+    current = workflow.current(current_phase)
+
+    if request.POST:
+        if current.stage.name not in submission:
+            submitted_form = current.stage.form(request.POST)
+            if submitted_form.is_valid():
+                submission[current.stage.name] = submitted_form.cleaned_data
+                phase = current
+                logs.append(
+                    f'{phase.stage}: Form was submitted'
+                )
+                form = None
+            else:
+                form = submitted_form
+        else:
+            phase = workflow.process(current_phase, request.POST['action'])
+            logs.append(
+                f'{current.stage}: {current.name} was updated to {phase.stage}: {phase.name}'
+            )
+    else:
+        phase = current
+        logs.clear()
+        submission.clear()
+
+    if phase.stage.name not in submission:
+        form = phase.stage.form
+    else:
+        form = None
+
+    request.session['logs'] = logs
+    request.session['submission'] = submission
+
+    context = {
+        'workflow': workflow,
+        'phase': phase,
+        'logs': logs,
+        'data': submission,
+        'form': form,
+    }
+    return TemplateResponse(request, 'apply/demo_workflow.html', context)
diff --git a/opentech/apply/workflow.py b/opentech/apply/workflow.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff774fc820c1e7fd550f9d221d6b8059eeed0ef7
--- /dev/null
+++ b/opentech/apply/workflow.py
@@ -0,0 +1,252 @@
+import copy
+
+from typing import List, Sequence, Type, Union
+
+from django.forms import Form
+from django.utils.text import slugify
+
+
+class Workflow:
+    name: str = ''
+    stage_classes: Sequence[Type['Stage']] = list()
+
+    def __init__(self, forms: Sequence[Form]) -> None:
+        if len(self.stage_classes) != len(forms):
+            raise ValueError('Number of forms does not equal the number of stages')
+
+        self.stages = [stage(form) for stage, form in zip(self.stage_classes, forms)]
+
+    def current(self, current_phase: Union[str, 'Phase']) -> Union['Phase', None]:
+        if isinstance(current_phase, Phase):
+            return current_phase
+
+        if not current_phase:
+            return self.first()
+
+        stage_name, phase_name, occurance = current_phase.split('__')
+        for stage in self.stages:
+            if stage.name == stage_name:
+                return stage.current(phase_name, occurance)
+        return None
+
+    def first(self) -> 'Phase':
+        return self.stages[0].next()
+
+    def process(self, current_phase: str, action: str) -> Union['Phase', None]:
+        phase = self.current(current_phase)
+        new_phase = phase.process(action)
+        if not new_phase:
+            new_stage = self.next_stage(phase.stage)
+            if new_stage:
+                return new_stage.first()
+        return new_phase
+
+    def next_stage(self, current_stage: 'Stage') -> 'Stage':
+        for i, stage in enumerate(self.stages):
+            if stage == current_stage:
+                try:
+                    return self.stages[i + 1]
+                except IndexError:
+                    pass
+
+        return None
+
+    def next(self, current_phase: Union[str, 'Phase']=None) -> Union['Phase', None]:
+        if not current_phase:
+            return self.first()
+
+        phase = self.current(current_phase)
+
+        for stage in self.stages:
+            if stage == phase.stage:
+                next_phase = stage.next(phase)
+                if not next_phase:
+                    continue
+                return next_phase
+
+        next_stage = self.next_stage(phase.stage)
+        if next_stage:
+            return stage.next()
+        return None
+
+    def __str__(self) -> str:
+        return self.name
+
+
+class Stage:
+    name: str = 'Stage'
+    phases: list = list()
+
+    def __init__(self, form: Form, name: str='') -> None:
+        if name:
+            self.name = name
+        self.form = form
+        # 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.occurance += 1
+            existing_phases.add(str(phase))
+            new_phases.append(copy.copy(phase))
+        self.phases = new_phases
+
+    def __str__(self) -> str:
+        return self.name
+
+    def current(self, phase_name: str, occurance: str) -> 'Phase':
+        for phase in self.phases:
+            if phase._internal == phase_name and int(occurance) == phase.occurance:
+                return phase
+        return None
+
+    def first(self) -> 'Phase':
+        return self.phases[0]
+
+    def next(self, current_phase: 'Phase'=None) -> 'Phase':
+        if not current_phase:
+            return self.first()
+
+        for i, phase in enumerate(self.phases):
+            if phase == current_phase:
+                try:
+                    return self.phases[i + 1]
+                except IndexError:
+                    pass
+        return None
+
+
+class Phase:
+    actions: Sequence['Action'] = list()
+    name: str = ''
+    public_name: str = ''
+
+    def __init__(self, name: str='', public_name: str ='') -> None:
+        if name:
+            self.name = name
+
+        if public_name:
+            self.public_name = public_name
+        elif not self.public_name:
+            self.public_name = self.name
+
+        self._internal = slugify(self.name)
+        self.stage: Union['Stage', None] = None
+        self._actions = {action.name: action for action in self.actions}
+        self.occurance: int = 0
+
+    def __eq__(self, other: object) -> bool:
+        to_match = ['name', 'occurance']
+        return all(getattr(self, attr) == getattr(other, attr) for attr in to_match)
+
+    @property
+    def action_names(self) -> List[str]:
+        return list(self._actions.keys())
+
+    def __str__(self) -> str:
+        return '__'.join([self.stage.name, self._internal, str(self.occurance)])
+
+    def __getitem__(self, value: str) -> 'Action':
+        return self._actions[value]
+
+    def process(self, action: str) -> Union['Phase', None]:
+        return self[action].process(self)
+
+
+class Action:
+    def __init__(self, name: str) -> None:
+        self.name = name
+
+    def process(self, phase: 'Phase') -> Union['Phase', None]:
+        # Use this to define the behaviour of the action
+        raise NotImplementedError
+
+
+# --- OTF Workflow ---
+
+class ChangePhaseAction(Action):
+    def __init__(self, phase: Union['Phase', str], *args: str, **kwargs: str) -> None:
+        self.target_phase = phase
+        super().__init__(*args, **kwargs)
+
+    def process(self, phase: 'Phase') -> Union['Phase', None]:
+        if isinstance(self.target_phase, str):
+            return phase.stage.current(self.target_phase, '0')
+        return self.target_phase
+
+
+class NextPhaseAction(Action):
+    def process(self, phase: 'Phase') -> Union['Phase', None]:
+        return phase.stage.next(phase)
+
+
+reject_action = ChangePhaseAction('rejected', 'Reject')
+
+accept_action = ChangePhaseAction('accepted', 'Accept')
+
+progress_stage = ChangePhaseAction(None, 'Invite to Proposal')
+
+next_phase = NextPhaseAction('Progress')
+
+
+class ReviewPhase(Phase):
+    name = 'Internal Review'
+    public_name = 'In review'
+    actions = [NextPhaseAction('Close Review')]
+
+
+class DeterminationWithProgressionPhase(Phase):
+    name = 'Under Discussion'
+    public_name = 'In review'
+    actions = [progress_stage, reject_action]
+
+
+class DeterminationPhase(Phase):
+    name = 'Under Discussion'
+    public_name = 'In review'
+    actions = [accept_action, reject_action]
+
+
+class DeterminationWithNextPhase(Phase):
+    name = 'Under Discussion'
+    public_name = 'In review'
+    actions = [NextPhaseAction('Open Review'), reject_action]
+
+
+rejected = Phase(name='Rejected')
+
+accepted = Phase(name='Accepted')
+
+
+class ConceptStage(Stage):
+    name = 'Concept'
+    phases = [
+        DeterminationWithNextPhase(),
+        ReviewPhase(),
+        DeterminationWithProgressionPhase(),
+        rejected,
+    ]
+
+
+class ProposalStage(Stage):
+    name = 'Proposal'
+    phases = [
+        DeterminationWithNextPhase(),
+        ReviewPhase(),
+        DeterminationWithNextPhase(),
+        ReviewPhase('AC Review', public_name='In AC review'),
+        DeterminationPhase(public_name='In AC review'),
+        accepted,
+        rejected,
+    ]
+
+
+class SingleStage(Workflow):
+    name = 'Single Stage'
+    stage_classes = [ConceptStage]
+
+
+class DoubleStage(Workflow):
+    name = 'Two Stage'
+    stage_classes = [ConceptStage, ProposalStage]
diff --git a/opentech/urls.py b/opentech/urls.py
index 9170ff6011991dac7f1b35c16beaf574bf762c50..4adc24202baca49bb2c2af00948994ab85ba87fb 100644
--- a/opentech/urls.py
+++ b/opentech/urls.py
@@ -10,6 +10,7 @@ from wagtail.wagtailadmin import urls as wagtailadmin_urls
 from wagtail.wagtailcore import urls as wagtail_urls
 from wagtail.wagtaildocs import urls as wagtaildocs_urls
 
+from opentech.apply import urls as apply_urls
 from opentech.esi import views as esi_views
 from opentech.search import views as search_views
 
@@ -22,6 +23,8 @@ urlpatterns = [
     url(r'^search/$', search_views.search, name='search'),
     url(r'^esi/(.*)/$', esi_views.esi, name='esi'),
     url('^sitemap\.xml$', sitemap),
+
+    url(r'^apply/', include(apply_urls)),
 ]
 
 
diff --git a/requirements.txt b/requirements.txt
index 62cc28d588b9785c386fc88336b4cca8b2fe556c..39136e562a2012ebf060279c94802196dfd1b4a6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,6 +8,7 @@ stellar==0.4.3
 wagtail-django-recaptcha==0.1
 uwsgidecorators==1.1.0
 mypy==0.550
+factory_boy==2.9.2
 flake8
 
 # Production dependencies
diff --git a/setup.cfg b/setup.cfg
index c6e0c156292251fb1e830d6de120b0fb0ad45a29..4ab5cc3deff7bec1cd567a0cbb99204f57f8bd0b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,6 +9,9 @@ ignore_errors = True
 check_untyped_defs = True
 ignore_errors = False
 
+[mypy-opentech.apply.workflow*]
+disallow_untyped_defs = True
+
 [flake8]
 ignore=E501,F405
 exclude=*/migrations/*
diff --git a/vagrant/provision.sh b/vagrant/provision.sh
index 380720c099df288c1cc870a9dc23434247e9df7e..77866f4b201720c13e624d15e2813e5d6635835b 100755
--- a/vagrant/provision.sh
+++ b/vagrant/provision.sh
@@ -50,4 +50,7 @@ alias djrunp="dj runserver_plus 0.0.0.0:8000"
 source $VIRTUALENV_DIR/bin/activate
 export PS1="[$PROJECT_NAME \W]\\$ "
 cd $PROJECT_DIR
+
+alias djtestapply="dj test opentech.apply --keepdb; mypy ."
+
 EOF