diff --git a/SAS/TMSS/backend/src/tmss/exceptions.py b/SAS/TMSS/backend/src/tmss/exceptions.py
index 82784f607a942acfc3874ee77252b849839f4170..f93899d05d2d98b55bb57a959442f33f5d66183b 100644
--- a/SAS/TMSS/backend/src/tmss/exceptions.py
+++ b/SAS/TMSS/backend/src/tmss/exceptions.py
@@ -17,6 +17,9 @@ class SubtaskCreationException(ConversionException):
 class SubtaskException(TMSSException):
     pass
 
+class SubtaskIllegalStateTransitionException(SubtaskException):
+    pass
+
 class SubtaskInvalidStateException(TMSSException):
     pass
 
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 b839825872027682578453db8cbf18389a60adcb..e07fb5ce651a899ea02d12ae33e479caf70e25cc 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-04-07 08:59
+# Generated by Django 3.0.9 on 2021-04-12 06:55
 
 from django.conf import settings
 import django.contrib.postgres.fields
@@ -678,6 +678,18 @@ class Migration(migrations.Migration):
                 ('unique_identifier', models.BigAutoField(help_text='Unique global identifier.', primary_key=True, serialize=False)),
             ],
         ),
+        migrations.CreateModel(
+            name='StationTimeline',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('station_name', models.CharField(editable=False, help_text='The LOFAR station name.', max_length=16)),
+                ('timestamp', models.DateField(editable=False, help_text='The date (YYYYMMDD).', null=True)),
+                ('sunrise_start', models.DateTimeField(help_text='Start time of the sunrise.', null=True)),
+                ('sunrise_end', models.DateTimeField(help_text='End time of the sunrise.', null=True)),
+                ('sunset_start', models.DateTimeField(help_text='Start time of the sunset.', null=True)),
+                ('sunset_end', models.DateTimeField(help_text='End time of the sunset.', null=True)),
+            ],
+        ),
         migrations.CreateModel(
             name='StationType',
             fields=[
@@ -704,6 +716,12 @@ class Migration(migrations.Migration):
                 'abstract': False,
             },
         ),
+        migrations.CreateModel(
+            name='SubtaskAllowedStateTransitions',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+            ],
+        ),
         migrations.CreateModel(
             name='SubtaskInput',
             fields=[
@@ -938,17 +956,6 @@ class Migration(migrations.Migration):
                 ('second', models.ForeignKey(help_text='Second Task Blueprint to connect.', on_delete=django.db.models.deletion.CASCADE, related_name='second_scheduling_relation', to='tmssapp.TaskBlueprint')),
             ],
         ),
-        migrations.CreateModel(
-            name='StationTimeline',
-            fields=[
-                ('station_name', models.CharField(max_length=16, null=False, editable=False, help_text='The LOFAR station name.')),
-                ('timestamp', models.DateField(editable=False, null=True, help_text='The date (YYYYMMDD).')),
-                ('sunrise_start', models.DateTimeField(null=True, help_text='Start time of the sunrise.')),
-                ('sunrise_end', models.DateTimeField(null=True, help_text='End time of the sunrise.')),
-                ('sunset_start', models.DateTimeField(null=True, help_text='Start time of the sunset.')),
-                ('sunset_end', models.DateTimeField(null=True, help_text='End time of the sunset.')),
-            ],
-        ),
         migrations.AddConstraint(
             model_name='taskrelationselectiontemplate',
             constraint=models.UniqueConstraint(fields=('name', 'version'), name='taskrelationselectiontemplate_unique_name_version'),
@@ -1133,6 +1140,16 @@ class Migration(migrations.Migration):
             name='task_relation_blueprint',
             field=models.ForeignKey(help_text='Task Relation Blueprint which this Subtask Input implements (NULLable).', null=True, on_delete=django.db.models.deletion.SET_NULL, to='tmssapp.TaskRelationBlueprint'),
         ),
