diff --git a/aleksis/apps/chronos/apps.py b/aleksis/apps/chronos/apps.py
index 01ad4f24878b9dffe4942b95fdefacf36561d134..6570299bb628b18ea70b24d19bd6fd4985f4901d 100644
--- a/aleksis/apps/chronos/apps.py
+++ b/aleksis/apps/chronos/apps.py
@@ -1,3 +1,6 @@
+from typing import Any, Optional
+
+import django.apps
 from django.db import transaction
 
 from reversion.signals import post_revision_commit
@@ -33,3 +36,74 @@ class ChronosConfig(AppConfig):
             transaction.on_commit(lambda: handle_new_revision.delay(revision.pk))
 
         post_revision_commit.connect(_handle_post_revision_commit, weak=False)
+
+    def _ensure_notification_task(self):
+        from django.conf import settings  # noqa
+
+        from celery import schedules
+        from django_celery_beat.models import CrontabSchedule, PeriodicTask
+
+        from aleksis.core.util.core_helpers import get_site_preferences
+
+        time_for_sending = get_site_preferences()["chronos__time_for_sending_notifications"]
+        active = get_site_preferences()["chronos__send_notifications_site"]
+
+        if active:
+            schedule = schedules.crontab(
+                minute=str(time_for_sending.minute), hour=str(time_for_sending.hour)
+            )
+            schedule = CrontabSchedule.from_schedule(schedule)
+            schedule.timezone = settings.TIME_ZONE
+            schedule.save()
+
+        possible_periodic_tasks = PeriodicTask.objects.filter(
+            task="chronos_send_notifications_for_next_day"
+        )
+
+        if not active:
+            possible_periodic_tasks.delete()
+
+        elif possible_periodic_tasks.exists():
+            task = possible_periodic_tasks[0]
+            for d_task in possible_periodic_tasks:
+                if d_task != task:
+                    d_task.delete()
+
+            if task.crontab != schedule:
+                task.interval, task.solar, task.clocked = None, None, None
+                task.crontab = schedule
+                task.save()
+
+        elif active:
+            PeriodicTask.objects.get_or_create(
+                task="chronos_send_notifications_for_next_day",
+                crontab=schedule,
+                defaults=dict(name="Send notifications for next day (automatic schedule)"),
+            )
+
+    def preference_updated(
+        self,
+        sender: Any,
+        section: Optional[str] = None,
+        name: Optional[str] = None,
+        old_value: Optional[Any] = None,
+        new_value: Optional[Any] = None,
+        **kwargs,
+    ) -> None:
+        if section == "chronos" and name in (
+            "send_notifications_site",
+            "time_for_sending_notifications",
+        ):
+            self._ensure_notification_task()
+
+    def post_migrate(
+        self,
+        app_config: django.apps.AppConfig,
+        verbosity: int,
+        interactive: bool,
+        using: str,
+        **kwargs,
+    ) -> None:
+        super().post_migrate(app_config, verbosity, interactive, using, **kwargs)
+
+        self._ensure_notification_task()
diff --git a/aleksis/apps/chronos/model_extensions.py b/aleksis/apps/chronos/model_extensions.py
index b324c83f67c6e5a24e25feca17ed31b24690dbc9..096b43e22c2c8094d547bbec57f70619cecb99e6 100644
--- a/aleksis/apps/chronos/model_extensions.py
+++ b/aleksis/apps/chronos/model_extensions.py
@@ -1,31 +1,22 @@
 import zoneinfo
 from datetime import date, timedelta
 from typing import Optional, Union
-from urllib.parse import urljoin
 
 from django.conf import settings
 from django.dispatch import receiver
-from django.urls import reverse
 from django.utils import timezone
-from django.utils.formats import date_format
 from django.utils.translation import gettext_lazy as _
-from django.utils.translation import ngettext
 
 from jsonstore import BooleanField
 from reversion.models import Revision
 
