Skip to content

Identifiers: Models, Views, Elements

Three levels, three different rules

"ID" means three unrelated things in this toolchain, assigned by different people at different times with different stability guarantees. Getting them confused is the source of most identity surprises, so they are named distinctly throughout the docs:

Level What it names Who assigns it Unique within Validated
Model ID an arch:Model — the container holding elements, relationships and views you, outside the file — diagram index model.id, --model-id, or the filename {base}{notation}/ — in practice, the whole graph Yes, when it comes from an index: slug pattern
View ID an arch:View / arch:Diagram — one diagram surface inside a model the diagram index views[].id, or the source file when the index names none its model Yes, when it comes from an index: slug pattern, unique per model
Element / relationship ID an arch:Element or arch:QualifiedRelationship inside the model the source file, or derived from it its model No

The distinction is ontological, not cosmetic. A model is the container: it holds the elements and relationships, and it holds the views that depict some of them. A diagram is a view, never a model. One element can appear in several views of the same model and remains one resource, which is the whole point of holding them in one namespace.

Model and view IDs are deliberate naming decisions — they are published addresses. Element and relationship IDs are read out of the source file, because that is where the visual content already lives and re-deriving them elsewhere would break the link between a shape and its triple.

A diagram index names the model once and lists its views, so several diagrams become views of one model and share its elements:

<https://example.org/la/plantuml/order-domain>                        a arch:Model .
<https://example.org/la/plantuml/order-domain/element/OrderService>   a arch:Element .
<https://example.org/la/plantuml/order-domain/view/order-flow>        a arch:View , arch:Diagram .
<https://example.org/la/plantuml/order-domain/view/fulfilment-flow>   a arch:View , arch:Diagram .

One OrderService, drawn in two diagrams, with an archvis:ArchNode in each view pointing at it. Without a view id — the legacy flat index, or no index at all — one file is one model with one view, and the notation supplies the view id: the BPMNDiagram id for BPMN, the model id itself for PlantUML. See ADR 0002 for why the levels were split, and Models hold views for the schema.

The IRI these produce

Five of the six converters mint IRIs from one pattern (core/.../IriMinting.kt); the ArchiMate converter builds the same shape itself, because its path segments are configurable (--path-model, --path-element, …) and so cannot be hard-coded:

{base}{notation}/{modelId}/{segment}/{localId}
Resource IRI
Model {base}{notation}/{modelId}
Named graphs {base}{notation}/{modelId}/graph/semantic · /graph/model · /graph/views · /graph/provenance
Per-source semantic graph {base}{notation}/{modelId}/graph/semantic/{repo}/{path}
Element {base}{notation}/{modelId}/element/{elementId}
Relationship {base}{notation}/{modelId}/relationship/{relationshipId}
View {base}{notation}/{modelId}/view/{viewId}
View node {base}{notation}/{modelId}/view/{viewId}/node/{nodeId}
View link {base}{notation}/{modelId}/view/{viewId}/link/{linkId}

So the model ID is a namespace and the element ID is a name inside it. An element ID needs to be unique only within its model; two models may both contain Task_1 without clashing. A model ID has no enclosing namespace beyond {base}{notation}/, which is why it is the one level that gets validated — see id rules.

All three levels in one output

Converting playground/bpmn/ with its index — model order-domain, two views — and --base-iri https://example.org/la/ produces the following. These are real triples, with repeated predicates folded onto one line and unrelated ones dropped; the prefixes are the ones the converter writes (arch: = https://meta.linked.archi/core#, archvis: = …/core-vis#, uml: = …/uml/onto#).

