Design a property graph once. Generate every schema from it.
Documentation · Marketplace · Examples · Model format · Design notes
A VS Code extension and CLI for authoring Labeled Property Graph schemas as text, viewing them as ERD-like diagrams, and generating database DDL and RDF artifacts from a single model.
- Authors the model as reviewable YAML, validated by a contributed JSON Schema — so completion and hover come from the YAML tooling you already have.
- Edits it on a canvas beside the file. Every canvas action becomes a targeted text splice, applied as a workspace edit. Coordinates live in a sidecar, so moving a box produces no semantic diff.
- Models inheritance and mixins as separate tools. An abstract label hierarchy says what a thing is and carries keys and edges down to every descendant; a mixin is a bag of properties a type applies, with no supertype and no identity. Both are flattened before any generator sees the model.
- Generates nine targets from one model: LadybugDB DDL, Neo4j constraints, FalkorDB schema, Memgraph schema, SHACL shapes, and an OWL ontology — plus three standards artifacts, GQL graph types (ISO/IEC 39075), PG-Schema, and LinkML.
- Imports what you already have. A model can start from a SHACL shapes graph, an OWL ontology or LadybugDB DDL rather than from an empty file. Several files are read together, because each carries what the others cannot — and whatever could not be recovered is reported rather than guessed.
- Reports every downgrade. Anything a target cannot enforce becomes an editor diagnostic and a comment at the lossy line of the artifact. Nothing disappears quietly.
- Models lists, enums, open types and cardinality — and cardinality is genuinely enforced where it can be: LadybugDB rejects a violating write, and SHACL bounds both directions.
- Stays interoperable. The model file is self-describing, its scalar types answer to their GQL names, and the JSON Schema is 2020-12 — so a model is readable outside this tool, not only inside it.
- Runs in CI. The same validation, with no editor present, so a pull request can be gated on schema validity.
Full feature tour: https://volland.github.io/lpg-modeler/
The canvas opens beside the model file, in the manner of Markdown preview. Every box is a node type, every row a property, and every action on it a targeted edit to the YAML.
This is fleet.lpg.yaml, one of the downloadable examples
below. Asset and Vehicle are abstract — drawn with a dashed border and an abstract
badge, and emitting no table. Truck sits three levels down: assetTag arrives from
Asset, vin from Vehicle, and each inherited row names its source with ↑. Properties
that came from a mixin are marked ◇ instead, because a supertype and a bag of properties
are not the same claim about a type. STATIONED_AT is declared once on Asset and drawn
on Asset alone: repeating it on four subtypes would suggest four declarations where the
model has one.
Selecting a type shows what a diagram box has no room for. Extends is a dropdown over the
model's own types. Mixins are checkboxes, not a parent dropdown — applying one is set
membership, and a type may apply several. Under Edges, DRIVES and STATIONED_AT are
marked ↑Vehicle and ↑Asset: they are declared on an ancestor and reach this type through
it. That is the reading a modeller actually needs — what can this type relate to — and it
is a list rather than a picture.
A mixin has no box on the canvas, because it is not a type. It gets the panel instead:
its properties, and every type applying it, so a change to Timestamped shows what it
would touch before you make it. Renaming one rewrites the mixins: list of every type that
names it, as a targeted edit.
Why the distinction is enforced rather than blurred: conflating reuse with subtyping
produces a hierarchy shaped by which properties happen to travel together rather than by
what a thing is. createdAt on twenty types does not make twenty subtypes of a
Timestamped. See the metamodel notes.
Five complete models, each checked in continuous integration — a test resolves every one of them and generates all nine targets, so the file you download is the file the test checked.
| Model | Shows |
|---|---|
social.lpg.yaml |
The starter: an abstract parent contributing a key, a mixin, and an edge with an abstract endpoint |
fleet.lpg.yaml |
Inheritance and mixins: a three-level abstract hierarchy, three mixins applied at different levels, and an edge declared once on the root |
catalog.lpg.yaml |
Enums, list-valued properties, a composite key, and an open type — where the targets start disagreeing |
kinship.lpg.yaml |
Endpoint bounds: exactly two parents, which no named multiplicity can express |
booking.lpg.yaml |
Value bounds, patterns, named constraints, and the raw SHACL escape hatch |
Save one as <name>.lpg.yaml in a workspace — the extension matches on that suffix — then:
npx lpg-modeler-cli check fleet.lpg.yaml
npx lpg-modeler-cli emit fleet.lpg.yaml --target ladybug --target shacl --out ./schemaOr start from a schema you already have, reading the shapes graph and the ontology together because each carries what the other cannot:
npx lpg-modeler-cli import domain.shacl.ttl domain.owl.ttl --out domain.lpg.yamlOr read a LadybugDB database you already run. It is opened read-only, and reading it needs the engine's runtime, which the CLI does not bundle:
npx -p lpg-modeler-cli -p @ladybugdb/core@0.19.1 lpg import graph.lbdb --out domain.lpg.yamlOr read a running database, and apply a reviewed script back to it. Every database target can
now be read and deployed to: LadybugDB by path, Memgraph and Neo4j over Bolt, FalkorDB over
Redis. Which engine a bolt:// URI is comes from the instance, not the URI — Memgraph answers
Neo4j's own SHOW CONSTRAINTS with an empty list rather than an error, so guessing would
report an empty schema and no failure:
npx -p lpg-modeler-cli -p neo4j-driver@6.2.0 lpg import bolt://localhost:7687 --out domain.lpg.yaml
npx -p lpg-modeler-cli -p neo4j-driver@6.2.0 lpg apply domain.memgraph.cypher --target memgraph --uri bolt://localhost:7687
npx -p lpg-modeler-cli -p neo4j-driver@6.2.0 lpg apply domain.neo4j.cypher --target neo4j --uri bolt://localhost:7687
npx -p lpg-modeler-cli -p redis@6.2.1 lpg import redis://localhost:6379 --out domain.lpg.yaml
npx -p lpg-modeler-cli -p @ladybugdb/core@0.19.1 lpg apply domain.ladybug.cypher --target ladybug --database graph.lbdbEach connecting command loads its own driver — neo4j-driver, redis or @ladybugdb/core —
as an optional peer, so a CI run that only checks and generates downloads none of them.
Browse them with commentary: https://volland.github.io/lpg-modeler/examples.html
From the VS Code Marketplace:
code --install-extension pavlyshyn.lpg-modelerThe CLI, for continuous integration:
npx lpg-modeler-cli check model/domain.lpg.yaml
npx lpg-modeler-cli emit model/domain.lpg.yaml --target ladybug --out schemaA monorepo of three packages.
| Package | Holds |
|---|---|
packages/core |
Parsing, the intermediate representation, resolution, validation, targeted edits, every generator, and the importers |
packages/cli |
The lpg command, wrapping core for continuous integration |
packages/vscode |
The extension: webview canvas, diagnostics, commands |
core must never import vscode — enforced by an ESLint rule and by a test that scans the
source. That single rule is what keeps generator tests runnable in plain Node with no editor
harness. See the design notes.
npm install
npm run build # builds core, then cli, then the extension and its webview
npm test # vitest, across all packages
npm run lintTo run the extension from source, open the repository in VS Code and launch the Run Extension target, or package it:
cd packages/vscode
npx @vscode/vsce package --no-dependenciesThe extension bundle is self-contained — esbuild inlines @lpg/core into
out/extension.js, so --no-dependencies is correct rather than a shortcut.
Every generator has golden-file tests for output stability. The Ladybug target additionally executes its generated DDL against an in-process LadybugDB instance and asserts that the declared constraints really do reject invalid data — a golden file alone only proves that output has not changed, not that it is valid.
Architecture and design intent live in lat.md/, a cross-linked knowledge graph
maintained with lat.md. Run lat check before opening
a pull request. The published website is generated from docs/.
MIT — see LICENSE.



