{"record":{"id":"f6cb834d6f973374","repo":"HumanSignal/label-studio","slug":"there-are-count-annotation-s-created-with-tag","errorCode":null,"errorMessage":"There are {count} annotation(s) created with tag \"{control_tag_from_data}\", you can't remove it","messagePattern":"There are (.+?) annotation\\(s\\) created with tag \"(.+?)\", you can't remove it","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"error","filePath":"label_studio/projects/models.py","lineNumber":769,"sourceCode":"\n            for sibling_tag, sibling_tag_info in parsed_config.items():\n                if sibling_tag_info.get('type') != 'Labels':\n                    continue\n                if control_to_names.intersection(sibling_tag_info.get('to_name') or []):\n                    labels_from_config_by_tag |= set(labels_from_config.get(sibling_tag, []))\n            return labels_from_config_by_tag\n\n        for control_tag_from_data, labels_from_data in created_labels.items():\n            # Check if labels created in annotations, and their control tag has been removed\n            if (\n                labels_from_data\n                and (\n                    (control_tag_from_data not in labels_from_config)\n                    and (control_tag_from_data not in dynamic_label_from_config)\n                )\n                and not check_control_in_config_by_regex(config_string, control_tag_from_data)\n            ):\n                raise ValidationError(\n                    f'There are {sum(labels_from_data.values(), 0)} annotation(s) created with tag '\n                    f'\"{control_tag_from_data}\", you can\\'t remove it'\n                )\n            control_tag_from_config = get_original_fromname_by_regex(config_string, control_tag_from_data)\n            labels_from_config_by_tag = set(labels_from_config[control_tag_from_config])\n            labels_from_config_by_tag = add_separated_video_object_labels(\n                control_tag_from_config, labels_from_config_by_tag\n            )\n            if 'Taxonomy' in tag_types:\n                custom_tags = Label.objects.filter(links__project=self).values_list('value', flat=True)\n                flat_custom_tags = set([item for sublist in custom_tags for item in sublist])\n                labels_from_config_by_tag |= flat_custom_tags\n            # check if labels from is subset if config labels\n            if not set(labels_from_data).issubset(set(labels_from_config_by_tag)):\n                different_labels = list(set(labels_from_data).difference(labels_from_config_by_tag))\n                diff_str = ''\n                for label in different_labels:\n                    annotation_label_count = self.summary.created_labels.get(control_tag_from_data, {}).get(label, 0)","sourceCodeStart":751,"sourceCodeEnd":787,"githubUrl":"https://github.com/HumanSignal/label-studio/blob/0b49e9b53917880baf1dd85d574fe5541a9aafb2/label_studio/projects/models.py#L751-L787","documentation":"When validating a new labeling config, Label Studio checks that control tags (from_name) referenced by existing annotations still exist in the config. If annotations were created with a tag that is absent from the new config (and not matched by dynamic label/regex rules), this ValidationError is raised, because removing the tag would orphan those annotation results.","triggerScenarios":"POST /api/projects/<id>/validate-label-config or project save where sum(labels_from_data.values()) > 0 (annotations exist for that control tag) and the tag name no longer appears in config_string nor in dynamic labels nor matches check_control_in_config_by_regex.","commonSituations":"Deleting a <Choices> or <Label> tag from the XML config after annotators used it; replacing a config wholesale with a new template; cleaning up 'unused' tags that actually still have annotation data.","solutions":["Re-add the control tag with that name to the labeling config","Delete the annotations that use the removed tag before saving the new config","Use dynamic labeling (regex) so removed concrete labels are tolerated while the tag itself stays","Export data first, then wipe project annotations before switching configs"],"exampleFix":"// before (tag removed)\n<View>\n  <Image name='image' value='$img'/>\n  <Choices name='quality' toName='image'>...</Choices>\n</View>\n// after (restore tag that annotations reference)\n<View>\n  <Image name='image' value='$img'/>\n  <Choices name='label' toName='image'>...</Choices>\n  <Choices name='quality' toName='image'>...</Choices>\n</View>","handlingStrategy":"validation","validationCode":"// Verify every control tag referenced by annotations exists in the new config\nconst annotations = await api.get('/api/annotations', {params:{project: projectId}});\nconst requiredTags = new Set(annotations.results.flatMap(a => a.result.map(r => r.from_name)));\nfor (const tag of requiredTags) {\n  if (!newConfig.includes(`name=\"${tag}\"`)) {\n    throw new Error(`Config must keep control tag: ${tag}`);\n  }\n}","typeGuard":"function configKeepsAllTags(config, tags) {\n  return tags.every(tag => config.includes(`name=\"${tag}\"`));\n}","tryCatchPattern":"try {\n  await projectApi.update(projectId, {label_config: newConfig});\n} catch (e) {\n  if (e.response?.status === 400 && /annotation\\(s\\) created with tag/.test(JSON.stringify(e.response.data))) {\n    const tag = JSON.stringify(e.response.data).match(/tag \\\\\"([^\\\\\"]+)\\\\\\\\\"/)?.[1];\n    // re-add tag `${tag}` to config or delete related annotations, then retry\n  } else throw e;\n}","preventionTips":["Treat existing annotation from_name values as an immutable contract for config edits","Before deleting a tag, query annotations to confirm zero usages","Prefer dynamic labeling for evolving label sets","Keep configs version-controlled with migration notes"],"tags":["label-studio","validation","labeling-config","annotations"],"backgroundTag":"schema-validation-failed","analyzedSha":"0b49e9b53917880baf1dd85d574fe5541a9aafb2","analyzedAt":"2026-08-29T00:39:52.578Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}