Skip to content

LeanIX Converter

Converts a LeanIX fact sheet export to RDF, typed in the published SAP LeanIX Meta Model v4 ontology (lmm: prefix) — or in the v3 ontology (lmm3:) under --meta-model v3, for a workspace that has not migrated.

The input is a file, not a workspace. Fetching is a separate tool — leanix-pull — and the separation is deliberate:

Different failure modes A pull fails on credentials, rate limits and a workspace someone reshaped. A conversion fails on the data. Bundled together, a token that expired at 03:00 looks like a broken graph.
Different cadence The workspace is authoritative and changes continuously; the graph is published on a schedule. A file between them is where that difference is allowed to exist.
Reproducibility The same export gives the same graph today and in six months. A converter that called the API could never say that.
Reviewability The export is committed, so a change to the inventory arrives as a diff somebody can read, before it becomes triples.
graph LR
  W[LeanIX workspace] -->|GraphQL: fact sheets| P[leanix-pull pull]
  W -->|REST: diagrams| D[leanix-pull pull-diagrams]
  P -->|factsheets.json| G[(git)]
  D -->|diagrams.json| G
  G --> C[leanix2linkedarchi]
  C -->|TriG| R[(graph)]

Two pulls, because a workspace holds two kinds of thing. The inventory becomes elements and relationships; the diagrams become views. An inventory export is not a view and never becomes one — see Views.

Ontology

Document Namespace Prefix
Ontology v4 https://meta.linked.archi/leanix/onto# lmm
Ontology v3 https://meta.linked.archi/leanix/v3/onto# lmm3
SHACL shapes https://meta.linked.archi/leanix/shapes# lmmsh
Metamodel manifest (v4) https://meta.linked.archi/leanix/metamodel# lmmmm
Metamodel manifest (v3) https://meta.linked.archi/leanix/v3/metamodel# lmm3mm
Viewpoints https://meta.linked.archi/leanix/viewpoints# lmmvp
Architecture processes https://meta.linked.archi/arch-processes# ap

ap: is not a LeanIX document. The LeanIX ontology reuses its lifecycle stage vocabulary rather than minting one, so ap: terms appear in this converter's output and its document has to be loaded to validate against them.

Every fact sheet class is a subclass of lmm:FactSheet, which is a subclass of arch:Element, so notation-agnostic queries work as they do for every other converter. A converted model declares arch:modelConformsToMetamodel lmmmm:LeanIXv4 — or lmm3mm:LeanIXv3 under --meta-model v3.

LeanIX fact sheet type Ontology class
Objective, Initiative lmm:Objective, lmm:Initiative
Business Capability lmm:BusinessCapability
Organization (→ Organization Unit) lmm:Organization, lmm:OrganizationUnit
Business Context (→ Process, Product, Value Stream) lmm:BusinessContext, lmm:Process, lmm:Product, lmm:ValueStream
Application (→ Business Application, Deployment, Microservice) lmm:Application, lmm:BusinessApplication, lmm:Deployment, lmm:Microservice
Interface, Data Object lmm:Interface, lmm:DataObject,
IT Component, Tech Category, Platform, Provider lmm:ITComponent, lmm:TechCategory, lmm:Platform, lmm:Provider
System (optional type) lmm:System

The metamodel is configured per workspace

This is the one thing LeanIX does that no other source here does: fact sheet types can be renamed, added and removed per tenant, their fields are defined per tenant, and a relation is a field whose name is derived from the type pair. The published ontology describes the out-of-the-box metamodel, which is what most workspaces still resemble — but not all of them.

So the converter keys on the name your workspace uses, and when that name is not one the ontology declares it does not invent a class:

[WARN] factsheets.json: 'ProductFamily' is not a fact sheet type the published LeanIX v4 ontology
declares, so its fact sheets are typed lmm:FactSheet only, with the name kept on dct:type. If it is a
renamed or custom type, map it with --type-mapping under 'elements:' keyed 'ProductFamily'.

Minting lmm:ProductFamily would put an undeclared term inside a published namespace this converter reads rather than owns — a term a reader cannot dereference, and one a future upstream release could give another meaning. lmm:FactSheet is kept because it is true, published, and enough for the naming shape to apply, and --type-mapping is how the type gets a real class. See Mapping a renamed or custom type.

Subtypes

A subtype is not a fact sheet type, and this catches everyone once. LeanIX models a Business Application as an Application fact sheet whose category is businessApplication — not as a BusinessApplication fact sheet type. The API's own subtype filter says as much: it is a category facet beside the FactSheetTypes facet, and --type BusinessApplication matches nothing because no such type exists.

Three consequences, in the order they bite:

  1. The pull has to ask. type always comes back as the parent, so the subtype has to be requested explicitly with subType: on the type in the pull configuration. Without it there is nothing in the export to convert, and nothing warns — no field was requested, so no request failed.
  2. It is per type, not a base field. category is not on BaseFactSheet, so asking for it on a type that does not declare it is a GraphQL error that fails the whole query for that type. leanix-pull introspect reads the real schema and writes subType: wherever the field is actually there, which is the reliable way to fill these in.
  3. The built-in defaults claim it for ITComponent only. That is the one type whose subtypes — software, hardware, service — are in the standard metamodel. Application's three are optional upstream and off by default, so a workspace that has not added them may have no category on Application at all.
types:
  - name: ITComponent
    subType: category
  - name: Application
    subType: category      # only once print-query or introspect has confirmed the field

Subtypes become classes, alongside the parent

A subtype is a statement of kind, so it reaches rdf:type and not a string. The published ontology already declares nine of them as subclasses — lmm:BusinessApplication rdfs:subClassOf lmm:Application — and the converter emits both classes:

<…/element/a100…> a arch:Element, arch:ModelConcept,
                    lmm:FactSheet, lmm:Application, lmm:BusinessApplication ;
    skos:prefLabel  "Ledger"@en ;
    dct:type        "Application" ;
    vocab:subType   "businessApplication" .

Both, not just the narrower one, so a query for every lmm:Application still finds this fact sheet with no reasoner in the loop. The subclass axiom entails the parent anyway; asserting it means the distinction is free to adopt rather than a breaking change.

