diff --git a/hc/front/forms.py b/hc/front/forms.py
index 046e9575..6d1d6769 100644
--- a/hc/front/forms.py
+++ b/hc/front/forms.py
@@ -85,9 +85,11 @@ class NameTagsForm(forms.Form):
 
 
 class AddCheckForm(NameTagsForm):
-    kind = forms.ChoiceField(choices=(("simple", "simple"), ("cron", "cron")))
+    kind = forms.ChoiceField(
+        choices=(("simple", "simple"), ("cron", "cron"), ("oncalendar", "oncalendar"))
+    )
     timeout = forms.IntegerField(min_value=60, max_value=31536000)
-    schedule = forms.CharField(max_length=100, validators=[CronValidator()])
+    schedule = forms.CharField(required=False, max_length=100)
     tz = forms.CharField(max_length=36, validators=[TimezoneValidator()])
     grace = forms.IntegerField(min_value=60, max_value=31536000)
 
@@ -97,6 +99,21 @@ class AddCheckForm(NameTagsForm):
     def clean_grace(self) -> td:
         return td(seconds=self.cleaned_data["grace"])
 
+    def clean_schedule(self):
+        kind = self.cleaned_data.get("kind")
+        validator = None
+        if kind == "cron":
+            validator = CronValidator()
+        elif kind == "oncalendar":
+            validator = OnCalendarValidator()
+        else:
+            # If kind is not cron or oncalendar, ignore the passed in value
+            # and use "* * * * *" instead.
+            return "* * * * *"
+
+        validator(self.cleaned_data["schedule"])
+        return self.cleaned_data["schedule"]
+
 
 class FilteringRulesForm(forms.Form):
     filter_subject = forms.BooleanField(required=False)
diff --git a/hc/front/tests/test_add_check.py b/hc/front/tests/test_add_check.py
index 0dc00343..6a44f7e7 100644
--- a/hc/front/tests/test_add_check.py
+++ b/hc/front/tests/test_add_check.py
@@ -19,7 +19,6 @@ class AddCheckTestCase(BaseTestCase):
             "kind": "simple",
             "timeout": "120",
             "grace": "60",
-            "schedule": "* * * * *",
             "tz": "Europe/Riga",
         }
         payload.update(kwargs)
@@ -50,11 +49,24 @@ class AddCheckTestCase(BaseTestCase):
 
     def test_it_saves_cron_schedule(self) -> None:
         self.client.login(username="alice@example.org", password="password")
-        r = self.client.post(self.url, self._payload(kind="cron"))
+        r = self.client.post(self.url, self._payload(kind="cron", schedule="0 0 * * *"))
 
         check = Check.objects.get()
         self.assertEqual(check.project, self.project)
         self.assertEqual(check.kind, "cron")
+        self.assertEqual(check.schedule, "0 0 * * *")
+
+        self.assertRedirects(r, self.redirect_url)
+
+    def test_it_saves_oncalendar_schedule(self) -> None:
+        self.client.login(username="alice@example.org", password="password")
+        payload = self._payload(kind="oncalendar", schedule="12:34")
+        r = self.client.post(self.url, payload)
+
+        check = Check.objects.get()
+        self.assertEqual(check.project, self.project)
+        self.assertEqual(check.kind, "oncalendar")
+        self.assertEqual(check.schedule, "12:34")
 
         self.assertRedirects(r, self.redirect_url)
 
@@ -81,12 +93,21 @@ class AddCheckTestCase(BaseTestCase):
 
     def test_it_validates_cron_expression(self) -> None:
         self.client.login(username="alice@example.org", password="password")
-        r = self.client.post(self.url, self._payload(schedule="* * *"))
+        r = self.client.post(self.url, self._payload(kind="cron", schedule="* * *"))
         self.assertEqual(r.status_code, 400)
 
     def test_it_validates_cron_expression_with_no_matches(self) -> None:
         self.client.login(username="alice@example.org", password="password")
-        r = self.client.post(self.url, self._payload(schedule="* * */100 * MON#2"))
+        r = self.client.post(
+            self.url, self._payload(kind="cron", schedule="* * */100 * MON#2")
+        )
+        self.assertEqual(r.status_code, 400)
+
+    def test_it_validates_oncalendar_expression(self) -> None:
+        self.client.login(username="alice@example.org", password="password")
+        r = self.client.post(
+            self.url, self._payload(kind="oncalendar", schedule="12:345")
+        )
         self.assertEqual(r.status_code, 400)
 
     def test_it_validates_tz(self) -> None:
