Your agent doesn't know what a Booking is. It can guess — and usually guess well — but the moment it composes a tool call, validates an action, or renders a result for a human, the guess is the bug.

An ontology gives the model a typed contract of your domain: what exists, what relates to what, what's allowed. It also explains why bolting one onto a normal app goes badly, and how to make it stick this time.

The trouble starts when you wire an ontology into a regular codebase. You end up maintaining two contracts: one for the agent (RDF triples, JSON-LD contexts, SHACL shapes) and one for the UI (props, components, validation). Drift is inevitable. The agent produces a Reservation, the API exposes a Booking, the UI expects a ReservationViewModel. Six months in, half your team is writing adapters between layers that were supposed to agree. The schema isn't the problem; having two schemas is.

This is the thesis: the schema that grounds your agent should be the same file that types your UI. When that's true, the ontology stops being a research artifact and starts being a product surface.

What an ontology actually buys your agent

A language model treats every prompt as a fresh string of tokens. It has no first-class notion of types, identity, or relations — only statistical resemblance. An ontology hands it something it can lean on: a vocabulary with explicit semantics, a graph of typed relations, and a set of constraints.

Concretely:

{
  "@context": {
    "@vocab": "https://schema.otf-kit.dev/booking#",
    "schema": "https://schema.org/",
    "Customer": "schema:Person",
    "startsAt": { "@id": "schema:startDate", "@type": "schema:DateTime" },
    "endsAt":   { "@id": "schema:endDate",   "@type": "schema:DateTime" }
  },
  "@type": "Booking",
  "@id": "booking:42",
  "name": "Sauna slot — Friday",
  "Customer": { "@id": "person:7", "name": "M. Vargas" },
  "startsAt": "2026-06-05T18:00:00Z",
  "endsAt":   "2026-06-05T19:00:00Z"
}

Enter fullscreen mode Exit fullscreen mode

Two things matter here. First, the @context declares the schema the agent must respect — that's the contract. Second, every field is a URI, which means a SPARQL endpoint, a SHACL validator, and your component layer can all consume the same shape without translation. The agent isn't inventing structure; it's filling in a typed form.

The win isn't "AI understands your data." The win is that the agent's mistakes become recoverable. A malformed startsAt fails SHACL. A missing Customer fails the schema. A hallucinated field is rejected before it reaches a user.

Where it falls apart in a normal app

The pitch lands. Engineering starts. Three things break first:

  1. Triple stores are slow at the read path. SPARQL endpoints and named-graph stores are great for federation and inference. They're miserable when a mobile client needs to render a list of 200 bookings in under 100ms.
  2. The schema drifts. The agent's Reservation becomes the API's Booking becomes the UI's ReservationViewModel. Each layer invents its own type aliases because nothing forces convergence.
  3. Mobile clients can't carry a graph. A 2 GB RDF dump is not a phone payload. You need a flat, denormalized projection that still respects the schema.

These aren't exotic problems — they're the same integration problems every typed system hits when it meets an untyped neighbour. The fix is the fix it's always been: pin the contract at the seam and make every consumer read from it.

The typed contract pattern: one schema, two consumers

Stop storing the schema in two places. Put it in one file and have both the agent and the UI read it. The agent reads it as a JSON-LD context for grounding; the UI reads it as the type system for components.

// schema.otf.ts — the file every layer imports
export const BookingSchema = {
  "@context": "https://schema.otf-kit.dev/booking",
  "@type": "Booking",
  fields: {
    id:        { type: "id",         required: true, uri: "schema:identifier" },
    name:      { type: "string",     required: true },
    customer:  { type: "ref:Person", required: true, uri: "schema:customer" },
    startsAt:  { type: "datetime",   required: true, uri: "schema:startDate" },
    endsAt:    { type: "datetime",   required: true, uri: "schema:endDate" },
    status:    { type: "enum",       values: ["pending","confirmed","cancelled"] },
  },
} as const;

Enter fullscreen mode Exit fullscreen mode

