Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions docs/openedx_tagging/decisions/0010-mutable-tag-external-id.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
.. _openedx-tagging-adr-0010:

10. Mutable tag ``external_id`` for institution-defined identifiers
=====================================================================

Status
------

Proposed

Context
-------

`Issue #625 <https://github.com/openedx/openedx-core/issues/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.