diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py b/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py
index 550efab2b7be2627304ce24c24f4cf95cd5cb9c0..910fc96e2c37ba32e21546ed87935083b3bba7a9 100644
--- a/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py
+++ b/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py
@@ -158,14 +158,23 @@ def can_run_within_timewindow_with_time_constraints(scheduling_unit: models.Sche
              constraints are met over the runtime of the observation, else False.
     """
     main_observation_task_name = get_target_observation_task_name_from_requirements_doc(scheduling_unit)
-    duration = timedelta(
-        seconds=scheduling_unit.requirements_doc['tasks'][main_observation_task_name]['specifications_doc']['duration'])
-    window_lower_bound = lower_bound
-    while window_lower_bound + duration < upper_bound:
-        window_upper_bound = window_lower_bound + duration
-        if can_run_anywhere_within_timewindow_with_time_constraints(scheduling_unit, window_lower_bound, window_upper_bound):
-            return True
-        window_lower_bound += min(timedelta(hours=1), upper_bound - window_lower_bound)
+    constraints = scheduling_unit.draft.scheduling_constraints_doc
+
+    # Check the 'at' constraint and then only check can_run_anywhere for the single possible time window
+    if 'at' in constraints['time']:
+        at = parser.parse(constraints['time']['at'], ignoretz=True)
+        if (at >= lower_bound and at + scheduling_unit.duration <= upper_bound):    # todo: suggestion: use scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['duration']
+            return can_run_anywhere_within_timewindow_with_time_constraints(scheduling_unit, lower_bound=at,
+                                                                            upper_bound=at + scheduling_unit.duration)
+    else:
+        duration = timedelta(
+            seconds=scheduling_unit.requirements_doc['tasks'][main_observation_task_name]['specifications_doc']['duration'])
+        window_lower_bound = lower_bound
+        while window_lower_bound + duration <= upper_bound:
+            window_upper_bound = window_lower_bound + duration
+            if can_run_anywhere_within_timewindow_with_time_constraints(scheduling_unit, window_lower_bound, window_upper_bound):
+                return True
+            window_lower_bound += min(timedelta(hours=1), upper_bound - window_lower_bound)
 
     return False
 
@@ -176,25 +185,21 @@ def can_run_anywhere_within_timewindow_with_time_constraints(scheduling_unit: mo
     i.e. the time constraints must be met over the full time window.
     :return: True if all time constraints are met over the entire time window, else False.
     """
-    can_run_at = True
     can_run_before = True
     can_run_with_after = True
     can_run_between = True
     can_run_not_between = True
     constraints = scheduling_unit.draft.scheduling_constraints_doc
 
-    # TODO TMSS-672 Move to can_run_within and make logic correct
-    if has_manual_scheduler_constraint(scheduling_unit):
-        at = parser.parse(constraints['time']['at'], ignoretz=True)
-        can_run_at = (at >= lower_bound and at+scheduling_unit.duration <= upper_bound) # todo: suggestion: use scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['duration']
-
+    # given time window needs to end before constraint
     if 'before' in constraints['time']:
         before = parser.parse(constraints['time']['before'], ignoretz=True)
-        can_run_before = (before <= upper_bound-scheduling_unit.duration)   # todo: suggestion: use scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['duration']
+        can_run_before = (upper_bound < before)
 
+    # given time window needs to start after constraint
     if 'after' in constraints['time']:
         after = parser.parse(constraints['time']['after'], ignoretz=True)
-        can_run_with_after = (lower_bound >= after)
+        can_run_with_after = (lower_bound > after)
 
     # Run within one of these time windows
     if 'between' in constraints['time']:
@@ -202,9 +207,9 @@ def can_run_anywhere_within_timewindow_with_time_constraints(scheduling_unit: mo
         for between in constraints['time']['between']:
             time_from = parser.parse(between["from"], ignoretz=True)
             time_to = parser.parse(between["to"], ignoretz=True)
-            if time_from >= lower_bound and time_to <= upper_bound:
+            if time_from <= lower_bound and time_to >= upper_bound:
                 can_run_between = True
-                break  # something inside the boundary so True and don't look any further
+                break  # constraint window completely covering the boundary, so True and don't look any further
             else:
                 can_run_between = False
 
@@ -216,11 +221,11 @@ def can_run_anywhere_within_timewindow_with_time_constraints(scheduling_unit: mo
             time_to = parser.parse(not_between["to"], ignoretz=True)
             if time_from <= upper_bound and time_to >= lower_bound:
                 can_run_not_between = False
-                break  # something outside the boundary so False and don't look any further
+                break  # constraint window at least partially inside the boundary, so False and don't look any further
             else:
                 can_run_not_between = True
 
-    return can_run_at & can_run_before & can_run_with_after & can_run_between & can_run_not_between
+    return can_run_before & can_run_with_after & can_run_between & can_run_not_between
 
 
 def can_run_within_timewindow_with_sky_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool:
@@ -233,7 +238,7 @@ def can_run_within_timewindow_with_sky_constraints(scheduling_unit: models.Sched
             if 'duration' in task['specifications_doc']:
                 duration = timedelta(seconds=task['specifications_doc']['duration'])
                 window_lower_bound = lower_bound
-                while window_lower_bound + duration < upper_bound:
+                while window_lower_bound + duration <= upper_bound:
                     window_upper_bound = window_lower_bound + duration
                     if can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit, window_lower_bound, window_upper_bound):
                         return True
@@ -309,7 +314,7 @@ def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBluep
     main_observation_task_name = get_target_observation_task_name_from_requirements_doc(scheduling_unit)
     duration = timedelta(seconds=scheduling_unit.requirements_doc['tasks'][main_observation_task_name]['specifications_doc']['duration'])
     try:
-        if has_manual_scheduler_constraint(scheduling_unit) and 'at' in constraints['time']:
+        if 'at' in constraints['time']:
             at = parser.parse(constraints['time']['at'], ignoretz=True)
             return max(lower_bound, at)
 
@@ -383,10 +388,10 @@ def compute_scores(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound:
     # TODO: TMSS-244 (and more?), compute score using the constraints in constraints['time']
     # TODO: TMSS-245 TMSS-250 (and more?),  compute score using the constraints in constraints['sky']
 
-    # for now (as a proof of concept and sort of example), just return 1's
+    # for now (as a proof of concept and sort of example), just return 1's. Return 1000 (placeholder value, change later) if the 'at' constraint is in, so it gets prioritised.
     scores = {'daily': 1.0,
-              'time': 1.0,
-              'sky': 1.0 }
+              'time': 1000.0 if ('at' in constraints['time'] and constraints['time']['at'] is not None) else 1.0,
+              'sky': 1.0}
 
     # add "common" scores which do not depend on constraints, such as project rank and creation date
     # TODO: should be normalized!
diff --git a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py
index e8fadb2c6085117007f7913c8ecee0fa3808b434..bcd9f1fb6aa1d3dbbed8334c186dd3f53cb1e161 100755
--- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py
+++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py
@@ -33,6 +33,7 @@ if skip_integration_tests():
 TEST_UUID = uuid.uuid1()
 
 from datetime import datetime, timedelta
+from lofar.common.datetimeutils import round_to_second_precision
 from lofar.common.json_utils import get_default_json_object_for_schema, add_defaults_to_json_object_for_schema
 from lofar.messaging.messagebus import TemporaryExchange, BusListenerJanitor
 
@@ -136,6 +137,76 @@ class TestDynamicScheduling(TestCase):  # Note: we use django.test.TestCase inst
                                                          scheduling_constraints_doc=constraints,
                                                          scheduling_constraints_template=constraints_template)
 
+    def test_simple_observation_with_at_constraint(self):
+        """
+        Test a simple observation with the 'at' constraint
+        """
+        scheduling_set = models.SchedulingSet.objects.create(**SchedulingSet_test_data())
+        scheduling_unit_draft = self.create_simple_observation_scheduling_unit('scheduling_unit for at constraint', scheduling_set=scheduling_set)
+        # Clear constraints
+        scheduling_unit_draft.scheduling_constraints_doc['sky'] = {}
+        scheduling_unit_draft.scheduling_constraints_doc['time']["between"] = []
+        scheduling_unit_draft.scheduling_constraints_doc['time']["not_between"] = []
+        scheduling_unit_draft.scheduling_constraints_doc['time'].pop('at', None)
+        scheduling_unit_draft.scheduling_constraints_doc['time'].pop("before", None)
+        scheduling_unit_draft.scheduling_constraints_doc['time'].pop('after', None)
+        # Set at constraint
+        at = round_to_second_precision(datetime.utcnow() + timedelta(minutes=10))
+        scheduling_unit_draft.scheduling_constraints_doc['time']['at'] = at.isoformat()
+        scheduling_unit_draft.save()
+        scheduling_unit_blueprint = create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft)
+
+        scheduled_scheduling_unit = do_dynamic_schedule()
+
+        # Assert the scheduling_unit has been scheduled and assert is has been scheduled at "at" timestamp
+        self.assertIsNotNone(scheduled_scheduling_unit)
+        self.assertEqual(scheduled_scheduling_unit.id, scheduling_unit_blueprint.id)
+        self.assertEqual(scheduled_scheduling_unit.status, 'scheduled')
+        self.assertEqual(scheduled_scheduling_unit.start_time, at)
+
+    def test_n_simple_observations_one_at_constraint(self):
+        """
+        Test n simple observations where only one of them has an 'at' constraint
+        """
+        n = 5   # No of SU to be created
+        target = 4  # SU id to be within the 'at' constraint
+        target_scheduling_unit_blueprint = None # SU which will be our target
+
+        # Create constraints to be assigned to all of the scheduling_units
+        from_timestamp = round_to_second_precision(datetime.utcnow())
+        to_timestamp = round_to_second_precision(datetime.utcnow() + timedelta(hours=12))
+        between_constraints = [{"from": from_timestamp.isoformat(), "to": to_timestamp.isoformat()},]
+        # Create at constraint to be assigned only to one of the scheduling_units
+        at = round_to_second_precision((datetime.utcnow() + timedelta(minutes=30)))
+
+        # Create n scheduling_units and set the proper constraints
+        for su in range(1, n+1):
+            scheduling_set = models.SchedulingSet.objects.create(**SchedulingSet_test_data())
+            scheduling_unit_draft = self.create_simple_observation_scheduling_unit('scheduling_unit %s' % su,
+                                                                                   scheduling_set=scheduling_set)
+            # Clear constraints
+            scheduling_unit_draft.scheduling_constraints_doc['sky'] = {}
+            scheduling_unit_draft.scheduling_constraints_doc['time']["between"] = between_constraints
+            scheduling_unit_draft.scheduling_constraints_doc['time']["not_between"] = []
+            scheduling_unit_draft.scheduling_constraints_doc['time'].pop("before", None)
+            scheduling_unit_draft.scheduling_constraints_doc['time'].pop('after', None)
+            scheduling_unit_draft.scheduling_constraints_doc['time'].pop("at", None)
+            scheduling_unit_draft.save()
+            if su == target:    # Only scheduling_unit with id 'target' is set within an 'at' constraint
+                scheduling_unit_draft.scheduling_constraints_doc['time']['at'] = at.isoformat()
+                scheduling_unit_draft.save()
+                target_scheduling_unit_blueprint = create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft)
+            else:
+                create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft)
+
+        scheduled_scheduling_unit = do_dynamic_schedule()
+
+        # Assert the 'target' scheduling_unit has been scheduled with priority and assert it is has been scheduled at "at" timestamp
+        self.assertIsNotNone(scheduled_scheduling_unit)
+        self.assertEqual(scheduled_scheduling_unit.id, target_scheduling_unit_blueprint.id)
+        self.assertEqual(scheduled_scheduling_unit.status, 'scheduled')
+        self.assertEqual(scheduled_scheduling_unit.start_time, at)
+
     @unittest.skip("FIX TEST, skipping it for now, see TODO comment in assign_start_stop_times_to_schedulable_scheduling_units")
     def test_three_simple_observations_no_constraints_different_project_priority(self):
         scheduling_unit_draft_low = self.create_simple_observation_scheduling_unit("scheduling unit low", scheduling_set=self.scheduling_set_low)
@@ -179,6 +250,7 @@ class TestDynamicScheduling(TestCase):  # Note: we use django.test.TestCase inst
         self.assertGreaterEqual(scheduling_unit_blueprint_medium.start_time - scheduling_unit_blueprint_high.stop_time, DEFAULT_INTER_OBSERVATION_GAP)
         self.assertGreaterEqual(scheduling_unit_blueprint_low.start_time - scheduling_unit_blueprint_medium.stop_time, DEFAULT_INTER_OBSERVATION_GAP)
 
+    @unittest.skip("Skipped because the corrected 'before' constraint broke scheduler behavior. See TMSS-705")
     def test_time_bound_unit_wins_even_at_lower_priority(self):
         # create two schedule units, one with high one with low prio.
         # first create them without any further constraints, and check if high prio wins.
@@ -198,7 +270,7 @@ class TestDynamicScheduling(TestCase):  # Note: we use django.test.TestCase inst
         self.assertEqual(scheduling_unit_blueprint_high.id, best_scored_scheduling_unit.scheduling_unit.id)
 
         #now update the low prio unit with a time constraint, "forcing" it to be run in a very thight upcoming time window.
