modelwrite github.com/modelwrite

Open source analytics platform · self-hosted · air-gapped

Modelwrite

The open source analytics platform for getting value from a SysML investment.

Your organisation has spent years building SysML models. Modelwrite is the open source analytics platform that shows what they are worth. It reads the models you already have, measures coverage, traceability and integration, tracks them baseline by baseline, and answers questions with the evidence attached. You keep your modelling tool.

Open the showcase Browse the examples Try your own model Install it

Proof is the differentiator, not the headline. Every number on this page names the command that produced it, and the limits sit at the same weight as the claims.

  • OKF 1.0
  • AGPL-3.0-or-later engine
  • Apache-2.0 analytics schema
  • XMI, SysML v2 textual, Capella/Arcadia
  • self-hosted, air-gapped
  • SQLite or PostgreSQL
The model-health view of the cafe-stand project in the Modelwrite workbench, captured from the running product. It reports 2 issues found: one orphaned node, menu-block, and one isolated group of three nodes, cafe-stand, provisioning-bay and service-window, cut off from the main body.
What the product is. Model health on the cafe-stand model, captured from the running workbench: two issues named on one screen — an orphaned node and an isolated group of three — rather than rolled into a score.

Try it

Two doors, both live.

The showcase is open — no registration, six seeded models, and you can see what a model’s coverage, orphans and migration losses actually look like. The registered trial is your own isolated workspace for fourteen days: bring your model, get its loss report, its orphaned items and its coverage. You sign in with a code we email you.

Live

The showcase

trial.modelwrite.org

Come and see what we can do.

Open, no registration. Six seeded models: coffee-machine, sandwich-toaster, purchasing-terminal, floor-robot, microduck and cafe-stand. The cafe-stand project holds the two-robot trade study, one branch with the floor-robot on floor care and one with microduck. An hourly reset returns it to the seed, so nobody can break it.

Open the showcase
Live

The registered trial

app.modelwrite.org

Load and analyse your own model.

Your own private workspace, entered with an emailed code — no password. The trial runs in a rolling 14-day window: any authenticated activity, even a read, renews the full 14 days. After 14 days idle it turns read-only for 7 days — reads work, writes are refused with a note naming the trial's end and how to start again — and after 21 days idle it is archived to a copy while your account is kept so the same email can start fresh. The login code is transactional — no marketing consent required — and marketing updates only if you opt in.

Live and session-authenticated, with cross-trial isolation proven. The login code is emailed through Postmark: register with your address and the code lands in your inbox.

01 / See it

Read the models you already have.

Modelwrite reads the SysML you already have. XMI and Cameo import carry a loss report that names what did not survive, so a loss is a decision you make rather than a surprise at acceptance. Three readers are registered today: SysML v1 XMI reads and writes, and SysML v2 textual notation and Capella/Arcadia read as viewers — they import, they do not write back. You keep your modelling tool.

Start at the drop zone

The workbench front door is one target: Drop your SysML model here, or choose a file. The format is detected from the file itself and the project is named from it, so there is nothing to choose and no project to create first. On the drop it reports in plain language what it detected, what it carried, whether it round-trips and the five biggest classes of loss, then offers one button that names the counts it is accepting. The artifact is retained content-addressed before anything is read, so accepting re-reads the bytes already stored rather than asking for the file again; nothing is committed until that button is pressed. When the model lands, the page links straight to its health view.

On the CoffeeMachine MagicDraw container dropped against a local server, the summary reads that it detected a Cameo/MagicDraw .mdzip, carried 41 blocks, 95 relationships and 25 requirements, round-trips exactly, and found 252 things outside what the reader carries — 124 it cannot map at all and 128 it carries with a named drop — plus 22 further declarations recognised as carrying no model content. The one button reads Import 41 blocks and 95 relationships, accepting these 252 named losses.

The five biggest classes of loss, as that page lists them, each with one real example:

