HumanSignal/label-studio · error · ValidationError

transition_name: Transition is auto-triggered and cannot be

Error message

transition_name: Transition is auto-triggered and cannot be executed manually

What it means

This 400 validation error is raised by FSMEntityTransitionAPI.post when the requested transition_name refers to a ModelChangeTransition subclass that has _triggers_on_create or _triggers_on_update set. Such transitions run automatically when the entity model is created or updated, so executing them through the manual transition endpoint would bypass (or duplicate) the automatic trigger. The FSM API refuses to invoke auto-triggered transitions by name via POST /api/fsm/entities/{entity_type}/{entity_id}/transition/.

Source

Thrown at label_studio/fsm/api.py:170

        entity = self.get_entity()

        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        transition_name = serializer.validated_data['transition_name']
        transition_data = serializer.validated_data.get('transition_data') or {}

        # Validate that transition is registered and manual (not auto-triggered)
        transition_class = transition_registry.get_transition(entity_name, transition_name)
        if not transition_class:
            raise ValidationError({'transition_name': ['Unknown transition for this entity']})

        # If it's a ModelChangeTransition and has any triggers configured, it's not manual
        if issubclass(transition_class, ModelChangeTransition):
            triggers_on_create = getattr(transition_class, '_triggers_on_create', False)
            triggers_on_update = getattr(transition_class, '_triggers_on_update', False)
            if triggers_on_create or triggers_on_update:
                raise ValidationError(
                    {'transition_name': ['Transition is auto-triggered and cannot be executed manually']}
                )

        # Execute transition
        StateManager = get_state_manager()
        try:
            state_record = StateManager.execute_transition(
                entity=entity,
                transition_name=transition_name,
                transition_data=transition_data,
                user=request.user,
                organization_id=getattr(request.user, 'active_organization_id', None),
            )
        except PydanticValidationError as e:
            # Pydantic schema validation errors from transition instantiation
            raise ValidationError({'detail': extract_message(e)})
        except TransitionValidationError as e:
            # Explicit validation failure

View on GitHub (pinned to 0b49e9b539)

Solutions

  1. Remove this transition_name from the manual API call; let the transition fire automatically on entity create/update instead.
  2. If a manual variant is needed, register a separate plain Transition (not a ModelChangeTransition with triggers) and call that name from the API.
  3. Check the transition class for _triggers_on_create/_triggers_on_update (set via trigger decorators/settings) and remove those triggers if the transition should truly be manual-only.
  4. Filter the registry listing used to build client UIs so auto-triggered transitions are not offered for manual execution.

Example fix

// before
POST /api/fsm/entities/task/123/transition/
{"transition_name": "task_updated_on_draft_unsaved"}  // auto-triggered on update

// after
// don't call it manually; update the entity and let the trigger fire:
Task.objects.filter(id=123).update(...)  // ModelChangeTransition fires automatically
// or register a manual-only transition and call it:
{"transition_name": "manually_approve_task"}
Defensive patterns

Strategy: validation

Validate before calling

from fsm.registry import transition_registry
from fsm.transitions import ModelChangeTransition

tc = transition_registry.get_transition(entity_name, transition_name)
manual_ok = tc is not None and not (
    issubclass(tc, ModelChangeTransition)
    and (getattr(tc, '_triggers_on_create', False) or getattr(tc, '_triggers_on_update', False))
)
if not manual_ok:
    raise ValueError(f'{transition_name} is not manually executable')

Type guard

def is_manual_transition(transition_class) -> bool:
    from fsm.transitions import ModelChangeTransition
    if transition_class is None or not issubclass(transition_class, ModelChangeTransition):
        return transition_class is not None
    return not (getattr(transition_class, '_triggers_on_create', False) or getattr(transition_class, '_triggers_on_update', False))

Try / catch

from rest_framework.exceptions import ValidationError
try:
    resp = client.post(f'/api/fsm/entities/{entity_type}/{entity_id}/transition/', payload)
except Exception as e:
    if 'auto-triggered and cannot be executed manually' in str(e):
        logger.info('transition %s is auto-triggered; skipping manual call', transition_name)
    else:
        raise

Prevention

When it happens

Trigger: POSTing to the FSM transition endpoint with a transition_name that resolves (via transition_registry.get_transition) to a ModelChangeTransition subclass whose _triggers_on_create or _triggers_on_update flag is True. Typically a client copies a transition name from the registry/code that is meant to fire from the model save path, not from the API.

Common situations: Automations that build transition calls from a registry listing all transitions without filtering auto-triggered ones; docs/SDK examples that show a transition name registered for model-save triggering; a transition class where someone later added on_create/on_update triggers, breaking previously-working manual API calls.

Related errors


AI-assisted analysis of HumanSignal/label-studio@0b49e9b539 (2026-08-29). Data as JSON: /api/errors/d1c1f636caed23c0. Report an issue: GitHub.