The raw token stays on vocab:subType under --ns-vocab, for the same reason the type token stays on dct:type: the class is this converter's reading of the token, the literal is what the workspace actually said. It is not a second dct:type — two values under one predicate with nothing to tell them apart is a collision this project has unpicked once already — and not schema:additionalType, which schema.org declares rdfs:subPropertyOf rdf:type, so a literal there would entail a literal in rdf:type position.

Custom subtypes: is_a or property

A subtype the ontology does not declare — one your workspace added, or software and its two siblings, which LeanIX ships and lmm: has no class for — resolves in this order:

Route How Result
Qualified mapping elements: keyed ITComponent/software your class
Bare mapping elements: keyed software or Software your class, under any type
Published class nothing to do lmm:BusinessApplication and the other eight
Minted class --ns-vocab vocab:Software
None — parent type only, reported once

Prefer the qualified key. A bare token is not unique across types: service is an ITComponent subtype in the standard metamodel, and nothing stops a workspace using the same word elsewhere meaning something else.

A minted name is capitalised — token externalPartner becomes vocab:ExternalPartner — because a class name is not a field value. The token itself stays exactly as the workspace spells it on vocab:subType.

When the type itself is renamed

The name on the left of the slash is the one your workspace uses, never the class you mapped it to. The two resolutions are independent: mapping a type does not change the key its subtypes are looked up under. So a workspace with UserGroup where the standard metamodel has Organization keys its subtypes UserGroup/…:

elements:
  UserGroup: https://meta.linked.archi/leanix/onto#Organization           # the type
  UserGroup/team: https://meta.linked.archi/leanix/onto#OrganizationUnit  # its subtype ✓
  # Organization/team: …                                                  # ✗ matches nothing
  businessUnit: https://example.org/house#BusinessUnit                    # bare — any type
ug-1  a lmm:FactSheet, lmm:Organization, lmm:OrganizationUnit ;   vocab:subType "team" .
ug-2  a lmm:FactSheet, lmm:Organization, house:BusinessUnit ;     vocab:subType "businessUnit" .
ug-3  a lmm:FactSheet, lmm:Organization, vocab:ExternalPartner ;  vocab:subType "externalPartner" .

The parent class comes from the type's own entry and the subtype's is added beside it, exactly as it is for a type the ontology declares. Organization/team is easy to reach for and is not silent — it falls to the last row of the table above, and the warning names the key that would have worked.

Every subtype of a renamed type needs one of the routes, because the published ontology declares classes under the standard names only. --ns-vocab alone is a reasonable answer where there are many and no strong opinion about each: every one gets a class in a namespace you own, with no mapping file to maintain, and the ones that matter can be mapped individually later.

The last row is the only lossy one, and it needs both a missing mapping and no --ns-vocab:

[WARN] factsheets.json: 'ITComponent' fact sheets with subtype 'software' reach no class: the published
LeanIX v4 ontology declares no lmm:Software, and nothing mapped it. The token is kept, but it is a string
rather than a type. Map it with --type-mapping under 'elements:' keyed 'ITComponent/software' — or keyed
'software' to map it wherever it appears — or pass --ns-vocab <your namespace> to have a class minted for
it there.

mapping-report lists every subtype in an export and which route it takes, before anything is converted, so this is answerable in CI rather than after the fact.

Meta Model v3 and v4

Both LeanIX meta models are live and both are published, so the version is an input rather than an assumption:

leanix2linkedarchi convert factsheets.json --meta-model v3 …
--meta-model v4 (default) --meta-model v3
Fact sheet classes lmm: — 20 classes lmm3: — 12 classes
Root class on every fact sheet lmm:FactSheet lmm3:FactSheet
arch:modelConformsToMetamodel lmmmm:LeanIXv4 lmm3mm:LeanIXv3
Relationship classes the sixteen lmm: relationships none — v3 publishes none
Attribute terms lmm: lmm:, unvalidated — see below

v3 has UserGroup, Project, Process and TechPlatform, the four types v4 replaced. They are renames with changed semantics rather than aliases — UserGroup became Organization, Project became Initiative, Process became a BusinessContext subtype, TechPlatform became Platform — so nothing is mapped across. Process and Product exist in both documents with different definitions, which is why the namespace is chosen once for the run rather than inferred per type.

Pointing the converter at the wrong version says so by name, rather than producing a pile of "unknown type" warnings that read as a defect in the export:

[WARN] factsheets.json: 'UserGroup' is not a fact sheet type the published LeanIX v4 ontology declares, so
its fact sheets are typed lmm:FactSheet only, with the name kept on dct:type. It is a v3 fact sheet type, so
this export probably belongs to Meta Model v3 — re-run with --meta-model v3.

No relationship class under v3. The v3 manifest is deliberately thin: fact sheet classes only, no relationships, no attributes, no viewpoints. The v4 relationship classes are not borrowed, because every endpoint shape names v4 classes at both ends — an lmm:Requiring between two lmm3:Applications would be a validation violation, not a richer graph. So a v3 relationship carries arch:QualifiedRelationship, the workspace's own field names on dct:type, and core's generic arch:hasQualifiedRelationship as the route in. The run says this once:

[WARN] Meta Model v3 publishes no relationships, so every relationship in this run carries
arch:QualifiedRelationship, the workspace's own field name on dct:type, and no LeanIX class. That is the
whole of what v3 declares. Use --type-mapping under 'relationships:' to point them at a vocabulary you have
chosen.

Two things still work as they do under v4, and both matter more than they look:

  • The two records LeanIX writes per edge still collapse to one relationship. The v4 relationship table is used as a de-duplication and direction oracle whatever the meta model — it asserts nothing, and ignoring it would double every edge. The relationship IRI keeps the published name for the same reason, so an edge holds the same IRI across a v3 → v4 migration: the one part of the graph that need not move when a workspace upgrades.
  • skos:broader still comes out of the hierarchy, since it is a SKOS statement rather than a LeanIX term.

Attributes still use the lmm: terms. Status, completion, tags, subscriptions and lifecycle are platform features rather than meta model versions, and lmm: is the only place they are declared. They attach through arch:domainIncludes, which entails no type, so an lmm:completion on an lmm3:Application does not quietly make it a v4 fact sheet. They go unvalidated under v3, though: LeanIXFactSheetAttributeShape targets lmm:FactSheet only, and says so. Labels are validated, because LeanIXFactSheetLabelShape targets lmm3:FactSheet as well — which is what makes a v3 export a supported input rather than an archived one.

