Skip to content
Open
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
181 changes: 181 additions & 0 deletions docs/openedx_tagging/decisions/0010-tag-code-field.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
.. _openedx-tagging-adr-0010:

10. Institution-defined tag code field
========================================

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 can reference in its own systems
(spreadsheets, internal catalogs, etc). Clicking it drives the workflow that
Comment on lines +17 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it always one identifier, or do institutions sometimes have multiple identifiers for the same tag (e.g. from different systems)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be only one identifier. Today Competency Frameworks/Taxonomies are sourced through hand crafted spreadsheets rather than any sort of source system probably 99% of the time. I'm pretty sure none of the other LMSs support multiple identifiers for competencies. If there were other identifiers, they would most likely be the CASE GUID or CTDL identifier, which should be explicitly stored as such.

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 code would make each harder to reason about
independently. This ADR is scoped to the internal, institution-defined code only.
Comment on lines +22 to +25

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I agree with that. If we can have a robust system that maps namespaced identifiers to tags, I could imagine us using a system where internal namespaces are peers to standardized ones.

That being said, if and when that time comes, we can always migrate the existing data that we have to some new, more general schema. So I do support the idea that we're focusing strictly on internal codes for now, with the simpler model that implies.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When support is added for the CASE and CTDL identifiers, I can definitely envision wanting something akin to a tag_identifiers table where the columns of that table are effectively just tag_id, other_id_type, and other_id to support other_id_types of CASE and CTDL. The institution id (code) and/or external_id could potentially be stored in this table too if desired. I wasn't trying to preclude that kind of an idea with this.

@mgwozdz-unicon mgwozdz-unicon Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although, it might make sense to continue keeping the third party identifiers separate from the institution/openedx identifiers. I'm not sure I'd want to migrate those (external_id and code) into this tags_identifiers table since they'd already be functioning fine where they are. Really the only reason to add a new table at all would be out of a fear that the number of additional identifiers might get unweildy, and really even just the 2 additional (CASE and CTDL) may not constitute "unweildy".


``Tag`` already has three identifiers, none of which fit:

- ``id``: internal primary key, never user-visible.
- ``uuid``: stable external reference, not human-readable or institution-assigned.
- ``external_id``: intended to link a Tag to a record in an externally-defined
taxonomy. It 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. Repurposing
it as an admin-editable Competency ID would conflict with that existing, load-bearing
contract.
Comment on lines +34 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand on why this is currently immutable, and why it must remain so? My understanding was that external_id was created for a similar use case to what's being proposed here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to jog my memory on this... I think it was because we needed an identifier that wasn't internal to Open edX (since sharing identifiers internal to the platform was deemed a security concern) that could be stable on import/export. Because otherwise, if you edit the external_id and re-import, then the platform has no way of knowing what you were editing. Whereas institutions need a mutable identifier that they can keep as human readable and in alignment with the current tag name that may be edited as well.

@ormsbee ormsbee Jul 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is it accurate to say that if we either exported the UUID, or provided a "previous_id" column in the import CSV format so that we could remap external_id entries, we wouldn't need this new code field?


**Placement constraint.** :ref:`openedx-tagging-adr-0002` keeps ``openedx_tagging``
standalone, and :ref:`openedx-learning-adr-0001` already rejected putting CBE tables
inside it for that reason. A CBE-named field on ``Tag`` would repeat that mistake,
but a single scalar identifier — the same shape ``external_id`` already fills — does
not. Every taxonomy listing, search, and export already reads ``Tag``; putting a
per-tag identifier in a separate app would force a cross-app join into that common
path for what is otherwise a plain string column.

Decision
--------

Add a new field, ``Tag.code``, to ``openedx_tagging`` as a **generic**,
non-CBE-specific identifier slot, available to any taxonomy:

- A plain, optional text field, matching ``external_id``'s shape.
- **Uniqueness:** enforced per-taxonomy, not globally unique (different
institutions/taxonomies may reasonably reuse a code) and not advisory-only (the CBE
workflow uses this value as a click target to create a ``CompetencyCriteria``
association; ambiguity within a taxonomy would be a routing bug, not a
data-quality nit).
- **Assignment:** user-entered when given in initial import; every non-Competency taxonomy leaves
``code`` fully optional with no fallback. For a Competency Taxonomy, every tag
needs one: creating a new tag without a code — via import, or via the existing
Taxonomy Editing UI, which has no field for it yet — generates a placeholder (e.g.
derived once from the tag's current name and position) instead of blocking the
tag's creation or an otherwise-valid import. If a user forgets to include the code column in their initial import, they can overwrite the placeholder
with their real code through a follow-up re-import, the same way any other field
is corrected. Re-importing an *existing* tag whose code is blank in the file leaves
its current value untouched rather than generating a new one — a blank cell isn't a
request to clear it since that is not allowed for Competency Taxonomies.
- **Completeness for Competency Taxonomies:** every tag in a Competency Taxonomy must
have a ``code``, guaranteed either by an explicit value or the placeholder fallback
above; every other taxonomy leaves it fully optional (see below for how this is
enforced without ``openedx_tagging`` knowing what a Competency Taxonomy is).
- **Read exposure:** ``code`` is exposed read-only outside of import, so it can still
be displayed on the Competency Management page and assignment workflow even though
it isn't editable there yet (planned for a later phase).
- **Migration:** additive only — a new nullable field, no backfill needed. No PII
annotation is required: ``code`` is institution-authored administrative metadata,
not personal data.

This keeps ``openedx_tagging`` free of CBE-specific concepts: the word "Competency"
never needs to appear in this app. The CBE applet's API and UI are responsible for
labeling ``code`` as "Competency ID" and driving the click-through workflow.

Completeness rule for Competency Taxonomies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``openedx_tagging`` must not need to know what a "Competency Taxonomy" is to enforce
completeness, per the placement constraint above. The base ``Taxonomy`` class gets a
property that defaults to "not required"; ``CompetencyTaxonomy``
(:ref:`openedx-learning-adr-0002`), a real subclass of ``Taxonomy`` defined outside
``openedx_tagging``, overrides it to require a code. The rule is enforced at the
point a tag is saved, so every path that creates one — import, the existing Taxonomy
Editing UI, or the generic API — is covered by one check rather than one per caller,
and none of them can accidentally create a code-less tag.

Import/export
~~~~~~~~~~~~~~

``code`` becomes an optional column in the import file format for every taxonomy. A
new tag with no ``code`` gets the placeholder fallback described above, whether it's
a Competency Taxonomy tag created via import or via the UI. Re-importing an existing
tag never clears or regenerates its ``code`` just because the file's column is blank
for that row; it only changes when the file supplies an explicit new value.

How a Competency Taxonomy itself gets created is out of this ADR's scope — that is
already covered by separate, in-progress work. This ADR's changes don't depend on
that work landing first, and don't introduce any new API endpoints of their own.

Rejected Alternatives
----------------------

Repurpose ``external_id``
~~~~~~~~~~~~~~~~~~~~~~~~~~

Reuse the existing ``external_id`` field instead of adding a new one. Rejected:
``external_id`` is de facto immutable and is the load-bearing lookup key for
import/export matching and system-defined-taxonomy re-sync. Giving it a second,
editable meaning would break that existing contract.

CBE-specific field directly on ``Tag``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Add a field explicitly named and documented as the "Competency ID" on ``Tag``, as a
deliberate exception to :ref:`openedx-tagging-adr-0002`'s boundary. Rejected: this is
the same schema location as the chosen decision, but naming it as a CBE concept bakes
domain semantics into a library meant to stay standalone, for no benefit over the
generic framing.

Competency-specific field in ``openedx_learning``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Add the identifier to a model in the new ``openedx_learning`` app instead, referenced
from ``Tag``. Rejected: this value is display data needed by every competency tag
listing, search, and export. A cross-app join for a plain string column adds cost with
no corresponding benefit, and no table in that app has the right cardinality (one row
per tag, not per taxonomy or per criterion).

``CompetencyTaxonomy`` as the home
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Attach the identifier to the ``CompetencyTaxonomy`` row itself. Rejected on
cardinality: ``CompetencyTaxonomy`` is one row per taxonomy; a Competency ID is one
value per tag.

Keep every code continuously derived from the tag's name and hierarchy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Treat the code as a live, computed value (e.g. ``PK-1.2``) that stays in sync with
the tag's current name and position, rather than a value the institution owns once
it exists. Rejected: once a tag has any code — real or a placeholder from the
fallback above — only a re-import changes it. Continuously recomputing it on every
rename or move would contradict that and mean the institution never really owns the
value once assigned.

Global uniqueness
~~~~~~~~~~~~~~~~~~

Enforce uniqueness across all taxonomies. Rejected: overreaches into assuming a
single global namespace for institution-chosen codes, which a shared library
shouldn't impose.

Advisory-only uniqueness
~~~~~~~~~~~~~~~~~~~~~~~~~

No database constraint, just a UI warning on collision. Rejected: the field is a
click target for creating a ``CompetencyCriteria`` association; a duplicate within a
taxonomy is a functional ambiguity, not just a data-quality nit.

Enforce completeness only during import, not at the model level
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Rejected: tags can already be created outside the import path today, through the
existing Taxonomy Editing UI or the generic tag-creation API, neither of which has a
way to supply a code directly. Enforcing the rule only during import would let those
paths silently create a code-less tag instead of falling back to a placeholder.

Changelog
---------

2026-07-03:

* Proposed.