ITEM_TAG — labelling versioned objects
ITEM_TAG is the openEHR mechanism for attaching small key/value labels to a versioned object — a Composition, EHR_STATUS, or a Demographic resource (Person, Agent, Group, Organisation, Role). Use it to mark a resource with categories, workflow flags, or pointers to a specific path inside the resource (for example /composition/start_time/value), without changing the underlying clinical content.
A tag has three fields:
| Field | Required | Purpose |
|---|---|---|
key |
yes | Identifier of the tag (case-insensitive). Unique per target + target_path. |
value |
no | Optional value associated with the tag. |
target_path |
no | Optional pointer into the target resource. Max 500 characters. |
Tags are scoped to the versioned object they are attached to — when a new version is committed, the tags follow along automatically.
After reading this guide, you should be able to:
- Manage tags on Composition, EHR_STATUS, and every demographic resource through dedicated endpoints
- Send tags inline on write endpoints with the
openehr-item-tagheader - Opt in to tag retrieval on
GETcalls via thePrefer: include_item_tagstoken - List all tags within an EHR or the demographic store with a single query
- Understand which tag features Cadasto does and does not support
For background, see the openEHR ITS-REST ITEM_TAG specification.
Two ways to manage tags
Cadasto exposes two complementary entry points for tags. Both write to the same underlying storage and both honour the same uniqueness and validation rules.
| Entry point | Use when |
|---|---|
/tags subresource |
You want to manage tags independently of the parent resource |
openehr-item-tag HTTP header |
You want to set tags in the same transaction as a create/update of the resource |
Both routes are available for Composition, EHR_STATUS, and every demographic resource (Person, Agent, Group, Organisation, Role). The /tags subresource on demographic targets lives under the same parent path as the resource itself, e.g. PUT /demographic/person/{uid}/tags.
Listing tags across a scope
Two read-only collection endpoints return every tag attached to any target within a given scope, with optional tag_key, tag_value, and tag_target_path filters:
| Scope | Endpoint | What it returns |
|---|---|---|
| EHR | GET /ehr/{ehr_uid}/tags |
Every tag on the EHR_STATUS and every Composition within the given EHR |
| Demographic | GET /demographic/tags |
Every tag on any Person, Agent, Group, Organisation, or Role across the demographic store |
Each entry includes a target (the resource the tag is attached to) and an owner_id (the EHR for EHR-scoped tags, or the demographic system for party-scoped tags). Use these endpoints to drive worklists or to discover all resources matching a workflow flag without iterating over each target individually.
The openehr-item-tag header
On every write endpoint that accepts tags, you can send openehr-item-tag to attach (or replace) tags in the same DB transaction as the write itself. This avoids the round-trip of "create resource → call /tags".
On every GET of a resource that supports tags, the response can include an openehr-item-tag header containing the current tag set, so a client can read the resource and its tags in a single call. This is opt-in — see Reading tags on GET below.
Request format
The header value accepts two interchangeable formats:
JSON (recommended) — matches the body shape of the /tags endpoint exactly:
openehr-item-tag: [{"key":"category","value":"final"}, {"key":"flag","value":"follow-up","target_path":"/composition/start_time/value"}]
A single tag may be sent as a bare object:
openehr-item-tag: {"key":"priority","value":"high"}
Spec format — the original ITS-REST grammar with quoted values separated by semicolons:
openehr-item-tag: key="category",value="final"; key="flag",value="follow-up",target_path="/composition/start_time/value"
Response format
When the header is emitted, it always uses the JSON form:
HTTP/1.1 200 OK
openehr-item-tag: [{"key":"category","value":"final"},{"key":"flag","value":"follow-up","target_path":"/composition/start_time/value"}]
The header is omitted when the resource has no tags or when the client did not opt in on a GET (see below). It is always emitted on a write that successfully attached tags via the openehr-item-tag request header.
Reading tags on GET — the Prefer: include_item_tags opt-in
The openehr-item-tag response header is opt-in on read endpoints. Set Prefer: include_item_tags (RFC 7240 token) to request inline tags on a GET:
GET /ehr/{ehr_uid}/composition/{uid}
Prefer: include_item_tags
Accept: application/json
Authorization: Bearer <token>
HTTP/1.1 200 OK
Preference-Applied: return=representation, include_item_tags
openehr-item-tag: [{"key":"category","value":"final"}]
Without the token the response does not include the header and Cadasto skips the lookup entirely. When the resource is tagged but you did not opt in, the tags can still be read via the /tags subresource.
The token composes with the other Prefer tokens that GET endpoints accept; send them as a comma-separated list or repeat the header:
Prefer: return=representation, include_item_tags, resolve_refs
The Preference-Applied response header echoes the tokens the server actually honoured — return=, include_item_tags, and resolve_refs when applicable — so clients can confirm which preferences took effect.
Receiving tags back on writes — automatic
On write endpoints (POST create, PUT update) the opt-in is automatic when the request itself carries an openehr-item-tag header: a tag-aware request gets a tag-aware response, with no need to also send the Prefer: include_item_tags token.
POST /ehr/{ehr_uid}/composition
Content-Type: application/json
Prefer: return=representation
openehr-item-tag: [{"key":"category","value":"final"}]
{ "_type": "COMPOSITION", "...": "..." }
HTTP/1.1 201 Created
Preference-Applied: return=representation, include_item_tags
openehr-item-tag: [{"key":"category","value":"final"}]
If a write does not carry tags in the request but you still want the current tag set returned (for example to read tags set previously via the /tags subresource), add Prefer: include_item_tags explicitly.
Endpoints that emit and accept the header
| Resource | Write endpoints that accept the header | Read endpoints that emit the header (with Prefer: include_item_tags) |
|---|---|---|
| Composition | POST create, PUT update |
GET by id, GET versioned, GET at_time |
| EHR_STATUS | PUT update |
GET by version id, GET at_time |
| Person | POST create, PUT update |
GET by id |
| Agent | POST create, PUT update |
GET by id |
| Group | POST create, PUT update |
GET by id |
| Organisation | POST create, PUT update |
GET by id |
| Role | POST create, PUT update |
GET by id |
DELETE endpoints intentionally ignore the openehr-item-tag header — tags are removed when the underlying versioned object is removed.
Example: create a Composition with tags in one call
POST /ehr/{ehr_uid}/composition
Content-Type: application/json
Authorization: Bearer <token>
openehr-item-tag: [{"key":"category","value":"final"},{"key":"reviewer","value":"alice"}]
{
"_type": "COMPOSITION",
"...": "..."
}
HTTP/1.1 201 Created
ETag: "...uid..."
Location: /ehr/{ehr_uid}/composition/{uid}
Preference-Applied: return=representation, include_item_tags
openehr-item-tag: [{"key":"category","value":"final"},{"key":"reviewer","value":"alice"}]
The same composition can later be re-tagged with a PUT, or its tags managed via the Composition API /tags subresource.
Cadasto-specific behaviour and limitations
[!warning] The following limitations are intentional. Sending an unsupported header or identifier shape returns
400 Bad Request.
object_version_id is not supported as a target
The openEHR specification allows tags to target either a versioned_object_uid (the resource as a whole) or an object_version_id (one specific version). Cadasto only supports the versioned_object_uid form. Tags are conceptually attached to the resource, not to a single version, and they automatically apply to whichever version is current.
If you provide an object_version_id (e.g. <uuid>::node::1) where a versioned_object_uid is expected on the /tags endpoints, the request is rejected with 400 Bad Request.
openehr-version-item-tag is not supported
The optional openehr-version-item-tag request header (which the spec defines for sending tags scoped to a specific version) is rejected by Cadasto with 400 Bad Request. Use openehr-item-tag instead.
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"title": "Bad Request",
"errors": [
{"detail": "Header 'openehr-version-item-tag' is not supported by this implementation."}
]
}
Tag keys are case-insensitive
Tag key is stored with case-insensitive collation. priority and Priority are the same tag — sending one will overwrite the other.
Uniqueness rule
Tags on a target are unique by (key, target_path). Sending a list with two entries that share both fields keeps the first occurrence; the duplicate is silently dropped (deterministic dedup).
Replace, not merge
Both PUT /tags and the openehr-item-tag header use replace semantics: the supplied list becomes the complete tag set for the target. Send an empty list to clear all tags. Tags that were present before but are not in the new list are removed.
Validation and error responses
When the header is malformed, Cadasto returns 400 Bad Request with all detected violations in a single response — you do not need to retry repeatedly to see the next error.
Tag-level violations
Each malformed tag produces one violation per problematic field, with a pointer indicating the array index and field:
Request:
openehr-item-tag: [{"key12":"priority","value":123,"target_path":42}]
Response:
{
"title": "Bad Request",
"errors": [
{"detail": "Each tag must have a non-empty string \"key\".", "pointer": "[0].key"},
{"detail": "Field \"value\" must be a string.", "pointer": "[0].value"},
{"detail": "Field \"target_path\" must be a string.", "pointer": "[0].target_path"}
]
}
Header-level violations
Errors that affect the whole header (invalid JSON, unsupported header) are returned as a single error without a pointer:
{
"title": "Bad Request",
"errors": [
{"detail": "Invalid JSON in 'openehr-item-tag' header."}
]
}
API references
For the full request/response schemas of the dedicated /tags endpoints on Composition and EHR_STATUS, plus the EHR-wide GET /ehr/{ehr_uid}/tags filter, see the EHR API reference. The demographic equivalents — /tags subresources on every party type and the cross-resource GET /demographic/tags filter — live in the Demographic API reference. All of these accept and emit the openehr-item-tag header on the operations listed in the table above.