--type-mapping remains the escape hatch, and is the natural tool for a workspace mid-migration: name the v4 class for each type that has already been reworked, and the v3 root stays underneath it.

Identity

A fact sheet's IRI is minted from its LeanIX id, which is a UUID:

https://example.org/la/leanix/leanix-acme/element/a1000000-0000-4000-8000-000000000001

This is the one place LeanIX is easier than the other sources. The id is stable across renames, so an element IRI survives a fact sheet being retitled — where a Backstage catalog has only names, and a rename moves a published IRI. Names are labels here and nothing else.

One export is one model, so the model id is the IRI namespace and the named graphs of every fact sheet in it. Splitting an export per fact sheet type would put the two ends of a relation in different namespaces and turn every internal reference into a dangling one.

Which way a relation points

A LeanIX relation is a field on both fact sheets it joins, under two different names. An application's link to a component is relApplicationToITComponent; the same link read from the component is relITComponentToApplication. An export scoped to both types therefore contains the edge twice, and emitting what was read would produce two relationships for one edge, pointing opposite ways.

The ontology decides. Each of the sixteen published relationships declares arch:domainIncludes and arch:rangeIncludes, so the direction is a modelling decision made upstream rather than a tie-break:

Unqualified property Qualified class Qualified property Direction
lmm:supports lmm:Supporting lmm:qualifiedSupports Application → BusinessCapability
lmm:requires lmm:Requiring lmm:qualifiedRequires Application → ITComponent
lmm:hasInterface lmm:InterfaceOwnership lmm:qualifiedHasInterface Application → Interface
lmm:usesData lmm:DataUsage lmm:qualifiedUsesData Application or Interface → DataObject
lmm:impacts lmm:Impact lmm:qualifiedImpacts Initiative → Application or Platform
lmm:drives lmm:Driving lmm:qualifiedDrives Objective → Initiative
lmm:usedByOrg lmm:OrganizationalUsage lmm:qualifiedUsedByOrg Organization → Application
lmm:providedBy lmm:Provision lmm:qualifiedProvidedBy ITComponent → Provider
lmm:consumesInterface lmm:InterfaceConsumption lmm:qualifiedConsumesInterface Application → Interface
lmm:categorizedBy lmm:Categorization lmm:qualifiedCategorizedBy ITComponent → TechCategory
lmm:parentOf lmm:FactSheetHierarchy lmm:qualifiedParentOf FactSheet → FactSheet
lmm:partOfPlatform lmm:PlatformMembership lmm:qualifiedPartOfPlatform Application or BusinessCapability or ITComponent → Platform
lmm:supportsContext lmm:ContextSupport lmm:qualifiedSupportsContext Application → BusinessContext
lmm:contextRealizes lmm:ContextRealization lmm:qualifiedContextRealizes BusinessContext → BusinessCapability
lmm:targetsCapability lmm:CapabilityTargeting lmm:qualifiedTargetsCapability Objective → BusinessCapability
lmm:composedOf lmm:SystemComposition lmm:qualifiedComposedOf System → Application or ITComponent

Because the mapping is semantic rather than name-derived, it also collapses pairs whose names look unrelated. relApplicationToInterface and relInterfaceToProviderApplication are one ownership edge, and both resolve to lmm:InterfaceOwnership pointing Application → Interface.

Each relationship gets the three representations core documents: the resource, the qualified predicate from the source to it, and — under --emit-direct-rel-triples — the direct triple, bridged back with rdf:reifies.

<…/relationship/requires--a1000000-…-000000000001--c1000000-…-000000000001>
    a                    arch:QualifiedRelationship, arch:ModelConcept, lmm:Requiring ;
    arch:source          <…/element/a1000000-…-000000000001> ;
    arch:target          <…/element/c1000000-…-000000000001> ;
    dct:type             "relApplicationToITComponent", "relITComponentToApplication" ;
    skos:notation        "r-app1-itc1", "r-itc1-app1" ;
    schema:validFrom     "2021-04-01"^^xsd:date ;
    schema:validThrough  "2027-12-31"^^xsd:date .

<…/element/a1000000-…-000000000001>
    lmm:qualifiedRequires <…/relationship/requires--a1000000-…-000000000001--c1000000-…-000000000001> .

dct:type carries the field names the workspace actually used, one per side the edge was read from — not the canonical name, which is a published class name the workspace may never use. skos:notation carries each LeanIX relation id, so either workspace record can be found again.

The relationship IRI is content-addressed from the canonical relation name and the two fact sheet ids, not from a LeanIX relation id: LeanIX gives each side its own record with its own id, so an id-derived IRI would mint two resources for one edge. Same reasoning as ADR 0005.

Relations the ontology does not declare

Every relationship the documented out-of-the-box metamodel has is now published, so this path is reached only by a relation a workspace invented. The two sides still merge, by a fallback rule: a field name of the form rel<A>To<B> has a derivable mirror rel<B>To<A>, and the lexicographically smaller of the two is canonical. Arbitrary, and deliberately so — with no ontology to consult there is no right direction, and what a graph needs is the same direction every time. The rule reads one field name and never what else the export contains, because choosing among the names actually observed would mean that widening a pull's scope silently moved every published relationship IRI.

The relationship resource then carries arch:QualifiedRelationship with no LeanIX subclass, reached by core's generic arch:hasQualifiedRelationship, and the run names the key to map it under:

[WARN] relation 'relApplicationToRiskAssessment' is not one the published LeanIX v4 ontology declares, so
the relationship carries arch:QualifiedRelationship and no LeanIX class. Map it with --type-mapping under
'relationships:' keyed 'relApplicationToRiskAssessment'.

Retyping a relation into another vocabulary

Case 3 of the type mapping — LeanIX output typed in ArchiMate, so it merges with ArchiMate models — needs predicates: and qualifiedPredicates: entries alongside each relationships: entry, not just the class.

A relationships: entry replaces the published class. Once relApplicationToITComponent is an am:Serving rather than an lmm:Requiring, lmm:qualifiedRequires no longer describes it: that predicate declares rdfs:range lmm:Requiring. Because rdfs:range is an entailment rather than a constraint, reusing it would have a reasoner infer lmm:Requiring back onto the resource and silently undo the retyping. So the converter withholds the published predicate and says so:

