diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListController.java b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListController.java
index f50283063accdbbba09e15a825188e7f2be4f790..69e05f468aed54dc22c83229007da45bbb0572b8 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListController.java
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/AppointmentListController.java
@@ -35,6 +35,8 @@ import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.date.DateComponentFactory;
 import org.olat.core.gui.components.date.DateElement;
+import org.olat.core.gui.components.dropdown.DropdownItem;
+import org.olat.core.gui.components.dropdown.DropdownOrientation;
 import org.olat.core.gui.components.form.flexible.FormItem;
 import org.olat.core.gui.components.form.flexible.FormItemContainer;
 import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement;
@@ -89,7 +91,8 @@ public abstract class AppointmentListController extends FormBasicController impl
 	private static final String CMD_EDIT = "edit";
 	
 	private FormLink backLink;
-	private FormLink addAppointmentLink;
+	private FormLink addSingleAppointmentLink;
+	private FormLink addRecurringAppointmentLink;
 	private FlexiTableElement tableEl;
 	private AppointmentDataModel dataModel;
 	
@@ -98,6 +101,7 @@ public abstract class AppointmentListController extends FormBasicController impl
 	private DialogBoxController confirmParticipationCrtl;
 	private FindingConfirmationController findingConfirmationCtrl;
 	private AppointmentEditController appointmentEditCtrl;
+	private RecurringAppointmentsController recurringAppointmentsCtrl;
 	private UserSearchController userSearchCtrl;
 	private ParticipationRemoveController removeCtrl;
 	private AppointmentDeleteController appointmentDeleteCtrl;
@@ -135,8 +139,8 @@ public abstract class AppointmentListController extends FormBasicController impl
 	protected abstract List<AppointmentRow> loadModel();
 	
 	protected void setAddAppointmentVisible(boolean visible) {
-		if (addAppointmentLink != null) {
-			addAppointmentLink.setVisible(visible);
+		if (addSingleAppointmentLink != null) {
+			addSingleAppointmentLink.setVisible(visible);
 		}
 	}
 	
@@ -165,8 +169,13 @@ public abstract class AppointmentListController extends FormBasicController impl
 			
 			List<Organizer> organizers = appointmentsService.getOrganizers(topic);
 			if (secCallback.canEditAppointment(organizers)) {
-				addAppointmentLink = uifactory.addFormLink("add.appointment", topButtons, Link.BUTTON);
-				addAppointmentLink.setIconLeftCSS("o_icon o_icon-lg o_icon_add");
+				DropdownItem addAppointmentDropdown = uifactory.addDropdownMenu("add.appointment", "add.appointment", topButtons, getTranslator());
+				addAppointmentDropdown.setOrientation(DropdownOrientation.right);
+				
+				addSingleAppointmentLink = uifactory.addFormLink("add.appointment.single", formLayout, Link.LINK);
+				addAppointmentDropdown.addElement(addSingleAppointmentLink);
+				addRecurringAppointmentLink = uifactory.addFormLink("add.appointment.recurring", formLayout, Link.LINK);
+				addAppointmentDropdown.addElement(addRecurringAppointmentLink);
 			}
 		}
 		
@@ -267,7 +276,7 @@ public abstract class AppointmentListController extends FormBasicController impl
 	}
 	
 	private void initSorters() {
-		List<FlexiTableSort> sorters = new ArrayList<>(8);
+		List<FlexiTableSort> sorters = new ArrayList<>(2);
 		sorters.add(new FlexiTableSort(translate(AppointmentCols.start.i18nHeaderKey()), AppointmentCols.start.name()));
 		sorters.add(new FlexiTableSort(translate(AppointmentCols.numberOfParticipations.i18nHeaderKey()), AppointmentCols.numberOfParticipations.name()));
 		FlexiTableSortOptions options = new FlexiTableSortOptions(sorters);
@@ -396,8 +405,10 @@ public abstract class AppointmentListController extends FormBasicController impl
 	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
 		if (source == backLink) {
 			fireEvent(ureq, Event.DONE_EVENT);
-		} else if (source == addAppointmentLink) {
-			doAddAppointment(ureq);
+		} else if (source == addSingleAppointmentLink) {
+			doAddSingleAppointment(ureq);
+		} else if (source == addRecurringAppointmentLink) {
+			doAddRecurringAppointment(ureq);
 		} else if (source instanceof FormLink) {
 			FormLink link = (FormLink)source;
 			String cmd = link.getCmd();
@@ -445,6 +456,12 @@ public abstract class AppointmentListController extends FormBasicController impl
 			}
 			cmc.deactivate();
 			cleanUp();
+		} else if (recurringAppointmentsCtrl == source) {
+			if (event == Event.DONE_EVENT) {
+				updateModel();
+			}
+			cmc.deactivate();
+			cleanUp();
 		} else if (appointmentDeleteCtrl == source) {
 			if (event == Event.DONE_EVENT) {
 				updateModel();
@@ -481,12 +498,14 @@ public abstract class AppointmentListController extends FormBasicController impl
 	}
 	
 	private void cleanUp() {
+		removeAsListenerAndDispose(recurringAppointmentsCtrl);
 		removeAsListenerAndDispose(findingConfirmationCtrl);
 		removeAsListenerAndDispose(appointmentDeleteCtrl);
 		removeAsListenerAndDispose(appointmentEditCtrl);
 		removeAsListenerAndDispose(userSearchCtrl);
 		removeAsListenerAndDispose(removeCtrl);
 		removeAsListenerAndDispose(cmc);
+		recurringAppointmentsCtrl = null;
 		findingConfirmationCtrl = null;
 		appointmentDeleteCtrl = null;
 		appointmentEditCtrl = null;
@@ -528,7 +547,7 @@ public abstract class AppointmentListController extends FormBasicController impl
 		}
 	}
 