+        migrations.AddField(
+            model_name='subtaskallowedstatetransitions',
+            name='new_state',
+            field=models.ForeignKey(editable=False, help_text='Subtask state after update (see Subtask State Machine).', on_delete=django.db.models.deletion.PROTECT, related_name='allowed_transition_to', to='tmssapp.SubtaskState'),
+        ),
+        migrations.AddField(
+            model_name='subtaskallowedstatetransitions',
+            name='old_state',
+            field=models.ForeignKey(editable=False, help_text='Subtask state before update (see Subtask State Machine).', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='allowed_transition_from', to='tmssapp.SubtaskState'),
+        ),
         migrations.AddField(
             model_name='subtask',
             name='cluster',
@@ -1165,7 +1182,7 @@ class Migration(migrations.Migration):
         ),
         migrations.AddConstraint(
             model_name='stationtimeline',
-            constraint=models.UniqueConstraint(fields=('station_name', 'timestamp'),  name='unique_station_time_line'),
+            constraint=models.UniqueConstraint(fields=('station_name', 'timestamp'), name='unique_station_time_line'),
         ),
         migrations.AddConstraint(
             model_name='schedulingunittemplate',
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 cc01b09d6659e81e82e1001073ba00d044e1aa95..2f1a453273ced309b6018a75b1b650f05963ae8c 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0002_populate.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0002_populate.py
@@ -16,24 +16,40 @@ class Migration(migrations.Migration):
 
 
     operations = [ migrations.RunSQL('ALTER SEQUENCE tmssapp_SubTask_id_seq RESTART WITH 2000000;'), # Start SubTask id with 2 000 000 to avoid overlap with 'old' (test/production) OTDB
-                   migrations.RunPython(populate_choices), # poppulates the "fixed" enums, with amongst others the subtask_states which are used next in the trigger
-                   migrations.RunSQL("CREATE OR REPLACE FUNCTION tmssapp_check_subtask_state_transition()                            \
-                                     RETURNS trigger AS                                                                              \
-                                     $BODY$                                                                                          \
-                                     BEGIN                                                                                           \
-                                       IF OLD.state_id='defining' AND NEW.state_id != 'defined' THEN                                 \
-                                         RAISE EXCEPTION 'ILLEGAL SUBTASK STATE TRANSITION FROM % TO %', OLD.state_id, NEW.state_id; \
-                                       END IF;                                                                                       \
-                                     RETURN NEW;                                                                                     \
-                                     END;                                                                                            \
-                                     $BODY$                                                                                          \
-                                     LANGUAGE plpgsql VOLATILE;                                                                      \
-                                     DROP TRIGGER IF EXISTS tmssapp_trigger_on_check_subtask_state_transition ON tmssapp_SubTask ;   \
-                                     CREATE TRIGGER tmssapp_trigger_on_check_subtask_state_transition                                \
-                                     AFTER UPDATE ON tmssapp_SubTask                                                                 \
+                   # add an SQL trigger in the database enforcing correct state transitions.
+                   # it is crucial that illegal subtask state transitions are block at the "lowest level" (i.e.: in the database) so we can guarantee that the subtask state machine never breaks.
+                   # see: https://support.astron.nl/confluence/display/TMSS/Subtask+State+Machine
+                   # Explanation of SQl below: A trigger function is called upon each create/update of the subtask.
+                   # If the state changes, then it is checked if the state transition from old to new is present in the SubtaskAllowedStateTransitions table.
+                   # If not an Exception is raised, thus enforcing a rollback, thus enforcing the state machine to follow the design.
+                   # It is thereby enforced upon the user/caller to handle these blocked illegal state transitions, and act more wisely.
+                   migrations.RunSQL("CREATE OR REPLACE FUNCTION tmssapp_check_subtask_state_transition() \
+                                     RETURNS trigger AS \
+                                     $BODY$ \
+                                     BEGIN \
+                                       IF TG_OP = 'INSERT' THEN \
+                                         IF NOT (SELECT EXISTS(SELECT id FROM tmssapp_subtaskallowedstatetransitions WHERE old_state_id IS NULL AND new_state_id=NEW.state_id)) THEN \
+                                            RAISE EXCEPTION 'ILLEGAL SUBTASK STATE TRANSITION FROM % TO %', NULL, NEW.state_id; \
+                                         END IF; \
+                                       END IF; \
+                                       IF TG_OP = 'UPDATE' THEN \
+                                         IF OLD.state_id <> NEW.state_id AND NOT (SELECT EXISTS(SELECT id FROM tmssapp_subtaskallowedstatetransitions WHERE old_state_id=OLD.state_id AND new_state_id=NEW.state_id)) THEN \
+                                           RAISE EXCEPTION 'ILLEGAL SUBTASK STATE TRANSITION FROM \"%\" TO \"%\"', OLD.state_id, NEW.state_id; \
+                                         END IF; \
+                                       END IF; \
+                                     RETURN NEW; \
+                                     END; \
+                                     $BODY$ \
+                                     LANGUAGE plpgsql VOLATILE; \
+                                     DROP TRIGGER IF EXISTS tmssapp_trigger_on_check_subtask_state_transition ON tmssapp_SubTask ; \
+                                     CREATE TRIGGER tmssapp_trigger_on_check_subtask_state_transition \
+                                     BEFORE INSERT OR UPDATE ON tmssapp_SubTask \
                                      FOR EACH ROW EXECUTE PROCEDURE tmssapp_check_subtask_state_transition();"),
+                   migrations.RunPython(populate_choices),
+                   migrations.RunPython(populate_subtask_allowed_state_transitions),
                    migrations.RunPython(populate_settings),
                    migrations.RunPython(populate_misc),
                    migrations.RunPython(populate_resources),
                    migrations.RunPython(populate_cycles),
                    migrations.RunPython(populate_projects) ]
+
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py
index 234f8ab359cd7dad4c3bb3abcad5d0e4dabc4cd8..86c316ad6047deb56c0033d14ee74f5abb4ab377 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py
@@ -16,8 +16,8 @@ from .common import AbstractChoice, BasicCommon, Template, NamedCommon, annotate
 from enum import Enum
 from django.db.models.expressions import RawSQL
 from django.core.exceptions import ValidationError
-
-from lofar.sas.tmss.tmss.exceptions import SubtaskSchedulingException
+from django.db.utils import InternalError
+from lofar.sas.tmss.tmss.exceptions import SubtaskSchedulingException, SubtaskIllegalStateTransitionException
 from django.conf import settings
 from lofar.sas.resourceassignment.resourceassignmentservice.rpc import RADBRPC
 import uuid
@@ -290,9 +290,16 @@ class Subtask(BasicCommon):
         # check if we have a start time when scheduling
         if self.state.value == SubtaskState.Choices.SCHEDULED.value and self.__original_state_id == SubtaskState.Choices.SCHEDULING.value:
             if self.start_time is None:
-                    raise SubtaskSchedulingException("Cannot schedule subtask id=%s when start time is 'None'." % (self.pk, ))
+                raise SubtaskSchedulingException("Cannot schedule subtask id=%s when start time is 'None'." % (self.pk, ))
 
-        super().save(force_insert, force_update, using, update_fields)
+        try:
+            super().save(force_insert, force_update, using, update_fields)
+        except InternalError as db_error:
+            # wrap in TMSS SubtaskIllegalStateTransitionException if needed
+            if 'ILLEGAL SUBTASK STATE TRANSITION' in str(db_error):
+                raise SubtaskIllegalStateTransitionException(str(db_error))
+            # else just reraise
+            raise
 
         # log if either state update or new entry:
         if self.state_id != self.__original_state_id or creating == True:
@@ -308,6 +315,14 @@ class Subtask(BasicCommon):
             self.__original_state_id = self.state_id
 
 
+class SubtaskAllowedStateTransitions(Model):
+    """
+    Table with the allowed subtask state transitions. See also the SQL trigger in populate which blocks any subtask state transitions which are not in this table, thus not allowed.
+    """
+    old_state = ForeignKey('SubtaskState', null=True, editable=False, on_delete=PROTECT, related_name='allowed_transition_from', help_text='Subtask state before update (see Subtask State Machine).')
+    new_state = ForeignKey('SubtaskState', null=False, editable=False, on_delete=PROTECT, related_name='allowed_transition_to', help_text='Subtask state after update (see Subtask State Machine).')
+
+
 class SubtaskStateLog(BasicCommon):
     """
     History of state changes on subtasks
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py
index 02575ebfc44cfa7d5261791d741ae95438bfab94..c4ebcdd63cdacaa8666aad6acd0256bb9e196ab4 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py
@@ -50,6 +50,55 @@ def populate_choices(apps, schema_editor):
         executor.map(lambda choice_class: choice_class.objects.bulk_create([choice_class(value=x.value) for x in choice_class.Choices]),
                      choice_classes)
 
+def populate_subtask_allowed_state_transitions(apps, schema_editor):
+    '''populate the SubtaskAllowedStateTransitions table with the allowed state transitions as defined by the design in https://support.astron.nl/confluence/display/TMSS/Subtask+State+Machine'''
+    DEFINING = SubtaskState.objects.get(value=SubtaskState.Choices.DEFINING.value)
+    DEFINED = SubtaskState.objects.get(value=SubtaskState.Choices.DEFINED.value)
+    SCHEDULING = SubtaskState.objects.get(value=SubtaskState.Choices.SCHEDULING.value)
+    SCHEDULED = SubtaskState.objects.get(value=SubtaskState.Choices.SCHEDULED.value)
+    UNSCHEDULING = SubtaskState.objects.get(value=SubtaskState.Choices.UNSCHEDULING.value)
+    QUEUEING = SubtaskState.objects.get(value=SubtaskState.Choices.QUEUEING.value)
+    QUEUED = SubtaskState.objects.get(value=SubtaskState.Choices.QUEUED.value)
+    STARTING = SubtaskState.objects.get(value=SubtaskState.Choices.STARTING.value)
+    STARTED = SubtaskState.objects.get(value=SubtaskState.Choices.STARTED.value)
+    FINISHING = SubtaskState.objects.get(value=SubtaskState.Choices.FINISHING.value)
+    FINISHED = SubtaskState.objects.get(value=SubtaskState.Choices.FINISHED.value)
+    CANCELLING = SubtaskState.objects.get(value=SubtaskState.Choices.CANCELLING.value)
+    CANCELLED = SubtaskState.objects.get(value=SubtaskState.Choices.CANCELLED.value)
+    ERROR = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value)
+
+    SubtaskAllowedStateTransitions.objects.bulk_create([
+        SubtaskAllowedStateTransitions(old_state=None, new_state=DEFINING),
+        SubtaskAllowedStateTransitions(old_state=DEFINING, new_state=DEFINED),
+        SubtaskAllowedStateTransitions(old_state=DEFINED, new_state=SCHEDULING),
+        SubtaskAllowedStateTransitions(old_state=SCHEDULING, new_state=SCHEDULED),
+        SubtaskAllowedStateTransitions(old_state=SCHEDULED, new_state=STARTING), # this is an odd one, as most (all?) subtasks are queued before execution...
+        SubtaskAllowedStateTransitions(old_state=SCHEDULED, new_state=QUEUEING),
+        SubtaskAllowedStateTransitions(old_state=SCHEDULED, new_state=UNSCHEDULING),
+        SubtaskAllowedStateTransitions(old_state=UNSCHEDULING, new_state=DEFINED),
+        SubtaskAllowedStateTransitions(old_state=QUEUEING, new_state=QUEUED),
+        SubtaskAllowedStateTransitions(old_state=QUEUED, new_state=STARTING),
+        SubtaskAllowedStateTransitions(old_state=STARTING, new_state=STARTED),
+        SubtaskAllowedStateTransitions(old_state=STARTED, new_state=FINISHING),
+        SubtaskAllowedStateTransitions(old_state=FINISHING, new_state=FINISHED),
+        SubtaskAllowedStateTransitions(old_state=CANCELLING, new_state=CANCELLED),
+
+        SubtaskAllowedStateTransitions(old_state=DEFINING, new_state=ERROR),
+        SubtaskAllowedStateTransitions(old_state=SCHEDULING, new_state=ERROR),
+        SubtaskAllowedStateTransitions(old_state=UNSCHEDULING, new_state=ERROR),
+        SubtaskAllowedStateTransitions(old_state=QUEUEING, new_state=ERROR),
+        SubtaskAllowedStateTransitions(old_state=STARTING, new_state=ERROR),
+        SubtaskAllowedStateTransitions(old_state=STARTED, new_state=ERROR),
+        SubtaskAllowedStateTransitions(old_state=FINISHING, new_state=ERROR),
+
+        SubtaskAllowedStateTransitions(old_state=DEFINED, new_state=CANCELLING),
+        SubtaskAllowedStateTransitions(old_state=SCHEDULED, new_state=CANCELLING),
+        SubtaskAllowedStateTransitions(old_state=QUEUED, new_state=CANCELLING),
+        SubtaskAllowedStateTransitions(old_state=STARTED, new_state=CANCELLING),
+        SubtaskAllowedStateTransitions(old_state=FINISHED, new_state=CANCELLING),
+        SubtaskAllowedStateTransitions(old_state=ERROR, new_state=CANCELLING)
+        ])
+
 def populate_settings(apps, schema_editor):
     Setting.objects.create(name=SystemSettingFlag.objects.get(value='dynamic_scheduling_enabled'), value=False)
 
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py
index e77cd1e3194586d1dbad0f4591ab987dc953d2fc..a1fa19dfbe1281281f184328e28c9f1528bc0274 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py
@@ -16,6 +16,12 @@ class SubtaskStateSerializer(DynamicRelationalHyperlinkedModelSerializer):
         fields = '__all__'
 
 
+class SubtaskAllowedStateTransitionsSerializer(DynamicRelationalHyperlinkedModelSerializer):
+    class Meta:
+        model = models.SubtaskAllowedStateTransitions
+        fields = '__all__'
+
+
 class SubtaskStateLogSerializer(DynamicRelationalHyperlinkedModelSerializer):
     class Meta:
         model = models.SubtaskStateLog
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py
index a1201686b644fd793dee48bcf869b90547b9781f..781b7d4bcbcc4aa38cc6dbf900c016bdd09cbe2b 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py
@@ -50,6 +50,11 @@ class SubtaskStateViewSet(LOFARViewSet):
     serializer_class = serializers.SubtaskStateSerializer
 
 
+class SubtaskAllowedStateTransitionsViewSet(LOFARViewSet):
+    queryset = models.SubtaskAllowedStateTransitions.objects.all()
+    serializer_class = serializers.SubtaskAllowedStateTransitionsSerializer
+
+
 class SubtaskStateLogViewSet(LOFARViewSet):
     queryset = models.SubtaskStateLog.objects.all()
     serializer_class = serializers.SubtaskStateLogSerializer
diff --git a/SAS/TMSS/backend/src/tmss/urls.py b/SAS/TMSS/backend/src/tmss/urls.py
index 1772bdc3b68f9bc789166e2efd9f0814ffa0b707..5306787cb405fa524cbb475cc7d7e76d1fe3c561 100644
--- a/SAS/TMSS/backend/src/tmss/urls.py
+++ b/SAS/TMSS/backend/src/tmss/urls.py
@@ -216,6 +216,7 @@ router.register(r'filesystem', viewsets.FilesystemViewSet)
 router.register(r'cluster', viewsets.ClusterViewSet)
 router.register(r'dataproduct_archive_info', viewsets.DataproductArchiveInfoViewSet)
 router.register(r'dataproduct_hash', viewsets.DataproductHashViewSet)
+router.register(r'subtask_allowed_state_transitions', viewsets.SubtaskAllowedStateTransitionsViewSet)
 router.register(r'subtask_state_log', viewsets.SubtaskStateLogViewSet)
 router.register(r'user', viewsets.UserViewSet)
 router.register(r'sap', viewsets.SAPViewSet)