[WARN] relation 'relApplicationToITComponent' is retyped by 'relationships:' to <…archimate3/onto#Serving>,
but 'qualifiedPredicates:' names no predicate for it — so it carries arch:hasQualifiedRelationship. The
published LeanIX predicate is not used here: its rdfs:range is the LeanIX class the retyping replaced, and a
reasoner would infer that class back onto the relationship. Add the qualified form of
<…archimate3/onto#Serving> under 'qualifiedPredicates:' keyed 'Requiring'.

The relationship stays reachable on arch:hasQualifiedRelationship, so nothing is orphaned — it just no longer says of what kind on the way in. The direct triple is dropped rather than substituted, since there is no generic core predicate to stand in for one; the resource still carries the statement.

ArchiMate publishes a qualified form for every relationship class, so completing the mapping is naming existing terms:

relationships:
  relApplicationToITComponent: https://meta.linked.archi/archimate3/onto#Serving
predicates:
  relApplicationToITComponent: https://meta.linked.archi/archimate3/onto#serves
qualifiedPredicates:
  relApplicationToITComponent: https://meta.linked.archi/archimate3/onto#qualifiedServes

playground/config/type-mapping-leanix.yml carries the full set for the sixteen relations it retypes.

The hierarchy

relToParent / relToChild are LeanIX's only structural relation and are two ends of one edge. They resolve to lmm:FactSheetHierarchy, running parent → child — named for what LeanIX calls it rather than "Composition", which in a merged graph would invite reading it as ArchiMate's composition, a different relationship with whole-part semantics this one does not assert.

skos:broader goes out alongside it, because a capability breakdown is also a taxonomy:

<…/relationship/parentOf--bc-1--bc-2>
    a            arch:QualifiedRelationship, arch:ModelConcept, lmm:FactSheetHierarchy ;
    arch:source  <…/element/bc-1> ;      # parent
    arch:target  <…/element/bc-2> ;      # child
    dct:type     "relToChild", "relToParent" .

<…/element/bc-1> lmm:qualifiedParentOf <…/relationship/parentOf--bc-1--bc-2> .
<…/element/bc-2> skos:broader          <…/element/bc-1> .

The ontology asks for skos:broader explicitly and is precise that it is additive, not a substitute: it carries no relationship resource, so it cannot be typed, annotated or validated. Note it runs the opposite way to the resource.

One rule the shapes hand back to the converter. A LeanIX hierarchy never crosses fact sheet types, but FactSheetHierarchyEndpointsShape only requires a fact sheet at each end — and explains why it stops there: the two ends need not share a class, since an Organization may legitimately parent an OrganizationUnit, so a shape comparing rdf:type would reject a correct model while one walking the subclass hierarchy would accept an Application under a Microservice. The rule is about the workspace's own type token, which the graph carries as dct:type and only the export has in front of it. So the converter checks it and warns:

[WARN] factsheets.json: a 'relToChild' edge runs from a BusinessCapability to an Application. A LeanIX
hierarchy does not cross fact sheet types, so this is either a relation that is not really a hierarchy —
check the --type-mapping entry for it — or an export that lost a type. Emitted as declared.

A warning rather than a refusal: the edge is what the workspace exported, so dropping it would hide data. Naming both types is what makes the usual cause — a mapping entry pointing at the wrong relationship — findable.

Relations leaving the pulled scope

Pulling a subset of fact sheet types is the normal case, so a relation naming a type that was not pulled is normal too. The converter mints the IRI that fact sheet would have, warns once, and leaves the node untyped. Because the id is stable, widening the pull later joins the two sides up: the IRI does not change.

Until then core-shapes#QualifiedRelationshipShape reports the dangling endpoint, which is the honest state of the graph — either a deliberate scope boundary or a fact sheet that was deleted.

What each fact sheet emits

The ontology now declares the attribute set every workspace has. Where a term was already published elsewhere it deliberately does not shadow it, and neither does this:

<…/element/a1000000-…-000000000001>
    a                     arch:Element, arch:ModelConcept, lmm:FactSheet, lmm:Application ;
    arch:inModel      <…/leanix/acme> ;
    skos:prefLabel        "Payments Gateway"@en ;
    skos:altLabel         "payments-gateway"@en ;
    skos:notation         "a1000000-0000-4000-8000-000000000001" ;
    skos:definition       "Authorises and captures card payments." ;
    dct:type              "Application" ;
    dct:created           "2020-06-02T09:30:00Z"^^xsd:dateTime ;
    dct:modified          "2026-08-01T11:05:00Z"^^xsd:dateTime ;
    schema:url            "https://acme.leanix.net/acme/factsheet/Application/a1000000-…"^^xsd:anyURI ;
    lmm:factSheetStatus   lmm:StatusActive ;
    lmm:completion        "0.83"^^xsd:decimal ;
    ap:atLifecycleStage   ap:Active ;
    lmm:hasLifecyclePhase <…/element/a1000000-…-000000000001/phase/active> ;
    schema:keywords       "Cloud", "PCI" ;
    lmm:hasTag            [ a lmm:Tag ; lmm:tagName "Cloud" ; lmm:tagGroup "Hosting" ;
                            skos:notation "t-cloud" ] ;
    lmm:hasSubscription   [ a lmm:Subscription ;
                            lmm:subscriberEmail "accountable@example.org" ;
                            lmm:subscriptionType lmm:Responsible ;
                            lmm:subscriptionRole "Application Owner" ] .

<…/element/a1000000-…-000000000001/phase/active>
    a                   lmm:LifecyclePhase ;
    ap:atLifecycleStage ap:Active ;
    lmm:phaseStart      "2021-04-01"^^xsd:date .
LeanIX Emitted as
fact sheet id skos:notation
display name skos:prefLabel, language-tagged
name, when it differs skos:altLabel
description skos:definition
fact sheet type name dct:type, verbatim
created / updated dct:created / dct:modified
fact sheet URL schema:url
status lmm:factSheetStatus → lmm:StatusActive / StatusArchived / StatusBroken
completion lmm:completion, an xsd:decimal in 0..1
lifecycle.asString ap:atLifecycleStage → ap:Plan / PhaseIn / Active / PhaseOut / Retired
dated phases lmm:hasLifecyclePhase → lmm:LifecyclePhase
tags lmm:hasTag → lmm:Tag, plus schema:keywords on the fact sheet
subscriptions lmm:hasSubscription → lmm:Subscription
relation activeFrom / activeUntil schema:validFrom / schema:validThrough
export timestamp prov:generatedAtTime, in the provenance graph