-        scheduling_unit_draft_low.scheduling_constraints_doc['time'] = { 'before': (now+scheduling_unit_draft_low.duration).isoformat()+'Z' }
+        scheduling_unit_draft_low.scheduling_constraints_doc['time'] = { 'before': (now+scheduling_unit_draft_low.duration+timedelta(seconds=10)).isoformat()+'Z' }
         scheduling_unit_draft_low.save()
         scheduling_unit_blueprint_low.refresh_from_db()
 
@@ -206,22 +278,20 @@ class TestDynamicScheduling(TestCase):  # Note: we use django.test.TestCase inst
         best_scored_scheduling_unit = find_best_next_schedulable_unit([scheduling_unit_blueprint_low, scheduling_unit_blueprint_high], now, tomorrow)
 
         # now we expect the scheduling_unit with the lowest project rank to be scheduled first because it can only run within this limited timewindow
-        self.assertEqual(scheduling_unit_draft_low.id, best_scored_scheduling_unit.scheduling_unit.id)
+        self.assertEqual(scheduling_unit_blueprint_low.id, best_scored_scheduling_unit.scheduling_unit.id)
 
 
         #  update the low prio unit. enlarge the time window constraint a bit, so both low and high prio units can fit
         # this should result that the high prio goes first, and the low prio (which now fits as well) goes second
-        scheduling_unit_draft_low.scheduling_constraints_doc['time'] = \
-            { 'before': (now+scheduling_unit_draft_low.duration+scheduling_unit_draft_high.duration).isoformat()+'Z' }
+        scheduling_unit_draft_low.scheduling_constraints_doc['time'] = { 'before': (now+scheduling_unit_draft_low.duration+scheduling_unit_draft_high.duration+timedelta(seconds=10)).isoformat()+'Z' }
         scheduling_unit_draft_low.save()
         scheduling_unit_blueprint_low.refresh_from_db()
 
         # call the method-under-test.
         best_scored_scheduling_unit = find_best_next_schedulable_unit([scheduling_unit_blueprint_low, scheduling_unit_blueprint_high], now, tomorrow)
 
-        # now we expect the scheduling_unit with the lowest project rank to be scheduled first because it can only
-        # run within this limited timewindow
-        self.assertEqual(scheduling_unit_blueprint_low.id, best_scored_scheduling_unit.scheduling_unit.id)
+        # now we again expect the scheduling_unit with the higher project rank to be scheduled first
+        self.assertEqual(scheduling_unit_blueprint_high.id, best_scored_scheduling_unit.scheduling_unit.id)
 
         # call the method-under-test again but search after first unit (should return low prio unit)
         stop_time_of_first =  best_scored_scheduling_unit.start_time + best_scored_scheduling_unit.scheduling_unit.duration
@@ -739,7 +809,7 @@ class TestSkyConstraints(unittest.TestCase):
                                                    {"rise": datetime(2020, 1, 1, 8, 0, 0), "set": datetime(2020, 1, 1, 12, 30, 0), "always_above_horizon": False, "always_below_horizon": False}]}
         self.target_rise_and_set_data_always_above = {"CS002": [{"rise": None, "set": None, "always_above_horizon": True, "always_below_horizon": False}]}
         self.target_rise_and_set_data_always_below = {"CS002": [{"rise": None, "set": None, "always_above_horizon": False, "always_below_horizon": True}]}
-        
+
         self.target_rise_and_set_patcher = mock.patch('lofar.sas.tmss.services.scheduling.constraints.template_constraints_v1.coordinates_timestamps_and_stations_to_target_rise_and_set')
         self.target_rise_and_set_mock = self.target_rise_and_set_patcher.start()
         self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data
@@ -760,58 +830,40 @@ class TestSkyConstraints(unittest.TestCase):
         timestamp = datetime(2020, 1, 1, 10, 0, 0)
         returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration))
         self.assertFalse(returned_value)
-        
-    # min_target_elevation
 
-    def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_constraint_returns_true(self):
-        self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data
-
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.1}
-        self.scheduling_unit_blueprint.save()
-        timestamp = datetime(2020, 1, 1, 10, 0, 0)  # target sets after obs ends (mocked response)
-        returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration))
-        self.assertTrue(returned_value)
-
-    def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_when_target_always_above_returns_true(self):
-        self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data_always_above
+    # min_target_elevation
 
+    def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_constraint_returns_true_when_met(self):
         self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.1}
         self.scheduling_unit_blueprint.save()
-        timestamp = datetime(2020, 1, 1, 10, 0, 0)  # target is always up (mocked response)
+        timestamp = datetime(2020, 1, 1, 10, 0, 0)
         returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration))
         self.assertTrue(returned_value)
 
-    def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_constraint_returns_false(self):
-        self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data
-
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.1}
-        self.scheduling_unit_blueprint.save()
-        timestamp = datetime(2020, 1, 1, 11, 0, 0)  # target sets before obs ends (mocked response)
-        returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration))
-        self.assertFalse(returned_value)
-
-    def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_when_target_is_always_below_returns_false(self):
-        self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data_always_below
-
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.1}
+    def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_target_elevation_constraint_returns_false_when_not_met(self):
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'min_target_elevation': 0.2}
         self.scheduling_unit_blueprint.save()
-        timestamp = datetime(2020, 1, 1, 10, 0, 0)  # target is never up (mocked response)
+        timestamp = datetime(2020, 1, 1, 11, 0, 0)
         returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration))
         self.assertFalse(returned_value)
 
 
-
-
 class TestTimeConstraints(TestCase):
     """
     Tests for the time constraint checkers used in dynamic scheduling with different boundaries
     Possible time constraints are
+    - at
     - after
     - before
     - between (one or more 'from-to')
     - not between (one or more 'from-to')
     """
 
+    def add_time_at_constraint(self, at_timestamp):
+        lst_at_constraint = self.scheduling_unit_blueprint.draft.scheduling_constraints_doc
+        lst_at_constraint['time']['at'] = at_timestamp.isoformat()
+        self.scheduling_unit_blueprint.save()
+
     def add_time_between_constraint(self, from_timestamp, to_timestamp):
         lst_between_constraints = self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["between"]
         time_constraint_dict = {"from": from_timestamp.isoformat(), "to": to_timestamp.isoformat()}
@@ -824,6 +876,13 @@ class TestTimeConstraints(TestCase):
         lst_between_constraints.append(time_constraint_dict)
         self.scheduling_unit_blueprint.save()
 
+    def clear_time_constraints(self):
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["between"] = []
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["not_between"] = []
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time'].pop('at', None)
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time'].pop("before", None)
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time'].pop('after', None)
+
     def setUp(self) -> None:
         # scheduling unit
         self.obs_duration = 120 * 60
@@ -834,113 +893,256 @@ class TestTimeConstraints(TestCase):
                                     obs_duration=self.obs_duration)
         self.scheduling_unit_blueprint = create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft)
 
+    # 'after' constraint
+
+    def test_can_run_anywhere_after_returns_true(self):
+
+        # Set datetime constraints before lower_bound
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 11, 0, 0).isoformat()
+        self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 2, 12, 0, 0)))
+
     def test_can_run_anywhere_after_returns_false(self):
+
+        # Set datetime constraints equal to lower_bound
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 12, 0, 0).isoformat()
+        self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 2, 12, 0, 0)))
+
         # Set datetime constraints after lower_bound
+        self.clear_time_constraints()
         self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 13, 0, 0).isoformat()
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                                                              datetime(2020, 1, 1, 12, 0, 0),
                                                                              datetime(2020, 1, 2, 12, 0, 0)))
 
         # Set datetime constraints to upper_bound
+        self.clear_time_constraints()
         self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 2, 12, 0, 0).isoformat()
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                                                              datetime(2020, 1, 1, 12, 0, 0),
                                                                              datetime(2020, 1, 2, 12, 0, 0)))
 
-    def test_can_run_anywhere_after_returns_true(self):
-        # Set datetime constraints before lower_bound
+        # Set datetime constraints after upper_bound
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 2, 13, 0, 0).isoformat()
+        self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                             datetime(2020, 1, 1, 12, 0, 0),
+                                                                             datetime(2020, 1, 2, 12, 0, 0)))
+
+    def test_can_run_within_after_returns_false(self):
+
+        # Set datetime constraints before lower bounds, but with too short window for obs duration
+        self.clear_time_constraints()
         self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 11, 0, 0).isoformat()
-        self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+        self.assertFalse(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 1, 13, 0, 0)))
+
+        # Set datetime constraints after lower bounds, and with too little space left in window for obs duration
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 14, 0, 0).isoformat()
+        self.assertFalse(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 1, 15, 0, 0)))
+
+    def test_can_run_within_after_returns_true(self):
+
+        # Set datetime constraints before lower bounds, and with sufficient window for obs duration
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 11, 0, 0).isoformat()
+        self.assertTrue(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 1, 14, 0, 0)))
+
+        # Set datetime constraints after lower bounds, but with sufficient space left in window for obs duration
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 13, 0, 0).isoformat()
+        self.assertTrue(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 1, 16, 0, 0)))
+
+    # 'before' constraint
+
+    def test_can_run_anywhere_before_returns_false(self):
+
+        # Set datetime constraints before lower_bound
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 1, 11, 0, 0).isoformat()
+        self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                                                             datetime(2020, 1, 1, 12, 0, 0),
                                                                             datetime(2020, 1, 2, 12, 0, 0)))
+
         # Set datetime constraints equal to lower_bound
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 12, 0, 0).isoformat()
-        self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 1, 12, 0, 0).isoformat()
+        self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                                                             datetime(2020, 1, 1, 12, 0, 0),
                                                                             datetime(2020, 1, 2, 12, 0, 0)))
 
-    def test_can_run_anywhere_before_returns_false(self):
-        # Set datetime constraints after upper_bound
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 2, 13, 0, 0).isoformat()
+        # Set datetime constraints after lower_bound
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 1, 13, 0, 0).isoformat()
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                                                              datetime(2020, 1, 1, 12, 0, 0),
                                                                              datetime(2020, 1, 2, 12, 0, 0)))
         # Set datetime constraints equal to upper_bound
+        self.clear_time_constraints()
         self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 2, 12, 0, 0).isoformat()
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                                                              datetime(2020, 1, 1, 12, 0, 0),
                                                                              datetime(2020, 1, 2, 12, 0, 0)))
-        # Set datetime constraints equal to upper_bound - duration + 1 sec
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = \
-            (datetime(2020, 1, 2, 12, 0, 0) - self.scheduling_unit_blueprint.duration + timedelta(seconds=1)).isoformat()
-        self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
-                                                                            datetime(2020, 1, 1, 12, 0, 0),
-                                                                            datetime(2020, 1, 2, 12, 0, 0)))
+
 
     def test_can_run_anywhere_before_returns_true(self):
-        # Set datetime constraints far before upper_bound (lower_bound)
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 1, 12, 0, 0).isoformat()
+
+        # Set datetime constraints after upper_bound
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 2, 13, 0, 0).isoformat()
         self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                                                             datetime(2020, 1, 1, 12, 0, 0),
                                                                             datetime(2020, 1, 2, 12, 0, 0)))
-        # Set datetime constraints equal to upper_bound - duration
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = \
-            (datetime(2020, 1, 2, 12, 0, 0) - self.scheduling_unit_blueprint.duration).isoformat()
-        self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+
+    def test_can_run_within_before_returns_false(self):
+
+        # Set datetime constraints after upper bound, but with too short window for obs duration
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 2, 13, 0, 0).isoformat()
+        self.assertFalse(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 2, 11, 0, 0),
+                                                                            datetime(2020, 1, 2, 12, 0, 0)))
+
+        # Set datetime constraints after lower bound, and with too little space left in window for obs duration
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 1, 13, 0, 0).isoformat()
+        self.assertFalse(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                                                             datetime(2020, 1, 1, 12, 0, 0),
                                                                             datetime(2020, 1, 2, 12, 0, 0)))
 
+    def test_can_run_within_before_returns_true(self):
+
+        # Set datetime constraints after upper bounds, and with sufficient window for obs duration
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 2, 13, 0, 0).isoformat()
+        self.assertTrue(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 2, 12, 0, 0)))
+
+        # Set datetime constraints after lower bounds, but with sufficient space left in window for obs duration
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 1, 15, 0, 0).isoformat()
+        self.assertTrue(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 2, 12, 0, 0)))
+
+    # 'between' constraint
+
     def test_can_run_anywhere_between_returns_false(self):
         """
         Test 'between' constraint with start/stop datetime constraints 'outside' upper_bound or lower_bound
         """
         # Set datetime constraints start > lower_bound and stop > upper_bound
+        self.clear_time_constraints()
         self.add_time_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 2, 15, 0, 0))
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                          datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
+
         # Set datetime constraints start < lower_bound and stop < upper_bound
+        self.clear_time_constraints()
         self.add_time_between_constraint(datetime(2020, 1, 1, 8, 0, 0), datetime(2020, 1, 2, 8, 0, 0))
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                          datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
+
         # Set datetime constraints start > lower_bound and stop > upper_bound (1 second only)
+        self.clear_time_constraints()
         self.add_time_between_constraint(datetime(2020, 1, 1, 12, 0, 1), datetime(2020, 1, 2, 12, 0, 1))
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                          datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
 