Those are the same numbers the XMI reader reports in the loss report below, because it is the same reader: the front door is a second entrance to one measurement, not a second measurement. Reproduce the drop against a server you are running:

curl -F "artifact=@sample/corpus/coffee-machine/legacy/CoffeeMachine-SysML-Model.mdzip" \
  http://127.0.0.1:8080/onboard

The three readers, and their directions

Three readers are registered in the workbench today. Two of them are viewers, and the workbench says so in the reader list, in the loss report and in the refusal when an export is attempted.

sysml-v1-xmi@2.4

reads and writes

SysML v1 XMI: MagicDraw, Cameo, and any XMI 2.x writer. It imports and it writes back, so it is the one reader the gate can measure a round trip on.

Reads and writes

sysml-v2-textual@1.0

viewer

SysML v2 textual notation. It reads a stated subset into OKF and cannot write it back, so a SysML v2 model is never exported from here.

Reads, does not write back

capella-arcadia@1.0

viewer

Capella/Arcadia: components, functions, functional and component exchanges, allocations and constraints. It reads the semantic model and cannot write it back.

Reads, does not write back

Capella/Arcadia, so a team can keep doing STPA where it already does it

A Capella model is a project folder, but only the .capella file holds the semantic model, and only it is read: the .aird diagram layer and the .afm viewpoint metadata are refused with a message naming them. Drop that one file.

The reason the reader is here is STPA. A team that does STPA in Capella — which is where MIT's STAMP Tools plugin lives — can keep doing it there, bring the Capella model in as a viewer, and have the platform hold the analysis as a versioned model and check its completeness. Nothing has to be converted to get there.

On the real In-Flight Entertainment System model (dbinfrago/Capella-IFE-sample, EPL-2.0, fetched outside this repository) the reader carries 335 elements, 29 requirements, 364 graph nodes and 787 graph edges, and names 3,451 content losses and 1,090 lossy id/name drops rather than dropping them in silence. The corpus is not committed because EPL-2.0 is not AGPL-compatible, so the test skips loudly when it has not been fetched. Reproduce it:

cargo test -p mw-binding-capella --test real_capella -- --nocapture

The loss report, on a real model

Put a MagicDraw SysML export through the XMI reader and it reports what it carried and what it did not. On the coffee-machine MagicDraw model the reader carries 41 structure elements (the blocks), 25 requirements, and 66 graph nodes with 95 graph edges. It reports 252 blocking losses in total: 124 unmappable content losses and 128 lossy drops, every one named.

41structure elements carried
25requirements carried
66 / 95graph nodes / edges
252blocking losses, every one named

Reproduce it:

cargo test -p mw-binding-xmi --test real_magicdraw -- --nocapture

SysML v2 textual, as a viewer

The engine reads a stated subset of SysML v2 textual notation. It imports and does not export, so a SysML v2 model cannot be written back. Reproduce it:

cargo test -p mw-binding-sysmlv2 --test real_examples -- --nocapture

02 / Measure it

Coverage, traceability, integration. The graph gaps lead.

Coverage names which requirements are satisfied, refined, verified or allocated. Traceability names the edges between them. Integration asks whether the model is one connected whole. The graph gaps expose the model nobody can see at a glance.

The three graph gaps, drawn on a small model A connected main body of four blocks. Off to one side, a dashed boundary encloses an isolated group of three blocks joined only to each other: Cafe Stand, Provisioning Bay and Service Window. Below, an orphaned node named menu-block has no edge at all. From the main body, one edge runs off and ends in an open circle, pointing at nothing: a dangling link. Numbered badges 1, 2 and 3 mark the orphan, the isolated group and the dangling link. THE MAIN BODY - ONE CONNECTED WHOLE Coffee Machine Brew Subsystem Water Subsystem Control Module A SECOND CONNECTED COMPONENT 2 Cafe Stand Provisioning Bay Service Window 1 menu-block NO EDGE AT ALL 3 DANGLING: ENDPOINT MISSING
The three gaps, on a model small enough to read. The main body is one connected whole. The isolated group is joined to itself and to nothing else — this is the shape cafe-stand actually has. The orphan has no edge at all. The dangling link runs off the model and stops at an endpoint that is not there. Every one of them is named by the platform, not folded into a score.
The model-health view for the cafe-stand platform model. It reports one isolated group of three nodes — Cafe Stand, Provisioning Bay and Service Window — cut off from the main body, with orphaned nodes, dangling links and uncovered requirements reported on the same screen.
Model health answers “what is broken in my model” on one screen. For cafe-stand it names the isolated group — Cafe Stand, Provisioning Bay and Service Window — rather than summarising the model into a score.

