Skip to main content
APSHANKAR Sagar
Seasoned ⭐️⭐️
Seasoned ⭐️⭐️
August 28, 2026
Question

Extending CogniteUnit — can we add translations to a unit and still get server-side conversion?

  • August 28, 2026
  • 1 reply
  • 42 views

Who we are

We run a web application for industrial bakery lines that uses CDF as its back end. It's used by both plant operators and management. We're currently migrating our data models to the Core Data Model, and while doing so we've extended it with views of our own — which is what we understood a data model to be for.

While migrating we've hit a case where two things we need seem to be mutually exclusive, and we'd like to know the intended pattern.

What we need

  1. Translations for some units. Not all of them — a specific set where the label carries meaning for the reader. Our headline case is throughput. We measure, depending on the line and the moment, trays per hour, where a tray may hold several products, or baguettes per hour, or products per hour. #/h is correct dimensionally but doesn't express that variety, and our users need the right word in the right language. (A secondary, milder case: length:kilom has longName: "Kilometer", and there are no accented aliases in the catalogue — to our French users that reads as a misspelling of "kilomètre".)

    To be clear, we are not asking you to add trays/h or baguettes/h to the global catalogue — those are ours, not everyone's. 🙂 That's exactly why we were hopeful about the Core Model: CogniteUnit is a view, so we assumed we could extend it and hang our own translations off it.
     
  2. Conversion, at the same time. Being able to ask for a trays/h series in trays/min or trays/week, server-side, is genuinely powerful for us — and we'd rather not re-implement it in every client.

What we already do, and what we hoped to do

We use conversion routinely with classic time series: the time series itself carries unitExternalId, we ask for target_unit or target_unit_system at read time, and it works well. When a unit we need is missing, we open a PR on the units-catalog repo — we've done that several times.

 

So concretely, what we hoped for while migrating:

- a trays/h unit of ours that points at, or resolves to, frequency:num-per-hr (#/h) — so the label says "trays/h" while conversion to trays/min or trays/week still works;
- and a unit of ours extending length:kilom, carrying a French kilomètre so we can render the right string per user.

How we structured it

We mirrored the CDM shapes rather than duplicating any of your data. Our unit view implements CogniteUnit and adds only translation relations:

 

space: MaMaTa_Core_Views
externalId: MamataUnit
version: "3"
implements:
  - { type: view, space: cdf_cdm,          externalId: CogniteDescribable,   version: v1 }
  - { type: view, space: cdf_cdm,          externalId: CogniteUnit,          version: v1 }
  - { type: view, space: MaMaTa_Core_Views, externalId: MamataTranslatable,  version: "2" }
properties:
  symbolTranslations:                      # our only own property
    container: { type: container, space: MaMaTa_Core_Containers, externalId: MamataUnit }
    containerPropertyIdentifier: symbolTranslations
    source:    { type: view, space: MaMaTa_Core_Views, externalId: MamataTextContent, version: "2" }

 

 

MamataTranslatable adds nameTranslations / descriptionTranslations the same way. Everything else — name, description, tags, alias sourceReference — stays in your cdf_cdm:CogniteDescribable and cdf_cdm:CogniteUnit containers. We store nothing twice.

Our time-series view implements CogniteTimeSeries and narrows the inherited unit relation to our unit view:

 

 

space: MaMaTa_Core_Views
externalId: MamataTimeSeries
version: "11"
implements:
  - { type: view, space: cdf_cdm, externalId: CogniteTimeSeries,  version: v1 }
  - { type: view, space: cdf_cdm, externalId: CogniteDescribable, version: v1 }
properties:
  unit:
    container: { type: container, space: cdf_cdm, externalId: CogniteTimeSeries }
    containerPropertyIdentifier: unit
    source:    { type: view, space: MaMaTa_Core_Views, externalId: MamataUnit, version: "3" }

 

 

For scale: ~5,000 time-series nodes, ~2,600 with a unit, and 19 units of our own, 16 of which have an exact catalogue twin (matched on the QUDT sourceReference).

 

What we tried

Conversion does work through our extended time-series view — as long as unit targets a catalogue node:

unit = DirectRelationReference("cdf_cdm_units", "temperature:deg_c")

   stored °C     served °F    served K    target_unit_system=Imperial
     -40.00        -40.00      233.15        -40.00
       0.00         32.00      273.15         32.00
      25.00         77.00      298.15         77.00
     100.00        212.00      373.15        212.00

So a customer-extended CogniteTimeSeries isn't the obstacle. The obstacle is the unit. Everything below was measured on a live project, cognite-sdk 8.3.0.

  1. unit → one of our own MamataUnit nodes. No unitExternalId is derived, and reads fail with does not have a unitExternalId, and cannot use unit conversion. We didn't expect this to work on its own — it's the baseline for what follows.
  2.  Our node carrying the same external id as the catalogue unit. We created temperature:deg_c in our own node space, same symbol, quantity and QUDT sourceReference. Polled 123 s: still no unitExternalId. The control — the same series pointed at cdf_cdm_units:temperature:deg_c — resolved on the first read.
  3.  A whole quantity family, copied. In case resolution needed the source and target units present, we copied all 29 catalogue units of Length + Velocity into our space under identical external ids, symbols, quantities and QUDT references. Still nothing after 123 s; control converted in 3 s.
  4.  Adding only our translation container to the catalogue instance itself. This is what we'd most like to do — an instance can hold data in several containers, so in principle we could attach our translations to your temperature:deg_c node without touching your properties:
    ▎ Cannot write to system space 'cdf_cdm_units' because it contains global data
    Same whether we write through our view or straight into our container.
  5. Setting unitExternalId on the DM-backed series via the classic API:
    ▎ Field 'unitExternalId' on a time series with instance id can only be updated through the models/instances endpoint
  6. Taking the conversion and giving up the translations — but we lose more than that. When unit points at a catalogue node, a type symbol … } comes back null. The reference itself is stored and visible via instances.retrieve, but the traversal can't cross from our space into the system space; asked directly, the API says Query is targeting both system spaces and user spaces. This is not allowed. So choosing conversion costs us the unit object on every typed read, not just its translations. (Relatedly: browsing cdf_cdm:CogniteUnit shows only our own 19 implementing nodes, none of the 393 catalogue ones, unless we name cdf_cdm_units explicitly — instances.list has no include_global, unlike views.list.)

