The Workflow Is the API
In The Agent Wanted a File I argued that an ontology tool does not need a chat panel, it needs a file form an agent can hold, and I closed with two promises: a textual notation to replace the verbose export, and vibe modeling extended from the schema to the data it describes. Both are built. What I did not anticipate is that the interesting design decision would be neither of them. It was how to let the agent reach the running application.
The notation
The notation is a small textual language for the metamodel. Files carry a .nai
extension, and nai pull now writes them instead of the export document:
class truck "Truck" extends vehicle with prioritized
> A kind of [[class:vehicle]] for heavy transport.
classify area = operations
property status "Status": enum(active "Active", retired "Retired") required
property route-length "Route length": number = path(-> depot | distance)
relation operates "operates" from operator to vehicle
property since "Since": date required
Line-oriented, indentation for structure, English only with translations in a sidecar file so a
German label is its own reviewable diff. The second promise is kept in the same notation: instances
have their own statement form, an ontology can lock its instance layer to the files as a separate
opt-in, and a seed verb bulk-loads data from a throwaway file for the cases that are
not worth a workspace. It is a syntax skin over the document the JSON, YAML and Turtle adapters
already produce, not a general RDF authoring surface: anything that cannot round-trip through that
document does not belong in the language.
Three decisions carry more weight than the grammar. There are no UUIDs in the files, ever; keys
are the only identity an author sees, and the mapping to durable ids lives in a committed sidecar
the CLI maintains. The formatter is canonical, so a pull and a reformat produce byte-identical
output for the same model, which is the only reason a git diff means anything. And renames are
declared, never inferred: you edit the key and add a was: marker, because a bare key
edit cannot find its own identity entry, and a tool that guesses here quietly deletes an entity and
creates its twin.
The temptation
The application now speaks MCP at a single endpoint, POST /api/mcp, authenticated
with a scoped token. The obvious way to build that server is to wrap the metamodel:
create_class, add_property, link_instance, one tool per
operation, thirty or so tools, an agent that can do anything a user can do. Every MCP tutorial
nudges you there, because it is the shape an API already has.
I did not build that, and the reason is the one from the earlier post. The value of ontology-as-code is not that the agent can write to the model. It is that the agent's trial and error happens locally, against a validator, where mistakes are free, and only a state that survived the validator and was made explicit by a plan ever reaches the database. A CRUD tool surface throws that away on the first call. Each tool call is a write, unreviewed, uncomposable, with no diff a human could read and nothing to approve except the agent's own account of what it did.
So the tools are the workflow verbs, not the model. export_model to pull.
plan to see the semantic diff. apply to commit it.
import_merge and seed as a deliberately blunt bulk on-ramp that creates
and updates but never deletes. Editing and validation stay off the wire entirely: the agent runs
nai check on files in its own sandbox, as often as it likes, and the server never
hears about the failed attempts.
The token's scope decides which of those tools the agent is even shown. A read token lists and
exports. Write adds the merge lane. Only operator sees plan and apply. A
token never grants what the person behind it could not do anyway, and an ontology you are not a
member of answers "not found" rather than "forbidden".
The command line got the same treatment rather than a parallel one. Every verb takes a
--remote flag and runs against the deployed instance over HTTPS with nothing but a URL
and a token, no local database and no credentials on disk, and it reaches the same server-side
operations the MCP tools do. One contract, two clients. The permission checks, the payload caps and
the exact wording of a refusal are identical whether a human or an agent is driving.
What the gate is actually for
The reason plan cannot be skipped is not tidiness. It is the class of change that a chat panel and a CRUD tool are both structurally unable to handle: a model edit that invalidates data already in the database.
Delete an enum value that live instances are using, and plan does not just report a diff. It runs the query, names the affected instances, counts them, and marks the change unresolved. Apply then refuses until a correction is declared in a changeset file: map the old value to a new one, or clear it. Making a property required behaves the same way, as does narrowing a multi-valued property to single. Apply also refuses on a stale plan fingerprint, so the thing you reviewed is the thing that runs, and it records the git SHA it applied from.
That is what the verbs buy. Not safety in the abstract, but a specific, boring guarantee: no model change reaches the data without someone having seen what it costs.
The price
There is one, and it is larger than it looks. An ontology locked to its files goes read-only in the web application: the model editing forms refuse every mutation, and git is the source of truth. The tool gave up being the editor for the thing it was built to edit.
What it became instead is more interesting than what it gave up. There is now a Text view that
renders the live model as .nai source, which for a managed ontology is read-only with
a note telling you to edit the repository files and run apply.
And there is a drift page: which commit the database is at, and the semantic difference between the files shipped with the running deployment and the model actually in the database.
The application stopped being the place you change the model and became the place you read it, draw it, argue about it in threads, and find out that the repository and the database have quietly diverged.
That trade is the whole argument. A model surface an agent can reach through thirty small writes is a model surface nobody reviews. A model surface it can only reach through pull, check, plan and apply is one where every change arrives as a diff, a plan and an approval. The application did not get an assistant. It got a lifecycle, and the agent has to walk through it like everyone else.