> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/features/data-discovery/per-column-annotation.md).

# Per-column annotation

Every dataset in the catalog exposes a **Structure** tab next to its Overview tab. The Structure tab lists the dataset's columns and lets an operator annotate each column with a description, tags, business-glossary terms, enum values, and a business name — the column-level counterparts to the entity-level Overview surfaces. Each column row opens a composer that hosts five sub-editors stacked vertically; saving a sub-editor calls a distinct backend endpoint per sub-surface.

This page covers the composer's five sub-editors, the permissions each one is gated by, the audit-trail coverage per sub-surface, and three load-bearing write-path caveats plus two latent UI hazards.

## Where to find it

Open any dataset's detail page → **Structure** tab. The columns table lists every column with its name, type, primary-key flag, source-side description, and the operator-authored annotations (when set). Clicking a column row expands the composer below the row, surfacing five sub-editors in order:

1. **Description** — free-text Markdown rendered with the same pipeline as the entity-level [description](/features/data-discovery/entity-description.md).
2. **Tags** — column-scoped tag chips, sharing the deployment-wide tag vocabulary with [Manual Object Tagging](/features/data-discovery/tagging.md).
3. **Glossary terms** — business-glossary terms linked to the column, separately from terms linked to the parent dataset (see [Business Glossary](/features/data-glossary/business-glossary.md)).
4. **Enum values** — operator-curated enumeration list documenting the column's allowed values (one row per enum entry with a label and a description).
5. **Business name** — a human-readable alternative to the technical column name, surfaced alongside the technical name everywhere the column is rendered (see [Business names](/features/data-discovery/business-names.md)).

The composer is the only place to author these annotations; the API is the same surface — third-party clients calling the per-sub-editor endpoints listed below write the same fields with the same caveats.

## Permissions

Each sub-editor is gated by a distinct permission:

| Sub-editor    | Permission                                                                                                         | What it gates                                                                                                                                    |
| ------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Description   | [`DATASET_FIELD_DESCRIPTION_UPDATE`](/configuration-and-deployment/enable-security/authorization/permissions.md)   | The Description sub-editor + `PUT /api/datasetfields/{id}/description`.                                                                          |
| Tags          | [`DATASET_FIELD_TAGS_UPDATE`](/configuration-and-deployment/enable-security/authorization/permissions.md)          | The Tags sub-editor + `PUT /api/datasetfields/{id}/tags`.                                                                                        |
| Term — add    | [`DATASET_FIELD_ADD_TERM`](/configuration-and-deployment/enable-security/authorization/permissions.md)             | The Add-term affordance on the Terms sub-editor. **See the wiring-bug caveat below — this permission is not the one the server enforces today.** |
| Term — delete | [`DATASET_FIELD_DELETE_TERM`](/configuration-and-deployment/enable-security/authorization/permissions.md)          | The remove-affordance on each term row + `DELETE /api/datasetfields/{id}/terms/{term_id}`.                                                       |
| Enum values   | [`DATASET_FIELD_ENUMS_UPDATE`](/configuration-and-deployment/enable-security/authorization/permissions.md)         | The Enum-values sub-editor + `POST /api/datasetfields/{id}/enum_values`.                                                                         |
| Business name | [`DATASET_FIELD_INTERNAL_NAME_UPDATE`](/configuration-and-deployment/enable-security/authorization/permissions.md) | The Business-name affordance + `PUT /api/datasetfields/{id}/name`.                                                                               |

All six are scoped against the dataset that owns the column in the URL, not the column itself — granting any of them on a dataset grants the corresponding edit on every column of that dataset.

## Activity trail

Each sub-editor emits its own Activity Feed event on save:

| Sub-editor           | Activity event                          | Payload                          |
| -------------------- | --------------------------------------- | -------------------------------- |
| Description          | `DATASET_FIELD_DESCRIPTION_UPDATED`     | Before / after description text. |
| Tags                 | `DATASET_FIELD_TAGS_UPDATED`            | Before / after tag-name sets.    |
| Term — add or delete | `DATASET_FIELD_TERM_ASSIGNMENT_UPDATED` | Before / after term-id sets.     |
| Enum values          | `DATASET_FIELD_VALUES_UPDATED`          | Before / after enum-value sets.  |
| Business name        | `DATASET_FIELD_INTERNAL_NAME_UPDATED`   | Before / after business name.    |

All five emit the full before/after payload — column-level annotations have a fuller audit shape than several entity-level mutations (see the [Activity Feed scope summary](/features/active-platform-features/activity-feed.md) and the [Audit trail scope](/configuration-and-deployment/enable-security/audit-trail-scope.md) for the platform-wide audit-coverage matrix).

## Known limitations and operator caveats

{% hint style="danger" %}
**The field-level "Add term" affordance silently fails for users holding `DATASET_FIELD_ADD_TERM`.** Two adjacent SecurityConstants entries are crossed in the platform's authorization wiring at `SecurityConstants.java:295-299`:

**Operator-visible effect of the column-term bug.** A user with `DATASET_FIELD_ADD_TERM` on a dataset sees the column-level Add-term button enabled (the UI gates on the documented permission). Clicking it submits the request and the server rejects it as `403 Forbidden`. The UI catches the error silently — the term row never appears in the column's Terms list, and there is no visible error toast. The operator sees the affordance, clicks it, sees nothing happen, retries, sees nothing happen again, and gives up. The workaround today: grant `DATA_ENTITY_ADD_TERM` (the entity-scope permission) instead of, or in addition to, `DATASET_FIELD_ADD_TERM`.

**Operator-visible effect of the alert-status bug.** A user with `DATASET_FIELD_ADD_TERM` can resolve any alert on any entity. The intended permission for alert-status mutation is `DATA_ENTITY_ALERT_RESOLVE` (see [Alerting](/features/active-platform-features/alerting.md)). Audit a deployment's `DATASET_FIELD_ADD_TERM` grants if alert-resolution audit matters to compliance.

**Mitigation today.** Grant `DATA_ENTITY_ADD_TERM` to operators who need to link terms to columns; treat `DATASET_FIELD_ADD_TERM` grants as also-granting alert-resolution. The upstream fix is two one-line changes to the platform's security-rule wiring; until it ships, the documented permission list and the runtime gate diverge.
{% endhint %}

| Endpoint                                                                    | Documented permission (UI gate) | Server-enforced permission (runtime gate) |
| --------------------------------------------------------------------------- | ------------------------------- | ----------------------------------------- |
| `PUT /api/alerts/{alert_id}/status` (resolve an alert)                      | `DATA_ENTITY_ALERT_RESOLVE`     | `DATASET_FIELD_ADD_TERM`                  |
| `POST /api/datasetfields/{dataset_field_id}/terms` (add a term to a column) | `DATASET_FIELD_ADD_TERM`        | `DATA_ENTITY_ADD_TERM`                    |

{% hint style="warning" %}
**Saving the column's enum-values list silently soft-deletes any pre-existing values not present in the submitted body.** The platform's `POST /api/datasetfields/{id}/enum_values` endpoint declares its operation as `createEnumValue` in the OpenAPI spec (the name implies create-one), but the underlying service is a **bulk replace** — items whose `id` is null are inserted; items whose `id` is present are updated; **every pre-existing row whose id is not in the submitted list is soft-deleted**.

The Structure-tab UI handles this correctly: when the operator edits any single row, the composer submits the **full** current set (preserving all existing rows that the operator did not touch). The hazard is on the **API surface** — a third-party SDK consumer reading the operation name in the OpenAPI spec, assuming create-one semantics, and submitting only the new row will silently soft-delete every other enum value on the column.

The silent soft-delete applies only when the column's enum values are **all operator-curated (INTERNAL)**. If even one **collector-ingested (EXTERNAL)** enum value is present on the column, the endpoint instead **rejects** any submission whose value-name set does not exactly match the values already on the column — the call fails with `400 Bad Request` ("User cannot create or delete external enum values") rather than soft-deleting. On such a column you can only edit the descriptions of the existing values; you cannot add or remove values through this endpoint.

