Find the gaps a review cannot see.

eventgraph holds an application's actors, commands, events, read-models and screens as a queryable graph instead of a diagram. Then check reports the gaps that are structurally impossible to leave open.

npm install -g eventgraph-cli
Get started
scaffold
Reads real source and writes back the part of the model it can prove — screens from file routing, endpoints from route registrations, aggregates from table declarations.
check
32 completeness rules in 5 lanes. Every one of them can be told it is wrong, in the model, with a reason.
slice · impact · query
Ask about one part — the flow around an event, the blast radius of a change — instead of rendering a picture nobody can read.
MCP server
The same model for an agent, with writes applied directly or returned as a diff to approve, whichever the project sets.

eventgraph checka real finding

What a finding looks like.

A takeaway app takes an order, charges the card, and shows the customer a tracking screen. The payment code is correct. The tracking screen is correct. And when a payment is refused, that fact reaches neither — so the customer waits for an order that is never coming.

Nothing in either file is wrong, which is why this survives review. It is only visible once both halves are in the same model: the event exists, and no edge leaves it.

commandpay-ordereventpayment-refused?
Every other edge in this model lands on something. This one has no read-model to project into and no policy to trigger.
$ eventgraph check
✗ orders.payment-refused
  no read-model or policy consumes this event  [event-no-consumer]
  → Add a projects-to edge to a read-model, a triggers edge to a policy, or set data.terminal to a reason.
 
1 error(s), 0 warning(s) across 21 nodes

That output is not a mock-up. It is regenerated from site/demo every time this page is built, so a rule whose wording changes cannot leave a stale claim here.

premisewhy not a board

A board is a view, not a data structure.

Event modelling is right about the shape of a flow — actor, command, event, read-model, screen — and wrong about the storage. A swimlane board holds that shape as a picture, which is why it fragments the moment a cross-cutting concern shows up: the flow you need to see spans four lanes that were laid out for a different question.

Here the graph is the storage and the board is one projection of it, rebuilt on demand around whatever you ask about. Ask for the swimlane around one event and you get exactly that, assembled from the same nodes that answer every other question.

The model is two kinds of file

A project file naming the contexts, and one model file per bounded context. Node ids are unique within a context, so a booking aggregate and a booking screen can coexist.

context: app
nodes:
  customer:     { type: actor }
  place-order:  { type: command, src: src/orders/place.ts }
  order-placed: { type: event, src: src/orders/place.ts }
edges:
  issues:   { customer: [place-order] }
  produces: { place-order: [order-placed] }

Naming src is what makes a node implemented, so there is no separate status line to keep in sync — and eventgraph verify checks those pointers still resolve, so the graph cannot quietly describe code that was renamed away.

eventgraph viewthe graph itself

One file, no server.

The frame below is the unmodified output of eventgraph view for the same model the finding above came from. Click a node to dim everything outside its neighbourhood; press Escape to clear it.

Open it on its own — it is a single self-contained HTML file, which is what makes it reviewable in a pull request.

Don't render everything

A whole-graph picture stops being readable within a few dozen nodes — the same failure that kills a growing swimlane board. So ask for a part, and every view prints how much of the graph it is showing, so a narrowed picture is never mistaken for the whole one.

eventgraph view --slice order-placed     the flow around one event
eventgraph view --focus place-order -d 2  two hops around a node
eventgraph view --type command,event      one lane at a time

eventgraph check32 rules, 5 lanes

Every rule can be told it is wrong.

A rule with no way to disagree with it becomes noise you learn to skip. So each finding names a flag that silences it — terminal, immortal, headless, detail — and most of those flags demand a reason, which lands in the model where the next reader will find it rather than in someone's memory.

The list below is generated from eventgraph rules. Every one of these was hardened against a real application rather than derived on paper.

bootstrap

3

An empty graph must not report success, or the plan-forward loop has nothing to pull on.

graph-empty
An empty graph is not a finished one. This is the entry point of the plan-forward loop: it names the first two things to write down.
no-actor
Nothing has a beneficiary until somebody is named.
no-aggregate
Without an aggregate there is no state, so commands change nothing and events belong nowhere.

structure

13

Does every node have the connections its own type requires to mean anything?

actor-idle
An actor who issues nothing and sees nothing does not belong in the model.
aggregate-no-events
An aggregate with no events has no state changes, so it is data, not an aggregate.
aggregate-no-lifecycle-end
Most aggregates need an end state — archived, cancelled, deleted. Missing one usually means the lifecycle was never thought through.
command-no-actor
Every command needs an origin: an actor, a policy, a screen — or a named non-human trigger.
command-no-effect
A command producing no event changes nothing observable.
event-no-consumer
A fact nobody reacts to is dead weight. Either something projects it, a policy acts on it, or it is explicitly terminal.
event-orphan
An event belonging to no aggregate has no owner of its consistency.
event-uncaused
Facts do not appear on their own — some command must produce them.
invariant-unenforced
An invariant no command upholds is prose, not design.
open-question-blocking
Unanswered questions gating other nodes should surface before more design is piled on top of them.
policy-incomplete
A policy is by definition event-in, command-out. Missing either half makes it inert.
read-model-unused
A projection no screen shows and no policy uses is work nobody asked for.
screen-empty
A screen that shows nothing and does nothing is a placeholder, not a design.

