From ab9f5b4f5a4db0b431f9fe7fa07a22d5fb30047d Mon Sep 17 00:00:00 2001 From: Yousif Yassi Date: Thu, 27 Nov 2025 14:18:23 -0500 Subject: [PATCH 1/2] fix: FIT-1059: Not sure 'Created' is the right name for the annotation state, perhaps 'Initial' could make more sense, take a look at the screenshot --- label_studio/tasks/serializers.py | 57 +++++++++++++++++++ .../components/state-chips/state-registry.ts | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/label_studio/tasks/serializers.py b/label_studio/tasks/serializers.py index e6c328f3aca4..ab175ac27bdb 100644 --- a/label_studio/tasks/serializers.py +++ b/label_studio/tasks/serializers.py @@ -689,8 +689,65 @@ def add_tasks(self, task_annotations, task_predictions, validated_tasks): logging.info(f'Tasks serialization success, len = {len(self.db_tasks)}') + # Backfill FSM states for bulk-created tasks + # bulk_create() bypasses save() so FSM transitions don't fire automatically + self._backfill_fsm_states(self.db_tasks) + return db_tasks + def _backfill_fsm_states(self, tasks): + """ + Backfill FSM states for tasks created via bulk_create(). + + bulk_create() bypasses the model's save() method, so FSM transitions + registered with triggers_on_create=True don't fire automatically. + This method manually initializes FSM states for those tasks. + + Args: + tasks: List of Task instances that were bulk-created + """ + if not tasks: + return + + try: + # Import here to avoid circular dependencies and to gracefully handle LSE-only imports + from lse_fsm.state_inference import backfill_state_for_entity + + logger.info(f'Backfilling FSM states for {len(tasks)} imported tasks') + + # Backfill initial state for each task + for task in tasks: + try: + backfill_state_for_entity(task, 'task', create_record=True) + except Exception as e: + # Log but don't fail the import if FSM backfill fails for a specific task + logger.warning( + f'FSM state backfill failed for task {task.id}: {e}', + extra={ + 'event': 'fsm.backfill_failed', + 'task_id': task.id, + 'project_id': task.project_id, + 'error': str(e), + }, + ) + + logger.info(f'FSM states backfilled for {len(tasks)} imported tasks') + + except ImportError: + # LSE not available (OSS), skip FSM backfill + logger.debug('LSE not available, skipping FSM state backfill for imported tasks') + except Exception as e: + # Don't fail import if FSM backfill fails + logger.error( + f'FSM state backfill failed for imported tasks: {e}', + extra={ + 'event': 'fsm.backfill_batch_failed', + 'task_count': len(tasks), + 'error': str(e), + }, + exc_info=True, + ) + @staticmethod def post_process_annotations(user, db_annotations, action): pass diff --git a/web/libs/app-common/src/components/state-chips/state-registry.ts b/web/libs/app-common/src/components/state-chips/state-registry.ts index b2d622aa0f26..c4e4e37bca62 100644 --- a/web/libs/app-common/src/components/state-chips/state-registry.ts +++ b/web/libs/app-common/src/components/state-chips/state-registry.ts @@ -248,7 +248,7 @@ export const stateRegistry = new StateRegistry(); stateRegistry.registerBatch({ CREATED: { type: StateType.INITIAL, - label: "Created", + label: "Initial", tooltips: { task: "Task has been created and is ready for annotation", annotation: "Annotation has been created", From 50f70fd806e48db121e45e75357d38e9d4612dcd Mon Sep 17 00:00:00 2001 From: Yousif Yassi Date: Fri, 28 Nov 2025 15:17:37 -0500 Subject: [PATCH 2/2] cleaning up the builk import backfill --- label_studio/tasks/serializers.py | 51 +++++-------------------------- 1 file changed, 7 insertions(+), 44 deletions(-) diff --git a/label_studio/tasks/serializers.py b/label_studio/tasks/serializers.py index ab175ac27bdb..241286a38619 100644 --- a/label_studio/tasks/serializers.py +++ b/label_studio/tasks/serializers.py @@ -12,6 +12,8 @@ from django.db import IntegrityError, transaction from drf_spectacular.utils import extend_schema_field from fsm.serializer_fields import FSMStateField +from fsm.state_manager import get_state_manager +from fsm.utils import is_fsm_enabled from label_studio_sdk.label_interface import LabelInterface from projects.models import Project from rest_flex_fields import FlexFieldsModelSerializer @@ -700,53 +702,14 @@ def _backfill_fsm_states(self, tasks): Backfill FSM states for tasks created via bulk_create(). bulk_create() bypasses the model's save() method, so FSM transitions - registered with triggers_on_create=True don't fire automatically. - This method manually initializes FSM states for those tasks. - - Args: - tasks: List of Task instances that were bulk-created + don't fire automatically. This sets initial CREATED state for newly imported tasks. """ - if not tasks: + if not tasks or not is_fsm_enabled(user=None): return - try: - # Import here to avoid circular dependencies and to gracefully handle LSE-only imports - from lse_fsm.state_inference import backfill_state_for_entity - - logger.info(f'Backfilling FSM states for {len(tasks)} imported tasks') - - # Backfill initial state for each task - for task in tasks: - try: - backfill_state_for_entity(task, 'task', create_record=True) - except Exception as e: - # Log but don't fail the import if FSM backfill fails for a specific task - logger.warning( - f'FSM state backfill failed for task {task.id}: {e}', - extra={ - 'event': 'fsm.backfill_failed', - 'task_id': task.id, - 'project_id': task.project_id, - 'error': str(e), - }, - ) - - logger.info(f'FSM states backfilled for {len(tasks)} imported tasks') - - except ImportError: - # LSE not available (OSS), skip FSM backfill - logger.debug('LSE not available, skipping FSM state backfill for imported tasks') - except Exception as e: - # Don't fail import if FSM backfill fails - logger.error( - f'FSM state backfill failed for imported tasks: {e}', - extra={ - 'event': 'fsm.backfill_batch_failed', - 'task_count': len(tasks), - 'error': str(e), - }, - exc_info=True, - ) + StateManager = get_state_manager() + for task in tasks: + StateManager.execute_transition(entity=task, transition_name='task_created', user=None) @staticmethod def post_process_annotations(user, db_annotations, action):