One observation that puzzles us. CDF does verify that the target of unit is really a unit: point it at a node with no cdf_cdm:CogniteUnit data and the write is refused, with a clear message saying the target must have data in that container. Our own unit nodes pass that check — by the API's own test they are CogniteUnits. They're accepted as the unit of a time series; they just never produce a unitExternalId. So being a valid CogniteUnit and being usable for conversion appear to be two different things, and only the second one depends on which space the node lives in.

Our questions

  1. Is there a supported way to have both — server-side conversion and a unit label/translation of ours — on the same series? A seintended to carry the display string, with clients joining separately for translations?
  2. Could the storage unit be resolved by sourceReference — our nodes carry the identical QUDT IRI — or by an explicit catalogue reference on CogniteUnit, instead of by space membership?
  3. What did you intend CogniteUnit to be extended for? Implementing it is allowed, and the resulting nodes pass the container check, but they can't drive conversion — so which use case is the interface serving?
  4. Is the system-space traversal limit intentional here? Not being able to read unit { … } once it points at the catalogue is what turns this into an either/or.
  5. Any plans for translated unit names and symbols in the catalogue, or a recommended pattern for customer-supplied translations that survives conversion?

Happy to share the reproduction script for any of the above.

1 reply

Everton Colling
Expert ⭐️⭐️⭐️⭐️
Expert ⭐️⭐️⭐️⭐️
September 1, 2026

Hi Sagar,

Thanks for the detailed writeup! The reproduction steps made it easy to follow, and your understanding of the behaviour is correct.

The short answer is "units of measure" are a special type of data, called global nodes and that is on purpose. Unit conversion is driven by the global unit catalog, not by the CogniteUnit interface. When a CogniteTimeSeries unit field points at a node in the cdf_cdm_units space, the service resolves a unitExternalId and picks up the quantity and conversion factors that belong to it. A node in your own space can implement CogniteUnit (we don't have a mechanism to prevent that today), but that will not be supported on any service that stores numeric data with conversion support (timeseries, records or the data modeling service).

This is also why you cannot add your container to the catalog node. Global nodes are shared across every project, so they are read only. The unit assignment and conversion is resolved by the global catalog. CogniteUnit is there so you can traverse from a time series to its unit and read the catalog values in a data modeling query using the Direct Relation connection definition.

For genuinely new units, the catalog PR is the right path, which you are already using.

For translations and your own labels, my recommendation would be a small mapping table of your own, keyed on the unit external ID. You can store it wherever makes more sense to you: instances in your space, records, or keep it directly in the client application. The client keeps using the standard external ID for conversion and reads, and looks up the display name and symbol from your table when rendering. Localization lives there too. This keeps the two concerns apart. CDF knows the physical quantity and your app knows what to show to the user. The CDF API does not carry application or location specific context, and we have no current plans to add it. This is a very common clint/application specific concern that many of our customers solve it already. Since you are maintaining the mapping table yourself, it's also easy to extend and update it, adding more units or even more localizations besides french when needed.

On the semantic units like "trays per hour" against "baguettes per hour", I would go a step further. That is not a difference in unit, it is a difference in what you are counting. Both are frequency:num-per-hr, which is exactly why conversion to per minute or per week works. I would recommend keeping the unit as frequency:num-per-hr, and storing what is counted as a separate property on your time series view, and let the app build the label from the two. Conversion then keeps working as is.

I also have two practical notes for your application. The unit catalog is small and changes slowly (we try to release a new catalog version at most every 2 months), so fetching all units once and caching them in your app works really well and avoid unnecessary requests to CDF. You could invalidate the cache once every day for example. And the system space traversal ergonomics is not specific to units, it applies to any global nodes in general. To resolve a direct relation to global nodes you have to apply a space filter on the global node space. We fully agree the ergonomics there are not great, and we have a backlog item to make the global nodes behave like regular ones when it comes to queries (no ETA for that yet, but we would like to address it in the upcoming 6 months horizon).

I hope I was able to clarify more on how units of measure work in CDF, but let me know if you have further questions related to this topic.