Vendoring a model from another repository
Imports let one model reference an item another one defines — but only across files that are already in your repository. Vendoring is how a model from somewhere else gets there: you fetch a copy, commit it, and modelith records where it came from.
modelith deps import https://github.com/acme/billing/blob/main/docs/payments.modelith.yaml docs/
That URL is the address of the file as it appears in your browser on github.com — open the model on GitHub and copy the address bar.
What you get
A copy of the file, byte for byte, with a provenance header added at the top:
# yaml-language-server: $schema=https://modelith.sh/schema/domain-model/v1.json
# modelith-vendored: DO NOT EDIT — this file is a copy. Change it at its origin.
# modelith-fetch: git
# modelith-origin: https://github.com/acme/billing
# modelith-path: docs/payments.modelith.yaml
# modelith-ref: main
# modelith-commit: 4f2c1e9c8b3ad0e5f71b2c9a6d4e8f30ab5c7d21
# modelith-imported: 2026-07-27
# modelith-digest: sha256:9a1f…
The header is a comment, not part of the schema — a model you write yourself never has one, and never meets any of this.
| Key | What it records |
|---|---|
vendored | That this file is a copy. Nothing enforces it; it is there so a person or an agent about to edit the file stops. |
fetch | How to get it again. git today. |
origin, path, ref | Where it came from and what to track. A tag in ref pins the copy; a branch follows it. |
commit | The commit that last touched this file at that ref — so it does not move when unrelated commits land. |
imported | When you fetched it. |
digest | SHA-256 of the file with the header lines removed, so stamping the header does not change it. |
Then add it to your model
deps import writes the file and stops. It does not edit your model's
imports: — it prints the line to add:
imports:
- ./docs/payments.modelith.yaml
The printed path is relative to the directory you ran the command in, because
that is the only thing deps import knows. An import path is relative to the
model that declares it, so if that model does not sit beside your working
directory, adjust it — exactly like any other import. Until you add that line,
the copy is an inert file that nothing reads.
That second step is deliberate. A vendored model is content someone else wrote
that will be rendered into your published Markdown, so deps import warns you
and leaves the decision — and the diff — visible.
A vendored model's prose ends up in your rendered .md. modelith escapes HTML
in prose fields, but the file is still somebody else's text landing in your
docs. Vendoring is designed for projects that already trust each other.
How a vendored file is treated differently
Once a file carries a provenance header, modelith knows it is not your work. Two things change, and nothing else:
- Completeness findings are suppressed. Missing invariants, entities no scenario exercises, unused enums and glossary terms are gaps in a document its own authors control. Without this, the GitHub Action — which lints every matched file — would fail your build over someone else's model.
- Its own
imports:raise nothing. A vendored model's imports name paths in its repository, which do not exist in yours. Those are skipped, along with the references that resolve through them.
Structural and semantic checks still run. A vendored file that is not a valid domain model breaks your build, and that is your problem to solve — by fetching a different ref, or by talking to whoever owns it.
modelith render --check skips a vendored file that has no committed .md:
its rendered Markdown belongs to its home repository, so you are not asked to
commit one. Rendering a vendored model by naming it still works, which is how a
deep link into it gets something to point at — and once you commit that .md,
--check treats it like any other and tells you when refreshing the copy has
left it stale.
The one thing --check will not do is fail over a vendored model this modelith
cannot render at all — one written against a newer schema version, say. It says
it skipped it and moves on; modelith lint is where that is reported, once.
Being skipped is an exemption, and it takes a clean provenance header to
claim one. A file whose header has a defect in it — a misplaced line, a missing
key — is checked like any other model. That way a mistyped comment can never
quietly switch a gate off: the header defect fails lint, and the rendered
output is still checked.
What it will not overwrite
The filename comes from the origin, so a copy can land on a file you already
have. deps import refuses two cases rather than clobbering them:
- A model you wrote. No provenance header means the file is yours, and no re-fetch could bring it back. Import into a different directory, or move the file aside first.
- A copy of a different model with the same basename. Two
payments.modelith.yamlfiles from two repositories cannot share a directory; give them separate ones.
A copy from the same repository at a different path is refused too, because modelith cannot tell a model that moved upstream from a second model whose file happens to share a name. The message offers both remedies: delete the copy and import again if it moved, or import into a different directory if they are two different models.
Re-importing over an existing copy of the same model at the same path is the
ordinary refresh, and that goes through — it reports replaced rather than
wrote. Only the origin's casing is ignored in that comparison, because
GitHub treats an owner and repository name case-insensitively.
Keeping the copy honest
Every modelith lint re-checks a vendored file against the digest in its own
header. If someone edits the copy, lint says so:
error [semantic] (root): this vendored file no longer matches the digest its
provenance header records (recorded sha256:9a1f…, computed sha256:2c7b…) — it
has been edited since it was imported. Refresh it with `modelith deps import
https://github.com/acme/billing/blob/main/docs/payments.modelith.yaml`, or
delete the provenance header if the change is a deliberate fork, which makes
this repository the file's home.
Both remedies are real. Re-running deps import replaces the copy with what
upstream has now. Deleting the header makes the file an ordinary model of
yours — an honest description of having forked it, and it re-enables the
completeness checks, because now it is your document.
This is drift detection, not a security boundary: anyone editing the file can recompute the header. It catches the well-meaning typo fix, which is the thing that actually happens.
Vendoring is one file, not a dependency tree
If the model you fetch imports models of its own, those are not fetched.
deps import tells you they exist:
Note: payments.modelith.yaml declares an import of its own (./ledger.modelith.yaml).
modelith vendors one file, not a dependency tree, and resolution is not
transitive — if you need items from those models, import them directly.
This matches how resolution already
works: payments.Thing reaches only items
defined directly in the file bound to payments. If the item you want lives
one hop further away, vendor that model too and give it its own scope. The
linter says so at the reference site when it can tell:
attribute type "payments.Carrier" names no enum "Carrier" in
"./payments.modelith.yaml" — that model imports a model of its own, and
resolution is not transitive: if "Carrier" is defined in one of them, add that
model to this model's `imports:` too and reference it with its own scope.
Fetching a tree would mean a directory layout, rewritten paths, and an answer
for what happens when two models want different versions of the same third
model — a package manager, for a problem an explicit second deps import
already solves.
Requirements and limits
ghmust be installed and authenticated. modelith implements no network transport of its own; it delegates to the GitHub CLI, which already solves authentication for private and internal repositories.- GitHub only, for now. A URL on another host is an error that asks you to open an issue. That is not a brush-off: the header records how it was fetched, so adding another transport is straightforward — what is missing is a real user to build it for, and an issue is how you become one.
lintandrendernever touch the network, whatever you pass them (ADR-0011). Everything undermodelith depsis opt-in, and nothing else fetches.