Skip to content

PlantUML Converter

Converts PlantUML diagrams (class, component, sequence, use case) to RDF.

Ontology

Types align to the published UML 2.5.1 Ontology (uml: prefix).

Default type mapping (built-in, no config needed):

PlantUML keyword UML ontology type
class uml:Class
interface uml:Interface
enum uml:Enumeration
abstract uml:Class + uml:isAbstract true
component uml:Component
package uml:Package
actor uml:Actor
usecase uml:UseCase
node uml:Node
device uml:Device
artifact uml:Artifact
database uml:Node
boundary, control, entity, collections, queue uml:Class
state uml:State — simple and composite alike
initialState uml:Pseudostate + uml:pseudostateKind uml:InitialPseudostate
finalState uml:FinalState

In a sequence diagram every participant is uml:Lifeline, whichever keyword asked for it — actor, database and boundary included. UML 2.5.1 §17.3: a participant in an Interaction is a Lifeline. Outside an Interaction the table above applies, so actor in a use case diagram is a uml:Actor. See ADR 0007.

The keyword itself

Where a keyword says something its UML class cannot, it is published as schema:keywords — a plain label, not a type claim:

<…/element/Ui>       a …, uml:Class ;    schema:keywords "boundary" .
<…/element/Order_DB> a …, uml:Lifeline ; schema:keywords "database" .
<…/element/Order>    a …, uml:Class .                                # `class` says nothing more

A keyword that names its own class is dropped rather than restated as a weaker string, and abstract becomes uml:isAbstract instead. This replaced uml:stereotype, which nothing upstream declared and which implied a UML profile application that never happened — the values are PlantUML keywords, and a user-written <<stereotype>> is not read by this converter.

PlantUML relationship UML ontology type
--> (association) uml:Association
*-- (composition) uml:Composition
o-- (aggregation) uml:Aggregation
--|> (generalization) uml:Generalization
..\|> (realization) uml:Realization
..> (dependency) uml:Dependency
<<include>> uml:Include
<<extend>> uml:Extend
Sequence messages uml:Message, with a uml:messageSort — see below

Sequence message sort

All three sequence arrow kinds are uml:Message, so the kind is carried by uml:messageSort instead of by the class. The values are the published uml:MessageSort individuals in uml/onto:

PlantUML uml:messageSort also
A -> B SynchCall — UML's own default for an unqualified message
A ->> B AsynchCall
A --> B (dotted) Reply
A -->> B (dotted async) Reply see below
B <- A SynchCall source A, target B — direction of travel, not writing order
A ->x B SynchCall uml:messageKind uml:Lost
<…/relationship/ClientApp__OrderService__create_order__60f79e38…> a uml:Message ;
    uml:messageSort <https://meta.linked.archi/uml/onto#SynchCall> .

asynchCall rather than asynchSignal for ->>: PlantUML does not distinguish a call from a signal, and a call is the commoner reading of an async arrow between participants. umlsh:MessageShape requires exactly one messageSort per message and constrains it with sh:class uml:MessageSort, so this is also what makes a converted sequence diagram conform.

-->> is both dotted and async — an asynchronous reply — and messageSort cannot say so, because reply is a sort in its own right rather than a modifier that combines with asynchCall. One of the two facts has to go, and the asynchrony is the one that goes: it keeps the arrow out of the set of calls, which is what a consumer asking "what calls what" needs, whereas dropping the return semantics would put a reply into that set.

The values are owl:NamedIndividuals of uml:MessageSort in the ontology namespace, not in a separate reference-data one. That is UML-DD-12 upstream: the nine UML Enumerations moved into uml/onto, closed with owl:oneOf, and uml/reference-data was retired. They are not affected by --type-mapping, which remaps classes rather than individuals — a value moved into another namespace would no longer be a member of the enumeration.

Lost messages

A ->x B is a message that was sent and never arrived. UML records that as uml:messageKind, not as a different sort — a lost synchronous call is still a synchCall — so it is emitted alongside:

