When an AI Agent Writes to Your System of Record
An agent that can only read is a demo. An agent that can write, update a record, assert a relationship, trigger a downstream decision, is a liability, unless the write path is governed. Most teams try to close that gap with prompting: longer system messages, more caveats, sterner instructions. Prompts are suggestions. Production systems need mechanisms.
This two-part series builds one such mechanism end to end, against a single running case study. This part sets up the problem: what the system looks like before the agent touches it, what we are asking the agent to do, why an LLM is genuinely the right tool for the job, and the four distinct ways it fails at that job. Only then do the technologies show up, each one introduced as the answer to a failure you have already watched happen. Part 2 implements the whole thing in code.
The principle underneath it fits in one sentence:
Automate the drafting. Gate the asserting.
Let the LLM do what it is uniquely good at, reading messy input and drafting structured claims. Never let those claims become facts without passing a gate the model cannot talk its way through.
The moment that matters
Start at the end, with the person the whole system exists to serve.
A procurement manager at a mid-sized manufacturer has to release the Q4 production build. One of the sign-offs on that release is supplier clearance: every supplier feeding the build must be current on certifications, free of unresolved audit findings, and within the risk tolerance the compliance policy sets. They open the internal Supplier Risk Console and type the question they actually have:
“Is Acme Speciality Chemicals cleared for the Q4 build?”
The manager’s job here is not to be curious. It is to sign off, in writing, in a system that will be read back to them if something goes wrong eighteen months from now. That distinction drives every architectural decision in this series, so it is worth stating plainly:
Hold that scene. We will come back to it twice: once when the naive system answers it, and once when the governed system does.
The job we are trying to automate
Behind that question sits a quarterly grind.
Every quarter, suppliers send in disclosures: self-assessment questionnaires, ESG and conflict-minerals declarations, third-party audit summaries, certification renewals, sub-tier sourcing statements. For our manufacturer that is roughly 8,000 documents a quarter against six analysts. No two suppliers use the same template. Perhaps a third of the content is free prose. Several languages are in play. The documents arrive as PDFs, email bodies, and spreadsheet attachments with merged cells.
An analyst reads a disclosure, works out what actually changed, and updates the supplier’s record. The work is not glamorous, but it is genuinely cognitive: deciding that “the May labour-practices audit raised significant concerns” is material and “we continue to monitor our environmental footprint” is not, is a judgement call, not a regex.
The result is the situation every compliance function knows: tier-1 suppliers get assessed properly, tier-2 gets assessed late, and tier-3, where a surprising share of real risk lives, does not get assessed at all. Not because anyone decided it was unimportant. Because there were six analysts.
That is worth saying clearly, because the rest of this series is about constraining the model, and constraint arguments are easy to mistake for skepticism. The LLM is not the problem. The LLM is the only reason this job is automatable at all. The problem is what happens between the model’s output and the system of record.
The system before the agent touches it
Four artifacts define the starting state. A reader who holds these four in their head can follow everything that follows.
1. The graph is already there
This is not a greenfield project. The knowledge graph is the system of record for supplier risk, and it was populated long before anyone proposed an agent: supplier master data, the plant registry, materials, contracts, and certifications, loaded from the ERP and reconciled quarterly.
That matters more than it sounds. The agent is not building a graph from nothing, where any plausible output is an improvement. It is writing into a store that other systems already trust and query. The bar is not “better than empty.” The bar is “does not corrupt what is already relied upon.”
The graph uses the procurement ontology developed in Ontology Engineering That Ships, under the proc: namespace.
New to RDF? The sixty-second version
A knowledge graph built on RDF stores everything as triples: subject → predicate → object. "Acme has risk rating Medium" is one triple. Millions of them form a graph, because the object of one triple is the subject of another.
The snippets below are written in Turtle, the readable RDF syntax. Four things to know and you can read all of them:
proc:is a namespace prefix, shorthand for a long URL (herehttps://w3id.org/proc/). Every name is globally unique, which is what lets graphs from different systems merge without collisions.ameans "is of type".proc:Supplier_Acme a proc:Supplierreads as "Acme is a Supplier".;means "same subject, next predicate"..ends the statement."2026-12-31"^^xsd:dateis a typed literal: the value plus what kind of value it is.
That is genuinely all you need for this series.
A fragment of the relevant state:
@prefix proc: <https://w3id.org/proc/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
proc:Supplier_Acme a proc:Supplier ;
proc:legalName "Acme Speciality Chemicals BV" ;
proc:country proc:NL ;
proc:riskRating proc:Medium . # as of Q2 2026
proc:Plant_Munich a proc:Plant ; # a plant WE operate
proc:country proc:DE .
proc:CERT-8871 a proc:Certification ;
proc:scheme proc:ISO14001 ;
proc:heldBy proc:Supplier_Acme ;
proc:validUntil "2026-12-31"^^xsd:date .
Two details in there will matter shortly. Acme currently carries a proc:Medium risk rating. And proc:Plant means a plant we operate, not any industrial facility in the world.
2. The ontology defines what the words mean
An ontology is the machine-readable answer to “what is a Supplier, and what is it allowed to be related to?” A database schema constrains storage; an ontology defines meaning, and it travels between systems.
Our model reuses one pattern worth noticing, because it shapes the failure modes later. A supply relationship is not a direct edge from supplier to plant. It is reified as its own class, because the relationship carries attributes of its own:
proc:SR-4417 a proc:SupplyRelationship ;
proc:supplier proc:Supplier_Acme ;
proc:commodity proc:Surfactant_SC40 ;
proc:suppliesPlant proc:Plant_Munich ;
proc:tier proc:Tier1 .
Acme supplies the SC-40 surfactant to our Munich plant, at tier 1.
3. The shapes define what is legal
The ontology says what the world is like. It does not say what a valid record looks like, and it deliberately cannot: OWL operates under the open world assumption, where a missing value means “not yet known,” not “invalid.”
For operational validation you need SHACL, the Shapes Constraint Language, which is closed-world and answers a different question: does this data conform? Here is the policy that governs supplier records, and the important thing about it is that it is policy you can read, review, and version, not code buried in an application:
@prefix sh: <http://www.w3.org/ns/shacl#> .
proc:SupplierShape a sh:NodeShape ;
sh:targetClass proc:Supplier ;
sh:property [
sh:path proc:riskRating ;
sh:in ( proc:Low proc:Medium proc:High ) ; # no invented risk levels
sh:maxCount 1 ; # and exactly one of them
] .
proc:SupplyRelationshipShape a sh:NodeShape ;
sh:targetClass proc:SupplyRelationship ;
sh:property [
sh:path proc:suppliesPlant ;
sh:class proc:Plant ; # must land on a plant WE operate
sh:minCount 1 ;
] .
If you want the full discipline around writing, layering, and CI-gating shapes, that is its own subject and I have written it up separately in Continuous Semantic Validation. For this series, two shapes are enough.
4. The document that starts the trouble
Acme’s Q3 disclosure arrives. Trimmed to the parts that matter:
FROM: compliance@acme-speciality.example
SUBJECT: Acme Speciality Chemicals - Q3 2026 Supplier Disclosure (ASC-2026-Q3)
3.2 Manufacturing footprint
Production of the SC-40 surfactant line transferred to our Rotterdam
facility in July, following the capacity expansion there.
4.1 Certifications
Our ISO 14001 certificate (reg. 14001-NL-8871) lapsed on 30 June 2026.
The recertification audit is scheduled for Q4.
7.1 Audit findings
The May labour-practices audit at our Johor site raised significant
concerns regarding contractor working hours. Remediation is underway.
Three short paragraphs. A competent analyst would extract one urgent fact, one location change, and one open finding, and would know which of the three blocks a production release. Let us see what the agent does with it.
The naive path
The obvious architecture is the one most teams build first, and it is not stupid. It is one hop:
The one-hop architecture. It works in the demo because the demo document is clean and the reviewer is the person who wrote the prompt.
The model reads the disclosure and drafts five claims. Here is the actual output, and it is worth reading closely, because it is not obviously wrong. It is fluent, well-formed, correctly typed RDF, and it would sail through a JSON-schema validator:
proc:CERT-8871 proc:validUntil "2026-06-30"^^xsd:date . # claim 1
proc:Supplier_Acme proc:riskRating proc:Catastrophic ; # claim 2
proc:conflictMineralFree true . # claim 5
proc:SR-4417 proc:suppliesPlant proc:Plant_Rotterdam . # claim 4
# Claim 3 is what is NOT here: nothing retires the existing
# `proc:riskRating proc:Medium`, so Acme ends up carrying both.
Five claims. One of them is good. These five follow us through both posts, so they are worth a moment each:
proc:Catastrophic is not a risk rating. The document said "significant concerns." The model, reasonably enough in English, escalated. But the compliance policy defines exactly three ratings, and downstream systems switch on them. A fourth value is not a severe rating; it is an unhandled enum case that will surface as a blank cell in a dashboard.
3 · Contradiction
The old proc:Medium rating is still there. The model added a rating without retiring the previous one, because nothing told it a supplier may only have one. Acme now carries two contradictory risk ratings at once, and every query that reads proc:riskRating gets whichever the store happens to return first.
4 · Conflated entity
"Our Rotterdam facility" is Acme's site, not our plant. The model read a possessive pronoun in a supplier's document and resolved it against our plant registry. proc:Plant_Rotterdam does not exist. The supply relationship now points into the void.
5 · Unfounded claim
Nothing in the document mentions conflict minerals. The model inferred compliance from silence, which is the single most dangerous move an extraction system can make. Note that this assertion is structurally perfect: right subject, right predicate, right datatype.
The part that should worry you
Now re-read that list and ask a different question: how would anyone find out?
Nothing errored. No exception was raised, no log line printed red, no alert fired. The extraction “succeeded.” The document was processed. The queue moved. From the operator’s point of view, the pipeline is healthy and 8,000 documents a quarter are being handled by a system that used to need six people.
Meanwhile the dangling proc:Plant_Rotterdam node quietly changes the answer to “which suppliers feed the Munich plant?”, and no one queries it in a way that reveals the break.
Why the obvious fixes do not hold
Everyone reaches for the same three fixes first. Each one helps. None of them closes the gap, and understanding why is the whole justification for the architecture in Part 2.
"Write a better prompt"
Adding "only use risk ratings Low, Medium, or High" to the system message does reduce claim 2. It does not eliminate it, because a prompt is a probability adjustment, not a constraint. More importantly it does not scale as policy: when compliance adds a fourth rating, you now have to find and update every prompt in every agent deployment, and you have no way to prove you got them all.
"Use structured outputs"
Constrained decoding and JSON Schema genuinely solve a class of problems, and you should use them. But look at the failure list again: every single one of those four errors is schema-valid. A schema can enforce that riskRating is a string from an enum. It cannot know that proc:Plant_Rotterdam is not a plant we operate, because that is a fact about the state of the graph, not the shape of the payload.
That second point is the crux, so let me put it directly: the constraints that matter here are relational, not syntactic. Whether an assertion is legal depends on what else is in the graph. proc:suppliesPlant proc:Plant_Rotterdam is a perfectly good triple in isolation. It is invalid only because our plant registry contains no such plant. No output-format mechanism can see that, because the information is not in the output; it is in the store.
The third fix is “have a human review everything,” which does work. It also returns you to six analysts and 8,000 documents, which is where we started. Human review is a scarce resource, and the design question is not whether to use it but how to spend it only on the cases that need it.
The mechanisms, and what each one is for
Now the technologies can show up, and each one has a job you have already seen it needs to do.
The graph is the substrate, because constraints need context
We covered this implicitly, but state it: the reason a knowledge graph is the right store here is not that graphs are fashionable. It is that claim 4 can only be caught by something that knows what else exists. A graph makes the surrounding context queryable at validation time, which is what turns “is this triple well-formed?” into “is this assertion legal given everything we already know?”
SHACL is executable policy, and it catches claims 2, 3 and 4
Point the shapes from earlier at the drafted output and three of the five problems fail immediately, with machine-readable reasons:
| Drafted assertion | Shape constraint | Result | |
|---|---|---|---|
| 2 | riskRating proc:Catastrophic |
sh:in ( Low Medium High ) |
Violation · value not permitted |
| 3 | second riskRating on the node |
sh:maxCount 1 |
Violation · cardinality exceeded |
| 4 | suppliesPlant proc:Plant_Rotterdam |
sh:class proc:Plant |
Violation · not a proc:Plant |
| 1 | validUntil "2026-06-30" |
xsd:date, single-valued |
Conforms · the true fact lands |
No matter how fluent the argument for it was, a hallucinated edge to a non-existent plant cannot land. And when policy changes, you edit one shape file, versioned in git, rather than re-litigating a prompt across five agent deployments.
Provenance is what makes mistakes survivable, and it is why claim 1 is trustworthy
SHACL says an assertion is legal. It says nothing about whether it is true. Claim 1 passed validation, but so would a fabricated expiry date of 2027-01-01.
The answer is to make every committed fact carry its own receipt, modelled with PROV-O: what evidence it came from, which model version drafted it, when, at what confidence, and who if anyone reviewed it. Provenance is stored as ordinary graph content, which means it is queryable with the same SPARQL as everything else.
The payoff arrives on the bad day. A model release starts asserting nonsense on a Tuesday. With provenance, “find and revoke everything model X asserted since Tuesday” is a query you run before lunch. Without it, it is an archaeology project across your entire graph, and the honest answer to “which facts can we still trust?” is “we don’t know.”
Evidence and confidence handle claim 5, which no shape can catch
Claim 5, the unfounded conflict-minerals assertion, is the interesting one, because it is structurally valid. It will pass every shape you write. There is no constraint expressible in SHACL that distinguishes “the model read this in the document” from “the model inferred this from silence.”
So it needs a different mechanism entirely: a mandatory evidence field, a confidence score, and a router that sends the grey zone to a human.
Where you set the thresholds is not a model parameter, it is a product decision about how much curation capacity you have and what a wrong fact costs. In compliance, a false assertion is expensive; thresholds sit high and the queue earns its salary.
MCP is the boundary, because capabilities should be enumerable
The last piece is where all of this gets enforced. The Model Context Protocol standardises how an agent discovers and calls tools, and it has an under-appreciated consequence:
That makes the MCP server the natural choke point. You are no longer hoping the model behaves; you are deciding what behaviours exist. The design rule is reads generous, writes narrow:
query_graph· read-only SPARQL against the live graph. Let the agent explore freely; exploration is where agents earn their keep.propose_fact· the only write path. It accepts a typed candidate assertion plus evidence and a confidence score. It does not write. It proposes.
Because MCP separates the tool contract from the model, the same governed surface serves every agent framework you run: one gate to maintain, audit, and harden, instead of one per integration. And because the boundary is a single point, it is also where your metrics live.
The architecture
Here is the whole system. Each named component is implemented, with code, in Part 2, so this diagram doubles as that post’s table of contents.
The shape of the argument is visible in the picture: there is exactly one arrow from the agent into the graph, and it passes through two independent checks and a provenance stamp on the way.
Where this leaves the procurement manager
Return to the sign-off. In the governed system, the console answers:
Acme Speciality Chemicals · NOT CLEARED for Q4 build Blocking: ISO 14001 certification lapsed 2026-06-30. Source: ASC-2026-Q3 §4.1 · asserted 2026-07-02 by
kg-writer-agent v1.4· confidence 0.94 · shapesproc-shapes v3.2· reviewed by A. Patel
One blocking fact, sourced to a section of a named document, stamped with the model and shape versions that produced it, and reproducible: run the query tomorrow and get the same answer. The manager can click through to §4.1 and read the sentence themselves.
The three bad assertions never landed. The unfounded conflict-minerals claim is sitting in a queue with a human’s name on it. And the fact that mattered, the one a human analyst would have flagged, is the one driving the decision.
What Part 2 builds
This post argued the case. Part 2 implements it, component by component, against this same Acme document:
propose_fact takes the arguments it takes, and how the read/write asymmetry is enforced at the network layer rather than requested in a prompt.
The gate
pySHACL validating a candidate against the live graph without committing it first, which is harder than it sounds. Turning a validation report into feedback an agent can act on, and the retry loop that resolves most rejections in one pass.
The ledger & the queue
PROV-O provenance in named graphs, the revocation query for the bad Tuesday, threshold policy, and the curation queue as a training-data flywheel.
The outcomes
Both pipelines run on the Acme document, side by side, with the actual outputs and the actual console answers, so the difference is visible rather than asserted.
This is the neuro-symbolic case made concrete: neural fluency is not reliability, so you don't fix it with a better prompt, you give the model a symbolic backbone it can't argue past. Neural drafts, shapes decide, provenance remembers. The retrieval half of the same pattern is GraphRAG with temporal knowledge graphs; the construction half is the entity-linking flywheel; the ontology and shapes underneath it come from the Agile Ontology Engineering series. A minimal open wireframe of this architecture lives at SHACL-Gated Agent Writes.