# ── model ID "order-domain" names the model and its named graphs ──
<https://example.org/la/bpmn/order-domain/graph/provenance> {
    # model-level facts the author declared. The converter's own name is not among them: it is
    # the prov:SoftwareAgent, not a second dct:creator. See ADR 0008.
    <https://example.org/la/bpmn/order-domain>
        dct:title       "Order Domain" ;
        dct:creator     "Commerce Team" ;
        owl:versionInfo "1.0" .

    # diagram-level facts, against the view
    <https://example.org/la/bpmn/order-domain/view/order-fulfillment>
        dct:source   "order-fulfillment.bpmn" ;
        dct:title    "Order Fulfillment Process" ;
        dct:modified "2025-11-20" .
}

<https://example.org/la/bpmn/order-domain/graph/semantic> {
    # ── element ID "Task_ValidateOrder" — the BPMN id attribute ──
    <https://example.org/la/bpmn/order-domain/element/Task_ValidateOrder>
        a bpmn:UserTask , arch:Element , arch:ModelConcept ;
        arch:inModel <https://example.org/la/bpmn/order-domain> ;
        bpmn:name "Validate Order" ;
        bpmn:id   "Task_ValidateOrder" .

    # ── relationship ID "Flow_1" — likewise from the file ──
    <https://example.org/la/bpmn/order-domain/relationship/Flow_1>
        a bpmn:SequenceFlow , arch:QualifiedRelationship , arch:ModelConcept ;
        arch:inModel <https://example.org/la/bpmn/order-domain> ;
        arch:source <https://example.org/la/bpmn/order-domain/element/Start_OrderReceived> ;
        arch:target <https://example.org/la/bpmn/order-domain/element/Task_ValidateOrder> .

    # ── view ID "order-fulfillment" — from the index `views[].id` ──
    <https://example.org/la/bpmn/order-domain/view/order-fulfillment>
        a arch:View , arch:Diagram , arch:ModelConcept ;
        arch:inModel <https://example.org/la/bpmn/order-domain> .
}

# ── the curated model: the arch:Model resource, and the folders holding the ids above ──
<https://example.org/la/bpmn/order-domain/graph/model> {
    <https://example.org/la/bpmn/order-domain>
        a arch:Model ;
        arch:modelConformsToMetamodel <https://meta.linked.archi/bpmn/metamodel#Bpmn202> .

    <https://example.org/la/bpmn/order-domain/view/order-fulfillment>
        dct:isPartOf <https://example.org/la/bpmn/order-domain/folder/Views> .
}

<https://example.org/la/bpmn/order-domain/graph/views> {
    # ── the view's shapes: node ID "Shape_Start", pointing back at the element ──
    <https://example.org/la/bpmn/order-domain/view/order-fulfillment/node/Shape_Start>
        a bpmndi:BPMNShape ;
        bpmndi:bpmnElement <https://example.org/la/bpmn/order-domain/element/Start_OrderReceived> ;
        bpmndi:id "Shape_Start" .
}

Every IRI segment after bpmn/ is one of the three IDs: the model ID once, then a view, element or relationship ID inside it, then a node or link ID inside the view. The second diagram in that index, returns-handling, adds another view and its own elements to the same model and the same named graphs.

Note where the model ID appears twice over: once as the namespace every other IRI is minted into, and once as arch:inModel on each concept. The second is what makes membership a statement rather than something to be read out of the IRI — which ADR 0008 §3 tells consumers not to do, and which no longer works at all once graph/semantic is split per input file.

PlantUML shows the same structure with the visual layer expressed in archvis: instead of BPMNDI. From playground/plantuml/, where the index makes three diagrams views of model shop:

<https://example.org/la/plantuml/shop/graph/semantic> {
    <https://example.org/la/plantuml/shop/element/Warehouse>
        a arch:Element , arch:ModelConcept , uml:Class ;
        arch:inModel <https://example.org/la/plantuml/shop> ;
        skos:notation  "Warehouse" ;
        skos:prefLabel "Warehouse"@en ;
        uml:namespace  "Inventory" .

    <https://example.org/la/plantuml/shop/relationship/Warehouse__StockItem>
        a arch:QualifiedRelationship , arch:ModelConcept , uml:Composition ;
        arch:inModel <https://example.org/la/plantuml/shop> ;
        arch:source    <https://example.org/la/plantuml/shop/element/Warehouse> ;
        arch:target    <https://example.org/la/plantuml/shop/element/StockItem> ;
        skos:prefLabel "[holds]"@en .

    <https://example.org/la/plantuml/shop/view/inventory-domain>
        a arch:View , arch:Diagram , arch:ModelConcept ;
        arch:inModel <https://example.org/la/plantuml/shop> ;
        skos:prefLabel "[Inventory Domain Model]"@en .
}