+        # Set datetime constraints start > lower_bound and stop < upper_bound
+        self.clear_time_constraints()
+        self.add_time_between_constraint(datetime(2020, 1, 1, 18, 0, 0), datetime(2020, 1, 1, 19, 0, 0))
+        self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                         datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
+
     def test_can_run_anywhere_between_returns_true(self):
         """
-        Test 'between' constraint with start/stop datetime constraints 'inside' upper_bound and lower_bound
+        Test 'between' constraint with start/stop datetime constraints 'outside' upper_bound and lower_bound
         """
-        # Set datetime constraints start > lower_bound and stop < upper_bound -duration
-        self.add_time_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 15, 0, 0))
+        # Set datetime constraints start < lower_bound and stop > upper_bound
+        self.clear_time_constraints()
+        self.add_time_between_constraint(datetime(2020, 1, 1, 11, 0, 0), datetime(2020, 1, 2, 13, 0, 0))
         self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
-                                         datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 20, 0, 0)))
+                                         datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
 
-        # Set datetime constraints start = lower_bound and stop = upper_bound - duration
-        self.add_time_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 15, 0, 0))
+        # Set datetime constraints start = lower_bound and stop = upper_bound
+        self.clear_time_constraints()
+        self.add_time_between_constraint(datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0))
         self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
-                                         datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 17, 10, 0)))
+                                         datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
+
+    def test_can_run_within_between_returns_true(self):
+        """
+        Test 'between' constraint with start/stop datetime constraints (within, not anywhere within)
+        """
+        # Set datetime constraints start > lower_bound and stop > upper_bound, large window
+        self.clear_time_constraints()
+        self.add_time_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 2, 12, 0, 0))
+        self.assertTrue(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                         datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 20, 0, 0)))
+
+        # Set datetime constraints start = lower_bound and stop = upper_bound, window just large enough for obs
+        self.clear_time_constraints()
+        self.add_time_between_constraint(datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 14, 0, 0))
+        self.assertTrue(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                         datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 14, 10, 0)))
+
+    def test_can_run_within_between_returns_false(self):
+        """
+        Test 'between' constraint with start/stop datetime constraints (within, not anywhere within)
+        """
+        # Set datetime constraints start < lower_bound and stop < upper_bound, too little overlap for obs
+        self.clear_time_constraints()
+        self.add_time_between_constraint(datetime(2020, 1, 1, 10, 0, 0), datetime(2020, 1, 1, 13, 0, 0))
+        self.assertFalse(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                         datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 20, 0, 0)))
+
+        # Set datetime constraints start > lower_bound and stop < upper_bound, constraint window too small for obs
+        self.clear_time_constraints()
+        self.add_time_between_constraint(datetime(2020, 1, 1, 14, 0, 0), datetime(2020, 1, 1, 15, 0, 0))
+        self.assertFalse(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                         datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 20, 10, 0)))
+
+    # 'not between' contraint
 
     def test_can_run_anywhere_not_between_returns_false(self):
         """
         Test 'not_between' constraint with start/stop datetime constraints 'inside' upper_bound or lower_bound
         """
         # Set datetime constraints start > lower_bound and stop > upper_bound
+        self.clear_time_constraints()
         self.add_time_not_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 2, 15, 0, 0))
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                              datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
 
         # Set datetime constraints start < lower_bound and stop > lower_bound and < upper_bound
+        self.clear_time_constraints()
         self.add_time_not_between_constraint(datetime(2020, 1, 1, 8, 0, 0), datetime(2020, 1, 2, 8, 0, 0))
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                              datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
 
         # Set datetime constraints start > lower_bound and stop < upper_bound
+        self.clear_time_constraints()
         self.add_time_not_between_constraint(datetime(2020, 1, 1, 16, 0, 0), datetime(2020, 1, 2, 8, 0, 0))
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                              datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
 
         # Set datetime constraints start < lower_bound and stop > upper_bound
+        self.clear_time_constraints()
         self.add_time_not_between_constraint(datetime(2020, 1, 1, 8, 0, 0), datetime(2020, 1, 2, 14, 0, 0))
         self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                             datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
@@ -950,23 +1152,78 @@ class TestTimeConstraints(TestCase):
         Test 'not_between' constraint with start/stop datetime constraints 'outside' upper_bound and lower_bound
         """
         # Set datetime constraints start < lower_bound and stop < lower_bound
+        self.clear_time_constraints()
         self.add_time_not_between_constraint(datetime(2020, 1, 1, 3, 0, 0), datetime(2020, 1, 1, 11, 0, 0))
         self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                              datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 16, 0, 0)))
 
         # Set datetime constraints start > upper_bound and stop > upper_bound
+        self.clear_time_constraints()
         self.add_time_not_between_constraint(datetime(2020, 1, 1, 16, 0, 0), datetime(2020, 1, 1, 20, 0, 0))
         self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
                                              datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 15, 0, 0)))
 
+    # several simultaneous time ranges in 'at' / 'between' / 'not between' constraints
+
     def execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary(self):
         """
-        Just a simple wrapper to call 'can_run_anywhere_within_timewindow_with_time_constraints' function
+        Just a simple wrapper to call 'can_run_within_timewindow_with_time_constraints' function
         with a 24 hours boundary 2020-01-01 12:00 - 2020-01-02 12:00
         """
         return (tc1.can_run_within_timewindow_with_time_constraints(
                     self.scheduling_unit_blueprint, datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)))
 
+    def test_can_run_within_at_constraint(self):
+        """
+        Test "at" constraint with both boundary and 'inside' upper_bound and lower_bound
+        """
+        # no constraints defined so should be OK
+        self.clear_time_constraints()
+        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # Set datetime constraint before lower_bound
+        self.clear_time_constraints()
+        self.add_time_at_constraint(datetime(2020, 1, 1, 11, 0, 0))
+        self.assertFalse(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 1, 14, 0, 0)))
+
+        # Set datetime constraint at lower_bound, but duration exceeds upper_bound
+        self.clear_time_constraints()
+        self.add_time_at_constraint(datetime(2020, 1, 1, 12, 0, 0))
+        self.assertFalse(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 1, 14, 0, 0)))
+
+        # Set datetime constraint at upper_bound
+        self.clear_time_constraints()
+        self.add_time_at_constraint(datetime(2020, 1, 1, 14, 0, 0))
+        self.assertFalse(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                             datetime(2020, 1, 1, 12, 0, 0),
+                                                                             datetime(2020, 1, 1, 14, 0, 0)))
+
+        # Set datetime constraint after upper_bound
+        self.clear_time_constraints()
+        self.add_time_at_constraint(datetime(2020, 1, 1, 15, 0, 0))
+        self.assertFalse(tc1.can_run_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint,
+                                                                            datetime(2020, 1, 1, 12, 0, 0),
+                                                                            datetime(2020, 1, 1, 14, 0, 0)))
+
+        # Set datetime constraint at lower_bound
+        self.clear_time_constraints()
+        self.add_time_at_constraint(datetime(2020, 1, 1, 12, 0, 0))
+        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # Set datetime constraint that fits the time window
+        self.clear_time_constraints()
+        self.add_time_at_constraint(datetime(2020, 1, 1, 18, 30, 0))
+        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # Set datetime constraint so that obs lasts till exactly upper_bound
+        self.clear_time_constraints()
+        self.add_time_at_constraint(datetime(2020, 1, 2, 9, 50, 0))
+        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
     def test_can_run_within_between_constraints(self):
         """
         Test multiple 'between' constraints within 24 boundary and check overall result of
@@ -976,39 +1233,41 @@ class TestTimeConstraints(TestCase):
         i.e. 12-14, 13-15, 14-16,..etc.., 9-11
         """
         # no constraints defined so should be OK
+        self.clear_time_constraints()
         self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
 
-        # Add constraints of 1hr, we still 'can_run'
+        # Add constraints of 1hr, we cannot run
         self.add_time_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 14, 0, 0))
         self.add_time_between_constraint(datetime(2020, 1, 1, 16, 0, 0), datetime(2020, 1, 1, 17, 0, 0))
-        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+        self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
 
-        # Add constraints of 2hr, we still 'can_run'
+        # Add constraints of 2hr, but partially outside the bounds, we still cannot run
         self.add_time_between_constraint(datetime(2020, 1, 2, 11, 0, 0), datetime(2020, 1, 2, 13, 0, 0))
+        self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # Add constraints of 2hr, we can run again
+        self.add_time_between_constraint(datetime(2020, 1, 1, 17, 0, 0), datetime(2020, 1, 1, 19, 0, 0))
         self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
 
         # Add constraint of 24hr constraint, we still 'can_run'
         self.add_time_between_constraint(datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0))
         self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
 
-        # Add constraint of 2hr, to fill the 'last gap', we 'can run'
-        self.add_time_between_constraint(datetime(2020, 1, 2, 10, 0, 0), datetime(2020, 1, 2, 12, 0, 0))
-        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
-
         # Clear all between constraints
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["between"] = []
+        self.clear_time_constraints()
 
-        # Add constraints 'outside' the 24hr, now we 'can not run'
-        self.add_time_between_constraint(datetime(2020, 1, 2, 13, 0, 0), datetime(2020, 1, 2, 14, 0, 0))
-        self.add_time_between_constraint(datetime(2020, 1, 2, 16, 0, 0), datetime(2020, 1, 2, 17, 0, 0))
+        # Add constraints after the 24hr, now we 'can not run'
+        self.add_time_between_constraint(datetime(2020, 1, 2, 13, 0, 0), datetime(2020, 1, 2, 15, 0, 0))
+        self.add_time_between_constraint(datetime(2020, 1, 2, 16, 0, 0), datetime(2020, 1, 2, 20, 0, 0))
         self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
 
-        # Add constraint 'outside' the 24hr, we 'still can not run'
+        # Add constraint before the 24hr, we 'still can not run'
         self.add_time_between_constraint(datetime(2020, 1, 1, 9, 0, 0), datetime(2020, 1, 1, 12, 0, 0))
         self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
 
-        # add one 'inside' constraint, 1 hour within block of 2 hour so overall must be ok
-        self.add_time_between_constraint(datetime(2020, 1, 1, 13, 30, 0), datetime(2020, 1, 1, 14, 30, 0))
+        # add one 'inside' constraint of 3 hours, so overall must be ok again.
+        # Note that 2 hrs would only be sufficient if they match the moving window exactly (here: full hour)
+        self.add_time_between_constraint(datetime(2020, 1, 1, 14, 30, 0), datetime(2020, 1, 1, 17, 30, 0))
         self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
 
     def test_can_run_within_not_between_constraints(self):
@@ -1020,6 +1279,7 @@ class TestTimeConstraints(TestCase):
         i.e. 12-14, 13-15, 14-16,..etc.., 9-11
         """
         # no constraints defined so should be OK
+        self.clear_time_constraints()
         self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
 
         # Add constraints of 1hr, we still 'can_run'
@@ -1039,12 +1299,60 @@ class TestTimeConstraints(TestCase):
         self.add_time_not_between_constraint(datetime(2020, 1, 2, 10, 0, 0), datetime(2020, 1, 2, 12, 0, 0))
         self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
 
-        # Clear all not_between constraints
-        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["not_between"] = []
+        self.clear_time_constraints()
+
         # Add 4 hr constraints within 24 hours boundary, we can run
         self.add_time_not_between_constraint(datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 16, 0, 0))
         self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
 