The agent gets the @context and field map as system instructions. The component layer gets the same map as TypeScript types. The schema file is the source of truth; the build emits two artifacts from it — one for the model, one for the runtime.

[[DIAGRAM: schema.otf.ts → JSON-LD context for the agent, TS types for the UI, SHACL shape for validation — all three consumers reading one file]]

A SPARQL endpoint can still back the data; the UI never speaks SPARQL. The schema file becomes the projection specification — a typed view of the graph, not the graph itself.

SHACL as the immune system

If the schema is the contract, SHACL shapes are the contract's immune system. They reject malformed graphs before they enter your pipeline. The point is to run validation at the boundary the agent crosses, not deep inside the app.

ex:BookingShape a sh:NodeShape ;
  sh:targetClass ex:Booking ;
  sh:property [
    sh:path schema:startDate ;
    sh:datatype xsd:dateTime ;
    sh:minCount 1 ;
  ] ;
  sh:property [
    sh:path ex:status ;
    sh:in ( "pending" "confirmed" "cancelled" ) ;
  ] .

Enter fullscreen mode Exit fullscreen mode

The agent produces JSON-LD; a SHACL validator accepts or rejects it. The component layer trusts what the validator returns. Mobile clients carry only validated payloads — which is why a 200-row list stays small and predictable, around 200–400 tokens per record, and renders fast.

This is where the win compounds: validation, type-checking, and rendering are all reading the same schema. There's no second source of truth to drift.

Cross-platform rendering: same entity, every surface

Once the UI binds to typed entities instead of freeform props, the cross-platform problem collapses. A Booking is a Booking on web, iOS, and Android — same field names, same validation, same status enums. The component layer reads the schema and renders the entity; the surface adapts.

<BookingCard
  booking={entity}                  // type: Booking (from schema.otf.ts)
  onCancel={() => mutate(status)}   // status enum from schema
/>

Enter fullscreen mode Exit fullscreen mode

There's no <BookingCardWeb> and <BookingCardNative> with different field expectations. When the schema gains a cancellationReason field, the type system flags every consumer — including the agent's tool definitions — and the component renders it without a separate PR to each platform. The same component name + props + look render on every surface from one codebase.

[[COMPARE: freeform props invented per platform vs schema-bound entities locked to one file]]

Where the durable layer lives

Here's the part that doesn't change when the model does. The schema file outlives any particular LLM. The component layer outlives any particular agent framework. The validation rules outlive any particular tool runner.

This is the seam: the agent is a consumer of the schema, the UI is a consumer of the schema, and the schema is the durable surface. Tooling churns — today's model, tomorrow's MCP server, next quarter's orchestrator — but the contract is the contract.

When the AI config lives next to the schema — the same place the component layer reads its types from — agents extend the kit instead of regenerating it. A documented CLAUDE.md, a .cursorrules, and a fixed set of tested ai/prompts/ give the agent the same grounding the schema gives the runtime. The agent and the app end up reasoning over the same world.

[[CONCEPT: the schema file as the single source of truth — one file, two consumers, zero drift]]

That's the part that doesn't churn. You swap the model, the schema stays. You swap the agent framework, the schema stays. The components render whatever the schema says is renderable, on whatever surface you ship to.

What this enables

Three concrete payoffs when the schema is the contract:

  • Agent mistakes fail loud, not silent. SHACL rejects a malformed startsAt before it hits a user. The UI never has to defend against bad data because bad data never arrives.
  • Mobile stays fast. A flat, validated projection of a booking is ~200–400 tokens. Carrying a graph is the alternative, and it doesn't fit on a phone — and a 100ms list render beats a federated SPARQL query every time.
  • Refactors touch one file. Adding cancellationReason updates the schema, the agent's tool definitions, the SHACL shape, and the component types in one move. The diff is local; the blast radius is one file.

The semantic web isn't a research pivot — it's a typed contract pattern that was waiting for agents. The hard part isn't the ontology. The hard part is making the schema the source of truth for every layer that touches it. That's the part most teams skip, and it's the part that costs them six months later.

Pin the contract. Let everything else churn.