Each gap is named, not summarised into a score, and trended baseline by baseline so drift is visible when a baseline changes. These are the metrics in the analytics schema, listed in the analytics section.

An analysis is a record, not a page that recomputes

Running an analysis stores a record: a named definition, run over one commit, stamped with the definition version and the engine version it was computed at, with the findings it measured and an evidence hash over their canonical bytes. The same definition over the same commit at the same engine version produces the same record, so re-running an unchanged analysis over an unchanged model stores the same record rather than a second one. Records accumulate in a library you can read back and compare, months later, without the engine that produced them.

Two built-in analyses ship today, both at definition version 1, on engine 0.2.0. A definition the bounded run vocabulary cannot express is refused with a reason, never silently approximated.

The ruling the frame exists to keep: the engine measures, and an analysis never writes to the model it measures. Running one resolves a commit, reads the document and stores the record; no write path to a model is reachable from it. Where the engine cannot measure at all — a document that carries no graph — the run says so with the reason on each finding and records itself as not measured, rather than publishing a zero that would read as “no gaps”.

The analyses library for the coffee-analyses project in the Modelwrite workbench. A 'Run an analysis' form offers Requirement coverage (v1) and Model health (graph gaps) (v1) against a branch, with the button 'Run and store the record'. Below it, 'Library (4)' lists four stored runs newest first. Each names its definition and version, commit short hash, branch, engine 0.2.0, a stored timestamp, severity tallies (5 gap / 0 unknown / 0 ok; 12 gap / 0 unknown / 13 ok; 2 gap / 0 unknown / 0 ok; 10 gap / 0 unknown / 15 ok), a run id and an evidence hash. Under the library, 'Compare two runs' offers a From and a To selector and the button 'Diff the two runs'.
What a stored analysis record looks like. Every row is a record rather than a recomputation: the definition and its version, the commit it examined, the branch, the engine version it was computed at, the severity tallies, and the run and evidence hashes that address it. Four runs are stored here — two definitions over two commits — and any two of them can be compared from the control at the foot of the page.

STPA completeness: five checks over an analysis you authored

The platform holds an authored STPA analysis — losses, hazards, system constraints, controllers, control actions, feedback, unsafe control actions and loss scenarios — as a versioned model, and checks its completeness and internal consistency. Five relational checks, each a relation between STPA elements rather than a judgement about the design:

Above those five sits a method-coverage layer that asks whether each STPA stage was performed at all before judging how consistently it was performed — because a relational check over a stage that was never done returns no findings, and no findings over nothing is not a clean result. That layer, its three states, and the ruling that keeps them honest are set out in the safety argument, where the distinction is the point.

Screenshots

What the product looks like.

Two more captures from the live showcase (trial.modelwrite.org), each chosen for the claim it proves. They are screen captures of the running workbench, not mock-ups. The model-health capture is in the measure section, where it does the arguing.

