HumanSignal/label-studio · warning · ValidationError

Conversion to {export_type} already started

Error message

Conversion to {export_type} already started

What it means

The convert endpoint post() uses get_or_create on ConvertedFormat excluding FAILED records; if a matching non-failed ConvertedFormat already exists (created == False), a conversion for that export_type was already started or completed, so it raises ValidationError 'Conversion to {export_type} already started' to prevent duplicate jobs.

Source

Thrown at label_studio/data_export/api.py:703

)
class ExportConvertAPI(generics.CreateAPIView):
    queryset = Export.objects.all()
    lookup_url_kwarg = 'export_pk'
    permission_required = all_permissions.projects_change

    def post(self, request, *args, **kwargs):
        snapshot = self.get_object()
        serializer = ExportConvertSerializer(data=request.data, context={'project': snapshot.project})
        serializer.is_valid(raise_exception=True)
        export_type = serializer.validated_data['export_type']
        download_resources = serializer.validated_data.get('download_resources')

        converted_format, created = ConvertedFormat.objects.exclude(
            status=ConvertedFormat.Status.FAILED
        ).get_or_create(export=snapshot, export_type=export_type)

        if not created:
            raise ValidationError(f'Conversion to {export_type} already started')

        start_job_async_or_sync(
            async_convert,
            converted_format.id,
            export_type,
            snapshot.project,
            request.build_absolute_uri('/'),
            download_resources=download_resources,
            on_failure=set_convert_background_failure,
        )
        return Response({'export_type': export_type, 'converted_format': converted_format.id})

View on GitHub (pinned to 0b49e9b539)

Solutions

  1. Don't re-POST; poll the existing ConvertedFormat status until it completes, then download via GET ?export_type=<type>.
  2. Treat this as idempotent: catch the error and just proceed to poll/download.
  3. If a job is stuck IN_PROGRESS indefinitely, have an admin reset/delete the ConvertedFormat row (or the FAILED ones are auto-retriable by design) and re-POST.
  4. Add client-side disable/dedupe on the convert button while a job is pending.

Example fix

// before (duplicate request)
POST /api/projects/1/exports/abc/convert  -> 400 already started
POST /api/projects/1/exports/abc/convert  -> 400 already started

// after
POST /api/projects/1/exports/abc/convert  (once)
GET  /api/projects/1/exports/abc?export_type=CSV  (poll until ready)
Defensive patterns

Strategy: try-catch

Validate before calling

def conversion_pending(snapshot, export_type):
    return snapshot.converted_formats.exclude(status='failed').filter(
        export_type=export_type
    ).exists()

Try / catch

try:
    trigger_conversion(snapshot_id, export_type)
except ValidationError as e:
    if 'already started' in str(e):
        pass  # idempotent: existing job will finish; go poll status
    else:
        raise
wait_for_conversion(snapshot_id, export_type)

Prevention

When it happens

Trigger: POSTing the convert request twice for the same export snapshot and export_type (CSV twice), or after a previous conversion is still IN_PROGRESS or already COMPLETED.

Common situations: Users double-clicking the export-convert button; clients retrying with unclear feedback; an earlier conversion succeeded so a new one is unnecessary; a previous job stuck IN_PROGRESS blocking retries without cleanup.

Related errors


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