<…/relationship/A__B__ping__cb3e0d2c…> a uml:Message ;
    uml:messageSort uml:SynchCall ;
    uml:messageKind uml:Lost .

Only Lost is emitted. Complete is derived in UML from a message having both a sendEvent and a receiveEvent, and this converter emits no uml:MessageEnd resources, so it has no grounds to claim it. Found would need a message arriving from outside the diagram, which PlantUML writes with a gate ([-> B) — and gates are reported by the library as MessageExo rather than Message, so the parser does not read them at all and those arrows are absent from the graph.

State diagrams

A state diagram converts to a state machine. The diagram kind is what selects this, so nothing here affects class or component diagrams.

Source Becomes
state Idle or a state named by a transition uml:State
state Running { … } uml:State — composite states were always right
[*] at the source of a transition uml:Pseudostate + uml:pseudostateKind uml:InitialPseudostate
[*] at the target of a transition uml:FinalState
Idle --> Running: start uml:Transition, notation Idle__Running__start
[*] --> Idle uml:Transition, notation start__Idle__transition

The two ends of [*] are different things in UML, and PlantUML reports which is which, so it is not inferred from the direction of the transition: the initial vertex is transient and the machine passes through it, the final one is a State the machine rests in.

A nested region gets its own pair, because PlantUML scopes the name to the region:

[*] --> Idle          → element/start
state Running {
  [*] --> Warming     → element/start_Running
  Hot --> [*]         → element/end_Running
}
Running --> [*]: done → element/end

Both vertices carry a skos:prefLabel of initial or final, because uml:Pseudostate and uml:FinalState are uml:Vertex and so uml:NamedElement, which umlsh:NamedElementShape requires to be labelled. Those are descriptions, not identifiers — the id comes from the region-scoped name above.

uml:Transition is declared a subclass of arch:QualifiedRelationship upstream, so a transition is still a qualified relationship with arch:source and arch:target, and the core shapes still apply.

Relationship ids

A relationship is named after its label, because the endpoints name a pair and two participants exchange many messages:

Source Relationship notation
ClientApp -> OrderService: create order ClientApp__OrderService__create_order
ClientApp --> OrderService: order created ClientApp__OrderService__order_created
A -> B (unlabelled) A__B__call
A --> B (unlabelled) A__B__reply

The label is lower-cased into the notation and left as written in skos:prefLabel, so fixing a capital does not move a published IRI. One arrow drawn in two views is therefore one relationship with a link in each view; a call and a reply between one pair are two.

The id is that notation with a digest appended

<…/relationship/ClientApp__OrderService__create_order__60f79e38d012a705ddc3>
    skos:notation "ClientApp__OrderService__create_order" .

The notation has no length limit — it composes two endpoint ids and a label, all author-supplied — and a path segment becomes a filename wherever the graph is written out as one document per resource, where 255 bytes is the ceiling. A diagram naming participants by the URL of their API contract, which is what you do when the participant is an API with a published spec, reached 644 bytes.

So the id is the notation truncated to 180 characters with a 20-character SHA-256 digest of the whole of it appended, and the untruncated notation is published as skos:notation. Identity is unchanged, because the digest is over exactly the notation: two views still merge, a call still separates from a reply.

Read skos:notation, not the IRI, when you need the endpoints and label back — beyond 180 characters the IRI holds only a prefix.

Turning off --emit-skos-notation removes the only complete copy, so a long id becomes unreadable. It stays on by default for that reason.

Full rules, the alternatives considered and the migration note are in ADR 0005.

Usage

# Class diagram → TriG
java -jar plantuml2linkedarchi.jar convert \
  diagram.puml \
  --base-iri https://example.org/la/ \
  --model-id demo \
  --format TRIG \
  -o out.trig

# Sequence diagram → Turtle
java -jar plantuml2linkedarchi.jar convert \
  sequence.puml \
  --base-iri https://example.org/la/ \
  --model-id demo \
  --format TURTLE \
  -o out.ttl

# Custom domain mapping
java -jar plantuml2linkedarchi.jar convert \
  diagram.puml \
  --base-iri https://example.org/la/ \
  --model-id demo \
  --format TRIG \
  --type-mapping config/type-mapping-plantuml-custom.yml \
  -o out.trig

CLI options

Option Default Description
<inputs> required PlantUML file(s) (.puml)
-o, --output required Output artifact, path[:FORMAT[:PROFILE]]. Repeatable — several artifacts are written from one conversion, which is faster than re-running the converter and is what makes them share one prov:generatedAtTime. PROFILE is full (default), no-geometry, no-views or no-diagrams; see output serialization
--base-iri required Base IRI for minting all resource IRIs, e.g. https://example.org/la/. An http/https IRI, so the resources it mints dereference
--model-id filename Model identifier for a single input. An index id wins over it, and giving it with several inputs is refused — see one model per --model-id. Unlike an index id, the value is not validated
--format inferred Default format for any --output that names none: TRIG / TURTLE / JSONLD / RDFXML / NTRIPLES / NQUADS. A file extension such as ttl is also understood
--ns-puml https://meta.linked.archi/uml/onto# Notation namespace
--ns-core https://meta.linked.archi/core# Override the core namespace
--ns-core-vis https://meta.linked.archi/core-vis# Override the core-vis namespace
--type-mapping none YAML type overrides
--include-views true Emit archvis: view nodes/links
--emit-skos-labels true Emit skos:prefLabel
--label-language en BCP-47 tag for skos:prefLabel literals
--emit-skos-notation true Emit skos:notation
--require-title false Fail instead of labelling a view with its own id when neither the index nor the diagram declares a title — see View titles
--emit-direct-rel-triples false Emit {src} {pred} {tgt} shortcuts, each bridged back to its relationship with rdf:reifies. Needs predicates: in a --type-mapping
--emit-extension-data false Map extension data onto the elements it annotates, from '!la- comments and from index elements: entries. See Extension data
--ns-global-id none Base IRI for link targets written without a prefix, so a bare id resolves into a cross-model graph. Requires --emit-extension-data
--svg-base-iri none Base IRI of published SVG renders. When set, each view gets a schema:image pointing at {base}/{path}/{modelId}.svg, so converted RDF links to the diagram rendered by render
--diagrams-index none Diagram index for batch processing. When given, only indexed files are converted — in every lifecycle state unless --exclude-states says otherwise — and each one's id becomes its model ID
--diagrams-root index location Root directory that index file paths are resolved against
--exclude-states none Leave index entries in these lifecycle states out of the run. Every state is converted by default, so this is the only option that withholds one. Comma-separated, and it cannot name all five. See Lifecycle states
--include-states none Deprecated and ignored: every state is converted by default. Use --exclude-states
--include-drafts false Deprecated and ignored: drafts are converted by default

Extension data

A .puml says what UML can express. To say which capability a component realizes, or which LeanIX factsheet it corresponds to, use extension data — see Extension data for the concepts, which are shared with BPMN.

PlantUML has no extension point of its own, so there are two routes in and they are additive.

In the source file

Comment lines, which every renderer ignores and no tool rewrites — the same mechanism this converter already uses for identity declarations:

'!la-prefix am: https://meta.linked.archi/archimate3/onto#
'!la-prefix kg: https://example.org/graph/
'!la-prefix lx: https://leanix.example.com/factsheet/
'!la-prefix x:  https://example.org/vocab#
@startuml
class OrderService
'!la-link OrderService am:realizes kg:CAP-OrderManagement
'!la-data OrderService x:costCentre CC-4711

class "Payment Gateway" as PayGw
'!la-link PayGw am:serves lx:APP-payments Backward
@enduml
plantuml2linkedarchi convert orders.puml \
  --base-iri https://example.org/la/ --model-id orders \
  --emit-extension-data \
  --format TRIG -o out.trig
<…/plantuml/orders/element/OrderService>
    am:realizes      <https://example.org/graph/CAP-OrderManagement> ;
    vocab:costCentre "CC-4711" .

<https://leanix.example.com/factsheet/APP-payments>
    am:serves <…/plantuml/orders/element/PayGw> .

class "Payment Gateway" as PayGw is addressed as PayGw, because that is the name the source refers to it by — the same name the annotation and any arrow use. Payment Gateway is its skos:prefLabel. See Naming an element.

Directive Form
'!la-prefix <prefix> <namespace> — the trailing : on the prefix is optional
'!la-link <element> <predicate> <target> [Forward\|Backward\|Both]
'!la-data <element> <predicate> <literal> — the literal is the rest of the line, so it may contain spaces
'!la-view-link <predicate> <target> [Forward\|Backward\|Both] — no subject, see below
'!la-view-data <predicate> <literal> — no subject
'!la-architecture-state <baseline\|target\|transitional> — a checked value, not a predicate; see Architecture state

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

Unlike identity declarations, these are read from the whole file, not just the header — a statement about an element belongs beside that element, which is the only reason to prefer this route over the index. A misspelled directive is reported rather than silently ignored:

[WARN] orders.puml:3: unknown directive '!la-lnik'. Accepted: !la-prefix, !la-link, !la-data,
!la-view-link, !la-view-data (and !la-model, !la-view for identity).

Statements about the diagram itself

'!la-view-link and '!la-view-data take no subject: a .puml file is one view, so there is nothing else they could be about. This is how an author says something true of the diagram as a whole rather than of anything drawn in it — which architecture state it depicts, which viewpoint it conforms to, which decision option it articulates:

'!la-prefix arch: https://meta.linked.archi/core#
'!la-prefix archvp: https://meta.linked.archi/core-viewpoints#
'!la-prefix kg: https://example.org/graph/
'!la-prefix x: https://example.org/vocab#
'!la-view-link arch:architectureState arch:Target
'!la-view-link arch:viewConformsToViewpoint archvp:Roadmap
'!la-view-data x:reviewedBy Jane Doe
@startuml
class OrderService
@enduml
<…/plantuml/orders/view/orders>
    arch:architectureState        arch:Target ;
    arch:viewConformsToViewpoint  archvp:Roadmap ;
    vocab:reviewedBy              "Jane Doe" .

arch:inView runs from a concept to the view, so pointing at a decision option needs the statement reversed — the same Backward token the element directives use:

'!la-view-link arch:inView kg:ReadReplicas Backward

There is no '!la-model-link. A model spans several files, so a model-wide claim written in one of them would have no evident owner; the index names a model exactly once, so model-level statements go there. See statements about a view or a model.

View titles

skos:prefLabel is what a consumer shows in a listing or a search result, and a view id is not a substitute: order-flow-v2 is an address, chosen to be stable and URL-safe rather than to be read.

Two places can declare a title, and the index wins with the diagram filling the gap — the rule identity already follows. Using the index title also keeps skos:prefLabel and dct:title from disagreeing, since the index title becomes the latter.

@startuml
title Order Fulfilment Flow
class OrderService
@enduml
views:
  - id: order-flow
    file: flow.puml
    title: Order Fulfilment Flow    # wins over the diagram's own

With neither, the view id is used and the run says so:

[WARN] flow.puml declares no title, so the view id 'flow' is published as its skos:prefLabel. Add
'title:' to the diagram index entry, or a 'title' line to the diagram — or pass --require-title to
make this an error.

--require-title makes it an error, for a publishing pipeline that should not ship a diagram labelled with its own address. Either route satisfies it.

Architecture state

Which reality the diagram describes — baseline, target, or a plateau between them:

'!la-architecture-state target
@startuml
class OrderService
@enduml
<…/plantuml/orders/view/orders> arch:architectureState arch:Target .

A checked value rather than a predicate and an IRI, so a typo fails the run:

[ERROR] Conversion failed: Unknown architectureState 'Targt' for orders.puml:1.
Accepted: baseline, target, transitional. … Note this is not the publication status: 'draft' and
'publish' belong under 'status:'.

That is the reason it is its own directive rather than one more '!la-view-link: written generically, '!la-view-link arch:architectureState arch:Targt emits a link to a term nothing defines and says nothing about it.

Three things worth knowing:

  • It is not gated by --emit-extension-data. Which reality a diagram describes is a property of the diagram, like its status — not data borrowed from a modelling tool's extension point, which is what that flag is about.
  • The spellings the ontology records are accepted: as-is, current, to-be, future, transition, intermediate, plus any casing and -/_/space mix. As Is works.
  • The index can declare it too, and if both do they must agree — a disagreement is refused rather than settled by precedence, because baseline and target are opposite claims about one diagram.

Full semantics, including why it is orthogonal to status:, are in Architecture state.

Naming an element

A PlantUML entity has two names, and an annotation may use either:

class "Payment Gateway" as PayGw

PayGw is the code — the name the source refers to the entity by, in every arrow and in every annotation, and the one the element IRI is built from: …/element/PayGw. Payment Gateway is the display name — what the picture shows, and the element's skos:prefLabel.

Write the code. It is what the rest of the file already uses, and it does not move when someone retitles the box, so neither the IRI nor an annotation keyed on it needs revisiting. The display name resolves as well; quote it when it contains a space:

'!la-link PayGw arch:refines kg:CAP-Payments             ' by code — prefer this
'!la-link "Payment Gateway" arch:refines kg:CAP-Payments ' by display name — same element

Where an entity has no as clause the two names are one string and there is nothing to choose: class OrderService mints …/element/OrderService, and participant "Book API" mints …/element/Book_API.

A name matching two elements is refused rather than guessed at, and one matching none is reported:

[WARN] orders.puml:12: no element 'Nowhere' in this model, so <am:realizes> was not attached to
anything. Check the name against the diagram.

Why identity comes from the code rather than the label is ADR 0010; the shared rules for both routes are in referring to an element.

Matching sequence diagram participants to known applications

A sequence diagram's participants are usually applications, so the same annotation matches each one to its record in a CMDB, an ArchiMate model, or wherever else the organization already describes it:

'!la-prefix skos: http://www.w3.org/2004/02/skos/core#
'!la-prefix cmdb: https://example.org/cmdb/application/
@startuml
actor Customer
participant "Web Shop" as Shop
participant "Payment Gateway" as PayGw
participant "Order Service" as Orders
database "Order DB" as DB

Customer -> Shop : checkout()
Shop -> PayGw : authorize(amount)
Shop -> Orders : createOrder()
Orders -> DB : persist(order)

'!la-link Shop skos:exactMatch cmdb:APP-webshop
'!la-link PayGw skos:exactMatch cmdb:APP-payment-gateway
'!la-link Orders skos:exactMatch cmdb:APP-order-service
@enduml
<…/plantuml/checkout/element/Shop>
    skos:prefLabel  "Web Shop"@en ;
    skos:exactMatch <https://example.org/cmdb/application/APP-webshop> .

<…/plantuml/checkout/element/PayGw>
    skos:prefLabel  "Payment Gateway"@en ;
    skos:exactMatch <https://example.org/cmdb/application/APP-payment-gateway> .

<…/plantuml/checkout/element/Orders>
    skos:prefLabel  "Order Service"@en ;
    skos:exactMatch <https://example.org/cmdb/application/APP-order-service> .

Each participant is addressed by its alias and labelled with its display name, so the annotations read exactly as the arrows above them do — and retitling "Web Shop" to "Web Shop (EU)" changes the label without touching the IRI or the skos:exactMatch that now points at it.

skos:exactMatch, not owl:sameAs, for this. The two say different things and a reasoner treats them differently:

  • skos:exactMatch says the participant and the CMDB entry describe the same real-world application, without claiming they are one RDF resource. Each keeps its own properties, and nothing is inferred onto the other. This is the normal case: Payment Gateway is this diagram's mention of an application that has its own, separately maintained, description elsewhere.
  • owl:sameAs asserts that two IRIs denote one resource. A reasoner is entitled to merge everything known about both — every property of the CMDB entry becomes true of the participant and vice versa. That is the right tool only when the two IRIs really are alternate names for one node, e.g. reconciling an id after a rename (see formerIds: in the diagram index), not when relating a diagram's mention of a thing to that thing's record elsewhere.

Getting this backwards is how a graph ends up merging two resources that were only ever meant to correspond. Same rule as BPMN's — see terms worth knowing for the fuller table, including skos:closeMatch and skos:relatedMatch for a looser correspondence than exact, and Stating a correspondence for the substitutability test and the instance-versus-type distinction.

In the diagram index

For a team that owns the pipeline rather than the diagrams. The .puml needs no annotations at all — see Element entries for the schema.

prefixes:
  am: https://meta.linked.archi/archimate3/onto#
  arch: https://meta.linked.archi/core#
  kg: https://example.org/graph/
model:
  id: orders
  links:
    arch:architectureState: arch:Baseline     # about the model
views:
  - id: order-flow
    file: orders.puml
    links:
      arch:architectureState: arch:Target     # about this view
    elements:
      OrderService:                           # about an element in it
        links:
          am:realizes: kg:CAP-OrderManagement

links: and data: written directly on an entry are about that entry — the view, or the model — rather than about anything drawn in it, and they are the only route for model-level statements. See statements about a view or a model.

Both routes apply in the same run. Where both declare the same prefix, the source file wins.

Architecture

PlantUML source (.puml)
  → PlantUmlParser (plantuml-mit library AST)
  → PumlModel (elements, relationships, views)
  → LinkedArchiEmitter (extends BaseLinkedArchiEmitter)
  → TriG / Turtle output

Render

Renders PlantUML sources to SVG using PlantUML's own layout engine. Pair it with --svg-base-iri on convert so the RDF links each view to its rendered diagram.

java -jar plantuml2linkedarchi.jar render \
  diagram.puml -o svg/

# Hyperlinked to the knowledge graph, driven by the shared index
java -jar plantuml2linkedarchi.jar render models/*.puml \
  --base-iri https://example.org/la/ \
  --diagrams-root models/ \
  --diagrams-index models/diagram-index.yaml \
  -o out/svg/
Option Default Description
<inputs> required Input PlantUML file(s) (.puml, .plantuml, .pu)
-o, --output-dir required Directory the SVG files are written to
--base-iri none When given, each element becomes a link to its element IRI ({base}plantuml/{modelId}/element/{id}), the same IRI convert mints
--diagrams-index none YAML index selecting which diagrams to render and their model IDs
--diagrams-root index location Root directory that index file paths are resolved against
--exclude-states none Leave index entries in these lifecycle states out of the run. Every state is rendered by default. Comma-separated, and it cannot name all five. Resolved exactly as convert resolves it, so a diagram in the graph has a picture
--include-states none Deprecated and ignored: every state is rendered by default. Use --exclude-states
--include-drafts false Deprecated and ignored: drafts are rendered by default
--[no-]render-archived true Draw entries with status: archived. --no-render-archived leaves them out; convert is unaffected, so the diagram keeps its IRIs but its image URL stops resolving
--layout AUTO Layout engine: AUTO, DOT or SMETANA — see Layout engine

Output is <modelId>.svg, mirroring any sub-directory of the index file: path. Per-file failures are reported as [WARN] Skipped … and the command exits 1. This is the shared render behaviour, identical to the BPMN converter.

modelId is the index id, and it names two things at once: this SVG and the IRI path segment convert mints ({base}plantuml/{id}/…), which is what makes the [[iri]] links in the diagram resolve against the graph. It is used unescaped in both, so it must be a slug — ^[a-z0-9][a-z0-9._-]*$ — and id and file: must each appear once in the index. A violation fails the command rather than being silently rewritten or dropped; see id rules. Without an index the model ID falls back to the source filename, which is not validated, so an index is what gets you predictable URLs. Changing an id renames the published SVG and moves every element IRI for that model.

View identity

A view ID can come from two places — the index views[].id, or a '!la-view: comment in the source (see Declaring identity in the source file) — and convert and render resolve it the same way:

  • Only one declares it — that value is used.
  • Both declare it, and agree — that value is used.
  • Both declare it, and disagree — the run fails, naming both values and both files, rather than one silently winning.
  • Neither declares it — it falls back to the model ID. A .puml file has no diagram ID of its own to fall back to instead, so this is also what a plain convert diagram.puml with no index and no '!la-view: gets: one file, one model, one view, all under the same ID.

This is the general identity-reconciliation rule (IdentityAgreement), applied to the view level — model IDs are reconciled the same way. See ADR 0001 for why identity is split between the two sites at all.

Links are added by annotating a copy of the source with PlantUML's own [[iri]] syntax — the .puml file on disk is not touched, and links the author already wrote are kept. Both declaration styles are annotated: keyword (component Foo, class "Foo" as F) and bracket shorthand ([Foo], (Use case), :Actor:, () Interface). Elements declared only inside a relationship ([A] --> [B]) have no declaration to annotate and stay unlinked. If annotation would break the diagram, it renders without links and logs a warning. See SVG Rendering → Hyperlinks.

Embed the SVG with <object>, <iframe> or inline <svg> — through an <img> tag or a Markdown ![](…) image, browsers render SVG without interactivity and no link is clickable.

Layout engine

Sequence, activity, mindmap, gantt, JSON and WBS diagrams are laid out by PlantUML itself. Class, component, use case, state and deployment diagrams go through Graphviz's dot algorithm — either the real dot binary or Smetana, the pure-Java port bundled inside the PlantUML JAR. --layout picks which:

--layout Behaviour
AUTO (default) Use dot when it is installed, otherwise fall back to Smetana and log a warning
DOT Require the dot binary; skip the file with an explanatory message if it is missing
SMETANA Always use the bundled engine — no external dependency, identical layout on every machine
# No Graphviz anywhere, fully self-contained
java -jar plantuml2linkedarchi.jar render diagram.puml --layout SMETANA -o svg/

# Fail rather than silently produce a different layout
java -jar plantuml2linkedarchi.jar render diagram.puml --layout DOT -o svg/

Smetana is implemented by inserting !pragma layout smetana into a copy of the source; a layout pragma the author already wrote always wins.

Which to choose: dot is the reference implementation and lays complex diagrams out better, so install it (brew install graphviz / apt-get install graphviz, or point GRAPHVIZ_DOT at the binary) where diagram quality matters. Prefer SMETANA in CI and containers, where a self-contained build and byte-stable output across machines are worth more — note that under AUTO the same source renders differently depending on whether the build agent happens to have Graphviz installed.

Validate

Runs SHACL validation on converted output. Shapes and ontologies are fetched from meta.linked.archi at runtime.

Default shapes are uml-shapes and core-shapes. PlantUML is a syntax rather than a metamodel, and this converter types its output in UML 2.5.1 — so UML is the notation whose shapes apply, and uml-shapes carries the naming rule (umlsh:NamedElementShape, a skos:prefLabel on every uml:NamedElement). Core covers what is not notation-specific: a QualifiedRelationship having exactly one arch:source and arch:target. The uml and core ontologies are loaded for rdfs:subClassOf reasoning.

Switch the naming rule off for a run with --without-shape labels.

java -jar plantuml2linkedarchi.jar validate -i out.ttl

# Validate against your own shapes instead
java -jar plantuml2linkedarchi.jar validate -i out.ttl --shapes ./shapes/my-rules.ttl

# Offline, reusing documents fetched by an earlier run
java -jar plantuml2linkedarchi.jar validate -i out.ttl --asset-dir .assets --offline

Exits 0 when the data conforms, 1 when violations are found, 2 on error.

See Validation for the shared engine, all options, coverage reporting and report format.