ux

8

Structural only — nothing about visual design, copy or layout, which are not graph problems.

actor-cannot-issue
A screen offers an actor a command they may not issue — the button is there, the permission is not.
command-buried
A command only reachable after more than 3 navigations is effectively hidden.
command-no-feedback
The user does something and never learns whether it worked. Every screen looks fine on its own, which is why reviews miss it. Followed transitively through policy chains, and satisfied by notifications as well as screens.
headless-rejection-lost
A sensor or scheduler cannot notice a refusal — it has no screen to show one on. So a command it issues that an invariant may reject needs somewhere for that rejection to go: a retry on the sender, or a decision saying the loss is accepted. Without either, the refused call is data that silently never arrives, and the first sign of it is a reader noticing the state is wrong.
no-entry-screen
Without an entry point there is nothing to measure reachability from.
screen-dead-end
A screen the user can only leave by going back. Fine for a detail view, suspicious otherwise.
screen-unreachable
No path of navigations leads here from any entry screen, so the user can never arrive.
unreadable-state
State whose store no reader may open. Not "nothing reads it yet" — nothing *can*, because visibility is refused at the store: a table without a public flag, a collection with no read rule, a private field. Both halves get built and tested and neither is wrong on its own; the writer works, the display works, and the display is empty forever. Set data.subscribable to false wherever that is known, and this finds the events written into it that were meant to be seen.

backend

6

Silent unless the model declares a backend surface, so it cannot start reporting on app models.

command-no-rejection
A command that upholds an invariant can refuse. If refusal produces no event, the caller is told nothing and the refusal cannot be observed, alerted on, or counted.
endpoint-anonymous
An endpoint that names no caller has no stated authorisation. Some endpoints genuinely are public, but that has to be a decision rather than an omission — the two look identical in code and differ only in intent.
failure-silenced
A rejection marked terminal is a swallowed error. `terminal` says "nothing reacts, deliberately", which is a defensible call for a success and almost never one for a failure — an unobserved refusal cannot be alerted on or counted. An unconsumed failure that is *not* marked terminal is already reported by event-no-consumer, so this rule deliberately covers only what that one lets through.
policy-not-idempotent
Across a real message boundary delivery is at-least-once, so every policy will eventually run twice on the same event. A policy that is not safe to repeat is a double-charge or a duplicate row waiting for a retry to happen.
policy-spans-aggregates
One reaction writing to several aggregates cannot be atomic — each write is its own transaction. Partial completion is therefore a state the system will reach, and it needs a compensating path rather than an assumption that it will not happen.
read-model-consistency-unstated
A projection fed by events is either read-your-own-write or lagging, and callers are built against whichever the author assumed. Leaving it unsaid is how "I saved it but the list is empty" reaches production.

platform

2

Silent unless the project declares platforms — then a node built on one but not the other reports.

platform-drift
A node built on one platform but missing on another. Two codebases of the same product drift apart silently, and the evidence sits between the repositories where no single-repo tool can reach it.
platform-unknown
An implementation is claimed for a platform the project never declared.

quickstartthe loop

Start from code you already have.

scaffold reads real source and writes back the part of the model it can prove — screens from file-based routing, endpoints from route registrations, aggregates from table declarations, each node pointing at the file it came from. It never guesses at the parts only a person knows, which is what the rest of the loop is for.

npm install -g eventgraph-cli

eventgraph init --yes             scaffold a project, no prompts
eventgraph scaffold | eventgraph apply -
eventgraph check --next 3         the most pressing gaps, in order

Then the loop is check --next → fill one gap → check → repeat, until the only findings left are the ones you have deliberately answered.

Then hand it to an agent

The same model over MCP, so an agent can query, check and write it without shelling out. Whether writes apply directly or come back as a diff to approve is the project's call, via agent.write.

npm install -g eventgraph-mcp

The server has no configuration of its own — it finds the project by walking up from the directory it is started in, the same way the CLI does. So register it per project, from the repository root, and every client below launches it with the right model already loaded.

Claude Code
claude mcp add eventgraph -- eventgraph-mcp

That writes it to your own machine only. Add --scope project to write .mcp.json in the repository instead, so everyone working on it gets the same server. claude mcp list confirms it connected.

Codex CLI
[mcp_servers.eventgraph]
command = "eventgraph-mcp"

In ~/.codex/config.toml. Note the underscore — Codex spells the key differently from everyone else.

Cursor · Windsurf · Zed · Claude Desktop

The common mcpServers block — .cursor/mcp.json for one project, or the client's global config for all of them.

{
  "mcpServers": {
    "eventgraph": { "command": "eventgraph-mcp" }
  }
}
VS Code · GitHub Copilot

Same shape, different key, in .vscode/mcp.json:

{
  "servers": {
    "eventgraph": { "type": "stdio", "command": "eventgraph-mcp" }
  }
}
Anything else

Skip the global install and let the client fetch it: command npx, args ["-y", "eventgraph-mcp"]. It speaks stdio and needs no environment, no token and no port.

Once it is connected, eventgraph_check is the tool to loop on — it answers "what is missing", which is the question an agent can act on, rather than "is this legal".

Prefer eventgraph_slice over reading the whole graph. A large model does not fit a context window any better than it fits a screen.