-	private void doAddAppointment(UserRequest ureq) {
+	private void doAddSingleAppointment(UserRequest ureq) {
 		appointmentEditCtrl = new AppointmentEditController(ureq, getWindowControl(), topic);
 		listenTo(appointmentEditCtrl);
 		
@@ -538,6 +557,16 @@ public abstract class AppointmentListController extends FormBasicController impl
 		cmc.activate();
 	}
 
+	private void doAddRecurringAppointment(UserRequest ureq) {
+		recurringAppointmentsCtrl = new RecurringAppointmentsController(ureq, getWindowControl(), topic);
+		listenTo(recurringAppointmentsCtrl);
+		
+		cmc = new CloseableModalController(getWindowControl(), "close", recurringAppointmentsCtrl.getInitialComponent(), true,
+				translate("add.appointment.recurring"));
+		listenTo(cmc);
+		cmc.activate();
+	}
+
 	private void doEditAppointment(UserRequest ureq, Appointment appointment) {
 		appointmentEditCtrl = new AppointmentEditController(ureq, getWindowControl(), appointment);
 		listenTo(appointmentEditCtrl);
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/RecurringAppointmentsController.java b/src/main/java/org/olat/course/nodes/appointments/ui/RecurringAppointmentsController.java
new file mode 100644
index 0000000000000000000000000000000000000000..8fee5eb9fa53b5cb743a26c2ef4864342d1f3c3c
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/RecurringAppointmentsController.java
@@ -0,0 +1,208 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.nodes.appointments.ui;
+
+import static org.olat.core.gui.components.util.KeyValues.entry;
+
+import java.time.DayOfWeek;
+import java.time.format.TextStyle;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.DateChooser;
+import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
+import org.olat.core.gui.components.form.flexible.elements.TextElement;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.components.util.KeyValues;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.util.DateUtils;
+import org.olat.core.util.StringHelper;
+import org.olat.course.nodes.appointments.Appointment;
+import org.olat.course.nodes.appointments.AppointmentsService;
+import org.olat.course.nodes.appointments.Topic;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 26 Jun 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class RecurringAppointmentsController extends FormBasicController {
+	
+	private TextElement locationEl;
+	private TextElement maxParticipationsEl;
+	private DateChooser recurringFirstEl;
+	private MultipleSelectionElement recurringDaysOfWeekEl;
+	private DateChooser recurringLastEl;
+
+	private final Topic topic;
+	
+	@Autowired
+	private AppointmentsService appointmentsService;
+
+	public RecurringAppointmentsController(UserRequest ureq, WindowControl wControl, Topic topic) {
+		super(ureq, wControl);
+		this.topic = topic;
+		
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		locationEl = uifactory.addTextElement("appointment.location", 128, null, formLayout);
+		locationEl.setHelpTextKey("appointment.init.value", null);
+		
+		maxParticipationsEl = uifactory.addTextElement("appointment.max.participations", 5, null, formLayout);
+		maxParticipationsEl.setHelpTextKey("appointment.init.value", null);
+		
+		recurringFirstEl = uifactory.addDateChooser("appointments.recurring.first", null, formLayout);
+		recurringFirstEl.setDateChooserTimeEnabled(true);
+		recurringFirstEl.setSecondDate(true);
+		recurringFirstEl.setSameDay(true);
+		recurringFirstEl.setMandatory(true);
+		
+		DayOfWeek[] dayOfWeeks = DayOfWeek.values();
+		KeyValues dayOfWeekKV = new KeyValues();
+		for (int i = 0; i < dayOfWeeks.length; i++) {
+			dayOfWeekKV.add(entry(dayOfWeeks[i].name(), dayOfWeeks[i].getDisplayName(TextStyle.FULL_STANDALONE, getLocale())));
+		}
+		recurringDaysOfWeekEl = uifactory.addCheckboxesHorizontal("appointments.recurring.days.of.week", formLayout,
+				dayOfWeekKV.keys(), dayOfWeekKV.values());
+		recurringDaysOfWeekEl.setMandatory(true);
+		
+		recurringLastEl = uifactory.addDateChooser("appointments.recurring.last", null, formLayout);
+		recurringLastEl.setMandatory(true);
+		
+		// Buttons
+		FormLayoutContainer buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
+		formLayout.add(buttonsCont);
+		buttonsCont.setRootForm(mainForm);
+		uifactory.addFormSubmitButton("save", buttonsCont);
+		uifactory.addFormCancelButton("cancel", buttonsCont, ureq, getWindowControl());
+	}
+
+	@Override
+	protected boolean validateFormLogic(UserRequest ureq) {
+		boolean allOk = super.validateFormLogic(ureq);
+		
+		maxParticipationsEl.clearError();
+		String maxParticipationsValue = maxParticipationsEl.getValue();
+		if (maxParticipationsEl.isVisible() && StringHelper.containsNonWhitespace(maxParticipationsValue)) {
+			try {
+				int value = Integer.parseInt(maxParticipationsValue);
+				if (value < 1) {
+					maxParticipationsEl.setErrorKey("error.positiv.number", null);
+					allOk &= false;
+				}
+			} catch (NumberFormatException e) {
+				maxParticipationsEl.setErrorKey("error.positiv.number", null);
+				allOk &= false;
+			}
+		}
+		
+		recurringFirstEl.clearError();
+		recurringDaysOfWeekEl.clearError();
+		recurringLastEl.clearError();
+		if (recurringFirstEl.getDate() == null || recurringFirstEl.getSecondDate() == null) {
+			recurringFirstEl.setErrorKey("form.legende.mandatory", null);
+			allOk &= false;
+		} else if (recurringFirstEl.getDate().after(recurringFirstEl.getSecondDate())) {
+			recurringFirstEl.setErrorKey("error.start.after.end", null);
+			allOk &= false;
+		}
+		
+		if (!recurringDaysOfWeekEl.isAtLeastSelected(1)) {
+			recurringDaysOfWeekEl.setErrorKey("form.legende.mandatory", null);
+			allOk &= false;
+		}
+		
+		if (recurringLastEl.getDate() == null) {
+			recurringLastEl.setErrorKey("form.legende.mandatory", null);
+			allOk &= false;
+		}
+		
+		if (recurringFirstEl.getDate() != null && recurringLastEl.getDate() != null
+				&& recurringFirstEl.getDate().after(recurringLastEl.getDate())) {
+			recurringLastEl.setErrorKey("error.first.after.start", null);
+			allOk &= false;
+		}
+		
+		return allOk;
+	}
+
+	@Override
+	protected void formCancelled(UserRequest ureq) {
+		fireEvent(ureq, Event.CANCELLED_EVENT);
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		doSaveReccuringAppointments();
+		fireEvent(ureq, Event.DONE_EVENT);
+	}
+
+	private void doSaveReccuringAppointments() {
+		Date firstStart = recurringFirstEl.getDate();
+		Date firstEnd = recurringFirstEl.getSecondDate();
+		Date last = recurringLastEl.getDate();
+		last = DateUtils.setTime(last, 23, 59, 59);
+		
+		Collection<DayOfWeek> daysOfWeek = recurringDaysOfWeekEl.getSelectedKeys().stream()
+				.map(DayOfWeek::valueOf)
+				.collect(Collectors.toList());
+		
+		List<Date> starts = DateUtils.getDaysInRange(firstStart, last, daysOfWeek);
+		for (Date start : starts) {
+			Appointment appointment = appointmentsService.createUnsavedAppointment(topic);
+			
+			appointment.setStart(start);
+			
+			Date end = DateUtils.copyTime(start, firstEnd);
+			appointment.setEnd(end);
+			
+			String location = locationEl.getValue();
+			appointment.setLocation(location);
+			
+			if (maxParticipationsEl.isVisible()) {
+				String maxParticipationsValue = maxParticipationsEl.getValue();
+				Integer maxParticipations = StringHelper.containsNonWhitespace(maxParticipationsValue)
+						? Integer.valueOf(maxParticipationsValue)
+						: null;
+				appointment.setMaxParticipations(maxParticipations);
+			}
+			
+			appointmentsService.saveAppointment(appointment);
+		}
+	}
+	
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+}
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties
index f0a3c1491454aa3383785f7c3bebd93a9c989679..7162825d087b256c9b6861e6bfcc4623a25b2582 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_de.properties
@@ -1,5 +1,7 @@
 add.appointment=Termin hinzuf\u00fcgen
 add.appointment.button=Hinzuf\u00fcgen
+add.appointment.recurring=Wiederkehrende Termine hinzuf\u00fcgen
+add.appointment.single=Einzelnen Termin hinzuf\u00fcgen
 add.appointment.title=Termin hinzuf\u00fcgen
 add.topic=Termine hinzuf\u00fcgen
 add.topic.title=$:\add.topic
diff --git a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties
index 726d3b9a9ab55697fad162b876d0fc52ffac3ba0..521802b15bec902e21889f9f67f77a5bd00d30a3 100644
--- a/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/nodes/appointments/ui/_i18n/LocalStrings_en.properties
@@ -1,5 +1,7 @@
 add.appointment=Add appointment
 add.appointment.button=Add
+add.appointment.recurring=Add recurring appointments
+add.appointment.single=Add single appointment
 add.appointment.title=Add appointment
 add.topic=Add appointments
 add.topic.title=$:\add.topic