Skip to content

Diagram Index Files

Purpose

A diagram index is a YAML file, passed as --diagrams-index, that serves three functions:

  1. File selection — controls which files are processed (unreleased entries skipped)
  2. Identity — diagrams[].id determines IRI path segments, and names the render output file. Read the next section before relying on that: the field is misnamed
  3. Provenance metadata — title, author, version, dates and lifecycle state flow into the provenance graph

Models hold views

A diagram is an arch:View inside an arch:Model, and the index says so directly: name the model, list the diagrams it holds.

model:
  id: order-domain                 # the arch:Model — IRI namespace and named graphs
  title: "Order Domain"
views:
  - id: order-flow                 # the arch:View — and the render SVG filename
    file: processes/order-flow.puml
    status: publish
    title: "Order Flow"
  - id: fulfilment-flow
    file: processes/fulfilment-flow.puml
    status: publish

Both diagrams convert into …/plantuml/order-domain, so an element drawn in both is one element with a node in each view:

<https://example.org/la/plantuml/order-domain/graph/semantic> {
    <https://example.org/la/plantuml/order-domain/element/OrderService>
        a arch:Element , arch:ModelConcept ;
        arch:inModel <https://example.org/la/plantuml/order-domain> .

    <https://example.org/la/plantuml/order-domain/view/order-flow>
        a arch:View , arch:Diagram , arch:ModelConcept ;
        arch:inModel <https://example.org/la/plantuml/order-domain> .

    <https://example.org/la/plantuml/order-domain/view/fulfilment-flow>
        a arch:View , arch:Diagram , arch:ModelConcept ;
        arch:inModel <https://example.org/la/plantuml/order-domain> .
}

<https://example.org/la/plantuml/order-domain/graph/model> {
    <https://example.org/la/plantuml/order-domain> a arch:Model .
}

<https://example.org/la/plantuml/order-domain/graph/views> {
    <…/view/order-flow/node/OrderService>      archvis:archElement <…/element/OrderService> .
    <…/view/fulfilment-flow/node/OrderService> archvis:archElement <…/element/OrderService> .
}

Several models can live in one index, each listing its own views:

models:
  - id: order-domain
    title: "Order Domain"
    views:
      - id: order-flow
        file: processes/order-flow.puml
  - id: payments
    views:
      - id: payment-authorisation
        file: payments/payment-auth.puml

Metadata follows the level it describes: title, created, modified and description on a view describe that diagram; author and version on the model describe the model, and a view inherits them unless it says otherwise.

Declaring identity in the source file

A PlantUML source can declare its own identity in header comments, which every renderer ignores:

'!la-model: order-domain
'!la-view: order-flow
@startuml
class OrderService
@enduml

Only the lines before @startuml are read. convert and render both read them, and a declaration in the source must agree with one in the index — a disagreement fails the run rather than one side silently winning:

[ERROR] Conversion failed: Conflicting view id for 'order-flow.puml': the source file declares
'order-flow', diagram-index.yaml declares 'renamed-flow'. Remove one of the two declarations.

An index entry may therefore omit id for such a file, and needs only file: and status:. For notations whose files cannot carry an identity — BPMN sources are tool-generated — the index remains the only declaration site. See ADR 0001.

Requiring a declared identity

Without a declaration the model ID falls back to the input filename, which is unvalidated and changes when the file is renamed. --require-id removes that fallback:

[ERROR] Conversion failed: --require-id is set and no model id was declared for 'Order Flow.puml'.
Without one the filename would become the identity, which is neither validated nor stable.
Declare one in the diagram index, or pass --model-id.

Publishing pipelines should set it. Ad-hoc local runs can leave it off and keep converting a file without an index.

Changing a published identity

An indexed run records the IDs it published in identity-lock.yaml, written beside the index and committed. A later run whose IDs differ fails, because changing them relocates every IRI in the model:

[ERROR] Conversion failed: Identity change for 'flow.puml': view id was published as
'order-flow', now 'fulfilment'. Declare the previous id under formerIds:, or pass
--allow-identity-change.

Declare the change to authorise it and keep the previous IRI resolvable:

views:
  - id: fulfilment
    formerIds: [order-flow]
    file: flow.puml
    status: publish
<https://example.org/la/plantuml/order-domain/view/fulfilment>
    dct:replaces <https://example.org/la/plantuml/order-domain/view/order-flow> .
<https://example.org/la/plantuml/order-domain/view/order-flow>
    owl:sameAs       <https://example.org/la/plantuml/order-domain/view/fulfilment> ;
    dct:isReplacedBy <https://example.org/la/plantuml/order-domain/view/fulfilment> .

