TL;DR
With Manta's agents, all reasoning is anchored in a per-tenant knowledge graph. Each entity (campaigns, creatives, KPIs, agent memories) is described with SHACL shapes, the standard way to declare structure for graph data. Those shapes already did two jobs well: they validated incoming data and they told the agents what kinds of things exist. What they did not do was propagate into the UI layer. The TypeScript responsible for rendering a campaign to a human stayed manual, effectively a second, disconnected definition.
That disconnect is what we removed. We built a generator that compiles SHACL into typed LDkit schemas, then contributed it upstream. The pipeline becomes shape, typed schema, UI component. And if the primary "reader" of the product is an LLM, then types cannot evaporate halfway through the stack. The details matter here: how shapes translate into schemas, how a hook plus router turns an entity identifier (IRI) into a component, and why every screen that displays an entity needs to compile against the same declaration the graph enforces.
1. A graph that describes its own entities
All business state in the system lives as RDF inside Manta's Reasoning Engine, an in-memory database with a Datalog reasoner. Data enters as triples, rules derive additional facts, and agents plan actions by running queries over the results.
RDF by itself does not define "Campaign." That definition comes from SHACL node shapes, machine-readable declarations that spell out what properties an entity should have, the expected datatypes, and how it links to other entities. A Meta campaign, for example, can be specified as having an integer dailyBudget, an objective constrained to a known set, and a reference to its account, another node in the graph. In web terms, the closest analog is JSON Schema, except oriented around graph structure rather than request payloads.
These shapes pay off in two ways. First, they enforce correctness at write time by validating what gets inserted. Second, they serve as a catalog for agents, describing what data exists and how it connects. That machine-readable contract is what makes it possible for an agent to operate against a tenant it has never encountered before.
2. The translation that was missing
Modern apps tend to follow a familiar arc: define schema on the backend, generate types (OpenAPI, tRPC, Prisma), ship a typed client, then hand typed data to the UI.
Our stack had most of those pieces, but not the translation between them. The graph schema was defined in SHACL, while the React UI carried its own hand-maintained TypeScript description of the entities it rendered. In practice, that left us with two parallel definitions of the same object: one enforced by the graph and another maintained in the frontend.
We already used LDkit as our RDF client, specifically because it bridges schema plus entity identifier (IRI) into typed reads: given a schema and an IRI, it generates the SPARQL, runs the query, and returns typed data. LDkit had generators for ShExC and JSON-LD contexts, but nothing that started from SHACL. There was an open request for it in their repository. We built the generator and contributed it upstream as PR #172.
3. The end-to-end chain

3.1 The generator
The generator takes SHACL shapes, walks each node shape, and emits LDkit schemas as TypeScript source. From a declaration like:
meta:CampaignShape a sh:NodeShape ;
sh:targetClass meta:Campaign ;
sh:property [ sh:path meta:dailyBudget ; sh:datatype xsd:integer ] ;
sh:property [ sh:path meta:objective ] .it produces:
export const MetaCampaignSchema = {
"@type": meta.Campaign,
dailyBudget: { "@id": meta.dailyBudget, "@type": xsd.integer, "@optional": true },
objective: { "@id": meta.objective, "@optional": true },
} as const;Operationally, it is one CLI call to regenerate everything. Schemas are split into one file per vocabulary, so a UI bundle that only touches Meta entities does not have to carry Shopify definitions as well. We also bake the namespace prefix into schema names to avoid collisions, so multiple platforms do not end up competing for the name "Campaign."
3.2 The hook
A view that renders a Meta Campaign receives a graph node containing JSON-LD plus an IRI. The view then fetches typed data through a useEntity hook.
export function MetaCampaignView({ data }: EntityViewProps) {
const iri = readEntityIri(data.jsonLd);
const { data: typed } = useEntity({ schema: MetaCampaignSchema, iri });
const currency = readVocabString(data.jsonLd, "currency");
const objective = getObjectiveDescriptor(typed?.objective?.toString());
const budget = formatBudget(typed?.dailyBudget, typed?.lifetimeBudget, currency);
const updated = formatRelativeUpdated(typed?.dateModified);
// …renders the card shell with status, objective, budget, dates
}At compile time, typed.dailyBudget is a number | undefined. The important point is not the annotation, it is the contract: pass a schema in, get typed fields out. The hook sits on top of LDkit and keeps components away from SPARQL entirely. Components request an IRI and render whatever the schema-backed query returns.
3.3 The router
To bind entity types to UI components, we keep a router, essentially a registry from graph type to view. Any surface that needs to render a node can resolve the correct component through this mapping.
Once that wiring exists, shape changes stop being a silent runtime mismatch. Rerun codegen and the TypeScript compiler will identify every view, hook call, and registry entry that no longer matches the graph declaration. The frontend is no longer free to drift from what SHACL enforces.
There is a caveat: not every relationship in the graph has a corresponding shape yet. Where the declaration is incomplete, views still fall back to loosely matched reads until the SHACL catches up.
4. Why it matters for agent-facing products
Agents act on the graph directly. Humans, on the other hand, interact through the UI. That means two different readers are interpreting the same underlying world. Approval workflows, audit trails, and any mechanism that asks a human to trust an agent all depend on one assumption: both readers are seeing the same thing.
Hand-maintained frontend types are where that assumption breaks. Rename a predicate in the graph and the UI can keep rendering the old field name. Add a constraint to a shape and the interface might never reflect it. Over time, the operator's view and the agent's reality diverge, and that divergence grows quickly in an expanding codebase.
With typed schema generation, alignment is treated as a built-in feature. When both the reasoning layer and the UI compile from the same SHACL source, agreement stops being a process people remember to follow and becomes something the system enforces structurally.
Further reading: Memory That Reasons: Why It Belongs in the Agent's Knowledge Graph · W3C SHACL Specification · W3C RDF 1.1 Concepts · LDkit — TypeScript SPARQL/RDF client · karelklima/ldkit#140 — the original feature request · karelklima/ldkit#172 — our SHACL-to-schema PR


