LeanIX Export Format¶
The files leanix-pull writes and leanix2linkedarchi reads. There are two, because a workspace holds two
different kinds of thing:
| File | Marker | Holds | Written by |
|---|---|---|---|
| fact sheet export | leanixExport: "v1" |
the inventory — elements and the edges between them | leanix-pull pull |
| diagram export | leanixDiagramExport: "v1" |
the diagrams, which become arch:View resources |
leanix-pull pull-diagrams |
Most of this page is about the first. The second is smaller and is described at the end.
Why there is a canonical format¶
The converter could have read a raw GraphQL response. It deliberately does not, because the raw response is shaped by the query that produced it:
- relations arrive as differently-named fields per type —
relApplicationToITComponenton an Application,relITComponentToApplicationon the component — each wrapped in its ownedges/nodeenvelope; - subscriptions are wrapped again,
completionis an object,lifecyclenests a phase list; - which fields are present at all depends on what was asked for.
A converter reading that would have to know the query, which couples the mapping to the pull configuration and makes a hand-written test fixture nearly impossible to author.
So the pull normalises once. Every fact sheet becomes one flat record with its relations in a single list that names its own type, and the converter depends on this file and nothing else — which is also what lets a fixture, or a completely different puller, feed it.
Shape¶
{
"leanixExport": "v1",
"exportedAt": "2026-08-15T09:12:00Z",
"workspace": {
"name": "acme",
"url": "https://acme.leanix.net/acme/"
},
"factSheetTypes": ["BusinessCapability", "Application", "Interface", "ITComponent"],
"factSheets": [
{
"id": "a1000000-0000-4000-8000-000000000001",
"type": "Application",
"name": "payments-gateway",
"displayName": "Payments Gateway",
"description": "Authorises and captures card payments.",
"status": "ACTIVE",
"createdAt": "2020-06-02T09:30:00Z",
"updatedAt": "2026-08-01T11:05:00Z",
"completion": 0.83,
"lifecycle": {
"current": "active",
"phases": [
{ "phase": "phaseIn", "startDate": "2020-06-01" },
{ "phase": "active", "startDate": "2021-04-01" }
]
},
"tags": [
{ "id": "t-cloud", "name": "Cloud", "group": "Hosting" },
{ "name": "PCI" }
],
"subscriptions": [
{ "email": "accountable@example.org", "type": "RESPONSIBLE", "roles": ["Application Owner"] }
],
"fields": {
"functionalSuitability": "adequate",
"technicalSuitability": "unreasonable"
},
"relations": [
{
"id": "r-app1-itc1",
"type": "relApplicationToITComponent",
"targetId": "c1000000-0000-4000-8000-000000000001",
"targetType": "ITComponent",
"activeFrom": "2021-04-01",
"activeUntil": "2027-12-31",
"description": "Runs on"
}
]
}
]
}
Envelope¶
| Key | Required | Meaning |
|---|---|---|
leanixExport |
no | Format version. Absent is accepted — a hand-written fixture should not need ceremony — but a newer version is refused rather than misread |
exportedAt |
no | When the workspace was read, ISO-8601. Emitted as prov:generatedAtTime in the provenance graph, and it is not the conversion time |
workspace.name |
no | Subdomain or host the export came from |
workspace.url |
no | Human-facing workspace base URL, the default base for each fact sheet's schema:url |
factSheetTypes |
no | The types the pull was scoped to, in pull order. Emitted as dct:type on the model, so an absent type is not read as an empty workspace |
factSheets |
yes | The records |
Fact sheet record¶
| Key | Required | Meaning |
|---|---|---|
id |
yes | LeanIX UUID. The element IRI is minted from it. A record without one is skipped and reported — nothing could identify it |
type |
yes | Fact sheet type as the workspace names it. Decides the emitted class. A record without one is skipped rather than given a guessed type |
name, displayName |
no | Labels. displayName wins for skos:prefLabel, both are retained verbatim |
description |
no | → skos:definition |
status |
no | ACTIVE, ARCHIVED, BROKEN |
createdAt, updatedAt |
no | ISO-8601 instants |
completion |
no | Ratio, 0..1 |
lifecycle |
no | current plus a list of { phase, startDate } |
tags |
no | { id?, name, group? }. name is required per tag |
subscriptions |
no | { email?, type?, roles[] }. One naming neither a person nor a role is dropped |
fields |
no | Workspace-defined fields as strings, keyed as the workspace names them |
relations |
no | Edges as this fact sheet declares them |
Relations¶
| Key | Required | Meaning |
|---|---|---|
type |
yes | The relation field name, e.g. relApplicationToITComponent |
targetId |
yes | Fact sheet at the far end. Without it the relation is skipped and reported — a relationship with one end is not a relationship |
id |
no | The LeanIX relation id, kept as skos:notation |
targetType |
no | The far end's type, used only in warnings when it is out of scope |
activeFrom, activeUntil |
no | ISO-8601 dates |
description |
no | → skos:definition on the relationship |
A relation is recorded as each fact sheet declares it, so an export scoped to both endpoint types contains the same edge twice, once from each side. That is not deduplicated here: the pull records what each fact sheet says, and which of the two is the canonical direction is a modelling decision the converter makes — see Which way a relation points.
A relation to the fact sheet's own id is skipped by the converter and reported: possible in a workspace, meaningless in a graph.
Conventions¶
Fields are strings. A LeanIX field can be a single select, a free text, a number or a date, and only
the workspace's data model says which — the JSON cannot, since an enum and a free-text field are both
strings there. One representation keeps the export one obvious shape and leaves the typing decision where
it can be configured (--ns-vocab). A multi-select is joined on ;; a structured value is left out
rather than given an invented one-line form.
Only what was asked for is present. The API adds fields unasked; sweeping them in would publish data nobody selected. The pull carries the fields its configuration names, and nothing else.
Nulls are omitted. An absent key and a null one mean the same thing — the workspace said nothing — so the writer leaves nulls out and every reader treats them alike.
The file is byte-stable. Fixed key order, pretty-printed, one trailing newline. An unchanged workspace produces an unchanged file, which is what makes the export reviewable as a diff.
Versioning¶
leanixExport is bumped only for a change a reader cannot absorb. Adding a field is not one: the
converter reads what it recognises and ignores the rest, so a new field reaches older converters
harmlessly. A converter meeting a version it does not know fails with a message naming both versions,
rather than silently dropping whatever changed.
The diagram export¶
A second file, written by leanix-pull pull-diagrams and read by leanix2linkedarchi --diagrams-export.
Format marker: leanixDiagramExport: "v1".
It is separate because a diagram is a view and an inventory is not. They answer different questions, come from different halves of the API — fact sheets over GraphQL, diagrams over the REST bookmarks collection — and change on different clocks: a diagram moves when a person drags a box, not when the inventory changes. One file would put cosmetic churn in the same diff as inventory change, and would make two independent pulls look like one artefact. It also means an installation that wants only the inventory never needs a token permitted to read bookmarks.
{
"leanixDiagramExport": "v1",
"exportedAt": "2026-08-16T09:12:00Z",
"workspace": { "name": "acme", "url": "https://acme.leanix.net/" },
"bookmarkType": "VISUALIZER",
"diagrams": [
{
"id": "d1000000-0000-4000-8000-000000000001",
"name": "Payments landscape",
"description": "How the payment applications hang together.",
"bookmarkType": "VISUALIZER",
"diagramKind": "FreeDraw",
"createdAt": "2026-01-02T10:00:00Z",
"updatedAt": "2026-08-01T12:00:00Z",
"url": "https://acme.leanix.net/diagrams/d1000000-0000-4000-8000-000000000001",
"ownerEmail": "owner@example.org",
"predefined": false,
"factSheetIds": [
"a1000000-0000-4000-8000-000000000001",
"c1000000-0000-4000-8000-000000000001"
],
"stateCoverage": {
"factSheetIdsFound": 2,
"recognisedKeys": ["factSheetId"],
"unrecognisedTopLevelKeys": ["viewport"]
}
}
]
}
| Field | Source |
|---|---|
id |
the bookmark's own id. The view IRI is minted from it, so a rename does not move a published address |
name, description, createdAt, updatedAt |
documented bookmark envelope fields |
bookmarkType |
verbatim. VISUALIZER is what LeanIX calls a diagram |
diagramKind |
which editor drew it, when the bookmark says |
url |
composed as {workspace}diagrams/{id} — the bookmark carries none, and a view a reader can open is what makes the graph checkable against its source |
ownerEmail |
the bookmark's user, when it names one |
predefined |
true when LeanIX ships the bookmark rather than a person having made it |
factSheetIds |
discovered, not parsed — see below |
stateCoverage |
how that discovery went |
state |
the raw layout blob, present only under --retain-state |
factSheetIds is a search result, not a parse¶
This is the one place in either export where the pull is not simply relaying what upstream said, and it is
worth knowing why. SAP documents the bookmark envelope, and then says only that state holds "the
diagram layout and filters" and that its content varies by bookmark type. There is no published schema
for the diagram case, and three editors write it, so there is probably not one shape even within it.
A parser written against a guessed schema would fail in the worst available way: silently, and indistinguishably from a diagram that happens to be empty. So the pull walks the whole blob and recognises a fact sheet reference only when two independent signals agree:
- a key that is, or ends in,
factSheetId/factSheetIds, and - a value shaped like a LeanIX id — a UUID.
Requiring both keeps it honest in both directions. The key alone would accept a placeholder or a name; the shape alone would sweep up every other UUID in a layout, and there are many — node ids, edge ids, style ids, the bookmark's own id. A node id promoted to a fact sheet id would mint a view node pointing at an element that does not exist, which is worse than finding nothing.
stateCoverage exists so the failure is never silent. unrecognisedTopLevelKeys names the keys that
contributed nothing — not a fault in itself, since a viewport or a type filter should contribute
nothing, but it is what lets somebody with a real workspace say which key a layout is hiding under. The
pull also warns per diagram when nothing was found, and the converter warns again with the count.
What the diagram export deliberately does not carry¶
No connectors. A line between two boxes is only meaningful as a relationship, and nothing
recognisable in a 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. So a view gets members and no archvis:Link.
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.
Both are recorded as absent rather than approximated, and --retain-state keeps the blob for a release
that can read them.
state is off by default. It is a layout, so keeping it would put every nudge of a box in the diff of
a committed file. Switch it on to investigate a diagram whose contents were not recognised.