formerIds: is read at the level it sits on. On a view entry it supersedes view IDs, on the model: entry it supersedes the model ID, and in the flat schema the entry's formerIds: supersede the model ID because that is what the entry id is:

model:
  id: order-domain
  formerIds: [orders]        # aliases the model IRI
views:
  - id: fulfilment
    formerIds: [order-flow]  # aliases the view IRI and the SVG name
    file: flow.puml

A former ID is held to the same slug shape as a current one, since it is published as an IRI and as a filename.

The alias is emitted on every run for as long as formerIds: names it, not only on the run that performed the rename: the lock records the new ID immediately, so a later run sees no change and would otherwise publish the model without the alias.

Because a view ID also names the rendered SVG, render writes the picture under the superseded name as well, so a documentation link to the previous image URL keeps resolving:

Rendered: flow.puml → out/svg/fulfilment.svg
Also wrote superseded name: order-flow.svg

A superseded model ID does not change the filename, so it produces alias triples only.

--allow-identity-change accepts an undeclared change and updates the record, for a model whose IRIs were never published outside the repository. It authorises the change without describing it, so no aliases are emitted. Full rules in ADR 0003.

Legacy flat schema

diagrams:
  - id: order-fulfillment
    file: order-fulfillment.bpmn
    status: publish

Still supported, and still means one model per file: diagrams[].id is the model id, and the notation supplies the view id — the BPMNDiagram id for BPMN, the model id itself for PlantUML, since a .puml has none. Reinterpreting it would move every IRI in every index that uses it, so it keeps its meaning. Use model: / models: for anything new, and for any set of diagrams that belong to one model.

Model, view and element IDs are three different things with different rules — see Identifiers.

It is not universal, and it is never mandatory. Which converters read one follows from what a single input file means to them:

Converters One input file is… Identity comes from
Diagram-oriented BPMN, PlantUML one diagram, authored in isolation the index (id), or the filename without one
Catalog-oriented Backstage, LeanIX one catalog file or directory / one fact sheet export the index (id), --model-id, or the filename
Model-oriented ArchiMate, Structurizr one entire model, views included --model-id, or the filename. No index support

BPMN and PlantUML behave identically here — same selection rules, same ID resolution, same file: matching — because both are collections of independent diagrams. ArchiMate and Structurizr have nothing to select between: the file is the model, so they take --model-id instead and have no render command either.

Backstage and LeanIX read an index for a reason the diagram-oriented converters do not have. Neither input is a diagram and neither needs selecting between, but both are somebody else's schema — a catalog file owned by Backstage, an export written from a LeanIX workspace — with no extension point to write into and nothing that would survive the next regeneration. For them the index is the only route for a statement the source format cannot carry. Both emit no arch:View, so index metadata that would land on a view lands on the model instead.

Fields

model:
  id: order-domain                 # Model id — the arch:Model, its IRI namespace and graphs
  title: "Order Domain"            # Model title
  author: "Commerce Team"          # Author / owner — inherited by every view
  version: "1.0"                   # Version string — inherited by every view
  architectureState: baseline      # Which reality the model describes — see below
  links: { … }                     # Statements about the model — see below
  data:  { … }                     # ditto, with literal values
views:
  - id: order-fulfillment          # View id — the arch:View, and the render SVG filename
    file: order-fulfillment.bpmn   # Filename (relative to --diagrams-root)
    status: publish                # Lifecycle state, decided per view — see below
    architectureState: target      # Which reality this diagram describes — see below
    supersededBy: order-flow-v2    # On a retired entry: what to read instead
    title: "Order Fulfillment"     # Diagram title
    created: "2024-03-15"          # Creation date
    modified: "2025-11-20"         # Last modification date
    description: "End-to-end flow" # Description
    links: { … }                   # Statements about the view — see below
    data:  { … }                   # ditto, with literal values
    elements: { … }                # Statements about elements in it — see below

What an entry produces

Converted with --base-iri https://example.org/la/ (real output, abbreviated):

# ── the model and its named graphs: from model.id ──
<https://example.org/la/bpmn/order-domain>                  a arch:Model .
<https://example.org/la/bpmn/order-domain/graph/semantic>   { … }
<https://example.org/la/bpmn/order-domain/graph/views>      { … }
<https://example.org/la/bpmn/order-domain/graph/provenance> { … }

# ── the view: from views[].id ──
<https://example.org/la/bpmn/order-domain/view/order-fulfillment>
    a arch:View , arch:Diagram ;
    dct:isPartOf <https://example.org/la/bpmn/order-domain> .

# ── elements and relationships: from IDs in the source file, shared by every view ──
<https://example.org/la/bpmn/order-domain/element/Task_ValidateOrder> a arch:Element .
<https://example.org/la/bpmn/order-domain/relationship/Flow_1>        a arch:QualifiedRelationship .

