diff --git a/greg/management/commands/start_schedule_tasks.py b/greg/management/commands/start_schedule_tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..2280d56698c6886d88114a91a6c3790fe8fd8614
--- /dev/null
+++ b/greg/management/commands/start_schedule_tasks.py
@@ -0,0 +1,82 @@
+import logging
+import signal
+import sys
+from threading import Event
+
+import daemon
+import lockfile
+from daemon import pidfile
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+from greg.schedule import ExpiringRolesNotification
+
+logging.config.dictConfig(settings.LOGGING)
+logger = logging.getLogger()
+
+
+def exception_handler(ex_cls, ex, tb):
+    logger.critical("Uncaught exception:", exc_info=(ex_cls, ex, tb))
+
+
+sys.excepthook = exception_handler
+
+
+class Command(BaseCommand):
+    """
+    This command starts a basic task runner. All tasks it is supposed to
+    run are given explicitly in code
+    """
+
+    help = "Start schedule task runner"
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            "--detach", action="store_true", help="Run detached as a dæmon"
+        )
+        parser.add_argument(
+            "--use-pidfile", action="store_true", help="Use a PID lockfile"
+        )
+
+    def handle(self, *args, **options):
+        lock_context = None
+        if options.get("use_pidfile"):
+            lock_context = pidfile.TimeoutPIDLockFile(
+                settings.SCHEDULE_TASKS["daemon"]["pid_file"]
+            )
+
+        try:
+            exit_event = Event()
+
+            def exit_wrapper(*args):
+                exit_event.set()
+
+            with daemon.DaemonContext(
+                pidfile=lock_context,
+                files_preserve=[x.stream.fileno() for x in logger.handlers],
+                stderr=sys.stderr,
+                stdout=sys.stdout,
+                signal_map={signal.SIGINT: exit_wrapper, signal.SIGTERM: exit_wrapper},
+                detach_process=options.get("detach"),
+            ):
+                logger.info(
+                    "Running %s", "detached" if options.get("detach") else "attached"
+                )
+
+                # For now there is just one task, but the idea is that more can
+                # be added
+                expiring_role_notification_task = ExpiringRolesNotification()
+
+                while not exit_event.is_set():
+                    expiring_role_notification_task.run()
+                    # The single task is set up for far only needs to run once
+                    # a day, but running every 6 hours just in case a run fails,
+                    # it is up the task it self to figure out if needs to do
+                    # something every time it is called
+                    exit_event.wait(60 * 60 * 6)
+
+        except lockfile.AlreadyLocked as e:
+            logger.warning("Can't start daemon: %s", e)
+            sys.exit(1)
+        finally:
+            logger.info("Stopped")
diff --git a/greg/management/commands/stop_schedule_tasks.py b/greg/management/commands/stop_schedule_tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..ba8af1cd1a9a35c6f0fa20f7de0d937e57395d19
--- /dev/null
+++ b/greg/management/commands/stop_schedule_tasks.py
@@ -0,0 +1,18 @@
+import os
+import signal
+
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+    help = "Stop schedule tasks"
+
+    def handle(self, *args, **options):
+        try:
+            with open(settings.SCHEDULE_TASKS["daemon"]["pid_file"], "r") as f:
+                pid = int(f.read().strip())
+
+            os.kill(pid, signal.SIGINT)
+        except FileNotFoundError:
+            pass
diff --git a/greg/migrations/0001_initial.py b/greg/migrations/0001_initial.py
index 7504a4e320ce449103f0aa674c34a41fbef8f1da..fddea6eade9e3f3a9b3ddbedea4cecc4cb3d8f2b 100644
--- a/greg/migrations/0001_initial.py
+++ b/greg/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 3.2.5 on 2021-08-04 11:07
+# Generated by Django 3.2.5 on 2021-08-06 10:47
 
 import datetime
 import dirtyfields.dirtyfields
@@ -104,6 +104,14 @@ class Migration(migrations.Migration):
             },
             bases=(dirtyfields.dirtyfields.DirtyFieldsMixin, models.Model),
         ),