**Mitigation today.** Treat the endpoint as "submit the full target enum set, every time." A GET-then-POST pattern is the safe shape. The upstream fix is a rename to `replaceEnumValues` (or a true append-only `createEnumValue` plus a separate `replaceEnumValues`); the spec's `description` field already acknowledges the actual behaviour ("Creates/updates/deletes enum values with their description").
{% endhint %}

{% hint style="warning" %}
**Saving the column's tag list as an empty array silently clears every operator-curated tag on the column.** The platform's `PUT /api/datasetfields/{id}/tags` endpoint deletes every INTERNAL-origin tag-to-column row first, then re-inserts the submitted set. Submitting `{"tagNameList": []}` succeeds with no error and leaves the column with zero operator-curated tags. The full before/after payload is captured in the `DATASET_FIELD_TAGS_UPDATED` activity event — the deletion is auditable, but a user fast-clicking through the Structure tab does not see it as destructive.

EXTERNAL-origin tags (collector-ingested via the `EXTERNAL_STATISTICS` channel) **survive** the operation — only INTERNAL-origin tag rows are cleared. A column whose tags came entirely from a collector retains them; a column with operator-added tags loses them.

**Mitigation today.** Submit the full target tag set on every save, not an empty list. The Structure-tab UI submits the current tag set correctly; the hazard, again, is on the API surface for third-party SDK consumers.
{% endhint %}

{% hint style="warning" %}
**Fast-switching between columns while a sub-editor is open may submit the previous column's data against the new column's id (unverified hazard).** The composer mounts all five sub-editors unconditionally for the active column. When an operator clicks a different column row while a sub-editor's modal is open, the sub-editor stays mounted but the active-column id atom updates; the form state was initialised from the previous column's data. Depending on which sub-editor is open and how the form-state closure was captured at mount time, a save click on the open modal **may** submit the previous column's values against the new column's id.

This hazard is currently **not verified by a runtime probe** — the failure shape is inferred from the composer's source structure. Saves on the affected sub-editors are gated by their permissions and the activity feed captures the after-state, so a fast-switching mistake would be auditable. Until a probe confirms or rules out the hazard, **always close the open sub-editor before clicking a different column**.

The upstream fix is straightforward (a confirm-on-close + form-reset on column-id change); the doc-side caveat persists until the fix lands.
{% endhint %}

## Where to next

* [Data entity detail page](/features/data-discovery/entity-detail-page.md) — the parent container for the Structure tab and the Overview tab; covers the per-class panel matrix that decides which tabs appear.
* [Entity description](/features/data-discovery/entity-description.md) — the entity-level counterpart to the column-level description sub-editor; shares the Markdown renderer and the load-bearing no-write-time-sanitisation caveat across six surfaces (this page is one of the six).
* [Custom metadata](/features/data-discovery/custom-metadata.md) — sibling per-entity Overview surface for typed key/value annotations; same authoring philosophy, different cardinality.
* [Manual Object Tagging](/features/data-discovery/tagging.md) — the read-side canonical home for tags, including the operator-caveat list for the global vocabulary the column-level Tags sub-editor writes into.
* [Business Glossary](/features/data-glossary/business-glossary.md) — the canonical home for terms; the column-level Terms sub-editor is one of three places terms can be linked (alongside the entity-level Terms panel and the term-mention `[[Namespace:Term]]` auto-link in any Markdown description).
* [Business names](/features/data-discovery/business-names.md) — the canonical home for business-name semantics; the column-level Business-name sub-editor shares the same vocabulary.
* [Permissions](/configuration-and-deployment/enable-security/authorization/permissions.md) — the canonical home for the six `DATASET_FIELD_*` permissions and the wiring-bug flag on `DATASET_FIELD_ADD_TERM`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.opendatadiscovery.org/features/data-discovery/per-column-annotation.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
