diff --git a/src/core/management/commands/housekeeping.py b/src/core/management/commands/housekeeping.py
index 93f96244985b2f829ba2ab7be6b457d1b22ba425..fddcc4e42b9e5473806335c82e3dd0c78fca25a6 100644
--- a/src/core/management/commands/housekeeping.py
+++ b/src/core/management/commands/housekeeping.py
@@ -1,16 +1,80 @@
+import time
+
 from django.core.management.base import BaseCommand
 from django.utils import timezone
 
 from ...models.messages import DirectMessage
+from ...models.schedules import ScheduleSource, ScheduleSourceImport
 from ...models.voucher import Voucher
 
 
 class Command(BaseCommand):
-    def handle(self, *args, **options):
+    def add_arguments(self, parser):
+        parser.add_argument('--forever', action='store_true', help='repeat the housekeeping forever (until Ctrl+C is pressed)')
+        parser.add_argument('--forever-delay', type=int, default=300, help='seconds to wait between housekeeping runs')
+
+    def _do_housekeeping(self):
         # clear all direct messages which are after their expiry date
+        print('Deleting messages ... ', end='', flush=True)
         deleted_msgs_count, _ = DirectMessage.objects.filter(autodelete_after__isnull=False, autodelete_after__lte=timezone.now()).delete()
-        print(f'Deleted {deleted_msgs_count} messages.')
+        print(deleted_msgs_count)
 
         # do auto-assignments
+        print('Auto-assigning vouchers ... ', end='', flush=True)
         vouchers_assigned = Voucher.do_auto_assignments()
-        print(f'Auto-assigned {vouchers_assigned} vouchers.')
+        print(vouchers_assigned)
+
+        # schedules
+        print('Schedule imports ... ', end='', flush=True)
+        schedule_results = {}
+        schedule_failure, schedule_success, schedule_skipped = 0, 0, 0
+        for schedule in ScheduleSource.objects.all():
+            # skip schedules which aren't due yet
+            if not schedule.is_due:
+                continue
+
+            # skip schedules where an import is already running
+            if schedule.has_running_import:
+                schedule_results[str(schedule)] = None
+                schedule_skipped += 1
+                continue
+
+            # create import job
+            job = ScheduleSourceImport(schedule_source=schedule, state=ScheduleSourceImport.State.PREPARED)
+            job.save()
+
+            # execute job
+            try:
+                result = job.do_import()
+                schedule_results[str(schedule)] = job.summary
+                if result:
+                    schedule_success += 1
+                else:
+                    schedule_failure += 1
+            except Exception as err:
+                schedule_results[str(schedule)] = 'EXCEPTION: ' + str(err)
+                schedule_failure += 1
+
+        # print schedule import results
+        print(schedule_success, '/', len(schedule_results), ' (', schedule_skipped, ' skipped)', sep='')
+        for k, v in schedule_results.items():
+            print('  ', k, ' => ', v, sep='')
+
+    def handle(self, *args, **options):
+        # call _do_housekeeping repeatedly (unless --forever is not set)
+        forever = options.get('forever')
+        while True:
+            if forever:
+                print(timezone.now().isoformat())
+
+            self._do_housekeeping()
+
+            if forever:
+                print()  # empty line
+                try:
+                    time.sleep(options.get('forever_delay'))
+                except KeyboardInterrupt:
+                    print('Aborted.')
+                    break
+            else:
+                break