+        migrations.CreateModel(
+            name='ScheduleTask',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=32)),
+                ('last_completed', models.DateTimeField(null=True)),
+            ],
+        ),
         migrations.CreateModel(
             name='Sponsor',
             fields=[
diff --git a/greg/models.py b/greg/models.py
index 73e5369760975e089e7f9f2243996f6d402260ef..a0db3da1a55c194ed4d5b04f52af2c7ed3c37239 100644
--- a/greg/models.py
+++ b/greg/models.py
@@ -299,3 +299,12 @@ class SponsorOrganizationalUnit(BaseModel):
             self.organizational_unit,
             self.hierarchical_access,
         )
+
+
+class ScheduleTask(models.Model):
+    """
+    Stores information about a task
+    """
+
+    name = models.CharField(max_length=32)
+    last_completed = models.DateTimeField(null=True)
diff --git a/greg/schedule.py b/greg/schedule.py
new file mode 100644
index 0000000000000000000000000000000000000000..e9a907e36be35fd8c55d6ff7fc215c468c01731d
--- /dev/null
+++ b/greg/schedule.py
@@ -0,0 +1,89 @@
+import time
+from datetime import date, datetime, timedelta
+from typing import Optional
+
+from abc import ABC, abstractmethod
+from django.utils import timezone
+
+from greg.models import PersonRole, Notification, ScheduleTask
+
+
+class BaseSchedule(ABC):
+    """
+    Provides common methods for tasks
+    """
+
+    task_object: ScheduleTask
+
+    def __init__(self, name: str):
+        self.task_object = ScheduleTask.objects.get(name=name)
+
+    def run(self):
+        self._run_internal()
+        self.task_object.last_completed = timezone.now()
+        self.task_object.save()
+
+    @abstractmethod
+    def _run_internal(self):
+        pass
+
+    def get_last_run(self) -> Optional[datetime]:
+        self.task_object.refresh_from_db()
+        return self.task_object.last_completed
+
+    def _store_notification(
+        self, identifier, object_type, operation, **kwargs
+    ) -> Notification:
+        return Notification.objects.create(
+            identifier=identifier,
+            object_type=object_type,
+            operation=operation,
+            issued_at=int(time.time()),
+            meta=kwargs,
+        )
+
+
+class ExpiringRolesNotification(BaseSchedule):
+    """
+    This task does a simple check for roles that will expire in 30 days
+    and creates entries in the notification table for them.
+
+    There should be an entry in the ScheduleTask-table with name role_expiration
+    before this task is run.
+
+    Some assumptions that are made:
+
+    - The task will be run every day, it does
+    not keep track of which roles it has or has not notified, it only
+    looks at which roles will expire in exactly 30 days
+
+    - If there are roles created that expire in less than 30 days, no
+    notification is necessary
+    """
+
+    EXPIRATION_THRESHOLD_DAYS = 30
+
+    def __init__(self):
+        # Will raise exception if there is not exactly one result
+        super().__init__("role_expiration")
+
+    def _run_internal(self):
+        last_run = self.get_last_run()
+        # Only run once per day
+        if last_run is None or last_run.date() != date.today():
+            check_date = datetime.today() + timedelta(
+                days=self.EXPIRATION_THRESHOLD_DAYS
+            )
+            self.__get_roles_about_to_expire(check_date)
+
+    def __get_roles_about_to_expire(self, end_date: date):
+        roles_about_to_expire = PersonRole.objects.filter(end_date=end_date)
+
+        for person_role in roles_about_to_expire:
+            meta = {"person_id": person_role.person.id, "role_id": person_role.role.id}
+            self._store_notification(
+                identifier=person_role.id,
+                object_type="PersonRole",
+                operation="expire_reminder",
+                **meta
+            )
diff --git a/greg/tests/test_expire_role.py b/greg/tests/test_expire_role.py
new file mode 100644
index 0000000000000000000000000000000000000000..d116eb92c8342fc4019872cca27dad4e00254116
--- /dev/null
+++ b/greg/tests/test_expire_role.py
@@ -0,0 +1,94 @@
+from datetime import datetime, timedelta
+
+import pytest
+from django.db.models import Q
+from django.utils import timezone
+
+from greg.models import (
+    ScheduleTask,
+    Role,
+    Person,
+    OrganizationalUnit,
+    PersonRole,
+    Notification,
+    Sponsor,
+)
+from greg.schedule import ExpiringRolesNotification
+
+
+@pytest.fixture
+def role_task():
+    ScheduleTask.objects.create(
+        name="role_expiration", last_completed=timezone.now() - timedelta(days=1)
+    )
+
+
+@pytest.fixture
+def role_bar() -> Role:
+    return Role.objects.create(type="role_bar", name_en="Role Bar")
+
+
+@pytest.fixture
+def person(role_bar: Role) -> Person:
+    return Person.objects.create(
+        first_name="Test",
+        last_name="Tester",
+        date_of_birth="2000-01-27",
+        email="test@example.org",
+        mobile_phone="123456789",
+    )
+
+
+@pytest.fixture
+def organizational_unit() -> OrganizationalUnit:
+    return OrganizationalUnit.objects.create(orgreg_id="12345", name_en="Test unit")
+
+
+@pytest.fixture
+def sponsor() -> Sponsor:
+    return Sponsor.objects.create(feide_id="sponsor@example.org")
+
+
+@pytest.fixture
+def person_role(
+    person: Person,
+    role_bar: Role,
+    organizational_unit: OrganizationalUnit,
+    sponsor: Sponsor,
+) -> PersonRole:
+    return PersonRole.objects.create(
+        person=person,
+        role=role_bar,
+        unit=organizational_unit,
+        start_date="2020-03-05",
+        end_date=datetime.today() + timedelta(days=30),
+        contact_person_unit="Contact Person",
+        available_in_search=True,
+        registered_by=sponsor,
+    )
+
+
+@pytest.mark.django_db
+def test_role_picked_up(role_task: ScheduleTask, person_role: PersonRole):
+    role_notification = ExpiringRolesNotification()
+    assert len(Notification.objects.filter(~Q(operation="add"))) == 0
+    role_notification.run()
+    notification = Notification.objects.get(operation="expire_reminder")
+    assert notification.identifier == person_role.id
+    role_notification.run()
+    notifications = Notification.objects.filter(operation="expire_reminder")
+    assert len(notifications) == 1
+
+
+@pytest.mark.django_db
+def test_no_notification_for_role_not_about_to_expire(
+    role_task: ScheduleTask, person_role: PersonRole
+):
+    person_role.end_date = datetime.today() + timedelta(days=31)
+    person_role.save()
+    role_notification = ExpiringRolesNotification()
+    assert len(Notification.objects.filter(operation="expire_reminder")) == 0
+    # Role should not be picked up since it expires in 31 days
+    role_notification.run()
+    notifications = Notification.objects.filter(operation="expire_reminder")
+    assert len(notifications) == 0
diff --git a/greg/tests/test_notifications.py b/greg/tests/test_notifications.py
index 8172a2be5cf1668a39a8f848bd62e420f349730f..584858f9337cbf6b6054c9ddce946ddb57095232 100644
--- a/greg/tests/test_notifications.py
+++ b/greg/tests/test_notifications.py
@@ -55,7 +55,7 @@ def consent() -> Consent:
 def person_identity(person: Person) -> PersonIdentity:
     return PersonIdentity.objects.create(
         person=person,
-        type=PersonIdentity.IdentityType.PASSPORT_NUMBER,
+        type=PersonIdentity.IdentityType.PASSPORT,
         source="Test",
         value="12345678901",
     )
diff --git a/gregsite/settings/base.py b/gregsite/settings/base.py
index 62e8a4920607e33cc61e1a1ee4a34b3e18d92a01..fb28c3b132d1a5df72cc2eeb3abe08bc02b9ca4b 100644
--- a/gregsite/settings/base.py
+++ b/gregsite/settings/base.py
@@ -206,6 +206,10 @@ NOTIFICATION_PUBLISHER = {
     "daemon": {"pid_file": "/tmp/greg_notification_publisher.lock", "poll_interval": 1},
 }
 
+SCHEDULE_TASKS = {
+    "daemon": {"pid_file": "/tmp/schedule_tasks.lock"},
+}
+
 
 INSTANCE_NAME = "local"
 INTERNAL_RK_PREFIX = "no.{instance}.greg".format(instance=INSTANCE_NAME)