The variant impact panel comparing cafe-stand main against with-microduck. The floor-care reference is marked changed, and requirement CS-4 Floor Kept Clear is satisfied by Cup Collector (floor-robot) on main and Gripper Arm (microduck) on with-microduck, each resolution named.
The variant impact panel proves the two-robot trade study. CS-4 · Floor Kept Clear is satisfied by Cup Collector (floor-robot) on main and Gripper Arm (microduck) on with-microduck — the changed floor-care choice is marked, and each cross-model edge resolves to a named element at its pinned revision.
The cafe-stand business process rendered as an orthogonally-routed diagram: axis-aligned edges, shared trunks for fan-out, a separate dashed lane for dependency edges, and jump arcs where edges cross.
The process diagram is the engineering-grade output. Edges are routed orthogonally — shared trunks for fan-out, a separate lane for Satisfy edges, and a jump arc at every crossing so a crossing is never mistaken for a junction.

03 / Ask it

Ask the model questions, with the evidence attached.

Agents and the MCP surface ask questions of the model, and every answer carries its basis: the element, the edge and the run that produced it. An agent reads and proposes. It cannot write. Only a human with the write role accepts a proposal into a commit, and the commit records both parties.

The analyses diff: two runs, compared by finding identity

A finding's id is a function of the definition, the check and the measured subject, never of the run. The same uncovered requirement therefore has the same address in two runs a month apart, which is what lets two stored runs be compared by finding identity — so what appeared, what was resolved and what was measured differently is read rather than inferred. The three lists are disjoint by construction, and the diff has its own address.

Measured, not asserted. On the coffee-analyses project, requirement coverage v1 run over commit f9591a6b and again over commit a79b8c81, where a requirement and its links were dropped, reads: 1 appeared, 1 resolved, 1 measured differently, 23 unchanged. The requirement the change adds appears as a gap. The requirement that lost its covering link is reported as measured differently rather than as newly broken, because its address did not move — it is the same subject, read with a different severity, and the evidence of both readings rides the change.

One thing the diff refuses to do is turn “could not be measured” into “clean”: the measured flag is part of the comparison, so a run that had no graph to measure and a run that measured nothing wrong are not reported as the same thing.

Ask anything, with the evidence attached

A question is answered from the model, not from prose about the model, and the answer carries its basis: the element, the edge and the run it came from. Open a stored run and each finding shows its one-line measurement and, under it, the engine's own values — the measurement itself, not a restatement of it. Where the engine could not measure, the finding says so and carries the reason, instead of a zero.

04 / Trust it

Trust it, because you can run it.

The round-trip gate compares two models and names every element and edge that did not survive. It writes a deterministic evidence record with no timestamp, and CI reproduces both committed records byte for byte on every push. The limits section is the reason the rest is believable.

Pass

Reference against itself

docs/evidence/
2026-09-17-roundtrip-self-pass.json
passed
true
referenceHash
a86cc4fa…4dcf7b
candidateHash
a86cc4fa…4dcf7b (identical)
roundtrip.equal
true
missing elements
0
missing edges
0
changed attributes
0
components
1 (99 nodes)
isolated nodes
0
coverage
15 of 25 covered (20 satisfied, 3 refined, 1 verified)

Proves the reference OKF is a fixed point of the gate: a model compared with itself passes, hashes and all. Reproduce it:

cargo run -p mw-gate -- \
  --reference sample/corpus/coffee-machine/okf/expected/coffee_machine_model.json \
  --candidate sample/corpus/coffee-machine/okf/expected/coffee_machine_model.json \
  --evidence /tmp/self-pass.json
Fail

Reference against the corrupted fixture

docs/evidence/
2026-09-17-roundtrip-corrupted-fail.json
passed
false
referenceHash
a86cc4fa…4dcf7b
candidateHash
9c9bbac6…305c2 (differs)
roundtrip.equal
false
missing elements
1
missing edges
3
components
3 (90, 8, 1)
isolated nodes
1
coverage
13 of 24 covered
the element it names
requirements:_2026x_1_12a70364_1789522470210_613186_5619
  • roundtrip: 1 missing elements, 0 extra elements, 3 missing edges, 0 extra edges, 0 changed attributes
  • integration: 1 isolated nodes
  • integration: 3 connected components

Proves the gate detects loss and fragmentation, and names what it found rather than reporting a score. Reproduce it:

