HumanSignal/label-studio · error · NotImplementedError

FSMStateField is read-only. Use transitions to change state.

Error message

FSMStateField is read-only. Use transitions to change state.

What it means

FSMStateField is declared read-only for DRF serializers; to_internal_value raises NotImplementedError because incoming state values must never be written directly through a serializer. Entity state is an internal concept managed exclusively by the StateManager's transitions, which create INSERT-only audit records. Attempting to bind or deserialize client data into this field hits this hard stop.

Source

Thrown at label_studio/fsm/serializer_fields.py:125

            return instance.state
        elif hasattr(instance, 'current_state'):
            # Fallback to current_state annotation from FSMStateQuerySetMixin
            return instance.current_state

        # Fallback: Query the state manager
        # This happens when the queryset wasn't annotated
        # StateManager has its own caching, so this is still efficient
        try:
            return StateManager.get_current_state_value(instance)
        except Exception:
            # If FSM is disabled or state model not found, return None
            return None

    def to_internal_value(self, data):
        """
        This field is read-only, so this should never be called.
        """
        raise NotImplementedError('FSMStateField is read-only. Use transitions to change state.')

View on GitHub (pinned to 0b49e9b539)

Solutions

  1. Mark the field read-only in your serializer: state = FSMStateField(read_only=True) or add 'state' to Meta.read_only_fields.
  2. Change entity state by invoking a registered transition (StateManager.execute_transition or the transition API) instead of PATCHing the field.
  3. Filter incoming payloads to exclude 'state' before passing request.data into the serializer.

Example fix

// before
class TaskSerializer(serializers.ModelSerializer):
    state = FSMStateField()
    class Meta:
        fields = '__all__'

// after
class TaskSerializer(serializers.ModelSerializer):
    state = FSMStateField(read_only=True)
    class Meta:
        fields = '__all__'
        read_only_fields = ('state',)
Defensive patterns

Strategy: validation

Validate before calling

data = request.data
if 'state' in data:
    return Response({'state': 'Read-only; use transitions to change state.'}, status=400)
serializer = MyEntitySerializer(data=data)

Type guard

def payload_has_no_state_field(data: dict) -> bool:
    return 'state' not in data

Try / catch

try:
    serializer = MyEntitySerializer(instance, data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save()
except NotImplementedError as e:
    if 'FSMStateField is read-only' in str(e):
        return Response({'detail': 'Use a state transition to change state.'}, status=400)
    raise

Prevention

When it happens

Trigger: Including 'state' (the FSMStateField column) in writable serializer fields and submitting it via PATCH/POST; constructing a serializer with data= containing the state key; generic model-crud code that iterates model fields and builds a serializer accepting state input.

Common situations: Copy-pasting an entity serializer and forgetting to declare state read-only; a bulk import/update endpoint that echoes all model fields as writable; frontend form code that round-trips the full serialized entity back to the server including the state value.

Related errors


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