diff --git a/hc/front/tests/test_validate_schedule.py b/hc/front/tests/test_validate_schedule.py
index fc332e05..3145f9f4 100644
--- a/hc/front/tests/test_validate_schedule.py
+++ b/hc/front/tests/test_validate_schedule.py
@@ -10,16 +10,28 @@ class ValidateScheduleTestCase(BaseTestCase):
         super().setUp()
         self.url = "/checks/validate_schedule/"
 
-    def _url(self, schedule: str) -> str:
-        return "/checks/validate_schedule/?" + urlencode({"schedule": schedule})
+    def _url(self, schedule: str, kind: str) -> str:
+        params = {"schedule": schedule, "kind": kind}
+        return "/checks/validate_schedule/?" + urlencode(params)
 
-    def test_it_works(self) -> None:
-        r = self.client.get(self._url("* * * * *"))
+    def test_it_validates_cron_schedule(self) -> None:
+        r = self.client.get(self._url("* * * * *", "cron"))
+        self.assertEqual(r.status_code, 200)
+        self.assertTrue(r.json()["result"])
+
+    def test_it_validates_oncalendar_schedule(self) -> None:
+        r = self.client.get(self._url("12:34", "oncalendar"))
         self.assertEqual(r.status_code, 200)
         self.assertTrue(r.json()["result"])
 
     def test_it_rejects_bad_schedules(self) -> None:
+        # Bad cron schedules
         for v in ["a", "* * * *", "1-1000 * * * *", "0 0 */100 * MON#2"]:
-            r = self.client.get(self._url(v))
+            r = self.client.get(self._url(v, "cron"))
+            self.assertEqual(r.status_code, 200)
+            self.assertFalse(r.json()["result"])
+        # Bad oncalendar schedules
+        for v in ["12:345"]:
+            r = self.client.get(self._url(v, "oncalendar"))
             self.assertEqual(r.status_code, 200)
             self.assertFalse(r.json()["result"])
diff --git a/hc/front/views.py b/hc/front/views.py
index 6c849085..34cf0173 100644
--- a/hc/front/views.py
+++ b/hc/front/views.py
@@ -665,14 +665,23 @@ def oncalendar_preview(request: HttpRequest) -> HttpResponse:
 
 
 def validate_schedule(request: HttpRequest) -> HttpResponse:
+    kind = request.GET.get("kind", "")
+    iterator: type[CronSim] | type[OnCalendar]
+    if kind == "cron":
+        iterator = CronSim
+    elif kind == "oncalendar":
+        iterator = OnCalendar
+    else:
+        return HttpResponseBadRequest()
+
     schedule = request.GET.get("schedule", "")
     result = True
     try:
-        # Does cronsim accept the schedule?
-        it = CronSim(schedule, now())
+        # Does cronsim/oncalendar accept the schedule?
+        it = iterator(schedule, now())
         # Can it calculate the next datetime?
         next(it)
-    except (CronSimError, StopIteration):
+    except (CronSimError, OnCalendarError, StopIteration):
         result = False
 
     return JsonResponse({"result": result})
diff --git a/static/css/checks.css b/static/css/checks.css
index a9c3f81c..46fcd46b 100644
--- a/static/css/checks.css
+++ b/static/css/checks.css
@@ -81,16 +81,27 @@
     margin-bottom: 20px;
 }
 
+/* Add Check Modal */
+
 #add-check-modal .modal-body {
     padding: 15px 40px;
 }
 
-.simple .create-cron-extra {
+#add-check-kind label {
+    display: inline-block;
+    margin-right: 20px;
+}
+
+.create-simple-extra,
+.create-cron-extra,
+.create-oncalendar-extra {
     display: none;
 }
 
