Schema Drift Reaches the Tool Definition
Context layer series Everyone who sells a context layer talks about freshness. Fresh rows, streaming…
Write context is the information an AI agent needs before it changes data: which values a field accepts, which fields are safe to write, what the change triggers downstream, whether a person has to approve it, and whether a retry is safe. Read context tells an agent what data means. Write context tells it what a write will do, and it has to be enforced at the tool, not requested in the prompt. Most context layers, ours included, only carry the first kind. This post defines the second kind and shows how to build it.
Here is the failure in one exchange. Ask an agent “what is Acme’s ARR” and a decent context layer gets you the right number. It knows that arr_usd lives on the account object, that it is in dollars, and that it comes from the billing feed. Now ask the same agent to “fix Acme’s ARR, it should be 240K.” Watch what happens.
“What is Acme’s ARR?”
The agent finds arr_usd, knows the unit and the source, returns the right number. Read context did its job.
“Fix Acme’s ARR, it should be 240K.”
The agent writes 240000. The call succeeds. Nothing told it the field is derived nightly from invoices, is read-only downstream, and feeds three dashboards and a commission run. By morning the billing job overwrites it and nobody knows an agent touched it.
That is a write context failure. The agent had everything it needed to read the field. It had nothing it needed to change it.
Every context layer on the market today, ours included, was built for the first question. This post is about the second one.
When an agent reads, context answers “what does this mean.” When an agent writes, context has to answer “what will this do.” Take the same field, arr_usd, and look at what the agent needs in each case.
Answers: what does this mean?
Schemas, glossaries and lineage give you this. Every vendor stops here.
Answers: what will this do?
Almost none of this is in a schema. Some is in app validation code. Most is in someone’s head.
The stakes are also different. A read failure gives you a wrong answer that a person can catch. A write failure changes state. It compounds. It triggers workflows. It shows up in reports a week later with no trace back to the tool call that caused it.
This is where agent pilots die. Teams get an agent answering questions in a few weeks. Then they try to let it update a ticket, change a subscription, or move a deal stage, and the project stalls for months because nobody can say what the agent is allowed to change and under what conditions.
Here is what an agent needs to know before it calls a tool that mutates data. If your context layer cannot supply these, your write tools are running on luck.
Not just the type. The actual set of values the target system accepts, the ones your business uses, and the ones that are deprecated. A status field that is a string in the schema is really an enum of six values, and one of them (“Churned”) triggers an offboarding workflow. The agent has to see the enum and the consequence, not “string.”
Which fields this system owns and which are copies. Writing to a derived field is at best a no-op and at worst a silent conflict with the upstream job. Lineage already tells you this. Nobody exposes it to the agent at write time.
The rules the app enforces in code and the rules the ops team enforces by habit. A discount over 20 percent needs a manager. A close date cannot move backwards. An account cannot be Active without a billing ID. The agent needs these as part of the tool, not as a paragraph in a system prompt it forgets by turn eight.
Agents retry. Networks flake. If a write is not idempotent, a retry creates a duplicate order or double posts a comment. The tool has to say whether repeating the call is safe, and if not, how to make it safe: an idempotency key, a precondition on the current value, a check before write.
Some writes should never happen without a person. The context has to encode which ones, and the tool has to route them through an approval step rather than trusting the model to ask. “Please confirm before making changes” in a prompt is not a control. It is a suggestion.
What reacts to this change. Which workflows fire, which downstream systems sync from this field, which reports use it. Read lineage tells you where a value came from. Write context needs the reverse: the blast radius.
The obvious move is to write all of this into the system prompt. Teams try it. It fails for three reasons.
The context has to live next to the tool, be generated from the systems that own the rules, and be enforced at the point where the write actually happens.
Here is how we handle this in Nexla, and the pattern is worth copying whether or not you use our platform.
When you build a server in MCP Studio, every tool is scoped at the connector layer, not in the model’s instructions. In the pipeline risk agent tutorial we built a read-only server by blocking every write and DDL at the connector and whitelisting the columns each tool can filter on. The model can be as confused as it likes. The write does not go through.
Write tools use the same mechanism in the other direction. Instead of blocking writes, the connector enforces the write context: allowed values, preconditions, approval routing, and audit. The tool the agent sees carries the semantics. The connector carries the enforcement. The model sits in between and does what it is good at, which is figuring out intent.
Agent
Figures out intent. Reads the tool definition on every call.
Tool (semantics)
Real enums, deprecated valuesWhich identifierDerived vs source of truthConsequence in one sentenceDry run by default
Gateway (enforcement)
Value in enumField is writablePrecondition on current state holdsApproval present if requiredCaller has rights to this row
Source system
The write lands only after every check passes.
Every step, pass or fail, goes to the audit log with agent identity, user identity, and the diff. The moving dot is the call. It stops at the gateway when a check fails.
Three things make this work.
Dry run by default. Every write tool has a mode where it validates and reports what would change without changing it. The agent uses it first, shows the user the diff, then commits. This is cheap to build and catches most mistakes.
Read-only tool sets as a first step. Ship the agent with reads only, watch what it tries to do, then add the write tools it actually needs with the write context those specific tools require. Least privilege per tool, not per server.
Policy checks before the write hits the source. All of the checks in the gateway box above run before the write, and the result goes into the audit log either way.
Here is a write tool for a support platform, first the way most teams ship it, then with write context.
{
"name": "update_customer_status",
"description": "Update the status of a customer account.",
"inputSchema": {
"type": "object",
"properties": {
"customer_id": { "type": "string" },
"status": { "type": "string" }
},
"required": ["customer_id", "status"]
}
}
{
"name": "update_customer_status",
"description": "Change the lifecycle status of a customer account. Setting status to CHURNED triggers offboarding and revokes access within 15 minutes. Setting status to ACTIVE requires an existing billing_id on the account. Use dry_run=true first and show the user the returned diff before committing.",
"inputSchema": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "Canonical customer identifier (cust_id_v2). Not the CRM account ID and not the billing account number."
},
"status": {
"type": "string",
"enum": ["TRIAL", "ACTIVE", "PAUSED", "CHURNED"],
"description": "Target status. SUSPENDED is deprecated, use PAUSED."
},
"reason_code": {
"type": "string",
"enum": ["NON_PAYMENT", "CUSTOMER_REQUEST", "FRAUD", "MIGRATION", "OTHER"],
"description": "Required when status is PAUSED or CHURNED."
},
"expected_current_status": {
"type": "string",
"description": "The status you last read. The write fails if the account changed since, so you never overwrite a concurrent update."
},
"dry_run": {
"type": "boolean",
"default": true,
"description": "When true, validates and returns the diff and downstream effects without writing."
}
},
"required": ["customer_id", "status", "expected_current_status"]
},
"annotations": {
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": true
},
"x-nexla": {
"source_of_truth": "support_platform.accounts.status",
"downstream": ["billing.subscription_state", "analytics.customer_health", "workflow.offboarding"],
"requires_approval_when": { "status": "CHURNED" },
"audit": "always"
}
}
The bare tool works. It also lets the agent set status to “actve”, set a Churned account back to Active without a billing ID, and trigger the offboarding workflow by accident. Every one of those calls returns 200.
Here is the same call through the grounded tool in three states. Click through them. This is the whole life of one write.
{
"mode": "dry_run",
"would_change": { "status": { "from": "ACTIVE", "to": "CHURNED" } },
"side_effects": [
"workflow.offboarding will start",
"billing.subscription_state will move to CANCELLED at next sync (hourly)",
"access revoked within 15 minutes"
],
"approval_required": true,
"approver_group": "cs-managers",
"blocking_errors": []
}
Nothing changed. The agent shows this diff to the user and asks one question: do you want to proceed. That question is backed by a real list of consequences, not a guess.
{
"mode": "commit",
"status": "applied",
"changed": { "status": { "from": "ACTIVE", "to": "CHURNED" } },
"approval": { "id": "apr_7f3k", "approved_by": "m.okafor", "at": "2026-08-18T09:41:12Z" },
"precondition_checked": { "expected_current_status": "ACTIVE", "actual": "ACTIVE" },
"audit_id": "aud_01J5X9",
"actor": { "user": "s.park", "agent": "support-copilot@v12", "harness": "claude-desktop" }
}
The write landed because every check passed: the value was in the enum, the field was writable, the precondition held, the approval existed, and the caller had rights to the row. The audit record names both the person and the agent.
{
"mode": "commit",
"status": "rejected",
"reason": "PRECONDITION_FAILED",
"detail": "expected_current_status=ACTIVE but account is PAUSED (changed 00:02:14 ago by billing.sync)",
"hint": "Re-read the account and present the new state to the user before retrying.",
"audit_id": "aud_01J5XA",
"actor": { "user": "s.park", "agent": "support-copilot@v12", "harness": "claude-desktop" }
}
This is the case that quietly corrupts data when the tool is bare. Here the gateway refuses, tells the agent why, and logs the refusal. The agent re-reads and asks again. Nobody has to notice a problem a week later.
The agent shows the dry run to the user. The user says yes. The approval step routes to a CS manager. Only then does the commit call run, and the audit log records the whole chain. If the row moved underneath the agent, the gateway says no and says why.
None of the semantics in that tool were hand written. They came from the schema (types), the observed data (enum values), lineage (source of truth and downstream), the platform’s own validation rules (the billing_id precondition), and the policy layer (approval). That is what a context layer for writes produces: not a knowledge graph diagram, but a tool the agent cannot easily misuse.