cargo run -p mw-gate -- \
  --reference sample/corpus/coffee-machine/okf/expected/coffee_machine_model.json \
  --candidate sample/corpus/coffee-machine/okf/corrupted/coffee_machine_model.json \
  --evidence /tmp/corrupted-fail.json

Both runs use the coffee-machine corpus in sample/corpus/coffee-machine. Neither record contains a timestamp: dates appear in filenames only, so a re-run is either byte-identical or it is a bug. That is what makes the result citable.

The safety argument: authored or computed

The analysis is AUTHORED; the check is COMPUTED. Modelwrite does not perform STPA, does not infer hazards from a design, and will never claim to. It holds your control structure and analysis as a versioned model, checks completeness and internal consistency, names everything missing, and reports each finding's basis.

A completeness check has three states, not two. Reading them as two is the failure the third one exists to prevent: a relational check over a stage that was never performed returns no findings, and an empty list is exactly what a safety engineer would otherwise take as “no problems found”.

Not started

never a clean result

Not started

The model lacks the elements the method needs, so nothing was measured over. It names which STPA stage is missing and never shows a clean result. A model with no hazards lands here: the checks reported no findings because they had nothing to measure, which is not the same as finding nothing wrong.

Gaps found

every finding named

Gaps found

The stages were performed and at least one relational check found a gap. Every finding is named, not counted into a score, and each one carries what it was computed over and what the model did not carry.

Performed

the only clean result

Performed

The only state in which a clean result is shown — and it names what it was measured over: the losses, hazards, SystemConstraints, controllers, control actions, UCAs and loss scenarios the clean result was actually computed over.

The STPA completeness page in the Modelwrite workbench for a project called stpa-no-hazards. Above the checks, an amber band reads 'Not started — the STPA analysis has not been performed on this model: no Hazard has been identified. The checks below reported no findings because they had nothing to measure, which is not the same as finding nothing wrong.' A line above it reads 'The analysis is AUTHORED; the check is COMPUTED.' A section headed 'The analysis has not started' names Hazard identification as the stage that was not performed. The checks follow: Unanalysed control actions (0), Control loops with no feedback (0), Hazards with no constraint marked '— cannot be evaluated yet' beside constraints reaching no element (0), and UCAs with no loss scenario (0). A trend table lists one commit with all counts at 0.
What a not-started analysis looks like, and why it is not a pass. This model carries a loss, a controller, a control action, four UCAs and four loss scenarios — and no hazard. Every relational check is therefore silent, and the page still refuses to call that clean: the band is amber and reads Not started, the missing stage is named, and the one check whose subject is absent reads “cannot be evaluated yet” where a zero would otherwise sit. Captured from the running workbench.

The MIL-STD-882 mapping, with the verdicts it actually reaches

A defence programme that must produce MIL-STD-882E artefacts asks a fair question: does using this vocabulary mean abandoning the standard it is accountable to. The project's published mapping answers with a verdict on every row and nothing mapped silently. exact where the target says the same thing; lossy where it says something close and the difference is named in the row; unmappable where there is no counterpart, so the concept stays in its own vocabulary.

Across the two tables, deduplicating the three mirrored pairs, that is 14 concepts mapped: 1 exact, 5 lossy and 8 unmappable. The spread is the finding rather than a shortcoming of the mapping. STPA and MIL-STD-882 share their ends — hazards, and the requirements or constraints that prevent them — and share almost nothing in the middle. 882 carries risk language STPA does not have; STPA carries control language 882 does not.

The sharpest disagreement is probability, and the document does not try to bridge it: 882 requires a probability for every hazard, because risk is severity and probability together, and STAMP's position is that the probability of a rare high-severity accident is not estimable. A programme that must report a risk assessment code reports one; the table says the platform does not supply it, rather than inventing a number. Nothing is converted in either direction, and the platform takes no side between the two methods. The mapping, and the claims that must be re-checked against a purchased copy of the standard, are in docs/stpa/mil-std-882-mapping.md.

