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’sprevious_idmatches an existing tag’sexternal_idwithin the taxonomy, and the row’siddiffers from it, the import treats the row as a rename: it updates that tag’sexternal_idto the newid(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_idthrough 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_idis 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_togetheron(taxonomy, external_id)) is unchanged and still applies to a rename: if the newidcollides with a different existing tag in the same taxonomy, the import rejects the row, the same way a duplicateexternal_idon tag creation already does today.No schema change and no migration:
external_idalready permits writes at the model layer, andprevious_idis 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.codefield.Tag.external_idbecomes mutable instead, through a newprevious_idimport column, per PR review and follow-up team discussion.
2026-07-03:
Proposed.