Four things here are less obvious than they look, and all four are the shapes' doing rather than taste:

States are individuals, not strings. lmm:factSheetStatus lmm:StatusActive, not "ACTIVE" — and LeanIXFactSheetAttributeShape says so in its message: "A string literal such as ACTIVE does not satisfy this." Same for subscription type. An unrecognised token is therefore reported and left out, because a literal would fail validation while looking like data.

Lifecycle stages come from elsewhere. They are ap: individuals from the Architecture Processes extension, which the LeanIX ontology reuses rather than minting its own. endOfLife maps to ap:Retired, read off the skos:closeMatch each stage carries to the LeanIX lifecycle docs. This is why validate loads arch-processes.

lmm:completion is xsd:decimal. A Kotlin Double gives xsd:double and fails sh:datatype, so it is emitted from a BigDecimal. The shape also bounds it to 0..1, so a percentage would be caught.

A lifecycle phase needs both halves. LeanIXLifecyclePhaseShape requires exactly one stage and one start date, so a phase missing either is skipped rather than emitted half-formed. A phase with no date is the fact sheet's current stage, which belongs on the fact sheet as ap:atLifecycleStage.

Two smaller notes. Phase nodes are IRIs derived from the element's own IRI — matching the ontology's example and keeping a re-conversion byte-stable — while tags and subscriptions are blank nodes, since neither has an identity worth minting an IRI from. And schema:keywords stays alongside lmm:tagName: the ontology holds the name in lmm: because this project publishes no cross-notation keyword term, and says that when one arrives tagName becomes a sub-property of it; until then schema:keywords is what makes "everything tagged Cloud" one query across PlantUML, Structurizr and LeanIX.

lmm:subscriber would be better than lmm:subscriberEmail — its range is arch:Stakeholder, so a subscriber would be the same kind of thing as a stakeholder named by any other notation, and lmm:Accountable is documented as the natural source for arch:conceptOwner. An export gives an email and no stable identity to mint a stakeholder from, and the ontology names the email a legitimate fallback for exactly this case.

Workspace-defined fields

The one attribute group with no published term, by design — they are defined per tenant, so there is no set to declare. The subtype field is not among them: it names a kind, so it is lifted out and resolved to a class instead. See Subtypes.

--ns-vocab puts the rest in a namespace you own:

--ns-vocab https://example.org/vocab#
<…/element/a1000000-…>  vocab:functionalSuitability "adequate" .

Without it they are dropped, and the run says how many:

[WARN] factsheets.json: 7 workspace-defined field value(s) were not emitted. These are the one attribute
group the published ontology declares no term for — they are defined per tenant, so there is no set to
publish. Pass --ns-vocab <your namespace> to emit them under a namespace you own.

Subscriber emails are personal data

lmm:subscriberEmail is emitted as the export recorded it. The workspace already shows it to everyone who can read the fact sheet, and it is the only stable handle on a subscriber an export carries — but a graph published more widely than the workspace exposes contact details more widely too. Switch subscriptions off at the pull (subscriptions: false) if that is not wanted, rather than converting and filtering afterwards.

Validation

leanix2linkedarchi validate -i out.trig

Loads leanix-shapes and core-shapes, with leanix, leanix-v3, arch-processes and core for reasoning — 23 target classes in total. Three groups of rule:

Group Shapes Checks
Naming LeanIXFactSheetLabelShape a language-tagged skos:prefLabel on every fact sheet
Attributes LeanIXFactSheetAttributeShape, LeanIXLifecyclePhaseShape, LeanIXSubscriptionShape, LeanIXTagShape status and lifecycle-stage value sets, completion datatype and 0..1 range, a phase's stage and date, a subscription's type and subscriber, a tag's name
Endpoints one per relationship, e.g. RequiringEndpointsShape that each of the sixteen relationships runs between the types it declares

Plus core-shapes#QualifiedRelationshipShape, which requires exactly one arch:source and one arch:target. The endpoint shapes deliberately do not repeat that cardinality — they add only the LeanIX types.

The endpoint shapes are what make the converter's own mapping checkable, which nothing did before. A --type-mapping entry pointing a relation at the wrong class is now caught by name:

sh:resultMessage "A lmm:Provision must run from an ITComponent."
sh:resultMessage "A lmm:Provision must run to a Provider."

Subtypes pass, because sh:class resolves the subclass closure: a lmm:BusinessApplication satisfies sh:class lmm:Application, and a lmm:Process satisfies sh:class lmm:BusinessContext.

Two things about the ontology set are easy to get wrong and both fail silently:

  • v3 is loaded whichever version the run emitted, because the naming shape targets lmm3:FactSheet too — so a --meta-model v3 graph needs no different asset set. A v3 graph matches fewer target classes, which --list-assets and the coverage line make visible: the attribute shapes target lmm:FactSheet and do not apply.
  • arch-processes is loaded because the lifecycle stage values are its individuals, so sh:in ( ap:Plan … ) has nothing to compare against without it.

A shape whose target class or value set the graph cannot resolve validates nothing and reports nothing — indistinguishable from conformance. --list-assets shows the full set.

A fully-scoped export conforms. A scope-limited one reports two violations per dangling endpoint rather than one, and that is a deliberate upstream decision worth knowing: core says an endpoint is missing, and the endpoint shape names the type that failed to arrive, which is what tells you which type to add to the pull. Both are kept at the same severity so the more useful message is not the easier one to ignore. --without-shape attributes turns the four attribute shapes off together, for a graph converted before that vocabulary was published.

Usage

# The whole pipeline: pull, then convert
leanix-pull pull --subdomain acme \
  --type BusinessCapability,Application,Interface \
  -o models/leanix/factsheets.json --write-index

leanix2linkedarchi convert models/leanix/factsheets.json \
  --diagrams-index models/leanix/factsheet-index.yaml \
  --diagrams-root models/leanix/ \
  --base-iri https://example.org/la/ \
  --ns-vocab https://example.org/vocab# \
  --format TRIG -o out.trig
