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
  • 0 replies
  • 20 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.