<https://example.org/la/plantuml/shop/graph/views> {
    # Warehouse is drawn in two of the three views, so it has a node in each — and both
    # nodes point at the one element.
    <https://example.org/la/plantuml/shop/view/inventory-domain/node/Warehouse>
        a archvis:ArchNode ;
        archvis:view        <https://example.org/la/plantuml/shop/view/inventory-domain> ;
        archvis:archElement <https://example.org/la/plantuml/shop/element/Warehouse> .

    <https://example.org/la/plantuml/shop/view/stock-replenishment/node/Warehouse>
        a archvis:ArchNode ;
        archvis:view        <https://example.org/la/plantuml/shop/view/stock-replenishment> ;
        archvis:archElement <https://example.org/la/plantuml/shop/element/Warehouse> .
}

Folder IRIs

The snippet above omits the arch:Folder triples every converter also emits into graph/model. They follow the same pattern as everything else, {base}{notation}/{modelId}/folder/{name}, so a query for a model's folders is the same query in every notation. BPMN previously built {base}folder/bpmn/{modelId}/{name}, outside the model namespace.

ArchiMate mints its IRIs separately

converter-archimate does not use IriMinting. It concatenates configurable path segments (--path-element, --path-view, …) and uses /connection/ where the shared pattern uses /link/. It is also the only converter that percent-encodes local IDs — see Escaping.

Where element and relationship IDs come from

Each converter takes whatever its notation offers. Two use identifiers the authoring tool assigned; two derive them from names; one composes them.

Converter Element ID Relationship ID
BPMN the id attribute on the element, falling back to infra:id, then to the CMOF fragment local name (e.g. _gen_uuid) for elements the file left unnamed same source. An element counts as a relationship when its type is a subclass of SequenceFlow, MessageFlow, Association, DataAssociation or ConversationLink
ArchiMate the Exchange identifier attribute. Required — a missing one fails the conversion (Missing attribute 'identifier' on <…>) rather than being invented the Exchange identifier on the relationship
Structurizr the workspace JSON id, falling back to the literal unknown the JSON id, falling back to rel-unknown
PlantUML the code — the name the source refers to the entity by, which is the as alias where there is one and the display text otherwise — put through slugify(): anything outside [A-Za-z0-9_\-.] becomes _, runs collapse, leading/trailing _ are trimmed, and an empty result becomes element. See below synthetic: {sourceId}__{targetId}__{discriminator} — see below
Backstage the entity reference as a path: {kind}/{namespace}/{name}, lowercased — see below synthetic: {type}--{sourceRef}--{targetRef}, the triple Backstage identifies a relation by — see below
LeanIX the fact sheet id, a UUID — stable across a retitling the relation id

Two consequences are worth knowing before you rely on an element IRI.

Backstage element IRIs move when you rename an entity. The ID is the entity reference, and a Backstage entity has nothing but its name, so renaming it relocates every address derived from it. BPMN, ArchiMate, Structurizr and LeanIX all carry tool-assigned identifiers that survive renames; PlantUML has an author-written identifier distinct from the label, and takes that.

Backstage relationship IRIs are order-dependent. rel-7 denotes whichever relationship was emitted seventh, so inserting a relationship earlier in a catalog file renumbers the ones after it. They are stable only for an unchanged input.

An element ID is not always one path segment. Backstage's is three and BPMN's anonymous composites are two, so a consumer must not assume the ID is the last segment of the IRI. See IRI path shape, which is the page to hand to anything generating documents or files from the graph.