render writes order-fulfillment.svg — named after the view, because an SVG depicts one diagram. The full output, including how the metadata fields land in the provenance graph, is in Identifiers.

Element entries

An index can also make statements about individual elements, which is the index route to extension data — for a team that owns the pipeline rather than the diagrams.

prefixes:                                   # index-level, for every model in the file
  am: https://meta.linked.archi/archimate3/onto#
  arch: https://meta.linked.archi/core#
  kg: https://example.org/graph/
  lx: https://leanix.example.com/factsheet/
  x: https://example.org/vocab#
model:
  id: order-domain
  prefixes:                                 # model-level, overriding index-level
    x: https://example.org/orders-vocab#
views:
  - id: order-flow
    file: order-flow.puml
    status: publish
    elements:
      OrderService:                         # the element, as the notation names it
        links:
          am:realizes: kg:CAP-OrderManagement
          arch:partOf:                      # a list, since YAML cannot repeat a key
            - kg:CAP-Commerce
            - kg:CAP-Fulfilment
          am:serves:                        # a map, to declare a direction
            target: lx:APP-order-api
            direction: Backward
        data:
          x:costCentre: CC-4711
Key Meaning
prefixes: prefix to namespace, at index or model level. YAML resolves nothing, so CURIEs need a declared table
elements: keyed by the element as the notation names it — see referring to an element
links: predicate to target. The object is a resource, so it is resolved
data: predicate to value. The object is a literal, used as written

links: and data: are separate because the difference is not inferable: CC-4711 is a literal and CAP-1 is an element id, and both are bare strings.

A link value may be a scalar, a list of scalars, or a map with target and direction. Requires --emit-extension-data; without it the entries are read and ignored.

Prefixes are declared at index or model level but never per view: a prefix is a vocabulary, and one that changed per diagram would make the same CURIE mean different things in one model's graph.

Statements about a view or a model

links: and data: also work one level up, written directly on an entry rather than inside elements:. There they are about that entry — the view, or the model — rather than about anything drawn in it:

prefixes:
  arch: https://meta.linked.archi/core#
  archvp: https://meta.linked.archi/core-viewpoints#
  kg: https://example.org/graph/
  x: https://example.org/vocab#
model:
  id: order-domain
  links:
    arch:architectureState: arch:Baseline     # about the model
views:
  - id: target-payments
    file: target-payments.puml
    links:
      arch:architectureState: arch:Target     # about this view
      arch:viewConformsToViewpoint: archvp:Roadmap
    data:
      x:reviewedBy: Jane Doe

  - id: option-b
    file: option-b.puml
    links:
      # arch:inView runs concept → view, so the statement is reversed
      arch:inView: { target: kg:ReadReplicas, direction: Backward }

This is for anything true of the diagram as a whole and of no element in it — which architecture state it depicts, which viewpoint it conforms to, which decision option it articulates. The blocks behave exactly as they do inside elements:: same prefixes, same scalar-or-list-or-map values, same direction tokens, same --emit-extension-data gate.

Model-level statements stay at model level. They are not copied onto the views, for the reason formerIds: is not: the two levels are different resources, and attaching a model-wide claim to every view would say something about each diagram that the author said about the whole model. A model-level statement is written once, in the one place the model is named — which is also why there is no in-file route for it, since a model spans several files.

In the legacy flat schema an entry is a model, so links: and data: there are model-level. There is no view for the index to describe, because the notation names the view. elements: works in both schemas.

Colons and quoting

This file is full of colons — CURIEs as keys and as values, and entity refs like component:default/order-service — so it is worth knowing exactly when YAML minds.

A colon inside a word is fine. YAML only ends a key at a colon followed by whitespace, so none of these need quoting:

elements:
  component:default/order-service:        # a ref as a key — fine
    links:
      am:realizes: kg:CAP-Orders          # CURIE key, CURIE value — fine

A colon followed by a space is not. It reads as a nested mapping and the file fails to parse, which at least fails loudly:

    data:
      x:note: see also: the runbook       # ERROR: mapping values are not allowed here
      x:note: "see also: the runbook"     # quoted — fine

Two values do need quoting, and these are the ones worth remembering, because YAML reads them as something other than text without complaining:

    data:
      x:window: "12:30"                   # unquoted 12:30 is the number 750
      x:note: "{n/a}"                     # unquoted {…} is a mapping, not a literal