05 / Analytics

Every metric, from the engine.

This list is generated from spec/analytics/metric_definitions.json, the authoritative schema produced by the engine. It is not maintained by hand. The schema is Apache-2.0, so anyone can build a connector.

06 / Limits

What it does not do

Each of these is a limit of the current release, and each one is checkable from the repository. This section is the reason the rest of the page is believable.

07 / FAQ

Does it replace my modelling tool?

No. Modelwrite reads the model you have and reports what it could not carry; it does not author your model for you, and the two viewer readers cannot write back at all. The SysML v1 XMI reader is the one that also writes, so the gate can measure a round trip on it — that is a measurement of the binding, not a way to edit your project. You keep your modelling tool.

08 / Install

Install and run it yourself

The engine crates, the server and the CLI are published on crates.io at 0.2.0 (cargo install mw-cli installs the CLI). To build from the repository you need Rust stable via rustup.

Build and test

cargo build --workspace
cargo test --workspace

Run the gate on the corpus

Expected output: GATE PASS.

cargo run -p mw-gate -- --reference sample/corpus/coffee-machine/okf/expected/coffee_machine_model.json --candidate sample/corpus/coffee-machine/okf/expected/coffee_machine_model.json

Deploy it

As a container, a Compose trial on one machine, or a Helm release.

docker build -f deploy/Dockerfile -t modelwrite/modelwrite:0.1.0 .
docker compose -f deploy/docker-compose.yml up --build
helm install modelwrite deploy/helm/modelwrite

The service runs in open mode unless you configure authentication.

Open mode means an anonymous admin. The image refuses to start that way unless MW_ALLOW_OPEN=yes is set, so for anything shared, set MW_AUTH_TOKEN or MW_AUTH_JWKS instead. The Helm chart does not do TLS, backups or high availability; see deploy/README.md for what it leaves to you.

09 / Architecture

Architecture

The engine crates are the contract. The server, the CLI and every connector build on them, and nothing bypasses the OKF crate and the gate. Three readers are registered against the binding contract: one reads and writes, two are viewers.

Clients

  • Web workbench: server-rendered HTML and inline SVG
  • mw command line and the CI runner
  • MCP clients and AI agents
  • REST integrations

mw-serverAGPL-3.0-or-later

  • HTTP API and the workbench UI
  • Commits and branches · three-way merge · locks as leases · audit log
  • Identity and roles · gate · migration · agent proposals and acceptance
  • Governance · analytics
  • Storage: SQLite for one user, PostgreSQL for a team

Engine cratesAGPL-3.0-or-later

  • okf · graph (including the STPA completeness checks) · gate · binding · binding-xmi (the SysML v1 XMI reader and writer) · binding-sysmlv2 (the SysML v2 textual viewer) · binding-capella (the Capella/Arcadia viewer)
  • agent (Reasoner and proposals) · capi (C ABI) · mcp (MCP server) · analytics

Interchange

  • OKF 1.0 format and schema — AGPL-3.0-or-later (repo default; the spec carries no standalone licence header)
  • Analytics schema (spec/analytics/) — Apache-2.0

Deployment

  • Docker image · Docker Compose trial · Helm chart · air-gapped bundle
How the round-trip gate produces an evidence record A reference OKF document and a candidate OKF document both feed the mw-gate, which writes one evidence record. Re-running the gate on the same two inputs produces the same record byte for byte. reference OKF expected corpus model candidate OKF the model you brought mw-gate no timestamp evidence record byte-identical on re-run

10 / Licence

Licence

The implementation — the engine crates, the server, the CLI and the binding crates (the importers and exporters) — is AGPL-3.0-or-later today. The analytics schema (spec/analytics/) is Apache-2.0. Project policy, stated in CONTRIBUTING.md, designates importers and exporters as Apache-2.0; that permissive interchange licence is not yet applied to the crates, which still carry the workspace AGPL licence. What you install is the thing you can read.