A PlantUML element ID is the name the source refers to it by

A PlantUML entity has two names, and they do different jobs:

class "Payment Gateway" as PayGw

Payment Gateway is the display name — what the picture shows a reader, and what becomes skos:prefLabel. PayGw is the code — what the source refers to the entity by, in every arrow and in every annotation. The ID is the code, slugified:

Declaration Display name Code Element IRI
class OrderService OrderService OrderService …/element/OrderService
class "Payment Gateway" as PayGw Payment Gateway PayGw …/element/PayGw
class "Payment Gateway" Payment Gateway Payment Gateway …/element/Payment_Gateway
participant "Book API" as BookApi Book API BookApi …/element/BookApi
[Mobile App] as Mobile Mobile App Mobile …/element/Mobile
() "PaymentApi" as PApi PaymentApi PApi …/element/PApi
[*] in a state machine initial *start* …/element/start

PlantUML sets the code to the display text when there is no as clause, so rows one and three need no special case — the code is always populated, and where the author wrote no alias the two names coincide.

The code is what an identifier needs to be, and the display name is not. PlantUML resolves an arrow endpoint by code, so two entities cannot share one; nothing constrains the display text, so two entities may show the same words. And a code is not edited for presentation, so retitling a box moves its skos:prefLabel and nothing else — the IRI holds, and so do the IRIs of the relationships composed from it. Full reasoning, including what goes wrong when two shapes share a label, is in ADR 0010.

Two things follow for an author.

The alias is a published address. as A and as AlphaApi are the same diagram and different IRIs, so an alias is worth naming with the care given to a model id.

An unaliased long name becomes a long address. participant "https://…/openapi.yaml" has that URL as its code. Relationship segments are bounded (below); element segments are not, so a short alias is the fix.

A Backstage element ID is the entity reference

Backstage identifies an entity by a reference — kind:namespace/name — and the whole of it is the ID, written as a path:

Entity Element IRI
kind: Component, name: payments-service …/element/component/default/payments-service
kind: API, name: payments-service …/element/api/default/payments-service
kind: Group, namespace: acme, name: team-payments …/element/group/acme/team-payments

The kind is part of the identity, not decoration. A Backstage name is unique per kind within a namespace, so the first two rows are two different entities — the ordinary shape of a service and the interface it publishes. An ID built from namespace and name alone gave them one IRI carrying both bs:Component and bs:API, both spec.type vocabularies, and every relationship of both.

Three segments rather than one, because a Backstage name may contain -, _ and ., so a single-segment component--default--pay--ments cannot be split back into a triplet. Neither : nor / is legal in a kind, a namespace or a name, so this form parses back deterministically and needs no percent-encoding.

Lowercased, because the specification compares references case-insensitively and mixed case is legal in a name — MyService and myservice are one entity and must not become two IRIs. The case the author wrote survives in the retained literals bs:kind, bs:name and bs:entityRef, the same split PlantUML makes when it lower-cases a label into an ID and leaves skos:prefLabel alone.

Every way of writing one reference therefore mints one IRI: payment-db, default/payment-db, Resource:payment-db and resource:default/payment-db all reach …/element/resource/default/payment-db.

A Backstage relationship is named after the edge

A catalog file carries no identifier for a relationship: every edge is derived from a spec field, so the converter has to synthesise one. It is composed from the triple Backstage's own relation model identifies a relation by — the type and the two entity references, each flattened into one path segment:

ownedBy--component-default-order-service--group-default-team-platform

The composed form is published as skos:notation, and it is the segment itself unless the composition exceeds the 255-byte filesystem limit, in which case IriSegment.bounded truncates it and appends a digest. Backstage caps metadata.name and metadata.namespace at 63 characters each, so an ordinary segment lands near eighty and the guard only fires on a maximum-length composition. This is the LeanIX converter's arrangement rather than PlantUML's, whose inputs include an author's free-text label and are therefore bounded unconditionally.

