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:
| 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:
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:
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-fulfillmentmint 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
idboth becomeunknown. For PlantUML the notation itself keeps codes unique within a diagram, so the remaining case is a nestednamespace a.b, which is reported as one group per segment —a.sharedandc.sharedboth yieldshared. 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.