10. Mutable tag external_id for institution-defined identifiers#

Status#

Proposed

Context#

Issue #625 asks the project to determine an ID field strategy for Competency-Based Education (CBE) competency data. Competency Management UX designs show a “Competency ID” on each competency tag: a short, human-readable identifier an institution assigns and can reference in its own systems (spreadsheets, internal catalogs, etc). Clicking it drives the workflow that creates a CompetencyCriteria association between the competency and content criteria rules.

This identifier is explicitly not a CASE or CTDL identifier. Those are external standards bodies’ IDs that may need their own storage in the future; conflating them with an institution’s internal identifier would make each harder to reason about independently. This ADR is scoped to the internal, institution-defined identifier only, and assumes one such identifier per tag: no LMS surveyed for this ADR supports multiple institution identifiers for the same competency.

Institutions do rename these identifiers: when a competency taxonomy is revised (for example, after a curriculum or accreditation-standard update), the existing tags get renumbered in place rather than the institution creating a brand new taxonomy for the same competencies. Any solution needs to survive that rename without losing the tag’s existing associations (e.g. its CompetencyCriteria links) or its import/export identity.

Tag already has an external_id field, used to link a Tag to a record in an externally-defined taxonomy. The name reflects that the identifier is external to Open edX, not external to the institution: the institution that assigns it is free to use it for exactly this kind of internal, institution-defined identifier. Today, though, external_id is de facto immutable: no API path mutates it after creation, tag re-sync code documents it as “must be an immutable ID”, and import/export uses it purely as the lookup key to find and update existing tags on re-import.

Decision#

Use Tag.external_id as the institution’s editable competency identifier, and enable a pathway for its value to change, rather than adding a new field.

  • The mutability pathway is import-only for now: the tag import file format gains a new optional column, previous_id. When a row’s previous_id matches an existing tag’s external_id within the taxonomy, and the row’s id differs from it, the import treats the row as a rename: it updates that tag’s external_id to the new id (and any other changed fields) in place, instead of deleting the old tag and creating a new one. This preserves the tag’s existing associations and its import/export identity across the rename.

  • Editing external_id through the Taxonomy Editing UI or the generic tag-update REST API is anticipated as future-phase work. Both are unchanged by this decision for now.

  • previous_id is import-only: it is not a new persisted field and does not appear in exports. Institutions should maintain their own record of prior identifiers for now; keeping that history within Open edX itself could be a future phase of work.

  • The existing per-taxonomy uniqueness constraint on external_id (unique_together on (taxonomy, external_id)) is unchanged and still applies to a rename: if the new id collides with a different existing tag in the same taxonomy, the import rejects the row, the same way a duplicate external_id on tag creation already does today.

  • No schema change and no migration: external_id already permits writes at the model layer, and previous_id is read per-row from the import file and consumed only while generating the import plan.

Future considerations#

Merging multiple existing tags into one is a future phase of work that is not precluded by this decision. Its semantics (what happens to each old tag’s associations, which tag’s other fields win) would be defined in a future document. previous_id is a transient import-file column, not a database field name, so supporting multiple prior identifiers later likely wouldn’t require renaming it or introducing a new column: it could just accept multiple values under the same previous_id key when that need arises. Alternatively, renaming it to previous_ids at that point wouldn’t be too costly either, since it’s an import-format detail rather than a stored field.

Rejected Alternatives#

Add a new Tag.code field#

An earlier draft of this ADR proposed a generic, per-taxonomy-unique identifier field, separate from external_id, to get a mutable identifier without disturbing external_id’s immutability contract. Community review converged on relaxing external_id itself instead: a second field would equal external_id in value the vast majority of the time, reading as duplication to end users.

Separate CompetencyMetadata table#

Proposed during review as a home for competency-specific tag fields, analogous to CompetencyTaxonomy. Rejected: no competency-specific tag metadata exists today beyond the identifier this ADR handles via external_id. If a descriptive field (e.g. a tag description or competency statement) is needed later, it belongs directly on Tag as a generic field, since other taxonomies could reasonably want tag descriptions too; a competency-specific table would need its own justification if and when competency-specific metadata actually arises.

Keep external_id strictly immutable#

Treat every external_id change as a delete-and-recreate instead of relaxing the immutability contract. Rejected: this loses the tag’s existing associations (e.g. its CompetencyCriteria links) and its import/export identity, exactly the problem institutions hit when they rename an identifier.

Changelog#

2026-07-07:

  • Revised: dropped the new Tag.code field. Tag.external_id becomes mutable instead, through a new previous_id import column, per PR review and follow-up team discussion.

2026-07-03:

  • Proposed.