+    # combined time contraints tests
+
+    def test_can_run_anywhere_combined_time_constraints(self):
+        """
+        Test multiple time constraints in combination and make sure that they block the time window as expected,
+        even though each constraint individually would allow the observation to run.
+        """
+
+        # Set before and after constraint with sufficient gap to fit observation, and assert True
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 12, 59, 59).isoformat()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 1, 15, 0, 1).isoformat()
+        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # set before and after constraint with slightly smaller gap for observation, and assert False
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 13, 0, 0).isoformat()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 1, 15, 0, 0).isoformat()
+        self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # set before and after constraint with large gap
+        # then and add additional between and not between constraints until window is blocked
+        # can run 13-8h
+        self.clear_time_constraints()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 13, 0, 0).isoformat()
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 2, 8, 0, 0).isoformat()
+        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # can run 13h-20h
+        self.add_time_between_constraint(datetime(2020, 1, 1, 11, 0, 0), datetime(2020, 1, 1, 20, 0, 0))
+        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # can run 13h-17h
+        self.add_time_not_between_constraint(datetime(2020, 1, 1, 17, 0, 0), datetime(2020, 1, 2, 4, 0, 0))
+        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # can not run anymore
+        self.add_time_not_between_constraint(datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 16, 0, 0))
+        self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # add another between window, can run 4h-8h
+        self.add_time_between_constraint(datetime(2020, 1, 1, 2, 0, 0), datetime(2020, 1, 2, 12, 0, 0))
+        self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
+        # move before constraint, can not run anymore
+        self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 2, 5, 0, 0).isoformat()
+        self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary())
+
 
 class TestReservedStations(unittest.TestCase):
     """
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/adapters/parset.py b/SAS/TMSS/backend/src/tmss/tmssapp/adapters/parset.py
index 00ed6e2a27944488a317e540fa46c972b8b0f13e..313aaf8090155c185fcc8ee7b62243dd52c8f74b 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/adapters/parset.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/adapters/parset.py
@@ -439,7 +439,7 @@ def _convert_to_parset_dict_for_pipelinecontrol_schema(subtask: models.Subtask)
 
     # DPPP steps
     dppp_steps = []
-    if "preflagger0" in spec:
+    if spec["preflagger0"]["enabled"]:
         dppp_steps.append('preflagger[0]')
         parset["Observation.ObservationControl.PythonControl.DPPP.preflagger[0].chan"] = "[%s]" % spec["preflagger0"]["channels"]
         parset["Observation.ObservationControl.PythonControl.DPPP.preflagger[0].abstime"] = "[]"
@@ -458,7 +458,7 @@ def _convert_to_parset_dict_for_pipelinecontrol_schema(subtask: models.Subtask)
         parset["Observation.ObservationControl.PythonControl.DPPP.preflagger[0].timeslot"] = "[]"
         parset["Observation.ObservationControl.PythonControl.DPPP.preflagger[0].type"] = "preflagger"
 
-    if 'preflagger1' in spec:
+    if spec["preflagger1"]["enabled"]:
         dppp_steps.append('preflagger[1]')
         parset["Observation.ObservationControl.PythonControl.DPPP.preflagger[1].corrtype"] = spec["preflagger1"]["corrtype"]
         parset["Observation.ObservationControl.PythonControl.DPPP.preflagger[1].abstime"] = "[]"
@@ -477,7 +477,7 @@ def _convert_to_parset_dict_for_pipelinecontrol_schema(subtask: models.Subtask)
         parset["Observation.ObservationControl.PythonControl.DPPP.preflagger[1].timeslot"] = "[]"
         parset["Observation.ObservationControl.PythonControl.DPPP.preflagger[1].type"] = "preflagger"
 
-    if 'aoflagger' in spec:
+    if spec["aoflagger"]["enabled"]:
         dppp_steps.append('aoflagger')
         parset["Observation.ObservationControl.PythonControl.DPPP.aoflagger.strategy"] = spec["aoflagger"]["strategy"]
         parset["Observation.ObservationControl.PythonControl.DPPP.aoflagger.autocorr"] = "F"
@@ -493,7 +493,7 @@ def _convert_to_parset_dict_for_pipelinecontrol_schema(subtask: models.Subtask)
         parset["Observation.ObservationControl.PythonControl.DPPP.aoflagger.timewindow"] = "0"
         parset["Observation.ObservationControl.PythonControl.DPPP.aoflagger.type"] = "aoflagger"
 
-    if "demixer" in spec:
+    if spec["demixer"]["enabled"]:
         dppp_steps.append('demixer')
         parset["Observation.ObservationControl.PythonControl.DPPP.demixer.baseline"] = spec["demixer"]["baselines"]
         parset["Observation.ObservationControl.PythonControl.DPPP.demixer.demixfreqstep"] = spec["demixer"]["demix_frequency_steps"]
@@ -514,6 +514,10 @@ def _convert_to_parset_dict_for_pipelinecontrol_schema(subtask: models.Subtask)
         parset["Observation.ObservationControl.PythonControl.DPPP.demixer.subtractsources"] = ""
         parset["Observation.ObservationControl.PythonControl.DPPP.demixer.targetsource"] = ""
         parset["Observation.ObservationControl.PythonControl.DPPP.demixer.type"] = "demixer"
+    else:
+        # ResourceEstimator wants these keys always
+        parset["Observation.ObservationControl.PythonControl.DPPP.demixer.freqstep"] = 1
+        parset["Observation.ObservationControl.PythonControl.DPPP.demixer.timestep"] = 1
 
     parset["Observation.ObservationControl.PythonControl.DPPP.steps"] = "[%s]" % ",".join(dppp_steps)
     parset["Observation.ObservationControl.PythonControl.DPPP.msout.storagemanager.name"] = spec["storagemanager"]
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/adapters/sip.py b/SAS/TMSS/backend/src/tmss/tmssapp/adapters/sip.py
index 76e057c97456f40d5b35c670c27f1f60d9d88ccc..30a2d4029769070ebf204aeda4fada4565e59f1b 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/adapters/sip.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/adapters/sip.py
@@ -2,6 +2,7 @@ from lofar.sas.tmss.tmss.exceptions import *
 from lofar.sas.tmss.tmss.tmssapp.models.scheduling import Dataproduct, SubtaskType, Subtask, SubtaskOutput, SIPidentifier, Algorithm
 from lofar.sas.tmss.tmss.tmssapp.models.specification import Datatype, Dataformat
 from lofar.lta.sip import siplib, ltasip, validator, constants
+from lofar.common.json_utils import add_defaults_to_json_object_for_schema
 
 import uuid
 import logging
@@ -182,13 +183,14 @@ def create_sip_representation_for_subtask(subtask: Subtask):
                 process_map=process_map)
 
         if subtask.specifications_template.name == "pipeline control":  #  todo: re-evaluate this because schema name might change
+            spec = add_defaults_to_json_object_for_schema(subtask.specifications_doc, subtask.specifications_template.schema)
             pipeline = siplib.AveragingPipeline(  # <-- this is what we need for UC1
                 pipeline_map,
                 numberofcorrelateddataproducts=get_number_of_dataproducts_of_type(subtask, Dataformat.Choices.MEASUREMENTSET.value),
-                frequencyintegrationstep=subtask.specifications_doc.get('demixer',{}).get('frequency_steps', 0),
-                timeintegrationstep=subtask.specifications_doc.get('demixer',{}).get('time_step', 0),
-                flagautocorrelations=subtask.task_blueprint.specifications_doc["flag"]["autocorrelations"],
-                demixing=True if 'demix' in subtask.task_blueprint.specifications_doc else False
+                frequencyintegrationstep=spec['demixer']['frequency_steps'] if spec['demixer']['enabled'] else 1,
+                timeintegrationstep=spec['demixer']['time_steps'] if spec['demixer']['enabled'] else 1,
+                flagautocorrelations=spec['preflagger1']['enabled'] and spec['preflagger1']['corrtype'] == 'auto',
+                demixing=spec['demixer']['enabled'] and (spec['demixer']['demix_always'] or spec['demixer']['demix_if_needed'])
             )
         # todo: distinguish and create other pipeline types. Probably most of these can be filled in over time as needed,
         #  but they are not required for UC1. Here are stubs to start from for the other types the LTA supports:
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py
index d4095c88c950c3c628f52c4a477d6389e9dd2699..2fffaacce2860830ce8cf931ccb535535ae69121 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 3.0.9 on 2021-02-22 09:32
+# Generated by Django 3.0.9 on 2021-03-23 17:08
 
 from django.conf import settings
 import django.contrib.postgres.fields
@@ -360,6 +360,15 @@ class Migration(migrations.Migration):
                 'abstract': False,
             },
         ),
+        migrations.CreateModel(
+            name='IOType',
+            fields=[
+                ('value', models.CharField(max_length=128, primary_key=True, serialize=False, unique=True)),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
         migrations.CreateModel(
             name='PeriodCategory',
             fields=[
@@ -453,7 +462,7 @@ class Migration(migrations.Migration):
                 ('name', models.CharField(help_text='Human-readable name of this object.', max_length=128)),
                 ('description', models.CharField(help_text='Short description for this reservation, used in overviews', max_length=255)),
                 ('start_time', models.DateTimeField(help_text='Start of this reservation.')),
-                ('stop_time', models.DateTimeField(help_text='Stop time of this reservation. If null, then this reservation is indefinitely.', null=True)),
+                ('stop_time', models.DateTimeField(help_text='Stop of this reservation. If null, then this reservation is indefinitely.', null=True)),
                 ('specifications_doc', django.contrib.postgres.fields.jsonb.JSONField(help_text='Properties of this reservation')),
             ],
             options={
@@ -464,13 +473,13 @@ class Migration(migrations.Migration):
             name='ReservationStrategyTemplate',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('tags',django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), blank=True, default=list, help_text='User-defined search keywords for object.', size=8)),
+                ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), blank=True, default=list, help_text='User-defined search keywords for object.', size=8)),
                 ('created_at', models.DateTimeField(auto_now_add=True, help_text='Moment of object creation.')),
                 ('updated_at', models.DateTimeField(auto_now=True, help_text='Moment of last object update.')),
                 ('name', models.CharField(help_text='Human-readable name of this object.', max_length=128)),
                 ('description', models.CharField(blank=True, default='', help_text='A longer description of this object.', max_length=255)),
                 ('version', models.CharField(help_text='Version of this template (with respect to other templates of the same name).', max_length=128)),
-                ('template', django.contrib.postgres.fields.jsonb.JSONField(help_text='JSON-data compliant with the JSON-schema in the scheduling_unit_template. This observation strategy template like a predefined recipe with all the correct settings, and defines which parameters the user can alter.')),
+                ('template', django.contrib.postgres.fields.jsonb.JSONField(help_text='JSON-data compliant with the JSON-schema in the reservation_template. This reservation strategy template like a predefined recipe with all the correct settings, and defines which parameters the user can alter.')),
             ],
             options={
                 'abstract': False,
@@ -928,12 +937,12 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='taskrelationdraft',
             name='input_role',
-            field=models.ForeignKey(help_text='Input connector type (what kind of data can be taken as input).', on_delete=django.db.models.deletion.CASCADE, related_name='taskrelationdraft_input_roles', to='tmssapp.TaskConnectorType'),
+            field=models.ForeignKey(help_text='Input connector type (what kind of data is given to the consumer).', on_delete=django.db.models.deletion.CASCADE, related_name='taskrelationdraft_input_roles', to='tmssapp.TaskConnectorType'),
         ),
         migrations.AddField(
             model_name='taskrelationdraft',
             name='output_role',
-            field=models.ForeignKey(help_text='Output connector type (what kind of data can be created as output).', on_delete=django.db.models.deletion.CASCADE, related_name='taskrelationdraft_output_roles', to='tmssapp.TaskConnectorType'),
+            field=models.ForeignKey(help_text='Output connector type (what kind of data is taken from the producer).', on_delete=django.db.models.deletion.CASCADE, related_name='taskrelationdraft_output_roles', to='tmssapp.TaskConnectorType'),
         ),
         migrations.AddField(
             model_name='taskrelationdraft',
@@ -1012,23 +1021,23 @@ class Migration(migrations.Migration):
         ),
         migrations.AddField(
             model_name='taskconnectortype',
-            name='input_of',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='input_connector_types', to='tmssapp.TaskTemplate'),
+            name='iotype',
+            field=models.ForeignKey(help_text='Is this connector an input or output', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.IOType'),
         ),
         migrations.AddField(
             model_name='taskconnectortype',
-            name='output_of',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='output_connector_types', to='tmssapp.TaskTemplate'),
+            name='role',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tmssapp.Role'),
         ),
         migrations.AddField(
             model_name='taskconnectortype',
-            name='role',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tmssapp.Role'),
+            name='task_template',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='output_connector_types', to='tmssapp.TaskTemplate'),
         ),
         migrations.AddField(
             model_name='taskblueprint',
             name='draft',
-            field=models.ForeignKey(help_text='Task Draft which this task instantiates.', on_delete=django.db.models.deletion.CASCADE, related_name='task_blueprints', to='tmssapp.TaskDraft'),
+            field=models.ForeignKey(help_text='Task Draft which this task instantiates.', on_delete=django.db.models.deletion.PROTECT, related_name='task_blueprints', to='tmssapp.TaskDraft'),
         ),
         migrations.AddField(
             model_name='taskblueprint',
@@ -1239,12 +1248,12 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='projectquotaarchivelocation',
             name='project_quota',
-            field=models.ForeignKey(help_text='Project to which this quota belongs.', on_delete=django.db.models.deletion.PROTECT, related_name='project_quota', to='tmssapp.ProjectQuota'),
+            field=models.ForeignKey(help_text='The ProjectQuota for this archive location', on_delete=django.db.models.deletion.PROTECT, related_name='project_quota_archive_location', to='tmssapp.ProjectQuota'),
         ),
         migrations.AddField(
             model_name='projectquota',
             name='project',
-            field=models.ForeignKey(help_text='Project to which this quota belongs.', on_delete=django.db.models.deletion.PROTECT, related_name='quota', to='tmssapp.Project'),
+            field=models.ForeignKey(help_text='Project to wich this quota belongs.', on_delete=django.db.models.deletion.PROTECT, related_name='quota', to='tmssapp.Project'),
         ),
         migrations.AddField(
             model_name='projectquota',
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0002_populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0002_populate.py
index 0fece500a4fdfb63d13d81b325dd60bc7c955b7b..92baffd4c15a8c025d234eeffed61ae9f443fabf 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0002_populate.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0002_populate.py
@@ -21,4 +21,4 @@ class Migration(migrations.Migration):
                    migrations.RunPython(populate_misc),
                    migrations.RunPython(populate_resources),
                    migrations.RunPython(populate_cycles),
-                   migrations.RunPython(populate_projects)]
+                   migrations.RunPython(populate_projects) ]
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py
index 9631cfc2fc3d8051ae1c586b673a8c4d3b553065..80a9fb61594cbe8996f45fe0b0b35a1c842fe319 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py
@@ -14,6 +14,18 @@ from django.urls import reverse as reverse_url
 import json
 import jsonschema
 
+class RefreshFromDbInvalidatesCachedPropertiesMixin():
+    """Helper Mixin class which invalidates all 'cached_property' attributes on a model upon refreshing from the db"""
+    def refresh_from_db(self, *args, **kwargs):
+        self.invalidate_cached_properties()
+        return super().refresh_from_db(*args, **kwargs)
+
+    def invalidate_cached_properties(self):
+        from django.utils.functional import cached_property
+        for key, value in self.__class__.__dict__.items():
+            if isinstance(value, cached_property):
+                self.__dict__.pop(key, None)
+
 # abstract models
 
 class BasicCommon(Model):
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py
index 9645087251cecf7f2b6c5eddd11779ee123f5f37..140b298db576485d9f3d8f23cb49f20daf15cd37 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py
@@ -10,7 +10,7 @@ from django.contrib.postgres.fields import JSONField
 from enum import Enum
 from django.db.models.expressions import RawSQL
 from django.db.models.deletion import ProtectedError
-from .common import AbstractChoice, BasicCommon, Template, NamedCommon, annotate_validate_add_defaults_to_doc_using_template, NamedCommonPK
+from .common import AbstractChoice, BasicCommon, Template, NamedCommon, annotate_validate_add_defaults_to_doc_using_template, NamedCommonPK, RefreshFromDbInvalidatesCachedPropertiesMixin
 from lofar.common.json_utils import validate_json_against_schema, validate_json_against_its_schema, add_defaults_to_json_object_for_schema
 from lofar.sas.tmss.tmss.exceptions import *
 from django.core.exceptions import ValidationError
@@ -23,7 +23,7 @@ from django.utils.functional import cached_property
 # Mixins
 #
 
-class ProjectPropertyMixin:
+class ProjectPropertyMixin(RefreshFromDbInvalidatesCachedPropertiesMixin):
     @cached_property
     def project(self): # -> Project:
         '''return the related project of this task
@@ -52,8 +52,16 @@ class Role(AbstractChoice):
         INSPECTION_PLOTS = "inspection plots"
         CALIBRATOR = "calibrator"
         TARGET = "target"
+        ANY = "any"
+
+
+class IOType(AbstractChoice):
+    """Defines the model and predefined list of possible IOType's for TaskConnectorType.
+    The items in the Choises class below are automagically populated into the database via a data migration."""
+    class Choices(Enum):
         INPUT = "input"
         OUTPUT = "output"
+        # maybe we can add an IN_PLACE="in_place" option in the future, but for now it's not needed.
 
 
 class Datatype(AbstractChoice):
@@ -156,11 +164,15 @@ class Setting(BasicCommon):
 
 
 class TaskConnectorType(BasicCommon):
+    ''' Describes the data type & format combinations a Task can accept or produce. The "role" is used to distinguish
+        inputs (or outputs) that have the same data type & format, but are used in different ways by the task. For
+        example, a calibration pipeline accepts measurement sets only, but distinghuishes between CALIBRATOR and
+        TARGET roles.'''
     role = ForeignKey('Role', null=False, on_delete=PROTECT)
     datatype = ForeignKey('Datatype', null=False, on_delete=PROTECT)
     dataformats = ManyToManyField('Dataformat', blank=True)
-    output_of = ForeignKey("TaskTemplate", related_name='output_connector_types', on_delete=CASCADE)
-    input_of = ForeignKey("TaskTemplate", related_name='input_connector_types', on_delete=CASCADE)
+    task_template = ForeignKey("TaskTemplate", related_name='output_connector_types', null=False, on_delete=CASCADE)
+    iotype = ForeignKey('IOType', null=False, on_delete=PROTECT, help_text="Is this connector an input or output")
 
 
 #
@@ -268,7 +280,7 @@ class DefaultReservationTemplate(BasicCommon):
 # Instance Objects
 #
 
-class Cycle(NamedCommonPK):
+class Cycle(RefreshFromDbInvalidatesCachedPropertiesMixin, NamedCommonPK):
     start = DateTimeField(help_text='Moment at which the cycle starts, that is, when its projects can run.')
     stop = DateTimeField(help_text='Moment at which the cycle officially ends.')
 
@@ -295,7 +307,7 @@ class CycleQuota(Model):
     resource_type = ForeignKey('ResourceType', on_delete=PROTECT, help_text='Resource type.')
 
 
-class Project(NamedCommonPK):
+class Project(RefreshFromDbInvalidatesCachedPropertiesMixin, NamedCommonPK):
     # todo: cycles should be protected since we have to manually decide to clean up projects with a cycle or keep them without cycle, however, ManyToManyField does not allow for that
     cycles = ManyToManyField('Cycle', related_name='projects', blank=True, help_text='Cycles to which this project belongs (NULLable).')
     priority_rank = FloatField(null=False, help_text='Priority of this project w.r.t. other projects. Projects can interrupt observations of lower-priority projects.') # todo: add if needed: validators=[MinValueValidator(0.0), MaxValueValidator(1.0)]
@@ -327,7 +339,7 @@ class ProjectQuota(Model):
     resource_type = ForeignKey('ResourceType', on_delete=PROTECT, help_text='Resource type.')  # protected to avoid accidents
 
 
-class ProjectQuotaArchiveLocation(Model):
+class ProjectQuotaArchiveLocation(RefreshFromDbInvalidatesCachedPropertiesMixin, Model):
     project_quota = ForeignKey('ProjectQuota', null=False, related_name="project_quota_archive_location", on_delete=PROTECT, help_text='The ProjectQuota for this archive location')
     archive_location = ForeignKey('Filesystem', null=False, on_delete=PROTECT, help_text='Location of an archive LTA cluster.')
 
@@ -364,7 +376,7 @@ class SchedulingSet(NamedCommon):
         super().save(force_insert, force_update, using, update_fields)
 
 
-class SchedulingUnitDraft(NamedCommon):
+class SchedulingUnitDraft(RefreshFromDbInvalidatesCachedPropertiesMixin, NamedCommon):
     requirements_doc = JSONField(help_text='Scheduling and/or quality requirements for this run.')
     copies = ForeignKey('SchedulingUnitDraft', related_name="copied_from", on_delete=SET_NULL, null=True, help_text='Source reference, if we are a copy (NULLable).')
     copy_reason = ForeignKey('CopyReason', null=True, on_delete=PROTECT, help_text='Reason why source was copied (NULLable).')
@@ -428,7 +440,7 @@ class SchedulingUnitDraft(NamedCommon):
         return self.scheduling_set.project
 
 
-class SchedulingUnitBlueprint(NamedCommon):
+class SchedulingUnitBlueprint(RefreshFromDbInvalidatesCachedPropertiesMixin, NamedCommon):
     class Status(Enum):
         DEFINED = "defined"
         FINISHED = "finished"
@@ -827,7 +839,7 @@ class TaskDraft(NamedCommon, ProjectPropertyMixin):
     #         return None
 
 
-class TaskBlueprint(NamedCommon):
+class TaskBlueprint(RefreshFromDbInvalidatesCachedPropertiesMixin, NamedCommon):
 
     specifications_doc = JSONField(help_text='Schedulings for this task (IMMUTABLE).')
     do_cancel = BooleanField(help_text='Cancel this task.')
@@ -985,13 +997,20 @@ class TaskRelationDraft(BasicCommon):
     # caveat: it might look like consumer has an incorrect related_name='produced_by'. But it really is correct, denends on the way you look at it
     consumer = ForeignKey('TaskDraft', related_name='produced_by', on_delete=CASCADE, help_text='Task Draft that has the input connector.')
 
-    input_role = ForeignKey('TaskConnectorType', related_name='taskrelationdraft_input_roles', on_delete=CASCADE, help_text='Input connector type (what kind of data can be taken as input).')
-    output_role = ForeignKey('TaskConnectorType', related_name='taskrelationdraft_output_roles', on_delete=CASCADE, help_text='Output connector type (what kind of data can be created as output).')
+    # this relation descibes a transfer of data from the output_role of the producer to the input_role of the consumer
+    input_role = ForeignKey('TaskConnectorType', related_name='taskrelationdraft_input_roles', on_delete=CASCADE, help_text='Input connector type (what kind of data is given to the consumer).')
+    output_role = ForeignKey('TaskConnectorType', related_name='taskrelationdraft_output_roles', on_delete=CASCADE, help_text='Output connector type (what kind of data is taken from the producer).')
 
     class Meta:
         # ensure there are no duplicate relations between tasks with the same in/out roles.
         constraints = [UniqueConstraint(fields=['producer', 'consumer', 'input_role', 'output_role'], name='TaskRelationDraft_unique_relation')]
 
+        # ensure that the roles are compatible, that is, the output we take is suitable for the input we provide to:
+        # input_role.dataformat == output_role.dataformat
+        # input_role.datatype == outputrole.datatype
+        # input_role.output = False
+        # output_role.output = True
+
     def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
         annotate_validate_add_defaults_to_doc_using_template(self, 'selection_doc', 'selection_template')
         super().save(force_insert, force_update, using, update_fields)
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py
index 72c334977eb4d4369ab8f78e35d31ae46341aa1a..4d274999457d157af50a67241c65a213a791a2bb 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py
@@ -40,7 +40,7 @@ def populate_choices(apps, schema_editor):
     each 'choice'type in Role, Datatype, Dataformat, CopyReason
     :return: None
     '''
-    choice_classes = [Role, Datatype, Dataformat, CopyReason,
+    choice_classes = [Role, IOType, Datatype, Dataformat, CopyReason,
                       SubtaskState, SubtaskType, StationType, Algorithm, SchedulingRelationPlacement,
                       Flag, ProjectCategory, PeriodCategory, Quantity, TaskType, ProjectRole]
 
@@ -349,16 +349,45 @@ def populate_misc(apps, schema_editor):
 
 def populate_connectors():
     # the TaskConnectorType's define how the Task[Draft/Blueprint] *can* be connected.
-    # TODO Need overview which we do actually need
-    TaskConnectorType.objects.create(role=Role.objects.get(value=Role.Choices.INPUT.value),
+
+    # NOTE: This is an explicit list of each possible link between tasks. This model suffices
+    # until the number of connectors throw too large. By then, we could consider introducing
+    # wild cards, like output_of=NULL meaning "any".
+    logger.info("POPULATING CONNECTORS")
+
+    # calibrator observation
+    TaskConnectorType.objects.create(role=Role.objects.get(value=Role.Choices.CORRELATOR.value),
                                  datatype=Datatype.objects.get(value=Datatype.Choices.VISIBILITIES.value),
-                                 output_of=TaskTemplate.objects.get(name='calibrator observation'),
-                                 input_of=TaskTemplate.objects.get(name='preprocessing pipeline'))
+                                 task_template=TaskTemplate.objects.get(name='calibrator observation'),
+                                 iotype=IOType.objects.get(value=IOType.Choices.OUTPUT.value))
 
+    # target observation
     TaskConnectorType.objects.create(role=Role.objects.get(value=Role.Choices.CORRELATOR.value),
                                  datatype=Datatype.objects.get(value=Datatype.Choices.VISIBILITIES.value),
-                                 output_of=TaskTemplate.objects.get(name='calibrator observation'),
-                                 input_of=TaskTemplate.objects.get(name='preprocessing pipeline'))
+                                 task_template=TaskTemplate.objects.get(name='target observation'),
+                                 iotype=IOType.objects.get(value=IOType.Choices.OUTPUT.value))
+
+    # preprocessing pipeline
+    TaskConnectorType.objects.create(role=Role.objects.get(value=Role.Choices.ANY.value),
+                                 datatype=Datatype.objects.get(value=Datatype.Choices.VISIBILITIES.value),
+                                 task_template=TaskTemplate.objects.get(name='preprocessing pipeline'),
+                                 iotype=IOType.objects.get(value=IOType.Choices.INPUT.value))
+
+    TaskConnectorType.objects.create(role=Role.objects.get(value=Role.Choices.ANY.value),
+                                 datatype=Datatype.objects.get(value=Datatype.Choices.VISIBILITIES.value),
+                                 task_template=TaskTemplate.objects.get(name='preprocessing pipeline'),
+                                 iotype=IOType.objects.get(value=IOType.Choices.OUTPUT.value))
+
+    # ingest
+    TaskConnectorType.objects.create(role=Role.objects.get(value=Role.Choices.ANY.value),
+                                 datatype=Datatype.objects.get(value=Datatype.Choices.VISIBILITIES.value),
+                                 task_template=TaskTemplate.objects.get(name='ingest'),
+                                 iotype=IOType.objects.get(value=IOType.Choices.INPUT.value))
+
+    TaskConnectorType.objects.create(role=Role.objects.get(value=Role.Choices.ANY.value),
+                                 datatype=Datatype.objects.get(value=Datatype.Choices.TIME_SERIES.value),
+                                 task_template=TaskTemplate.objects.get(name='ingest'),
+                                 iotype=IOType.objects.get(value=IOType.Choices.INPUT.value))
 
 
 def populate_permissions():
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/UC1-scheduling-unit-observation-strategy.json b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/UC1-scheduling-unit-observation-strategy.json
index 07081c0e3098153f07f55d8078608ece8776bec7..33a51e3c0f967a083a8cd8e212f68eddfed5f3bb 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/UC1-scheduling-unit-observation-strategy.json
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/UC1-scheduling-unit-observation-strategy.json
@@ -20,7 +20,7 @@
       "tags": [],
       "specifications_doc": {
         "flag": {
-          "rfi_strategy": "auto",
+          "rfi_strategy": "HBAdefault",
           "outerchannels": true,
           "autocorrelations": true
         },
@@ -115,7 +115,7 @@
       "tags": [],
       "specifications_doc": {
         "flag": {
-          "rfi_strategy": "auto",
+          "rfi_strategy": "HBAdefault",
           "outerchannels": true,
           "autocorrelations": true
         },
@@ -138,7 +138,7 @@
       "tags": [],
       "specifications_doc": {
         "flag": {
-          "rfi_strategy": "auto",
+          "rfi_strategy": "HBAdefault",
           "outerchannels": true,
           "autocorrelations": true
         },
@@ -176,7 +176,7 @@
       "tags": [],
       "specifications_doc": {
         "flag": {
-          "rfi_strategy": "auto",
+          "rfi_strategy": "HBAdefault",
           "outerchannels": true,
           "autocorrelations": true
         },
@@ -207,7 +207,7 @@
       "consumer": "Pipeline 1",
       "tags": [],
       "input": {
-        "role": "input",
+        "role": "any",
         "datatype": "visibilities"
       },
       "output": {
@@ -223,7 +223,7 @@
       "consumer": "Pipeline 2",
       "tags": [],
       "input": {
-        "role": "input",
+        "role": "any",
         "datatype": "visibilities"
       },
       "output": {
@@ -239,7 +239,7 @@
       "consumer": "Pipeline target1",
       "tags": [],
       "input": {
-        "role": "input",
+        "role": "any",
         "datatype": "visibilities"
       },
       "output": {
@@ -259,7 +259,7 @@
       "consumer": "Pipeline target2",
       "tags": [],
       "input": {
-        "role": "input",
+        "role": "any",
         "datatype": "visibilities"
       },
       "output": {
@@ -279,11 +279,11 @@
       "consumer": "Ingest",
       "tags": [],
       "input": {
-        "role": "input",
+        "role": "any",
         "datatype": "visibilities"
       },
       "output": {
-        "role": "correlator",
+        "role": "any",
         "datatype": "visibilities"
       },
       "dataformat": "MeasurementSet",
@@ -295,11 +295,11 @@
       "consumer": "Ingest",
       "tags": [],
       "input": {
-        "role": "input",
+        "role": "any",
         "datatype": "visibilities"
       },
       "output": {
-        "role": "correlator",
+        "role": "any",
         "datatype": "visibilities"
       },
       "dataformat": "MeasurementSet",
@@ -311,11 +311,11 @@
       "consumer": "Ingest",
       "tags": [],
       "input": {
-        "role": "input",
+        "role": "any",
         "datatype": "visibilities"
       },
       "output": {
-        "role": "correlator",
+        "role": "any",
         "datatype": "visibilities"
       },
       "dataformat": "MeasurementSet",
@@ -327,11 +327,11 @@
       "consumer": "Ingest",
       "tags": [],
       "input": {
-        "role": "input",
+        "role": "any",
         "datatype": "visibilities"
       },
       "output": {
-        "role": "correlator",
+        "role": "any",
         "datatype": "visibilities"
       },
       "dataformat": "MeasurementSet",
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/short-observation-pipeline-ingest-scheduling-unit-observation-strategy.json b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/short-observation-pipeline-ingest-scheduling-unit-observation-strategy.json
index ac3277566c7e385713036301a3c2a6af7bd3c911..bd7eea6fc5ab98a051c05833e09c7baec4604a42 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/short-observation-pipeline-ingest-scheduling-unit-observation-strategy.json
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/short-observation-pipeline-ingest-scheduling-unit-observation-strategy.json
@@ -52,7 +52,7 @@
       "tags": [],
       "specifications_doc": {
         "flag": {
-          "rfi_strategy": "auto",
+          "rfi_strategy": "HBAdefault",
           "outerchannels": true,
           "autocorrelations": true
         },
@@ -83,7 +83,7 @@
       "consumer": "Pipeline",
       "tags": [],
       "input": {
-        "role": "input",
+        "role": "any",
         "datatype": "visibilities"
       },
       "output": {
@@ -99,7 +99,7 @@
       "consumer": "Ingest",
       "tags": [],
       "input": {
-        "role": "input",
+        "role": "any",
         "datatype": "visibilities"
       },
       "output": {
@@ -133,4 +133,4 @@
       "name": "Tile Beam"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/subtask_template-pipeline-1.json b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/subtask_template-pipeline-1.json
index 8307de613566df0b7a19d2417a24b740d3f41e7a..e52ab545b6fb1fc8224b83a9144f880dbd0fed1f 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/subtask_template-pipeline-1.json
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/subtask_template-pipeline-1.json
@@ -12,6 +12,11 @@
       "type": "object",
       "additionalProperties": false,
       "properties": {
+        "enabled": {
+          "type": "boolean",
+          "title": "Enabled",
+          "default": false
+        },
         "channels": {
           "title": "Channels",
           "type": "string",
@@ -19,7 +24,7 @@
         }
       },
       "required": [
-        "channels"
+        "enabled"
       ],
       "default": {}
     },
@@ -29,6 +34,11 @@
       "type": "object",
       "additionalProperties": false,
       "properties": {
+        "enabled": {
+          "type": "boolean",
+          "title": "Enabled",
+          "default": false
+        },
         "corrtype": {
           "title": "Correlations",
           "type": "string",
@@ -41,7 +51,7 @@
         }
       },
       "required": [
-        "corrtype"
+        "enabled"
       ],
       "default": {}
     },
@@ -51,6 +61,11 @@
       "type": "object",
       "additionalProperties": false,
       "properties": {
+        "enabled": {
+          "type": "boolean",
+          "title": "Enabled",
+          "default": false
+        },
         "strategy": {
           "title": "Strategy",
           "type": "string",
@@ -62,7 +77,7 @@
         }
       },
       "required": [
-        "strategy"
+        "enabled"
       ],
       "default": {}
     },
@@ -72,6 +87,11 @@
       "type": "object",
       "additionalProperties": false,
       "properties": {
+        "enabled": {
+          "type": "boolean",
+          "title": "Enabled",
+          "default": false
+        },
         "baselines": {
           "title": "Baselines",
           "type": "string",
@@ -142,14 +162,7 @@
         }
       },
       "required": [
-        "baselines",
-        "frequency_steps",
-        "time_steps",
-        "demix_frequency_steps",
-        "demix_time_steps",
-        "ignore_target",
-        "demix_always",
-        "demix_if_needed"
+        "enabled"
       ],
       "default": {}
     },
@@ -164,6 +177,5 @@
     }
   },
   "required": [
-    "storagemanager"
   ]
 }
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/task_template-preprocessing_pipeline-1.json b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/task_template-preprocessing_pipeline-1.json
index 74278f49310705212c20f65d8afe9aa61fb6ed97..0c6e37c3eb7f976d4836e5354ee565726497499e 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/task_template-preprocessing_pipeline-1.json
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/task_template-preprocessing_pipeline-1.json
@@ -24,10 +24,9 @@
         "rfi_strategy": {
           "type": "string",
           "title": "RFI flagging strategy",
-          "default": "auto",
+          "default": "HBAdefault",
           "enum": [
             "none",
-            "auto",
             "HBAdefault",
             "LBAdefault"
           ]
@@ -122,16 +121,7 @@
         }
       },
       "required": [
-        "frequency_steps",
-        "time_steps",
-        "ignore_target",
-        "sources"
       ],
-      "options": {
-        "dependencies": {
-          "demix": true
-        }
-      },
       "default": {}
     },
     "storagemanager": {
@@ -139,12 +129,12 @@
       "title": "Storage Manager",
       "default": "dysco",
       "enum": [
-        "basic",
+        "standard",
         "dysco"
       ]
     }
   },
   "required": [
-    "storagemanager"
+    "average"
   ]
-}
\ No newline at end of file
+}
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py
index 47086104958108a4cc364a1c07c84c200d909d64..8e21947208819f013ba1c7d23bda3586cd774f91 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py
@@ -102,6 +102,11 @@ class RoleSerializer(serializers.ModelSerializer):
         model = models.Role
         fields = '__all__'
 
+class IOTypeSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = models.IOType
+        fields = '__all__'
+
 class SchedulingRelationPlacementSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.SchedulingRelationPlacement
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py
index 856c523be56c5a471099ab484f6eb04412b678a8..5c1513c829161770f6a6a8101976cbb03d0f5537 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py
@@ -598,7 +598,8 @@ def create_preprocessing_subtask_from_task_blueprint(task_blueprint: TaskBluepri
     # step 1: create subtask in defining state, with filled-in subtask_template
     subtask_template = SubtaskTemplate.objects.get(name='pipeline control')
     default_subtask_specs = get_default_json_object_for_schema(subtask_template.schema)
-    subtask_specs = _generate_subtask_specs_from_preprocessing_task_specs(task_blueprint.specifications_doc, default_subtask_specs)
+    task_specs_with_defaults = add_defaults_to_json_object_for_schema(task_blueprint.specifications_doc, task_blueprint.specifications_template.schema)
+    subtask_specs = _generate_subtask_specs_from_preprocessing_task_specs(task_specs_with_defaults, default_subtask_specs)
     cluster_name = task_blueprint.specifications_doc.get("storage_cluster", "CEP4")
     subtask_data = { "start_time": None,
                      "stop_time": None,
@@ -1524,63 +1525,44 @@ def schedule_independent_subtasks_in_task_blueprint(task_blueprint: TaskBlueprin
 
 
 def _generate_subtask_specs_from_preprocessing_task_specs(preprocessing_task_specs, default_subtask_specs):
-    # preprocessing task default spec: {
-    #   "storagemanager": "dysco",
-    #   "flag": {"outerchannels": true, "autocorrelations": true, "rfi_strategy": "auto"},
-    #   "demix": {"frequency_steps": 64, "time_steps": 10, "ignore_target": false, "sources": {}},
-    #   "average": {"frequency_steps": 4, "time_steps": 1}}
-    # pipelinecontrol subtask default spec: {
-    #   "storagemanager": "dysco",
-    #   "demixer": {"baselines": "CS*,RS*&", "frequency_steps": 4, "time_steps": 1, "demix_frequency_steps": 4,
-    #               "demix_time_steps": 1, "ignore_target": false, "demix_always": [], "demix_if_needed": []},
-    #   "aoflagger": {"strategy": "HBAdefault"},
-    #    "preflagger0": {"channels": "0..nchan/32-1,31*nchan/32..nchan-1"},
-    #    "preflagger1": {"corrtype": "auto"}}
-
     # todo: check that this is actually how these need to be translated
     # todo: especially check when defaults are NOT supposed to be set because the task implies to not include them
 
-    # todo: translate task "sources": {} - I guess this is demix_always/demix_if_needed?
-    # todo: set subtask demixer properties "baselines": "CS*,RS*&", "demix_always": [], "demix_if_needed": []
-
-    subtask_specs = {}
-    subtask_specs['storagemanager'] = preprocessing_task_specs.get('storagemanager',
-                                                                   default_subtask_specs.get('storagemanager'))
-
-    # todo: we depend on valid json here with knowledge about required properties. To generalize, we need to expect things to not be there.
-    if 'demix' or 'average' in preprocessing_task_specs:
-        # todo: should we exclude defaults in subtask.demixer if only one of these is defined on the task?
-        subtask_specs['demixer'] = default_subtask_specs['demixer']
-        if 'demix' in preprocessing_task_specs:
-            subtask_specs['demixer'].update({
-                "demix_frequency_steps": preprocessing_task_specs['demix']['frequency_steps'],
-                "demix_time_steps": preprocessing_task_specs['demix']['time_steps'],
-                "ignore_target": preprocessing_task_specs['demix']['ignore_target']
-            }),
-        if 'average' in preprocessing_task_specs:
-            subtask_specs['demixer'].update({
-                "demix_frequency_steps": preprocessing_task_specs['demix']['frequency_steps'],
-                "frequency_steps": preprocessing_task_specs['average']['frequency_steps'],
-                "demix_time_steps": preprocessing_task_specs['demix']['time_steps'],
-                "time_steps": preprocessing_task_specs['average']['time_steps'],
-                "ignore_target": preprocessing_task_specs['demix']['ignore_target']
-            }),
-    if 'flag' in preprocessing_task_specs:
-        if preprocessing_task_specs["flag"]["rfi_strategy"] != 'none':
-            subtask_specs.update({"aoflagger": {"strategy": preprocessing_task_specs["flag"]["rfi_strategy"]}})
-
-            if preprocessing_task_specs["flag"]["rfi_strategy"] == 'auto':
-                # todo: handle 'auto' properly: we need to determine input dataproduct type and set LBA or HBA accordingly
-                #   either here or allow 'auto' in subtask json and translate it when we connect obs to pipe subtask
-                default_strategy = default_subtask_specs['aoflagger']['strategy']
-                subtask_specs.update({"aoflagger": {"strategy": default_strategy}})
-                logger.warning('Translating aoflagger "auto" strategy to "%s" without knowing whether that makes sense!' % default_strategy)
-
-        if preprocessing_task_specs["flag"]["outerchannels"]:
-            subtask_specs.update({"preflagger0": {"channels": "0..nchan/32-1,31*nchan/32..nchan-1"}})
-
-        if preprocessing_task_specs["flag"]["autocorrelations"]:
-            subtask_specs.update({"preflagger1": {"corrtype": "auto"}})
+    # todo: set subtask demixer properties "baselines": "CS*,RS*&"
+
+    subtask_specs = default_subtask_specs
+    subtask_specs['storagemanager'] = preprocessing_task_specs['storagemanager']
+
+    # averaging (performed by the demixer)
+    subtask_specs["demixer"]["enabled"]         = True
+    subtask_specs['demixer']["frequency_steps"] = preprocessing_task_specs['average']['frequency_steps']
+    subtask_specs['demixer']["time_steps"]      = preprocessing_task_specs['average']['time_steps']
+
+    # demixing
+    subtask_specs['demixer']["demix_frequency_steps"] = preprocessing_task_specs['demix']['frequency_steps']
+    subtask_specs['demixer']["demix_time_steps"]      = preprocessing_task_specs['demix']['time_steps']
+    subtask_specs['demixer']["ignore_target"]         = preprocessing_task_specs['demix']['ignore_target']
+    subtask_specs['demixer']["demix_always"]          = [source for source,strategy in preprocessing_task_specs['demix']['sources'].items() if strategy == "yes"]
+    subtask_specs['demixer']["demix_if_needed"]       = [source for source,strategy in preprocessing_task_specs['demix']['sources'].items() if strategy == "auto"]
+
+    # flagging
+    if preprocessing_task_specs["flag"]["rfi_strategy"] != 'none':
+        subtask_specs["aoflagger"]["enabled"] = True
+        subtask_specs["aoflagger"]["strategy"] = preprocessing_task_specs["flag"]["rfi_strategy"]
+    else:
+        subtask_specs["aoflagger"]["enabled"] = False
+
+    if preprocessing_task_specs["flag"]["outerchannels"]:
+        subtask_specs["preflagger0"]["enabled"] = True
+        subtask_specs["preflagger0"]["channels"] = "0..nchan/32-1,31*nchan/32..nchan-1"
+    else:
+        subtask_specs["preflagger0"]["enabled"] = False
+
+    if preprocessing_task_specs["flag"]["autocorrelations"]:
+        subtask_specs["preflagger1"]["enabled"] = True
+        subtask_specs["preflagger1"]["corrtype"] = "auto"
+    else:
+        subtask_specs["preflagger1"]["enabled"] = False
 
     return subtask_specs
 
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py b/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py
index 617ecfed46f6f83fa1b02623081932c8462e6bae..e6d9c06ebe4e38f60a459788c6d16f41569b237c 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py
@@ -179,8 +179,8 @@ def create_task_drafts_from_scheduling_unit_draft(scheduling_unit_draft: models.
         producer_task_draft = scheduling_unit_draft.task_drafts.get(name=task_relation_definition["producer"])
         consumer_task_draft = scheduling_unit_draft.task_drafts.get(name=task_relation_definition["consumer"])
         dataformat = models.Dataformat.objects.get(value=task_relation_definition["dataformat"])
-        input_role = models.TaskConnectorType.objects.get(role=task_relation_definition["input"]["role"], datatype=task_relation_definition["input"]["datatype"])
-        output_role = models.TaskConnectorType.objects.get(role=task_relation_definition["output"]["role"], datatype=task_relation_definition["output"]["datatype"])
+        input_role = models.TaskConnectorType.objects.get(task_template=consumer_task_draft.specifications_template, role=task_relation_definition["input"]["role"], datatype=task_relation_definition["input"]["datatype"], iotype=models.IOType.objects.get(value=models.IOType.Choices.INPUT.value))
+        output_role = models.TaskConnectorType.objects.get(task_template=producer_task_draft.specifications_template, role=task_relation_definition["output"]["role"], datatype=task_relation_definition["output"]["datatype"], iotype=models.IOType.objects.get(value=models.IOType.Choices.OUTPUT.value))
         selection_template = models.TaskRelationSelectionTemplate.objects.get(name=task_relation_definition["selection_template"])
 
         try:
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py
index d53ace784b028f01ba199a80e067090526a66a41..620742eaa77f9aedd8400e88f862121fcb2e2dbf 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py
@@ -280,6 +280,11 @@ class RoleViewSet(LOFARViewSet):
     serializer_class = serializers.RoleSerializer
 
 
+class IOTypeViewSet(LOFARViewSet):
+    queryset = models.IOType.objects.all()
+    serializer_class = serializers.IOTypeSerializer
+
+
 class SchedulingRelationPlacement(LOFARViewSet):
     queryset = models.SchedulingRelationPlacement.objects.all()
     serializer_class = serializers.SchedulingRelationPlacementSerializer
diff --git a/SAS/TMSS/backend/src/tmss/urls.py b/SAS/TMSS/backend/src/tmss/urls.py
index 66e58162725f917a20c5020e6492ad6f39bed7d0..afe222f05f2ef50547b85a34cd591755dbd77c40 100644
--- a/SAS/TMSS/backend/src/tmss/urls.py
+++ b/SAS/TMSS/backend/src/tmss/urls.py
@@ -117,6 +117,7 @@ router.register(r'tags', viewsets.TagsViewSet)
 
 # choices
 router.register(r'role', viewsets.RoleViewSet)
+router.register(r'iotype', viewsets.IOTypeViewSet)
 router.register(r'datatype', viewsets.DatatypeViewSet)
 router.register(r'dataformat', viewsets.DataformatViewSet)
 router.register(r'copy_reason', viewsets.CopyReasonViewSet)
diff --git a/SAS/TMSS/backend/test/t_scheduling.py b/SAS/TMSS/backend/test/t_scheduling.py
index 5bcfa16e9e29e9e82b75a3c5f13dff663a89289d..6a6ff816fce2866f0f34a9c07c805aac6a83bf6c 100755
--- a/SAS/TMSS/backend/test/t_scheduling.py
+++ b/SAS/TMSS/backend/test/t_scheduling.py
@@ -408,7 +408,7 @@ class SchedulingTest(unittest.TestCase):
             # connect obs to pipeline
             scheduling_unit_doc['task_relations'].append({"producer": "Observation",
                                                           "consumer": "Pipeline",
-                                                          "input": { "role": "input", "datatype": "visibilities" },
+                                                          "input": { "role": "any", "datatype": "visibilities" },
                                                           "output": { "role": "correlator", "datatype": "visibilities" },
                                                           "dataformat": "MeasurementSet",
                                                           "selection_doc": {},
diff --git a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py
index d3da150deaa98063eb7c714b99090c37447c8597..f0c8c331dc951757c7e98c3a3c90b467591446f7 100755
--- a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py
+++ b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py
@@ -577,8 +577,7 @@ class TaskRelationSelectionTemplateTestCase(unittest.TestCase):
 class TaskConnectorTestCase(unittest.TestCase):
     @classmethod
     def setUpClass(cls) -> None:
-        cls.input_of_url = test_data_creator.post_data_and_get_url(test_data_creator.TaskTemplate(), '/task_template/')
-        cls.output_of_url = test_data_creator.post_data_and_get_url(test_data_creator.TaskTemplate(), '/task_template/')
+        cls.task_template_url = test_data_creator.post_data_and_get_url(test_data_creator.TaskTemplate(), '/task_template/')
 
     def test_task_connector_list_apiformat(self):
         r = requests.get(BASE_URL + '/task_connector_type/?format=api', auth=AUTH)
@@ -589,7 +588,8 @@ class TaskConnectorTestCase(unittest.TestCase):
         GET_and_assert_equal_expected_code(self, BASE_URL + '/task_connector_type/1234321/', 404)
 
     def test_task_connector_POST_and_GET(self):
-        tc_test_data = test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=self.output_of_url)
+        tc_test_data = test_data_creator.TaskConnectorType(task_template_url=self.task_template_url)
+
         # POST and GET a new item and assert correctness
         r_dict = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', tc_test_data, 201, tc_test_data)
         url = r_dict['url']
@@ -598,7 +598,7 @@ class TaskConnectorTestCase(unittest.TestCase):
     def test_task_connector_POST_invalid_role_raises_error(self):
 
         # POST a new item with invalid choice
-        test_data_invalid_role = dict(test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=self.output_of_url))
+        test_data_invalid_role = dict(test_data_creator.TaskConnectorType(task_template_url=self.task_template_url))
         test_data_invalid_role['role'] = BASE_URL + '/role/forbidden/'
         r_dict = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', test_data_invalid_role, 400, {})
         self.assertTrue('Invalid hyperlink' in str(r_dict['role']))
@@ -606,7 +606,7 @@ class TaskConnectorTestCase(unittest.TestCase):
     def test_task_connector_POST_invalid_datatype_raises_error(self):
 
         # POST a new item with invalid choice
-        test_data_invalid = dict(test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=self.output_of_url))
+        test_data_invalid = dict(test_data_creator.TaskConnectorType(task_template_url=self.task_template_url))
         test_data_invalid['datatype'] = BASE_URL + '/datatype/forbidden/'
         r_dict = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', test_data_invalid, 400, {})
         self.assertTrue('Invalid hyperlink' in str(r_dict['datatype']))
@@ -614,26 +614,18 @@ class TaskConnectorTestCase(unittest.TestCase):
     def test_task_connector_POST_invalid_dataformats_raises_error(self):
 
         # POST a new item with invalid choice
-        test_data_invalid = dict(test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=self.output_of_url))
+        test_data_invalid = dict(test_data_creator.TaskConnectorType(task_template_url=self.task_template_url))
         test_data_invalid['dataformats'] = [BASE_URL + '/dataformat/forbidden/']
         r_dict = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', test_data_invalid, 400, {})
         self.assertTrue('Invalid hyperlink' in str(r_dict['dataformats']))
 
-    def test_task_connector_POST_nonexistant_input_of_raises_error(self):
+    def test_task_connector_POST_nonexistant_task_template_raises_error(self):
 
         # POST a new item with wrong reference
-        test_data_invalid = dict(test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=self.output_of_url))
-        test_data_invalid['input_of'] = BASE_URL + "/task_template/6353748/"
+        test_data_invalid = dict(test_data_creator.TaskConnectorType(task_template_url=self.task_template_url))
+        test_data_invalid['task_template'] = BASE_URL + "/task_template/6353748/"
         r_dict = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', test_data_invalid, 400, {})
-        self.assertTrue('Invalid hyperlink' in str(r_dict['input_of']))
-
-    def test_task_connector_POST_nonexistant_output_of_raises_error(self):
-
-        # POST a new item with wrong reference
-        test_data_invalid = dict(test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=self.output_of_url))
-        test_data_invalid['output_of'] = BASE_URL + "/task_template/6353748/"
-        r_dict = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', test_data_invalid, 400, {})
-        self.assertTrue('Invalid hyperlink' in str(r_dict['output_of']))
+        self.assertTrue('Invalid hyperlink' in str(r_dict['task_template']))
 
     def test_task_connector_POST_existing_outputs_works(self):
 
@@ -644,16 +636,16 @@ class TaskConnectorTestCase(unittest.TestCase):
         url = r_dict['url']
 
         # POST a new item with correct reference
-        test_data_valid = dict(test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=self.output_of_url))
-        test_data_valid['output_of'] = url
+        test_data_valid = dict(test_data_creator.TaskConnectorType(task_template_url=self.task_template_url))
+        test_data_valid['task_template'] = url
         POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', test_data_valid, 201, test_data_valid)
 
     def test_task_connector_PUT_nonexistant_raises_error(self):
-        PUT_and_assert_expected_response(self, BASE_URL + '/task_connector_type/9876789876/', test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=self.output_of_url), 404, {})
+        PUT_and_assert_expected_response(self, BASE_URL + '/task_connector_type/9876789876/', test_data_creator.TaskConnectorType(task_template_url=self.task_template_url), 404, {})
 
     def test_task_connector_PUT(self):
-        tc_test_data1 = test_data_creator.TaskConnectorType(role="correlator", input_of_url=self.input_of_url, output_of_url=self.output_of_url)
-        tc_test_data2 = test_data_creator.TaskConnectorType(role="beamformer", input_of_url=self.input_of_url, output_of_url=self.output_of_url)
+        tc_test_data1 = test_data_creator.TaskConnectorType(role="correlator", task_template_url=self.task_template_url)
+        tc_test_data2 = test_data_creator.TaskConnectorType(role="beamformer", task_template_url=self.task_template_url)
 
         # POST new item, verify
         r_dict = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', tc_test_data1, 201, tc_test_data1)
@@ -665,7 +657,7 @@ class TaskConnectorTestCase(unittest.TestCase):
         GET_OK_and_assert_equal_expected_response(self, url, tc_test_data2)
 
     def test_task_connector_PATCH(self):
-        tc_test_data = test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=self.output_of_url)
+        tc_test_data = test_data_creator.TaskConnectorType(task_template_url=self.task_template_url)
 
         # POST new item, verify
         r_dict = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', tc_test_data, 201, tc_test_data)
@@ -683,7 +675,7 @@ class TaskConnectorTestCase(unittest.TestCase):
         GET_OK_and_assert_equal_expected_response(self, url, expected_data)
 
     def test_task_connector_DELETE(self):
-        tc_test_data = test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=self.output_of_url)
+        tc_test_data = test_data_creator.TaskConnectorType(task_template_url=self.task_template_url)
 
         # POST new item, verify
         r_dict = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', tc_test_data, 201, tc_test_data)
@@ -693,27 +685,15 @@ class TaskConnectorTestCase(unittest.TestCase):
         # DELETE and check it's gone
         DELETE_and_assert_gone(self, url)
 
-    def test_task_relation_blueprint_CASCADE_behavior_on_inputs_template_deleted(self):
-        input_of_url = test_data_creator.post_data_and_get_url(test_data_creator.TaskTemplate(), '/task_template/')
-        tc_test_data = test_data_creator.TaskConnectorType(input_of_url=input_of_url, output_of_url=self.output_of_url)
-        # POST new item
-        url = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', tc_test_data, 201, tc_test_data)['url']
-        # verify
-        GET_OK_and_assert_equal_expected_response(self, url, tc_test_data)
-        # DELETE dependency
-        DELETE_and_assert_gone(self, input_of_url)
-        # assert
-        GET_and_assert_equal_expected_code(self, url, 404)
-
-    def test_task_relation_blueprint_CASCADE_behavior_on_outputs_template_deleted(self):
-        output_of_url = test_data_creator.post_data_and_get_url(test_data_creator.TaskTemplate(), '/task_template/')
-        tc_test_data = test_data_creator.TaskConnectorType(input_of_url=self.input_of_url, output_of_url=output_of_url)
+    def test_task_relation_blueprint_CASCADE_behavior_on_template_deleted(self):
+        task_template_url = test_data_creator.post_data_and_get_url(test_data_creator.TaskTemplate(), '/task_template/')
+        tc_test_data = test_data_creator.TaskConnectorType(task_template_url=task_template_url)
         # POST new item
         url = POST_and_assert_expected_response(self, BASE_URL + '/task_connector_type/', tc_test_data, 201, tc_test_data)['url']
         # verify
         GET_OK_and_assert_equal_expected_response(self, url, tc_test_data)
         # DELETE dependency
-        DELETE_and_assert_gone(self, output_of_url)
+        DELETE_and_assert_gone(self, task_template_url)
         # assert
         GET_and_assert_equal_expected_code(self, url, 404)
 
diff --git a/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py b/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py
index 7966ebf804157257cddc5f6b63d1d774d20694ad..7ace3e3ad11b88a2c9f1e169c8b01b7dc8d5e57d 100755
--- a/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py
+++ b/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py
@@ -268,21 +268,11 @@ class TaskRelationSelectionTemplateTest(unittest.TestCase):
 
 class TaskConnectorTest(unittest.TestCase):
 
-    def test_POST_TaskConnector_prevents_missing_input_of(self):
+    def test_POST_TaskConnector_prevents_missing_task_template(self):
 
         # setup
         test_data_1 = dict(TaskConnectorType_test_data())
-        test_data_1['input_of'] = None
-
-        # assert
-        with self.assertRaises(IntegrityError):
-            models.TaskConnectorType.objects.create(**test_data_1)
-
-    def test_POST_TaskConnector_prevents_missing_output_of(self):
-
-        # setup
-        test_data_1 = dict(TaskConnectorType_test_data())
-        test_data_1['output_of'] = None
+        test_data_1['task_template'] = None
 
         # assert
         with self.assertRaises(IntegrityError):
diff --git a/SAS/TMSS/backend/test/tmss_test_data_django_models.py b/SAS/TMSS/backend/test/tmss_test_data_django_models.py
index 03bd63e347821654485556b3e2c146e9aea2d92b..08c549f734feed11c0cda5fe64edd974297cb0af 100644
--- a/SAS/TMSS/backend/test/tmss_test_data_django_models.py
+++ b/SAS/TMSS/backend/test/tmss_test_data_django_models.py
@@ -106,8 +106,8 @@ def TaskRelationSelectionTemplate_test_data(name="my_TaskRelationSelectionTempla
 def TaskConnectorType_test_data() -> dict:
     return {"role": models.Role.objects.get(value='calibrator'),
             "datatype": models.Datatype.objects.get(value='instrument model'),
-            "output_of": models.TaskTemplate.objects.create(**TaskTemplate_test_data()),
-            "input_of": models.TaskTemplate.objects.create(**TaskTemplate_test_data()),
+            "task_template": models.TaskTemplate.objects.create(**TaskTemplate_test_data()),
+            "iotype": models.IOType.objects.get(value=models.IOType.Choices.OUTPUT.value),
             "tags": []}
 
 def Cycle_test_data() -> dict:
diff --git a/SAS/TMSS/backend/test/tmss_test_data_rest.py b/SAS/TMSS/backend/test/tmss_test_data_rest.py
index 551a251a68857807ceb4e4bf63699d6bd575c44d..759885c6f84320b6f452ade940b1db2bfe8e4eb5 100644
--- a/SAS/TMSS/backend/test/tmss_test_data_rest.py
+++ b/SAS/TMSS/backend/test/tmss_test_data_rest.py
@@ -221,18 +221,15 @@ class TMSSRESTTestDataCreator():
             return self._task_relation_selection_template_url
 
 
-    def TaskConnectorType(self, role="correlator", input_of_url=None, output_of_url=None):
-        if input_of_url is None:
-            input_of_url = self.cached_task_template_url
-    
-        if output_of_url is None:
-            output_of_url = self.cached_task_template_url
+    def TaskConnectorType(self, role="correlator", iotype="output", task_template_url=None):
+        if task_template_url is None:
+            task_template_url = self.cached_task_template_url
     
         return {"role": self.django_api_url + '/role/%s'%role,
                 "datatype": self.django_api_url + '/datatype/image',
                 "dataformats": [self.django_api_url + '/dataformat/Beamformed'],
-                "output_of": output_of_url,
-                "input_of": input_of_url,
+                "task_template": task_template_url,
+                "iotype": self.django_api_url + '/iotype/%s'%iotype,
                 "tags": []}
 
 
@@ -434,10 +431,10 @@ class TMSSRESTTestDataCreator():
             selection_doc = self.get_response_as_json_object(template_url+'/default')
 
         if input_role_url is None:
-            input_role_url = self.post_data_and_get_url(self.TaskConnectorType(), '/task_connector_type/')
+            input_role_url = self.post_data_and_get_url(self.TaskConnectorType(iotype="input"), '/task_connector_type/')
     
         if output_role_url is None:
-            output_role_url = self.post_data_and_get_url(self.TaskConnectorType(), '/task_connector_type/')
+            output_role_url = self.post_data_and_get_url(self.TaskConnectorType(iotype="output"), '/task_connector_type/')
     
         return {"tags": [],
                 "selection_doc": selection_doc,
@@ -533,10 +530,10 @@ class TMSSRESTTestDataCreator():
             selection_doc = self.get_response_as_json_object(template_url+'/default')
 
         if input_role_url is None:
-            input_role_url = self.post_data_and_get_url(self.TaskConnectorType(), '/task_connector_type/')
+            input_role_url = self.post_data_and_get_url(self.TaskConnectorType(iotype="input"), '/task_connector_type/')
     
         if output_role_url is None:
-            output_role_url = self.post_data_and_get_url(self.TaskConnectorType(), '/task_connector_type/')
+            output_role_url = self.post_data_and_get_url(self.TaskConnectorType(iotype="output"), '/task_connector_type/')
     
         # test data
         return {"tags": [],
diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js
index b0202b05cf33dda39dc34f4d273b4c9530976897..e4709c550415ae27ab9207f2e503cf6626fb6dce 100644
--- a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js
+++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js
@@ -974,14 +974,18 @@ function ViewTable(props) {
     })
   }
 
-  const navigateTo = (props) => () => {
-    if (props.cell.row.values['actionpath']) {
-      return history.push({
-        pathname: props.cell.row.values['actionpath'],
-        state: {
-          "id": props.value,
-        }
-      })
+  const navigateTo = (cellProps) => () => {
+    if (cellProps.cell.row.values['actionpath']) {
+      if (!props.viewInNewWindow) {
+        return history.push({
+          pathname: cellProps.cell.row.values['actionpath'],
+          state: {
+            "id": cellProps.value,
+          }
+        })
+      } else {
+        window.open(cellProps.cell.row.values['actionpath'] , '_blank');
+      }
     }
     // Object.entries(props.paths[0]).map(([key,value]) =>{})
   }
diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js
index 0fd8c88cce18cf3a98c9006ee9d86ae2124d2fb7..c8784e6282287e5a80e6deccb958f7e5a77e3d31 100644
--- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js
+++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js
@@ -58,6 +58,12 @@ export class SchedulingUnitSummary extends Component {
         if (constraint) {
             const objectType = typeof constraint;
             switch(objectType) {
+                case "number": {
+                    if ((constraint+"").indexOf(".")>=0) {
+                        constraint = parseFloat(constraint.toFixed(2));
+                    }
+                    break;
+                }
                 case "string": {
                     try {
                         const dateConstraint = moment.utc(constraint);
@@ -112,6 +118,15 @@ export class SchedulingUnitSummary extends Component {
         this.setState({constraintsDoc: jsonOutput});
     }
 
+    redirectToSUDetails = () => {
+        if (!this.props.viewInNewWindow) {
+            this.props.history.push(`/schedulingunit/view/blueprint/${this.props.schedulingUnit.id}`);
+        } else {
+            window.open(`/schedulingunit/view/blueprint/${this.props.schedulingUnit.id}`, '_blank');
+        }
+    }
+
+
     render() {
         const schedulingUnit = this.props.schedulingUnit;
         const suTaskList = this.props.suTaskList;
@@ -124,7 +139,7 @@ export class SchedulingUnitSummary extends Component {
             { schedulingUnit &&
                 <div className="p-grid timeline-details-pane" style={{marginTop: '10px'}}>
                     <h6 className="col-lg-10 col-sm-10">Details</h6>
-                    <Link to={`/schedulingunit/view/blueprint/${schedulingUnit.id}`} title="View Full Details"><i className="fa fa-eye"></i></Link>
+                    <Link onClick={this.redirectToSUDetails} title="View Full Details"><i className="fa fa-eye"></i></Link>
                     <Link to={this.props.location?this.props.location.pathname:"/su/timelineview"} onClick={this.closeSUDets} title="Close Details"><i className="fa fa-times"></i></Link>
                     <div className="col-4"><label>Name:</label></div>
                     <div className="col-8">{schedulingUnit.name}</div>
diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js
index 56437953e4ee0ed1bd91c69fda2fd9512bb7f703..fd0d2b161d76feb6acd61c2ac80abf1d17de8ffe 100644
--- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js
+++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js
@@ -872,6 +872,7 @@ export class TimelineView extends Component {
                             <div className={isSUDetsVisible || isTaskDetsVisible || (canExtendSUList && !canShrinkSUList)?"col-lg-4 col-md-4 col-sm-12":((canExtendSUList && canShrinkSUList)?"col-lg-5 col-md-5 col-sm-12":"col-lg-6 col-md-6 col-sm-12")}
                                  style={{position: "inherit", borderRight: "5px solid #efefef", paddingTop: "10px"}}>
                                 <ViewTable 
+                                    viewInNewWindow
                                     data={this.state.suBlueprintList} 
                                     defaultcolumns={[{name: "Name",
                                                         start_time:
@@ -974,6 +975,7 @@ export class TimelineView extends Component {
                                      style={{borderLeft: "1px solid #efefef", marginTop: "0px", backgroundColor: "#f2f2f2"}}>
                                     {this.state.isSummaryLoading?<AppLoader /> :
                                         <SchedulingUnitSummary schedulingUnit={suBlueprint} suTaskList={this.state.suTaskList}
+                                                viewInNewWindow        
                                                 constraintsTemplate={this.state.suConstraintTemplate}
                                                 stationGroup={this.state.stationGroup}
                                                 closeCallback={this.closeSUDets}></SchedulingUnitSummary>
diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js
index 82e62ab263baf9a3f72c7d396990a61471beee44..04476ccc0ee18e424696da8250c4fcb397915d4c 100644
--- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js
+++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js
@@ -777,7 +777,7 @@ export class WeekTimelineView extends Component {
                             {/* SU List Panel */}
                             <div className={isSUDetsVisible || (canExtendSUList && !canShrinkSUList)?"col-lg-4 col-md-4 col-sm-12":((canExtendSUList && canShrinkSUList)?"col-lg-5 col-md-5 col-sm-12":"col-lg-6 col-md-6 col-sm-12")}
                                  style={{position: "inherit", borderRight: "5px solid #efefef", paddingTop: "10px"}}>
-                                <ViewTable 
+                                <ViewTable viewInNewWindow
                                     data={this.state.suBlueprintList} 
                                     defaultcolumns={[{name: "Name",
                                                         start_time:"Start Time", stop_time:"End Time"}]}
@@ -853,6 +853,7 @@ export class WeekTimelineView extends Component {
                                      style={{borderLeft: "1px solid #efefef", marginTop: "0px", backgroundColor: "#f2f2f2"}}>
                                     {this.state.isSummaryLoading?<AppLoader /> :
                                         <SchedulingUnitSummary schedulingUnit={suBlueprint} suTaskList={this.state.suTaskList}
+                                                viewInNewWindow
                                                 constraintsTemplate={this.state.suConstraintTemplate}
                                                 closeCallback={this.closeSUDets}
                                                 stationGroup={this.state.stationGroup}