12:30 is sexagesimal in YAML 1.1 — 12×60+30 — so an unquoted time-like value silently becomes an integer and reaches the graph as "750". A value starting with { is read as a flow mapping; that one is refused rather than published, naming the quoting fix, since a mapping with no target: is never a statement anyone meant. Values starting with *, & or @ are reserved and fail to parse.

The rule that covers all of it: quote any literal that is not plainly a word. Keys and CURIEs need nothing.

PlantUML has an in-file equivalent for the view — '!la-view-link and '!la-view-data — and the two routes are additive like the element ones. BPMN and the other notations have the index route only: bpmn:extensionElements nests inside the element it annotates, so there is nowhere in the file to put a statement about the diagram. See extension data.

Where a model ID comes from

Every command resolves a model ID for each input file, index or no index. The chain is first match wins. The view ID resolves alongside it: the index views[].id when there is one, otherwise whatever the notation supplies.

Command Resolution
bpmn2linkedarchi convert index model id → --model-id → filename
plantuml2linkedarchi convert index model id → --model-id → filename
backstage2linkedarchi convert index model id → --model-id → filename
archimate2linkedarchi convert --model-id → filename (always a single input, -i)
structurizr2linkedarchi convert --model-id → filename
bpmn2linkedarchi render index model id → filename
plantuml2linkedarchi render index id → filename

"filename" means the input file's name without its extension.

One model per --model-id

A model ID identifies one model, so one explicit value cannot serve a batch. Passing --model-id with more than one input file is refused:

[ERROR] --model-id 'both' cannot be used with 2 input files: a model ID names one model.
Convert the files one at a time, or use --diagrams-index to give each one its own id.

Nothing is written when that happens. Passing it alongside an index is allowed but has no effect, and says so, because the index supplies an id for every file it selects:

[WARN] --model-id 'ignored-value' is ignored: the diagram index supplies the id for every
file it selects.

What is validated

Only the index path. A model ID taken from --model-id or from a filename is used as given, so --model-id "My Model" or a source file called Order Flow.bpmn still produces IRIs containing spaces. That is why anything published should come from an index (BPMN, PlantUML, Backstage) or from an explicitly chosen --model-id (ArchiMate, Structurizr) — as in --model-id payment-platform, never the filename by accident.

View and element IDs are not validated at all, and model IDs are not checked across repositories. Identifiers → What is guaranteed lists what holds and what does not; whether identity should be declared here or in the source file is evaluated in ADR 0001.

id rules

Model and view ids are used unescaped as IRI path segments ({base}{notation}/{model}/view/{view}), and a view id also names the render output file, so both must match:

^[a-z0-9][a-z0-9._-]*$

Lowercase letters and digits, then any of -, _, .. A view id must also be unique within its model, and a file may appear once in the index. Anything else fails the command, convert and render alike, naming the offending entries:

[ERROR] Render failed: Invalid id(s) in models/diagram-index.yaml:
view id 'Order Fulfillment Process' (file: order-fulfillment.bpmn). Model and view ids
become IRI path segments, and a view id also names the rendered SVG, so both must match
^[a-z0-9][a-z0-9._-]*$ …

Why each restriction:

Rejected Reason
space, % not legal in an IRI; needs percent-encoding in every published link
#, ? truncates the IRI into a fragment or query, so it identifies something else
/, .. adds IRI path segments, and writes the SVG outside -o
uppercase distinct IRIs that collide on case-insensitive filesystems
duplicate view id in one model two views share one set of IRIs, and one SVG overwrites the other
duplicate model id in the flat schema each flat entry is its own model, so the two would merge unintentionally

Ids are not normalised for you. Slugifying silently would leave the graph and the filename disagreeing, which is the mismatch a shared id exists to prevent.

Excluded entries are validated too: a broken id fails the run even when --exclude-states leaves the entry out, so the problem surfaces before the entry is published rather than on the day it goes live.

One view, one file

A file appears once, and each view id is used once in its model. Both directions are rejected:

Message
One view id twice in a model Duplicate view id(s) … — the two views would share one set of IRIs, and one SVG would overwrite the other
Two views, the same file: Duplicate file(s) … — only one entry can apply to a file, so the others would produce no SVG and no IRIs

Equivalent spellings of one path count as the same file, so nested/x.bpmn and ./nested/./x.bpmn collide rather than slipping through.

An index cannot publish one diagram under two ids. Copy or link the rendered SVG instead.

Element and relationship ids must agree across views

Views of one model share one namespace — that is the point, and it is what makes one component drawn in three diagrams one resource. It also means the same local id in two files is the same concept, so a conflict is a defect:

[ERROR] Conversion failed: Identity collision in model 'order-domain':
<…/bpmn/order-domain/element/Task_ShipOrder> is claimed by two views:
'proc-a.bpmn' says Ship Order (Element/ModelConcept/ServiceTask),
'proc-c.bpmn' says Dispatch Parcel (Element/ModelConcept/ServiceTask).

Nothing is written when that happens. Agreement is not an error: two views showing the same element with the same name and type merge silently, which is the intended case. The check compares name and type, so it cannot catch two genuinely different things that happen to share both — BPMN ids like Task_1 from separate templated processes are worth reviewing when you group them.

Relationships are held to the same rule. A PlantUML relationship id is built from its label (ADR 0005), so two views drawing the same arrow agree on the id and on the label and merge quietly — one arch:QualifiedRelationship with a link in each view — while a call and a reply between the same pair are two relationships. What still reaches this check is what identity cannot settle:

  • BPMN ids are tool-assigned and only unique per file, so Flow_1 in two grouped processes is two different flows on one IRI.
  • Two views disagreeing about the type — an Association in one and a Dependency in the other.
  • Two views whose labels are different words, not a different spelling of the same words.

A difference of spelling is reconciled, not refused

create order against Create Order, or Command Cancel Request against Command (Cancel) Request. These are not collisions. A relationship id folds case and punctuation on purpose, so both views always meant one message, and an identifier that ignores those differences cannot then insist on them.

They still need settling, because both views emitted a skos:prefLabel for that one IRI and SKOS allows one per language. So the run keeps the first spelling seen, drops the other, and warns:

[WARN] <…/element/Task_1> is labelled 'Ship order' in 'a.bpmn' and 'ship order' in 'b.bpmn'. Both mean
the same thing — the id folds case and punctuation — so 'Ship order' was kept and the other dropped,
because a resource can carry only one skos:prefLabel per language. Spell it the same way in both sources
to choose for yourself.

Output is still written. First-seen wins, so the choice depends on input order — spell it the same way in both sources if you want to make the choice yourself.

The error, when there is one, says which kind it is, because the remedies differ: a type disagreement needs distinct ids or separate models, whereas genuinely different labels need either agreement or separation.

How metadata flows into RDF

In provenance graph (BPMN converter)

<.../bpmn/order-fulfillment/graph/provenance> {
    <.../bpmn/order-fulfillment>
        dct:source "order-fulfillment.bpmn" ;     # source filename
        dct:title "Order Fulfillment Process" ;   # ← from index
        dct:creator "Commerce Team" ;             # ← from index (author)
        owl:versionInfo "1.0" ;                   # ← from index
        dct:created "2024-03-15" ;                # ← from index
        dct:modified "2025-11-20" ;               # ← from index
        dct:description "End-to-end flow" ;       # ← from index
        adms:status
            <http://purl.org/adms/status/Completed> .  # ← from index (status)
}

Every predicate above carries exactly one value, and that is the point. The conversion timestamp used to be emitted as a second dct:created beside the author's date, and the converter's own name as a second dct:creator beside the author — one predicate, one subject, two answers. Both facts describe the run rather than the diagram, so they moved to the PROV activity and its agent. See ADR 0008.

Those two facts live in the same graph, as PROV, describing the run instead of the diagram:

<.../bpmn/order-fulfillment/graph/provenance> {
    <{base}provenance/run/9c2f1e04> a prov:Activity ;
        prov:startedAtTime "2026-06-28T12:07:57Z"^^xsd:dateTime ;
        prov:endedAtTime   "2026-06-28T12:07:59Z"^^xsd:dateTime ;
        prov:wasAssociatedWith <{base}provenance/agent/7d1c40ba> ;
        prov:used              <{base}provenance/source/group-models/8c44d1a4e9f0/models-bpmn-order-fulfillment-bpmn> .

    <{base}provenance/agent/7d1c40ba> a prov:SoftwareAgent, schema:SoftwareApplication ;
        schema:name            "bpmn2linkedarchi" ;   # which converter of the image ran
        schema:softwareVersion "1.3.0" ;
        dct:identifier         "registry.gitlab.com/…/converters@sha256:89ab…" .

    <.../bpmn/order-fulfillment>
        prov:wasGeneratedBy <{base}provenance/run/9c2f1e04> ;
        prov:wasDerivedFrom <{base}provenance/source/group-models/8c44d1a4e9f0/models-bpmn-order-fulfillment-bpmn> .
}

The run, agent and source nodes sit under {base}provenance/, not under this model — they are shared across every model and run, so one file at one commit is one node and one run is one activity. Only the statements about them are per-model. See ADR 0008 §2.

Current metadata support per converter

Converter Index metadata → provenance Notes
BPMN ✓ Full All fields emitted (title, author, version, created, modified, description, status)
PlantUML ✓ Full All fields emitted (title, author, version, created, modified, description, status)
Structurizr ✗ Does not use --diagrams-index
Backstage ✓ Full All fields emitted (title, author, version, created, modified, description, status)
ArchiMate ✗ Does not use --diagrams-index (single-file converter)

Metadata is emitted by BaseLinkedArchiEmitter.emitProvenance(), which every index-aware converter calls with the resolved ModelMetadata. BPMN builds the same triples in its own emitter, because it overlays the dc:/dcterms: fields embedded in the BPMN file first.

Shared index between convert and render

Both the convert and render commands read the same index file:

# Convert: uses id for IRI minting, status for filtering and provenance, metadata for provenance
plantuml2linkedarchi convert models/*.puml \
  --diagrams-index models/diagram-index.yaml -o out/model.trig

# Render: uses id for SVG filename, status for filtering
plantuml2linkedarchi render models/*.puml \
  --diagrams-index models/diagram-index.yaml -o out/svg/

Usage with --diagrams-root

When files are in subdirectories, use --diagrams-root to resolve relative paths:

models/
├── diagram-index.yaml
├── processes/
│   └── checkout.bpmn        ← file: processes/checkout.bpmn
└── events/
    └── payment-event.bpmn   ← file: events/payment-event.bpmn
bpmn2linkedarchi convert models/**/*.bpmn \
  --diagrams-root models/ \
  --diagrams-index models/diagram-index.yaml \
  -o out/bpmn.trig

When --diagrams-root is omitted, paths resolve against the directory holding the index file.

How file: is matched

An input file is matched against the index by its path relative to the root, falling back to its bare file name. Both spellings work, so an index for a flat directory can say file: checkout.bpmn while one for a tree says file: processes/checkout.bpmn, and equivalent spellings of one path (./processes/checkout.bpmn) match too.

The bare-name fallback only applies when that name identifies exactly one entry. Given processes/x.bpmn and events/x.bpmn, a lookup for x.bpmn matches neither rather than guessing.

Every command applies these rules identically — they live in one DiagramSelection in core — and warns when an index selected nothing at all:

[WARN] No input file matched the diagram index. Check --diagrams-root and the file: paths.

That warning is the thing to look for when a run reports Converted OK — 0 triples.

Lifecycle states

status: says where a diagram is in its life. It describes the diagram; it does not decide whether the diagram exists. Every state is converted and rendered, and each one reports itself in the graph as adms:status, so a consumer that wants only current diagrams filters on that.

status: Converted and rendered adms:status
draft yes adms/status/UnderDevelopment
in-review yes adms/status/UnderDevelopment
publish (default) yes adms/status/Completed
deprecated yes adms/status/Deprecated
archived yes adms/status/Withdrawn

Converting everything is the safer default in both directions. A diagram that has been released cannot simply vanish when it falls out of favour, because documentation, other models and downstream graphs already link to its IRIs — so deprecated and archived stay and say what they are. And a diagram nobody has released yet is still worth having in a graph that reviewers and tooling can read, marked plainly as unfinished rather than absent.

Withholding a diagram is therefore an explicit request: --exclude-states. To take one out permanently, remove its entry.

obsolete is accepted as a synonym for deprecated, and published for publish. Case and the choice of -, _ or a space are ignored, so In Review and in-review are the same state — for status: and for --exclude-states alike.

What each state means

draft and in-review are both unreleased, and both carry the same adms:status, so the difference between them is neither what gets published nor what the graph says — it is whose turn it is.

  • draft — work in progress. The author is still deciding what the diagram says, and nobody is waiting on it.
  • in-review — submitted, and waiting for someone else to validate it.

The distinction still matters even though both are converted: it drives the transitions below, and it is what an author excludes by name when a build must not publish unreleased work.

That makes in-review the gate into publication, and the only state a published diagram can return to without being retired. A diagram that needs re-validation goes back to in-review, not to draft: it is already public, and "unfinished" is not something a public resource can be.

draft ⇄ in-review → publish ⇄ deprecated ⇄ archived
                       ↓ ↑
                    in-review

The legal moves, in full:

From May become
draft in-review
in-review draft, publish
publish in-review, deprecated, archived
deprecated publish, archived
archived publish, deprecated

Two moves are refused, for one reason each:

  • Anything published back to draft. Its IRIs are public. Send it to in-review instead.
  • draft straight to publish. That skips the validation in-review exists to require.

Nothing is a dead end: a deprecated or archived diagram can be reinstated without editing anything by hand.

Transitions are checked against identity-lock.yaml, which records the state each entry was last seen in alongside the ids it was published under. A state the record has never seen is not a move, so an index adopting states for the first time passes freely and the run after it starts enforcing — the same bootstrap the identity check has, and no migration step:

[ERROR] Conversion failed: Illegal state change for 'order-flow.puml': 'publish' cannot become
'draft'. 'publish' has been published, and a published diagram cannot become work in progress
again — its IRIs are already public. Send it to 'in-review' instead. Allowed from 'publish':
in-review, deprecated, archived. Move through one of those, or pass --allow-state-change.

--allow-state-change accepts the move and updates the record, for the case where the lifecycle is in the way rather than helping.

Withholding something that was already published

Moving a live diagram to a state the build excludes takes it out of that build. That is a link breaking, so it is said out loud — once, on the run where it happens:

[WARN] 'order-flow.puml' was published as 'publish' and is now 'in-review', which this run
excludes, so it is left out and its IRIs stop resolving. Drop 'in-review' from --exclude-states to
keep publishing it while it is out of use.

It is a warning rather than an error because the author asked for it twice over: once by moving the diagram, once by excluding the state. Stop excluding the state to keep publishing the diagram while it is being validated, or accept the gap.

This only arises in a build that passes --exclude-states. Without it, sending a diagram back for validation changes what the graph says about it and nothing else — the IRIs keep resolving, so there is no link to break and nothing to warn about.

An unrecognised value fails the run, in status: and in --exclude-states:

[ERROR] Conversion failed: Unknown status 'publsih' for 'order-flow.puml' in
/repo/models/diagram-index.yaml. Accepted: draft, in-review, publish, deprecated, archived.
A status decides whether the entry is published and what the graph says about it, so an
unrecognised one is refused rather than treated as publishable.

The check is worth having because the failure it replaces was silent. status: used to be a free string in which only the exact token draft had any effect, so status: Draft published a draft and status: archived published an archived diagram as though it were current.

Validation runs over the whole index before anything is filtered out, so a typo on an excluded entry fails on the run that introduced it rather than months later on the run that first tried to publish it.

Withholding a state from a build

--exclude-states names the states to leave out. Everything else is converted:

# Every state, each carrying its own adms:status
plantuml2linkedarchi convert models/*.puml --diagrams-index index.yaml -o out/model.trig

# A release build that must not publish unreleased work
plantuml2linkedarchi convert models/*.puml --diagrams-index index.yaml \
  --exclude-states draft,in-review -o out/release.trig

Exclusion rather than inclusion because the two fail in opposite directions. An inclusion list omits by default, so forgetting to name a state silently drops diagrams and the missing IRIs surface later as dead links. An exclusion list publishes by default, so forgetting to name one publishes something early — visible, and undone by a rerun.

Naming every state is refused. It would convert nothing and write an empty graph, which looks like a successful build:

[ERROR] Conversion failed: --exclude-states names every state (draft, in-review, publish,
deprecated, archived), so the run would convert nothing and write an empty graph. Leave at least
one state in.

Both convert and render resolve the state set identically, so a diagram in the graph has a picture and a picture has a graph.

--include-states and --include-drafts are accepted and ignored, and the run says so. They only ever added to the processed set, and that set is now everything, so anything they could have named is already in. Reinterpreting --include-states as "only these" would quietly narrow the output of every pipeline that passes it, so it does nothing instead:

[WARN] --include-states no longer changes anything: every state is converted by default, so draft
would have been included regardless. The flag is deprecated and ignored. To leave states out, use
--exclude-states.

What reaches the graph

A declared state is published as adms:status, whose values are concepts from the ADMS status vocabulary. A consumer that already reads DCAT-AP understands the lifecycle without knowing anything about Linked.Archi:

<https://example.org/la/plantuml/order-domain/graph/provenance> {
    <…/view/current> dct:title "Current Flow" ;
        adms:status <http://purl.org/adms/status/Completed> .

    <…/view/leaving> adms:status <http://purl.org/adms/status/Deprecated> .
}

The status sits on the view, beside that entry's title and dates, because it describes the diagram the entry selected. A status: on the model: entry is a default for its views, so one line can retire a whole model; it is emitted on each view rather than on the model. In the flat schema, where there is no view, it lands on the model like every other entry-level field.

An entry that declares no status: is published and emits no adms:status. Silence is not an author calling a diagram finished, so nothing is asserted on their behalf.

Architecture state

This section covers the index route

For every route across every notation and level — including element scope and ArchiMate, neither of which the index reaches — see Architecture state.

architectureState: says which reality a diagram describes: how the architecture is today, how it is meant to end up, or a plateau in between. Without it a graph merges them — a target-state component and its baseline counterpart become one resource making contradictory claims, and a query for "what runs today" returns things nobody has built yet.

architectureState: arch:architectureState Meaning
baseline arch:Baseline how the architecture exists today
target arch:Target how it should exist once planned changes are realised
transitional arch:Transitional a stable plateau on the way from one to the other

The spelling variants the core ontology records as skos:altLabel are accepted too: as-is, current and current-state for baseline; to-be, future and future-state for target; transition, intermediate and intermediate-state for transitional. Case and the choice of -, _ or a space are ignored, so As Is and as-is are one value.

Not the same thing as status:

The two are easy to confuse and answer different questions.

Field Question Subject
status: has this diagram been released? the diagram, as a document
architectureState: which reality does it describe? the architecture

A target-state diagram can be perfectly published — finished, reviewed, and describing a future that does not exist yet. Neither field implies anything about the other, which is why they are separate fields rather than one vocabulary. Writing draft under architectureState: fails the run and says where it belongs.

Each level describes itself, and nothing is inherited. This is where it differs from status:, where a model-level value is only a default and the model is never the subject. arch:architectureState is defined on a Model as readily as on a view — the core ontology's own example uses a Model — so a model saying baseline is a claim about the model, not a default for its views:

model:
  id: order-domain
  architectureState: baseline        # the model is the baseline architecture
views:
  - id: as-built
    file: as-built.puml              # says nothing of its own, and inherits nothing
  - id: planned
    file: planned.puml
    architectureState: target        # this diagram describes the target

A consumer asking about a view that says nothing of its own follows dct:isPartOf to its model. In the flat schema an entry is a model, so the value there is the model's; there is no view for the index to describe.

An unrecognised value fails the run, naming the entry:

[ERROR] Conversion failed: Unknown architectureState 'Targt' for 'planned.puml' in
/repo/models/diagram-index.yaml. Accepted: baseline, target, transitional. A state says which
reality the diagram describes, so an unrecognised one is refused rather than published as a link to
a term nothing defines. Note this is not the publication status: 'draft' and 'publish' belong under
'status:'.

Failing closed is the point of the field existing at all. The same statement can be written through the generic annotation route — links: { arch:architectureState: arch:Targt } — and there the typo is emitted as a link to an IRI nothing defines, with no complaint, because a generic predicate has no set of values to check against. This one does.

Saying nothing asserts nothing. Unlike status:, there is no default: a repository that has never distinguished baseline from target is not thereby claiming everything is baseline.

PlantUML sources can declare it in the file as well, with '!la-architecture-state. If both the file and the index declare one they must agree — baseline and target are opposite claims about one diagram, so a disagreement is refused rather than settled by precedence, which is where this differs from a prefix declared twice.

Naming a successor

A status says a diagram is on its way out. supersededBy: says where to go instead, which is the half of a deprecation a reader actually needs:

model:
  id: order-domain
views:
  - id: order-flow-v1
    file: order-flow-v1.puml
    status: deprecated
    supersededBy: order-flow-v2
  - id: order-flow-v2
    file: order-flow-v2.puml
    status: publish
<https://example.org/la/plantuml/order-domain/graph/provenance> {
    <…/view/order-flow-v1>
        adms:status      <http://purl.org/adms/status/Deprecated> ;
        dct:isReplacedBy <…/view/order-flow-v2> .

    <…/view/order-flow-v2> dct:replaces <…/view/order-flow-v1> .
}

Not the same thing as formerIds:

The two look alike and mean opposite things, so it is worth being explicit:

What it says Triples
formerIds: one resource, previously published under another name owl:sameAs, dct:replaces, dct:isReplacedBy
supersededBy: two resources, one replacing the other dct:isReplacedBy, dct:replaces

supersededBy: deliberately emits no owl:sameAs. A successor is a different diagram, so claiming the two IRIs denote one thing would merge them — and any reasoner would then merge their elements too. Use formerIds: to rename a diagram, supersededBy: to replace one.

Rules

supersededBy: is read at the level the entry sits on, like formerIds:: on a view entry it names another view of the same model, and in the flat schema another model. Succession across models is not expressible, since the reference is resolved within the index.

Four things are refused, all so that following dct:isReplacedBy arrives somewhere real:

Refused Because
a successor not in the index the reference would point at an IRI nothing mints
a successor in a withheld state a default build publishes nothing for it
an entry naming itself to rename a diagram, use formerIds:
a cycle (v1 → v2 → v1) following the chain would never reach a current diagram

supersededBy: also requires a retired status: — deprecated or archived. "Current, and also replaced" is not a state a consumer can act on, and it usually means the two entries were swapped:

[ERROR] Conversion failed: supersededBy on a current entry in /repo/models/diagram-index.yaml:
'order-flow-v1.puml' is 'publish'. Only a retired diagram has a successor, so the entry needs
'deprecated' or 'archived'. Did the two entries get swapped?

Chains are allowed, so a diagram replaced twice keeps a trail: v1 archived → v2 deprecated → v3 published.

Full rules in ADR 0004.