# Without an index: the model id comes from --model-id, or the filename
leanix2linkedarchi convert factsheets.json \
  --model-id leanix-acme \
  --base-iri https://example.org/la/ \
  --format TRIG -o out.trig

Options

Option Default Description
--base-iri required Base IRI for minting resource IRIs
--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
-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
--meta-model v4 Which published meta model the workspace is shaped by: v4 | v3
--model-id filename Model identifier. One export is one model, so one id
--require-id false Fail instead of deriving a model id from the filename
--diagrams-index none Fact sheet index: model identity, lifecycle state, extension data
--diagrams-root index's directory Root for resolving file: paths in the index
--exclude-states none Leave index entries in these lifecycle states out of the run
--type-mapping none Repoint fact sheet types and relations, for a renamed or custom metamodel
--ns-vocab none Namespace for attributes the published ontology has no term for
--factsheet-url-base from the export Base URL for each fact sheet's schema:url. Pass an empty value to emit none
--diagrams-export none LeanIX diagram export. The only route to an arch:View — see Views
--diagram-index none Per-diagram YAML declaring each diagram's viewpoint: and status:
--ns-viewpoints lmmvp: Namespace for a viewpoint written as a bare name in the diagram index
--emit-extension-data false Map the index's elements: / links: / data: into the graph
--ns-global-id none Base IRI for link targets written without a prefix
--emit-direct-rel-triples false Also emit {src} lmm:requires {tgt}, bridged with rdf:reifies
--emit-stakeholders false Mint arch:Stakeholder per subscriber and arch:conceptOwner from the accountable one
--emit-skos-labels true Emit skos:prefLabel and skos:altLabel (negatable)
--label-language en BCP-47 tag for the labels
--emit-skos-notation true Emit skos:notation (negatable)

Plus --git-provenance, --image-ref and --run-timestamp, shared with every other converter (ADR 0008).

mapping-report

The other half of leanix-pull introspect. That says what a workspace declares; this says what the converter will do with it — without writing a graph, so it can run before a first conversion and in CI as a gate.

leanix2linkedarchi mapping-report models/leanix/factsheets.json
Fact sheet types (5):
  ok       Application             2  lmm:Application
  ok       BusinessCapability      2  lmm:BusinessCapability
  ok       Platform                1  lmm:Platform
Fact sheet subtypes (2):
  ok       Application/businessApplication  1  lmm:BusinessApplication
  mapped   ITComponent/software             1  onto#SystemSoftware
Relations (11):
  ok       relApplicationToITComponent      1  lmm:Requiring
  ok       relPlatformToApplication         1  lmm:PlatformMembership
  ok       relToChild                       1  lmm:FactSheetHierarchy
16 published, 1 mapped by --type-mapping, 0 falling back.
Everything in this export reaches a class. Nothing to do.

Subtypes are keyed Type/subtype, which is both the unambiguous name and the mapping key to use. An export with none says so, because a workspace with no subtypes and a pull that never asked for them look identical from here and the second is more likely:

No fact sheet in this export carries a subtype. If the workspace has any, the pull did not ask: a subtype
rides on a field (conventionally 'category'), so it needs 'subType: category' on the type in the pull
configuration. 'leanix-pull introspect' writes that wherever the schema has the field.

A workspace with its own types, subtypes and relations gets told what it costs and how to fix it:

  FALLBACK ProductFamily                    1  lmm:FactSheet only
  FALLBACK ITComponent/software             1  no lmm:Software — map it, or --ns-vocab mints one
  FALLBACK relProductFamilyToApplication    1  no LeanIX class

What falls back, and what it costs:
  ProductFamily: lmm:FactSheet only — map it under 'elements:' keyed 'ProductFamily'
  ITComponent/software: no lmm:Software — map it under 'elements:' keyed 'ITComponent/software' — or
    keyed 'software' to catch it under any type — or --ns-vocab
  relProductFamilyToApplication: no LeanIX class — map it under 'relationships:' keyed
    'relApplicationToProductFamily'

An unmapped subtype is the narrower loss: the fact sheet is still typed as its parent, so it is findable,
but nothing distinguishes it from the rest of that type.

--ns-vocab is deliberately not treated as resolved here, even though the converter would mint a class from it: this command takes no such option, and a report that called it ok would hide the fact that a published class was available and something pointed elsewhere.

It reads an export rather than a pull configuration, deliberately: the export is what gets converted, so the report covers the data that will actually reach the graph. --type-mapping is taken into account, so an entry that resolves a fall-back shows as mapped rather than FALLBACK.

--fail-on-unmapped exits 1 when anything falls back, for a pipeline that should not publish a graph with untyped edges. Without it the command always exits 0, which is the right default: a fall-back is emitted, not dropped — the resource is reachable, it just carries no LeanIX class.

--meta-model applies here too, and must match what the conversion will use or the report describes a run that will not happen. Under v3 relations come back as n/a rather than FALLBACK, because v3 publishes no relationships — there is no class to reach and no mapping is missing, so --fail-on-unmapped stays usable for a v3 workspace instead of always failing:

Reporting against LeanIX Meta Model v3 (lmm3:)

Fact sheet types (12):
  ok       UserGroup               2  lmm3:UserGroup
Relations (12):
  n/a      relApplicationToITComponent      1  Meta Model v3 publishes no relationships

12 published, 0 mapped by --type-mapping, 0 falling back. 12 with no class to reach.

A type the other version declares is reported as a version mismatch rather than a missing mapping, and the remedy names --meta-model instead of a mapping key.

Views, from diagrams

A LeanIX diagram is a view. An inventory export is not. That distinction decides what may be said in the graph, so it is worth stating plainly before the mechanics.

An export of fact sheets answers "what is in the workspace": a set of elements and the edges between them, with no statement about what anyone chose to show, to whom, or in what arrangement. A view minted for it would be a diagram nobody drew, its membership would be "everything that was pulled", and its arch:viewConformsToViewpoint would have to be invented. So a conversion given only an inventory emits no arch:View at all — not an empty one, and not an empty views graph either, so a consumer can tell "this workspace has no diagrams" from "the diagrams were not pulled".

LeanIX does have diagrams — Free Draw, Data Flow — and those are views. They arrive through leanix-pull pull-diagrams and reach the graph through --diagrams-export:

leanix2linkedarchi convert models/leanix/factsheets.json \
  --diagrams-export models/leanix/diagrams.json \
  --diagram-index models/leanix/diagram-index.yaml \
  --base-iri https://example.org/la/ --format TRIG -o out.trig

The two are converted together rather than separately because a view is only worth having if its nodes point at real elements, and the element IRIs are minted from the same fact sheet ids the diagrams reference.

# in the semantic graph
<…/view/d1000000-…-000000000002>
    a                          arch:View, arch:Diagram, arch:ModelConcept ;
    arch:inModel           <…/leanix/acme> ;
    skos:prefLabel             "Capability map, board edition"@en ;
    skos:notation              "d1000000-0000-4000-8000-000000000002" ;
    dct:type                   "FreeDraw" ;
    dct:created                "2025-11-11T08:30:00Z"^^xsd:dateTime ;
    dct:modified               "2026-07-20T16:45:00Z"^^xsd:dateTime ;
    schema:url                 "https://acme.leanix.net/acme/diagrams/d1000000-…"^^xsd:anyURI ;
    arch:viewConformsToViewpoint lmmvp:CapabilityMap .

# in the model graph, which holds the folder tree
<…/view/d1000000-…-000000000002>  dct:isPartOf  <…/folder/Views> .

# in the views graph
<…/view/d1000000-…-000000000002/node/b1000000-…-000000000001>
    a                   archvis:ArchNode ;
    archvis:view        <…/view/d1000000-…-000000000002> ;
    archvis:archElement <…/element/b1000000-…-000000000001> .

# in the provenance graph, from the diagram index
<…/view/d1000000-…-000000000002>  adms:status  <http://purl.org/adms/status/UnderDevelopment> .

The view IRI is minted from the LeanIX diagram id, so a diagram being retitled does not move a published address — the same property a fact sheet's id gives an element. dct:type keeps the editor's own token verbatim, as a fact sheet's type does, because no published term claims it.

The pay-off is that "which diagrams show this application" becomes one query, and a notation-agnostic one: it is the same archvis:archElement traversal that answers it for a BPMN process or an ArchiMate view.

Members, and three things deliberately not invented

A node per fact sheet on the canvas, and no archvis:Link. A line between two boxes is only meaningful as a relationship, and nothing recognisable in a LeanIX layout says which of the relationships joining two fact sheets it stands for — two can be joined by lmm:Requiring and lmm:DataUsage at once. Choosing would put an arrow in the graph that nobody drew.

No geometry. Nothing documented says where the coordinates are, and coordinates are the part of a layout that changes whenever somebody nudges a box. A Structurizr view carries none either, so a view without geometry is an established shape rather than a degraded one.

No viewpoint unless one is declared. See below.

A diagram that yielded no members is reported, because an empty canvas and a layout format the pull could not read produce identical output and only the person looking at the workspace can tell which:

[WARN] diagrams.json: 1 of 3 diagram(s) reference no fact sheet, so their views have no members. Either
those diagrams are empty, or the pull could not read their layout — 'leanix-pull pull-diagrams
--retain-state' says which.

A diagram can also show a fact sheet type the inventory pull did not include. The node is emitted against the IRI that fact sheet would have — the id is stable, so widening the pull joins the two sides without any published address moving — and it is reported, because until then the view has a member nothing describes.

The viewpoint is declared, never inferred

arch:viewConformsToViewpoint is what makes a view answerable, and the published catalogue (lmmvp:) has five: ApplicationPortfolio, TechnologyLandscape, InterfaceMap, CapabilityMap, TransformationRoadmap.

The tempting inference is from a diagram's contents, and the published documents rule it out: lmmvp:ApplicationPortfolio and lmmvp:CapabilityMap declare identical arch:includesConcept sets — lmm:Application, lmm:BusinessCapability, lmm:Organization — so no set of fact sheet types can distinguish them. Nor does the bookmark say: a Free Draw canvas is a blank canvas whatever somebody drew on it.

So it is a judgement, recorded per diagram in --diagram-index:

diagrams:
  - id: d1000000-0000-4000-8000-000000000001
    viewpoint: InterfaceMap
    status: publish
  - id: d1000000-0000-4000-8000-000000000002
    viewpoint: https://example.org/viewpoints#PaymentsFlow
    title: Capability map, board edition
    status: draft
Field Effect
viewpoint: a bare name resolved against lmmvp:, or an absolute IRI used as written
status: adms:status on the view, in the provenance graph. Omit it and none is claimed
title: wins over the workspace name for skos:prefLabel — renaming a diagram for publication should not mean renaming it upstream

A bare name is checked against the published set, and an unrecognised one is refused rather than minted:

[WARN] 'PaymentsFlow' is not a viewpoint the published LeanIX catalogue declares, so diagram 'Payments
landscape' (d1000000-…) carries no arch:viewConformsToViewpoint. The published set is
ApplicationPortfolio, TechnologyLandscape, InterfaceMap, CapabilityMap, TransformationRoadmap. For a
viewpoint of your own, write its full IRI in the index, or point --ns-viewpoints at your namespace.

Same rule as every other term here: lmmvp: is a published namespace this converter reads and does not own, so an undeclared term in it is one a reader cannot dereference. A team maintaining its own viewpoints writes the full IRI, or points --ns-viewpoints at its namespace — in which case bare names resolve there and are not checked, since the published set does not govern somebody else's catalogue.

A misspelled status: fails the run, as it does in every other index: reading an unknown value as the default is how a typo publishes work in progress. And an index entry for a diagram the export does not contain — normally one somebody deleted — is reported rather than conjured into a view.

Stakeholders and ownership

--emit-stakeholders turns LeanIX subscriptions into people:

<…/element/a1000000-…>
    arch:conceptOwner   <…/stakeholder/owner-example.org> ;
    lmm:hasSubscription [ a lmm:Subscription ;
                          lmm:subscriber       <…/stakeholder/owner-example.org> ;
                          lmm:subscriberEmail  "owner@example.org" ;
                          lmm:subscriptionType lmm:Accountable ;
                          lmm:subscriptionRole "Business Owner" ] .

<…/stakeholder/owner-example.org>
    a               arch:Stakeholder ;
    skos:prefLabel  "owner@example.org"@en ;
    skos:notation   "owner@example.org" ;
    schema:email    "owner@example.org" .