-from aleksis.apps.chronos.models import (
-    Event,
-    ExtraLesson,
-    LessonSubstitution,
-    SupervisionSubstitution,
-)
-from aleksis.core.models import Announcement, Group, Notification, Person
+from aleksis.core.models import Announcement, Group, Person
 from aleksis.core.util.core_helpers import get_site_preferences
 
 from .managers import TimetableType
 from .models import Lesson, LessonPeriod, Subject
 from .util.change_tracker import timetable_data_changed
+from .util.notifications import send_notifications_for_object
 
 
 @Person.property_
@@ -171,7 +162,7 @@ Person.add_permission(
 @receiver(timetable_data_changed)
 def send_notifications(sender: Revision, **kwargs):
     """Send notifications to users about the changes."""
-    if not get_site_preferences()["chronos__send_notifications"]:
+    if not get_site_preferences()["chronos__send_notifications_site"]:
         return
 
     for change in sender.changes.values():
@@ -193,182 +184,4 @@ def send_notifications(sender: Revision, **kwargs):
             # Skip this because it's not in the current range for notifications
             continue
 
-        recipients = []
-        if isinstance(change.instance, LessonSubstitution):
-            recipients += change.instance.lesson_period.lesson.teachers.all()
-            recipients += change.instance.teachers.all()
-            recipients += Person.objects.filter(
-                member_of__in=change.instance.lesson_period.lesson.groups.all()
-            )
-        elif isinstance(change.instance, Event):
-            recipients += change.instance.teachers.all()
-            recipients += Person.objects.filter(member_of__in=change.instance.groups.all())
-        elif isinstance(change.instance, ExtraLesson):
-            recipients += change.instance.teachers.all()
-            recipients += Person.objects.filter(member_of__in=change.instance.groups.all())
-        elif isinstance(change.instance, SupervisionSubstitution):
-            recipients.append(change.instance.teacher)
-            recipients.append(change.instance.supervision.teacher)
-        print(change.instance, recipients)
-
-        description = ""
-        if isinstance(change.instance, LessonSubstitution):
-            # Date, lesson, subject
-            subject = change.instance.lesson_period.lesson.subject
-            day = change.instance.date
-            period = change.instance.lesson_period.period
-
-            if change.instance.cancelled:
-                description += (
-                    _(
-                        "The {subject} lesson in the {period}. period on {day} has been cancelled."
-                    ).format(subject=subject.name, period=period.period, day=date_format(day))
-                    + " "
-                )
-            else:
-                description += (
-                    _(
-                        "The {subject} lesson in the {period}. period "
-                        "on {day} has some current changes."
-                    ).format(subject=subject.name, period=period.period, day=date_format(day))
-                    + " "
-                )
-
-                if change.instance.teachers.all():
-                    description += (
-                        ngettext(
-                            "The teacher {old} is substituted by {new}.",
-                            "The teachers {old} are substituted by {new}.",
-                            change.instance.teachers.count(),
-                        ).format(
-                            old=change.instance.lesson_period.lesson.teacher_names,
-                            new=change.instance.teacher_names,
-                        )
-                        + " "
-                    )
-
-                if change.instance.subject:
-                    description += (
-                        _("The subject is changed to {subject}.").format(
-                            subject=change.instance.subject.name
-                        )
-                        + " "
-                    )
-
-                if change.instance.room:
-                    description += (
-                        _("The lesson is moved from {old} to {new}.").format(
-                            old=change.instance.lesson_period.room.name,
-                            new=change.instance.room.name,
-                        )
-                        + " "
-                    )
-
-            if change.instance.comment:
-                description += (
-                    _("There is an additional comment: {comment}.").format(
-                        comment=change.instance.comment
-                    )
-                    + " "
-                )
-
-        elif isinstance(change.instance, Event):
-            if change.instance.date_start != change.instance.date_end:
-                description += (
-                    _(
-                        "There is an event that starts on {date_start}, {period_from}. period "
-                        "and ends on {date_end}, {period_to}. period:"
-                    ).format(
-                        date_start=date_format(change.instance.date_start),
-                        date_end=date_format(change.instance.date_end),
-                        period_from=change.instance.period_from.period,
-                        period_to=change.instance.period_to.period,
-                    )
-                    + "\n"
-                )
-            else:
-                description += (
-                    _(
-                        "There is an event on {date} from the "
-                        "{period_from}. period to the {period_to}. period:"
-                    ).format(
-                        date=date_format(change.instance.date_start),
-                        period_from=change.instance.period_from.period,
-                        period_to=change.instance.period_to.period,
-                    )
-                    + "\n"
-                )
-
-            if change.instance.groups.all():
-                description += (
-                    _("Groups: {groups}").format(groups=change.instance.group_names) + "\n"
-                )
-            if change.instance.teachers.all():
-                description += (
-                    _("Teachers: {teachers}").format(teachers=change.instance.teacher_names) + "\n"
-                )
-            if change.instance.rooms.all():
-                description += (
-                    _("Rooms: {rooms}").format(
-                        rooms=", ".join([room.name for room in change.instance.rooms.all()])
-                    )
-                    + "\n"
-                )
-        elif isinstance(change.instance, ExtraLesson):
-            description += (
-                _("There is an extra lesson on {date} in the {period}. period:").format(
-                    date=date_format(change.instance.date),
-                    period=change.instance.period.period,
-                )
-                + "\n"
-            )
-
-            if change.instance.groups.all():
-                description += (
-                    _("Groups: {groups}").format(groups=change.instance.group_names) + "\n"
-                )
-            if change.instance.room:
-                description += (
-                    _("Subject: {subject}").format(subject=change.instance.subject.name) + "\n"
-                )
-            if change.instance.teachers.all():
-                description += (
-                    _("Teachers: {teachers}").format(teachers=change.instance.teacher_names) + "\n"
-                )
-            if change.instance.room:
-                description += _("Room: {room}").format(room=change.instance.room.name) + "\n"
-            if change.instance.comment:
-                description += (
-                    _("Comment: {comment}.").format(comment=change.instance.comment) + "\n"
-                )
-        elif isinstance(change.instance, SupervisionSubstitution):
-            description += _(
-                "The supervision of {old} on {date} between the {period_from}. period "
-                "and the {period_to}. period in the area {area} is substituted by {new}."
-            ).format(
-                old=change.instance.supervision.teacher.full_name,
-                date=date_format(change.instance.date),
-                period_from=change.instance.supervision.break_item.after_period_number,
-                period_to=change.instance.supervision.break_item.before_period_number,
-                area=change.instance.supervision.area.name,
-                new=change.instance.teacher.full_name,
-            )
-
-        url = urljoin(
-            settings.BASE_URL,
-            reverse(
-                "my_timetable_by_date",
-                args=[dt_start.date().year, dt_start.date().month, dt_start.date().day],
-            ),
-        )
-
-        for recipient in recipients:
-            if recipient.preferences["chronos__send_notifications"]:
-                n = Notification(
-                    recipient=recipient,
-                    sender=_("Timetable"),
-                    title=_("There are current changes to your timetable."),
-                    description=description,
-                    link=url,
-                )
-                n.save()
+        send_notifications_for_object(change.instance)
diff --git a/aleksis/apps/chronos/preferences.py b/aleksis/apps/chronos/preferences.py
index fff08ee1c17c883b96fee9d987ff854934b4d2c3..6391b696b3852d7b281a94366e04b5a3bc3fd27e 100644
--- a/aleksis/apps/chronos/preferences.py
+++ b/aleksis/apps/chronos/preferences.py
@@ -83,7 +83,7 @@ class TimeForSendingNotifications(TimePreference):
 @site_preferences_registry.register
 class SendNotifications(BooleanPreference):
     section = chronos
-    name = "send_notifications"
+    name = "send_notifications_site"
     default = True
     verbose_name = _("Send notifications for current timetable changes")
 
diff --git a/aleksis/apps/chronos/tasks.py b/aleksis/apps/chronos/tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..90f3ed3eacdaccab2ebd7008d8976e4d49b7798b
--- /dev/null
+++ b/aleksis/apps/chronos/tasks.py
@@ -0,0 +1,25 @@
+from django.utils import timezone
+
+from aleksis.apps.chronos.models import (
+    Event,
+    ExtraLesson,
+    LessonSubstitution,
+    SupervisionSubstitution,
+)
+from aleksis.apps.chronos.util.notifications import send_notifications_for_object
+from aleksis.core.celery import app
+
+
+@app.task(name="chronos_send_notifications_for_next_day")
+def send_notifications_for_next_day():
+    """Send notifications for next day."""
+    next_day = timezone.now().date() + timezone.timedelta(days=1)
+
+    relevant_objects = []
+    relevant_objects += LessonSubstitution.objects.on_day(next_day)
+    relevant_objects += ExtraLesson.objects.on_day(next_day)
+    relevant_objects += Event.objects.on_day(next_day)
+    relevant_objects += SupervisionSubstitution.objects.filter(date=next_day)
+
+    for instance in relevant_objects:
+        send_notifications_for_object(instance)
diff --git a/aleksis/apps/chronos/util/notifications.py b/aleksis/apps/chronos/util/notifications.py
new file mode 100644
index 0000000000000000000000000000000000000000..9cfaea28f2b779e61082fd04fd0a377a264dab92
--- /dev/null
+++ b/aleksis/apps/chronos/util/notifications.py
@@ -0,0 +1,182 @@
+from typing import Union
+from urllib.parse import urljoin
+
+from django.conf import settings
+from django.urls import reverse
+from django.utils.formats import date_format
+from django.utils.translation import gettext_lazy as _
+from django.utils.translation import ngettext
+
+from aleksis.core.models import Notification, Person
+
+from ..models import Event, ExtraLesson, LessonSubstitution, SupervisionSubstitution
+
+
+def send_notifications_for_object(
+    instance: Union[ExtraLesson, LessonSubstitution, Event, SupervisionSubstitution]
+):
+    """Send notifications for a change object."""
+    recipients = []
+    if isinstance(instance, LessonSubstitution):
+        recipients += instance.lesson_period.lesson.teachers.all()
+        recipients += instance.teachers.all()
+        recipients += Person.objects.filter(
+            member_of__in=instance.lesson_period.lesson.groups.all()
+        )
+    elif isinstance(instance, Event):
+        recipients += instance.teachers.all()
+        recipients += Person.objects.filter(member_of__in=instance.groups.all())
+    elif isinstance(instance, ExtraLesson):
+        recipients += instance.teachers.all()
+        recipients += Person.objects.filter(member_of__in=instance.groups.all())
+    elif isinstance(instance, SupervisionSubstitution):
+        recipients.append(instance.teacher)
+        recipients.append(instance.supervision.teacher)
+
+    description = ""
+    if isinstance(instance, LessonSubstitution):
+        # Date, lesson, subject
+        subject = instance.lesson_period.lesson.subject
+        day = instance.date
+        period = instance.lesson_period.period
+
+        if instance.cancelled:
+            description += (
+                _(
+                    "The {subject} lesson in the {period}. period on {day} has been cancelled."
+                ).format(subject=subject.name, period=period.period, day=date_format(day))
+                + " "
+            )
+        else:
+            description += (
+                _(
+                    "The {subject} lesson in the {period}. period "
+                    "on {day} has some current changes."
+                ).format(subject=subject.name, period=period.period, day=date_format(day))
+                + " "
+            )
+
+            if instance.teachers.all():
+                description += (
+                    ngettext(
+                        "The teacher {old} is substituted by {new}.",
+                        "The teachers {old} are substituted by {new}.",
+                        instance.teachers.count(),
+                    ).format(
+                        old=instance.lesson_period.lesson.teacher_names,
+                        new=instance.teacher_names,
+                    )
+                    + " "
+                )
+
+            if instance.subject:
+                description += (
+                    _("The subject is changed to {subject}.").format(subject=instance.subject.name)
+                    + " "
+                )
+
+            if instance.room:
+                description += (
+                    _("The lesson is moved from {old} to {new}.").format(
+                        old=instance.lesson_period.room.name,
+                        new=instance.room.name,
+                    )
+                    + " "
+                )
+
+        if instance.comment:
+            description += (
+                _("There is an additional comment: {comment}.").format(comment=instance.comment)
+                + " "
+            )
+
+    elif isinstance(instance, Event):
+        if instance.date_start != instance.date_end:
+            description += (
+                _(
+                    "There is an event that starts on {date_start}, {period_from}. period "
+                    "and ends on {date_end}, {period_to}. period:"
+                ).format(
+                    date_start=date_format(instance.date_start),
+                    date_end=date_format(instance.date_end),
+                    period_from=instance.period_from.period,
+                    period_to=instance.period_to.period,
+                )
+                + "\n"
+            )
+        else:
+            description += (
+                _(
+                    "There is an event on {date} from the "
+                    "{period_from}. period to the {period_to}. period:"
+                ).format(
+                    date=date_format(instance.date_start),
+                    period_from=instance.period_from.period,
+                    period_to=instance.period_to.period,
+                )
+                + "\n"
+            )
+
+        if instance.groups.all():
+            description += _("Groups: {groups}").format(groups=instance.group_names) + "\n"
+        if instance.teachers.all():
+            description += _("Teachers: {teachers}").format(teachers=instance.teacher_names) + "\n"
+        if instance.rooms.all():
+            description += (
+                _("Rooms: {rooms}").format(
+                    rooms=", ".join([room.name for room in instance.rooms.all()])
+                )
+                + "\n"
+            )
+    elif isinstance(instance, ExtraLesson):
+        description += (
+            _("There is an extra lesson on {date} in the {period}. period:").format(
+                date=date_format(instance.date),
+                period=instance.period.period,
+            )
+            + "\n"
+        )
+
+        if instance.groups.all():
+            description += _("Groups: {groups}").format(groups=instance.group_names) + "\n"
+        if instance.room:
+            description += _("Subject: {subject}").format(subject=instance.subject.name) + "\n"
+        if instance.teachers.all():
+            description += _("Teachers: {teachers}").format(teachers=instance.teacher_names) + "\n"
+        if instance.room:
+            description += _("Room: {room}").format(room=instance.room.name) + "\n"
+        if instance.comment:
+            description += _("Comment: {comment}.").format(comment=instance.comment) + "\n"
+    elif isinstance(instance, SupervisionSubstitution):
+        description += _(
+            "The supervision of {old} on {date} between the {period_from}. period "
+            "and the {period_to}. period in the area {area} is substituted by {new}."
+        ).format(
+            old=instance.supervision.teacher.full_name,
+            date=date_format(instance.date),
+            period_from=instance.supervision.break_item.after_period_number,
+            period_to=instance.supervision.break_item.before_period_number,
+            area=instance.supervision.area.name,
+            new=instance.teacher.full_name,
+        )
+
+    day = instance.date if hasattr(instance, "date") else instance.date_start
+
+    url = urljoin(
+        settings.BASE_URL,
+        reverse(
+            "my_timetable_by_date",
+            args=[day.year, day.month, day.day],
+        ),
+    )
+
+    for recipient in recipients:
+        if recipient.preferences["chronos__send_notifications"]:
+            n = Notification(
+                recipient=recipient,
+                sender=_("Timetable"),
+                title=_("There are current changes to your timetable."),
+                description=description,
+                link=url,
+            )
+            n.save()