The grounded tool above is the output. The input is six sources, the same six that produce good read tools, read in a different direction. Hover each card to see what it contributes to a write.
Schema and samples
Types and the real set of values. Sampling 10,000 rows of the accounts table shows four live status values and one that has not appeared since last year. That is where the enum and the deprecated flag come from.
Lineage
Which field is owned here and which is a copy. The CRM’s arr_usd is a nightly copy from billing, so the write tool does not expose it. The same graph gives the downstream list: what reacts when status changes.
Application validation
The rules the app already enforces in code. “ACTIVE requires billing_id” exists in the app’s validation layer. The connector reads the error contract and turns it into a precondition the agent sees before calling.
Business docs
Policies written for humans. “Churn reversals need a CS manager” lives in a runbook. Helix ties that sentence to the status field and it becomes an approval rule on the tool.
Execution history
What past calls did. If 40 percent of calls that set PAUSED omitted reason_code and failed, the tool makes reason_code conditionally required and the description says so.
Access policy
Who is allowed to write which rows. Row and column scoping apply to writes the same way they apply to reads, and the agent identity adds a second layer: this agent is allowed to call this tool at all.
None of these is new. Every one of them already exists in your stack. The work is pulling them together at the point where the tool is generated, and keeping them attached when the source changes. That second part is the subject of the third post in this series.
You do, for humans using the UI. The agent calls the API. Most application APIs skip the UI’s approval flow because the API was built for integrations that were trusted by default. The agent is not trusted by default. Either the API grows an approval step, or the gateway adds one before the call reaches the API. The gateway is faster to ship and it works across all forty systems at once.
RBAC answers who can write. Write context answers what a correct write looks like and what it causes. A user with full rights to the accounts table can still set status to a value the workflow engine does not understand, or overwrite a row that changed two seconds ago. RBAC does not stop either. Enums, preconditions, and consequence metadata do.
Sandboxes are good for testing and useless for production writes, because the point of the write is that it lands. A dry run against the real system with real policy checks gives you the safety of a sandbox with the truth of production. That is why dry run is the default mode, not a separate environment.
You do not need to do every system at once. Here is the order that worked for us and for the teams we have watched do it.
Week 1
Ship reads only
Build the server with read tools, turn on the audit log, and let the agent run. Collect every attempted write the agent wanted to make. That list is your requirements document.
Week 2
Pick one write, ground it
Take the most common attempted write. Pull the enum from samples, the downstream list from lineage, the rule from app validation, the approval from the runbook. Generate the tool. Run it in dry run mode only.
Week 3
Turn on commit behind approval
Enable commit for that one tool with approval required on every call. Watch the rejections. Each rejection is either a bug in the grounding or a real save. Fix the first kind, celebrate the second.
Week 4
Relax approval where the data says you can
Look at four weeks of audit. Writes that were approved 100 percent of the time with no downstream incident move to auto-approve with a precondition. Writes that got rejected keep the human. Then pick the next write and repeat.
The pattern compounds. Each write tool you ground produces enum, lineage, and policy facts that the next tool reuses. By the fifth tool you are mostly clicking approve on generated definitions.
Before you expose any tool that mutates data to an agent, check these.
If you can tick all ten, your agent is ready to write. If you can only tick the first four, keep it read-only and fix the rest.
What is write context for AI agents?
Write context is the information an agent needs before it mutates data: allowed values, derived versus source-of-truth fields, business rules, idempotency and retry semantics, approval requirements, and the downstream blast radius of the change. It lives on the tool definition and is enforced at the gateway.
Can AI agents safely write to enterprise systems?
Yes, when the write tool carries real enums, preconditions, and consequence metadata, and the gateway enforces policy and approval before the write reaches the source. A bare write tool with two string parameters is not safe, because every bad call still returns 200.
What is a dry run in an agent write tool?
A dry run validates the write and returns the diff, the side effects, and any blocking errors without changing anything. The agent shows that diff to the user first, then commits. Make dry run the default mode on every write tool.
How do you stop an agent from overwriting a concurrent change?
Require a precondition parameter such as expected_current_status. The gateway rejects the write if the record changed between the agent’s read and its write, tells the agent why, and logs the refusal. The agent re-reads and asks again.
Is write context just RBAC?
No. RBAC answers who is allowed to write. Write context answers what a correct write looks like and what it causes. A fully authorized user can still set an invalid status value or trigger an offboarding workflow by accident. Enums, preconditions, and consequence metadata stop that, RBAC does not.
Do agent writes need human approval?
Some do. Declare the approval rule on the tool and enforce it at the gateway, then relax it where the audit log shows a write was approved every time with no downstream incident. A prompt that says “please confirm before making changes” is a suggestion, not a control.
Read context got solved because it was the easy half. The schemas, glossaries and lineage were already there. Write context is harder because the rules live in application code, in ops habits, and in the heads of the people who run the systems. Pulling that into the context layer and enforcing it at the tool is the work.
Write tools get the same treatment read tools already get: generated from the governed data product, scoped at the connector, logged end to end. If you are trying to move an agent from answering questions to taking actions and hitting the wall described here, get in touch. We want to see the write that scared you.
Read next in this series: The Tool Description Is the Context, Schema Drift Reaches the Tool Definition, How to Evaluate a Context Layer, and Whose Identity Is Calling? Background: Task-specific vs system-specific MCP servers, The C in MCP, and the Helix Context Layer.
Context layer series Everyone who sells a context layer talks about freshness. Fresh rows, streaming…
Context layer series MCP tool schema design is the practice of writing a tool’s name,…
Context layer series Two MCP servers sit in front of the same warehouse. You ask…