This used to be a counter — rel-1, rel-2, in emission order — assigned per conversion call while the IRI namespace is per model. An index-driven run is one call per file, so two files contributing to one model each restarted at rel-1 and their first relationships collided on one IRI, producing a single resource with two arch:source values. The reasoning for the replacement is ADR 0005, which settled the same question for PlantUML.

Two properties follow. An id does not depend on the order files are converted in, so adding a file does not renumber anything. And the same edge declared from both ends — a spec.parent on the child and a spec.children on the parent — is one relationship rather than two, because both compose the same notation.

The residual: namespace=default, name=a-b and namespace=default-a, name=b flatten to the same segment. Reaching it needs a namespace that is a prefix of another endpoint's name plus a shared type and shared other endpoint. It is accepted rather than engineered away because ConversionVerifier.checkRelationshipsHaveEndpoints reports a relationship carrying two arch:source values, which is what a collision produces.

A PlantUML relationship is named after its label

The endpoints name a pair, not an edge between them — two participants exchange many messages, and two classes can be joined by more than one kind of relationship. So what distinguishes an arrow is what the author wrote on it, and that is what the ID is built from. See ADR 0005.

Source Relationship notation
ClientApp -> OrderService: create order ClientApp__OrderService__create_order
ClientApp --> OrderService: order created ClientApp__OrderService__order_created
A --> B: owns A__B__owns
A -> B (unlabelled) A__B__call
A --> B (unlabelled) A__B__reply
A ->> B (unlabelled) A__B__async
A --> B (unlabelled, class diagram) A__B__association
two arrows identical in all of the above …__x, then …__x_2

The ID is that notation, bounded

The column above is the notation, published as skos:notation. The ID is the notation truncated to 180 characters with a 20-character SHA-256 digest of the whole of it appended:

notation  ClientApp__OrderService__create_order
ID        ClientApp__OrderService__create_order__60f79e38d012a705ddc3

The digest exists because the notation has no length limit and a filename does. Composing two endpoint IDs and a label concatenates three pieces of author-supplied text, and a path segment becomes a filename wherever the graph is written out as one document per resource — 255 bytes on every mainstream filesystem. A diagram that names participants by the URL of their API contract, which is ordinary practice, produced 644-byte segments.

Only the length is affected. The digest is over exactly the notation, so every property below holds as stated: two views still agree, a call still separates from a reply, and reordering still changes nothing. Where the notation exceeds 180 characters the IRI holds only its first 180, and skos:notation is the only complete copy — so read the notation, not the IRI, when you need the endpoints and label back.

Element IDs are not bounded this way. An element ID is a single slugified code rather than a composition, so it has not been seen to exceed the limit, but nothing enforces it — an author who names a participant after a URL and gives it no alias has that URL as its code. A short as alias keeps both the element segment and the relationship segments composed from it small.

Three properties follow, and the third is the one to know before relying on an IRI:

One arrow drawn in two views is one relationship. Both views mint the same ID, so the graph holds a single arch:QualifiedRelationship with an archvis:Link in each view — the same merge that makes one component drawn in three diagrams one element.

The ID is stable under reordering. Inserting an arrow does not renumber the ones after it, unlike Backstage's rel-n above. The _2 suffix survives only for arrows that are indistinguishable in the source.

The label is lower-cased into the ID, and left alone in the graph. create order and Create Order are one message and one IRI, so fixing a capital does not move a published address. The skos:prefLabel keeps the author's text.

Where two views spell one message differently the run warns and keeps the first spelling, dropping the other, because a resource can carry only one skos:prefLabel per language. Both files are named so you can settle it at the source. It is not an error: the ID folds case and punctuation on purpose, so an identifier that ignores those differences cannot then insist on them. What does fail the run is a genuine disagreement — two views claiming one ID with different types, or with labels that are different words rather than a different spelling.

