Skip to content

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 — relApplicationToITComponent on an Application, relITComponentToApplication on the component — each wrapped in its own edges / node envelope;
  • subscriptions are wrapped again, completion is an object, lifecycle nests 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:

  1. a key that is, or ends in, factSheetId / factSheetIds, and
  2. 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.