-.cron .create-simple-extra {
-    display: none;
+.simple .create-simple-extra,
+.cron .create-cron-extra,
+.oncalendar .create-oncalendar-extra {
+    display: block;
 }
 
 #add-check-period,
diff --git a/static/js/add-check-modal.js b/static/js/add-check-modal.js
index 95d2b78b..b2c9d487 100644
--- a/static/js/add-check-modal.js
+++ b/static/js/add-check-modal.js
@@ -3,7 +3,8 @@ $(function () {
     var modal = $("#add-check-modal");
     var period = document.getElementById("add-check-period");
     var periodUnit = document.getElementById("add-check-period-unit");
-    var scheduleField = document.getElementById("add-check-schedule");
+    var cronField = document.getElementById("add-check-schedule");
+    var onCalendarField = document.getElementById("add-check-schedule-oncalendar");
     var grace = document.getElementById("add-check-grace");
     var graceUnit = document.getElementById("add-check-grace-unit");
 
@@ -25,15 +26,14 @@ $(function () {
 
     function updateScheduleExtras() {
         var kind = $('#add-check-modal input[name=kind]:checked').val();
-        modal.removeClass("cron").removeClass("simple").addClass(kind);
-
-        if (kind == "simple" && !scheduleField.checkValidity()) {
-            scheduleField.setCustomValidity("");
-            scheduleField.value = "* * * * *";
-        }
+        modal.removeClass("simple").removeClass("cron").removeClass("oncalendar").addClass(kind);
+        // Include cron schedule in POST data only if kind = "cron"
+        cronField.disabled = kind != "cron";
+        // Include OnCalendar schedule in POST data only if kind = "oncalendar"
+        onCalendarField.disabled = kind != "oncalendar";
     }
 
-    // Show and hide fields when user clicks simple/cron radio buttons
+    // Show and hide fields when user clicks simple/cron/oncalendar radio buttons
     $("#add-check-modal input[type=radio][name=kind]").change(updateScheduleExtras);
 
     modal.on("shown.bs.modal", function() {
@@ -69,22 +69,26 @@ $(function () {
 
     var currentSchedule = "";
     function validateSchedule() {
-        var schedule = scheduleField.value;
+        var kind = $('#add-check-modal input[name=kind]:checked').val();
+        if (kind == "simple") return;
+
+        var field = kind == "cron" ? cronField : onCalendarField;
 
         // Return early if the schedule has not changed
-        if (schedule == currentSchedule)
+        if (field.value == currentSchedule)
             return;
 
-        currentSchedule = schedule;
+        currentSchedule = field.value;
         var token = $('input[name=csrfmiddlewaretoken]').val();
-        $.getJSON(base + "/checks/validate_schedule/", {schedule: schedule}, function(data) {
-            if (schedule != currentSchedule)
+        var payload = {kind: kind, schedule: field.value};
+        $.getJSON(base + "/checks/validate_schedule/", payload, function(data) {
+            if (field.value != currentSchedule)
                 return;  // ignore stale results
 
-            scheduleField.setCustomValidity(data.result ? "" : "Please enter a valid cron expression");
+            field.setCustomValidity(data.result ? "" : "Please enter a valid expression");
         });
     }
 
     $("#add-check-schedule").on("keyup change", validateSchedule);
-
+    $("#add-check-schedule-oncalendar").on("keyup change", validateSchedule);
 });
diff --git a/templates/front/add_check_modal.html b/templates/front/add_check_modal.html
index 294c1a60..61f0444c 100644
--- a/templates/front/add_check_modal.html
+++ b/templates/front/add_check_modal.html
@@ -72,7 +72,7 @@
                         <label class="col-sm-3 control-label">
                             Schedule
                         </label>
-                        <div class="col-sm-9">
+                        <div id="add-check-kind" class="col-sm-9">
                             <label class="radio-container">
                                 <input type="radio" name="kind" value="simple" checked>
                                 <span class="radiomark"></span>
@@ -83,6 +83,11 @@
                                 <span class="radiomark"></span>
                                 Cron
                             </label>
+                            <label class="radio-container">
+                                <input type="radio" name="kind" value="oncalendar">
+                                <span class="radiomark"></span>
+                                OnCalendar
+                            </label>
                         </div>
                     </div>
 
@@ -126,7 +131,21 @@
                         </div>
                     </div>
 
-                    <div class="form-group create-cron-extra">
+                    <div class="form-group create-oncalendar-extra">
+                        <label for="add-check-schedule-oncalendar" class="col-sm-3 control-label">
+                            OnCalendar Expression(s)
+                        </label>
+                        <div class="col-sm-9">
+                            <textarea
+                                id="add-check-schedule-oncalendar"
+                                name="schedule"
+                                class="form-control"
+                                rows="3"
+                                maxlength="100">*-*-* *:*:*</textarea>
+                        </div>
+                    </div>
+
+                    <div class="form-group create-cron-extra create-oncalendar-extra">
                         <label for="add-check-tz" class="col-sm-3 control-label">
                             Time Zone
                         </label>
diff --git a/templates/front/details.html b/templates/front/details.html
index 72aa5ab7..9f656d19 100644
--- a/templates/front/details.html
+++ b/templates/front/details.html
@@ -176,7 +176,7 @@
                     </td>
                     {% endif %}
                 </tr>
-                {% if check.kind == "cron" %}
+                {% if check.kind == "cron" or check.kind == "oncalendar" %}
                 <tr>
                     <th>Time Zone</th>
                     <td>