Rewording an arrow's label does move its IRI. An arrow has no identifier of its own to fall back on, which is what separates it from an element: retitling a shape moves nothing, because a shape is addressed by the name the source refers to it by rather than by the text it displays.

Where view IDs come from

How many views one input file produces differs per notation:

Converter Views per file View ID Typed as
BPMN one per BPMNDiagram Normally none of its own — the BPMNDiagram is remapped onto the IRI of the Process it depicts, resolved BPMNDiagram → plane → bpmnElement → Collaboration → participants → processRef. The process is the view. Only when no process is reachable does it fall back to /view/{bpmnDiagramId} arch:Diagram, on the process IRI
ArchiMate one per <view> the Exchange identifier arch:View
Structurizr many — one per entry across the system landscape, system context, container, component, deployment, dynamic and filtered view arrays the view key, falling back to view-{n} arch:Diagram
PlantUML exactly one the index views[].id, or a '!la-view: comment in the source — the two must agree if both declare it; without either, the model ID, since a .puml has no diagram id of its own. Only the first @startuml block is parsed; further blocks are dropped arch:View + arch:Diagram
Backstage none Backstage is catalog data. No view triples are emitted at all —

One typing gap remains, worth knowing when querying the merged graph: Structurizr types views arch:Diagram only. BPMN and PlantUML emit arch:View and arch:Diagram together, and ArchiMate emits arch:View, so a query for either class finds those three; Structurizr views answer only to arch:Diagram.

arch:Model is asserted on the model resource by ArchiMate, BPMN and PlantUML. Structurizr and Backstage mint the model IRI and hang provenance off it without typing it.

View node and link IDs come from the diagram-interchange layer: for BPMN the BPMNShape / BPMNEdge local names, for ArchiMate and Structurizr the node and connection identifiers, and for PlantUML the element and relationship IDs themselves.

Escaping

IriMinting interpolates the local ID into the IRI path as-is. It does not percent-encode, and no shared validation applies to element, relationship or view IDs.

Converter Local IDs percent-encoded
ArchiMate Yes — every segment goes through URLEncoder.encode(…, UTF_8) with + rewritten to %20
BPMN No. Only SVG href values are encoded. Folder segments were encoded until folder IRIs moved to the shared pattern; the ids they are built from are validated instead
PlantUML Not needed — slugify() already restricts IDs to [A-Za-z0-9_\-.]
Structurizr No. View key values may contain spaces, and they reach the IRI unescaped
Backstage No, and none is needed — a reference's parts are limited to [a-z0-9A-Z] separated by [-_.], and a relationship id composes those with -- (or, once bounded, __ and hex)
LeanIX No, and none is needed — a fact sheet id is a UUID

Structurizr is the practical gap: a workspace whose view key is System Landscape produces an IRI containing a literal space. BPMN is safe in practice because BPMN id values are XML NCNames, which cannot contain spaces or /.

What is guaranteed, and what is not

Guaranteed. Re-converting an unchanged source file produces identical IRIs — every converter is deterministic, and no IRI is derived from the filename or a timestamp. This is what lets the aggregation repo overwrite a model in place and get a clean diff, as described in Versioning & overwrites.

Not guaranteed.

  • Model ID uniqueness across repositories. Within one index it is enforced. Across the source repos the aggregation graph pulls from, nothing checks it: two repos publishing bpmn/order-fulfillment mint identical named-graph IRIs and one silently shadows the other on merge.
  • Element ID uniqueness within a model where the source omits identifiers. Two Structurizr elements without an id both become unknown. For PlantUML the notation itself keeps codes unique within a diagram, so the remaining case is a nested namespace a.b, which is reported as one group per segment — a.shared and c.shared both yield shared. The run warns, naming the file and both display names.
  • Stability across a rename, for Backstage, whose entities have nothing but their names.

Where identity ought to be authored — index, source file, or both — is a separate question, evaluated in ADR 0001: Where model identity lives.