Nink AI Engineering © Nink AI Engineering GmbH

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 model graph of the Fleet example ontology: Vehicle with its subclasses Lorry, Van and Car, and five labelled relations to Station, Vehicle Category, Maintenance Event, Rental Agreement and Buyback Contract
The object under discussion: the Fleet example ontology, drawn by the application from the same model the files describe.

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.

The Text view of the Fleet ontology: model.nai in a syntax-highlighted read-only editor, with a Sections list on the left and the notice "Model is managed as code"
The web application showing the model in the language, and declining to let you edit it: "Model is managed as code — edit the repository files and run nai 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 Fleet ontology overview page: a banner reading "Model and instances managed as code, view drift", kind cards counting classes, relations, properties, nodes and edges, and a status strip with the applied commit, the applied timestamp and a Text Drift link
The status strip carries the provenance apply recorded: the commit the database is at, when it landed, and a link to the drift page.

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.