The ontology invites exactly this: lmm:subscriber ranges over arch:Stakeholder, so a subscriber becomes the same kind of thing as a stakeholder named by any other notation, and lmm:Accountable is documented as "the natural source for arch:conceptOwner". The pay-off is that "who owns this application" stops being a LeanIX question: the same query answers it for a Backstage spec.owner or an ArchiMate assignment.

Three decisions worth knowing:

  • Only the accountable subscriber owns. Responsible keeps a fact sheet's data accurate and Observer only follows it, so neither answers for the thing. Both still get lmm:subscriber.
  • One resource per person, keyed on the lower-cased email, so someone subscribed to forty fact sheets is one stakeholder and Accountable@ and accountable@ are the same person.
  • A subscription with no email gets no stakeholder, and the run says so once. An export offers no other identity for a person; the subscription still stands on its roles.

This creates resources for people, which is why it is opt-in

The only identity an export carries is an email address, so the address ends up in an IRI as well as in the lmm:subscriberEmail literal it already appears in. Hashing the IRI would hide nothing — the literal is right there — while making the resource unrecognisable, so it is left readable. What protects it is the flag: nothing about people is emitted unless you ask.

An IRI also travels further than a literal. If a consumer materialises the graph as files, a person becomes a filename. Decide that deliberately; subscriptions: false at the pull is the way to keep contact details out of the export altogether.

The fact sheet index

Same format as every other converter's — model: with views:, one view per file — so nothing new has to be learned. leanix-pull --write-index generates a starting point and never overwrites it afterwards, because the index holds the editorial decisions a pull knows nothing about.

# models/leanix/factsheet-index.yaml
prefixes:
  am: https://meta.linked.archi/archimate3/onto#
  kg: https://example.org/graph/
model:
  id: leanix-acme            # → …/leanix/leanix-acme and its named graphs
  title: "LeanIX — acme"
  architectureState: baseline
views:
  - id: factsheets
    file: factsheets.json
    status: publish
    elements:
      a1000000-0000-4000-8000-000000000001:      # by fact sheet id
        links: { am:realizes: kg:CAP-OrderManagement }
        data:  { x:costCentre: "CC-4711" }

An inventory is not a diagram, so no arch:View is emitted and there is no render command. The converter is catalog-oriented in the sense of Diagram index files: index metadata that would land on a view lands on the model instead.

Extension data can refer to a fact sheet by its id, its displayName or its name. LeanIX does not require names to be unique, so a name matching two fact sheets is refused and reported rather than attached to whichever was parsed last — use the id.

A directory of exports with an index gives one model per file, which is right: two exports are two snapshots, and merging them into one model would interleave them.

Mapping a renamed or custom type

--type-mapping keys on the names your workspace uses — including on the left of the slash in a qualified subtype key, which is the one place that rule is easy to forget (When the type itself is renamed). elements: takes fact sheet type names, and subtype names in either the qualified Type/subtype or the bare form (Subtypes); relationships:, predicates: and qualifiedPredicates: take relation field names, falling back to the canonical name the converter resolved — Requiring — where no field name matches. All three sections resolve the same way, so a mapping keyed consistently cannot retype a relation while leaving its predicates behind.

# A workspace that renamed things, mapped back onto the published vocabulary
elements:
  BusinessApp: https://meta.linked.archi/leanix/onto#Application
  Capability:  https://meta.linked.archi/leanix/onto#BusinessCapability
relationships:
  relBusinessAppToCapability: https://meta.linked.archi/leanix/onto#Supporting
predicates:
  relBusinessAppToCapability: https://meta.linked.archi/leanix/onto#supports
qualifiedPredicates:
  relBusinessAppToCapability: https://meta.linked.archi/leanix/onto#qualifiedSupports

The same mechanism retypes LeanIX output in another vocabulary entirely, so it merges with models converted from other notations — see playground/config/type-mapping-leanix.yml for a full ArchiMate mapping. A mapped class replaces the published one; lmm:FactSheet stays either way, so the naming shape still applies. With ArchiMate classes in the output, validate --shapes relationships brings ArchiMate's own source/target rules to bear on a LeanIX-derived graph.

Give every retyped relation its predicates as well as its class — Retyping a relation into another vocabulary explains why reusing the published ones would put the replaced class back by inference.

Known limitations

Nothing in the published vocabulary is missing any more: as of 2026-08-16 all sixteen relationships, the full attribute set and an endpoint shape per relationship are published, and the upstream request file is closed. What remains is either deliberate or ours:

  • Workspace-defined fields need --ns-vocab. Deliberate: they are defined per tenant, so there is no set to publish. Everything else lands on a published term.
  • A relation a workspace invented gets no LeanIX class. Also deliberate — the converter will not mint a term into a namespace it reads rather than owns — and --type-mapping is the route to giving it one.
  • A v3 run types no relationship and validates no attribute. Upstream's decision, not a gap here: the v3 manifest publishes fact sheet classes only. See Meta Model v3 and v4.
  • The meta model version is an option, not a detection. Nothing in a GraphQL export states which version a workspace runs, and the fact sheet types only suggest it — a v4 workspace that pulled no Organization looks like neither. So --meta-model is explicit, defaults to v4, and the converter names the other version when a type belongs to it rather than guessing.
  • Deletions are not detected by the converter. An export is a snapshot; a fact sheet deleted in the workspace simply stops appearing. What removes its triples from a published graph is republishing the model, so a pipeline that merges converter output into a long-lived store has to replace the model's named graphs rather than add to them.
  • A view carries members, not connectors or geometry. Diagrams are read — see Views — but only their membership is trustworthy. SAP publishes no schema for a diagram's layout, so a connector cannot be resolved to the relationship it stands for and a coordinate cannot be located at all. Both are omitted rather than approximated, and pull-diagrams --retain-state keeps the raw blob for a release that can read them.
  • A viewpoint has to be declared. Not a defect to fix: two published lmmvp: viewpoints cover identical concept sets, so no inference from a diagram's contents could tell them apart. The diagram index is where the judgement is recorded.
  • A saved diagram is the last-saved view. Upstream's own caveat. A diagram reflects the layout as it was saved, so a converted view can lag the inventory it draws from — which is also why the two exports carry their own timestamps.
  • The relation field-name table is a best effort. Field names are per workspace, so the table covers the documented metamodel; print-query and --type-mapping are how a reshaped workspace closes the gap.