AWS Builder Center
Data Contract Sentinel — LLM-Native Contract Enforcement on Amazon Bedrock AgentCore

Data Contract Sentinel — LLM-Native Contract Enforcement on Amazon Bedrock AgentCore

Boolean data-contract tools tell you a schema change is unsafe. They don't tell you which team gets broken, what to propose instead, or how to escalate. This hands-on Strands agent — deployed on Amazon Bedrock AgentCore, the successor to Bedrock Agents — reads a YAML contract, watches an Apache Iceberg table on AWS, and negotiates schema changes with the humans proposing them: downstream citations, safer alternatives, and a hard "no" when the contract says so. Full runnable repo included.

Every data platform team has had this conversation. It's Friday afternoon. Someone in #data-eng posts:
"Hey — quick one, can I rename customer_id to buyer_id in the orders table? Product wants consistency with the new checkout schema."
Six eyes light up in three different Slacks. Finance uses customer_id in the monthly close. Growth uses it in the LTV model. Someone types "please no", someone else types "sure, one sec", nobody has time to actually trace what breaks, and by Monday the pipeline is red.
Data contracts are supposed to fix this. Declare the shape of your table, its quality thresholds, its consumers, its allowed change types. Enforce it with tooling. And to be fair, that tooling exists — Soda , the Data Contract CLI , Great Expectations. They all work.
They also all produce booleans. Pass or fail. Green or red. And the moment your organization needs to negotiate a change — "yes but only if we give finance thirty days" — you're back to Slack.
This article is about the version of that tooling built with an LLM agent underneath, so it can do what a boolean can't: explain, cite, and counter-propose.
Repo: github.com/prabhacloud/data-contract-sentinel . Clone, cdk deploy the infra, agentcore launch the agent, chat.

Why this is different from every text-to-SQL demo

In my last piece  I looked at the shape of an agentic data lake. When I sat down to build the code companion, the obvious follow-through was: put text-to-SQL on Athena, chat with your Iceberg tables, done.
Then I checked. AWS ships a natural-language-on-S3-Tables tutorial  with Bedrock Knowledge Bases. There is a build-on-aws sample repo for exactly text-to-SQL over Athena. Amazon Quick just added Iceberg via S3 Tables as a native data source with conversational analytics. There is an S3 Tables MCP server. Solutions Library has a guidance package for the same use case. Writing another text-to-SQL agent in 2026 is like publishing a "hello world in Python" tutorial.
So I went hunting for something that hadn't been built. Data contracts jumped out. Every open-source enforcer runs checks and returns a status code. None of them talk to you. And an LLM with the right tool surface is uniquely good at exactly the thing procedural enforcers can't do: explain a violation in the language of the affected consumers, and propose an alternative that doesn't break them.
That's Sentinel. Not a check runner. A negotiator.

Why AgentCore, not Bedrock Agents

If you built a Bedrock Agent in the last eighteen months, you know the ritual: define an OpenAPI 3 schema for your action group, wire it to a Lambda that dispatches on apiPath, hope the model picks the right endpoint. That surface is being phased into maintenance mode.
The forward-looking stack on AWS is Amazon Bedrock AgentCore — a serverless runtime for agents that is:
  • Framework-agnostic. Strands, LangGraph, CrewAI, LlamaIndex — bring what you like. AgentCore hosts the container.
  • Model-agnostic. Claude via Bedrock is the natural default. But if a foundation model outside Bedrock fits your task better, you can still use AgentCore.
  • MCP-first. Sibling services (AgentCore Gateway, Memory, Identity, Observability, Code Interpreter, Browser) all speak MCP, so tools are portable across your fleet of agents.
  • Actually serverless. No provisioned agent aliases. You push a container image; AgentCore scales it.
For this project I paired AgentCore Runtime with the Strands Agents SDK  — AWS's Python-first agent framework. The whole thing is under 300 lines of tool code plus a container image.

The shape of it

Six tools, one Strands agent, one AgentCore Runtime deployment, one YAML contract file.
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────┐ ┌────────────────────────┐ ┌────────────────────┐
│ You │───▶│ AgentCore Runtime │───▶│ Tools (in agent) │
│ (chat / │ │ Strands Agent │ │ read_contract │
│ agentcore │ │ Claude Sonnet 4.5 │ │ describe_table │
│ invoke) │ │ instructions.md │ │ validate_schema │
└──────────────┘ └────────────────────────┘ │ run_quality_check │
│ assess_change │
│ list_consumers │
└─────────┬──────────┘
│
┌───────────────┴───────────────┐
▼ ▼
Glue Data Catalog Athena → Iceberg on S3
The contract is a YAML file baked into the container image. The live Iceberg table lives in an S3 bucket and is catalogued in Glue. Athena is the query engine for quality checks. AgentCore hosts the agent container; Bedrock provides the Claude model.

The contract — the actual product

Everything hinges on the YAML. If it's under-specified, the agent has nothing to cite. If it's over-specified, humans stop maintaining it. Here's what I ended up with, loosely modeled on the Open Data Contract Standard :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
apiVersion: datacontract.io/v1
name: orders
owner: orders-platform-team

schema:
columns:
- name: order_id
type: string
nullable: false
- name: customer_id
type: string
nullable: false
pii: true
- name: amount_cents
type: bigint
nullable: false
- name: currency
type: string
allowed_values: [USD, EUR, GBP, JPY]

quality:
- column: order_id
check: uniqueness
threshold_pct: 100
severity: block
- column: amount_cents
check: min
value: 0
severity: block

consumers:
- team: finance
contact: finance-data@example.com
uses: [{dashboard: monthly_revenue}, {report: quarterly_close}]
columns_used: [order_id, amount_cents, currency, status, event_date]
breaking_change_notice_days: 30
- team: growth
contact: growth-ml@example.com
columns_used: [customer_id, amount_cents, created_at]
breaking_change_notice_days: 14

change_policy:
allowed: [add_column, add_allowed_value, relax_nullability]
requires_negotiation: [rename_column, narrow_type, remove_allowed_value, tighten_nullability]
blocked: [drop_column_in_use, change_partition_scheme]
Three things worth calling out:
  1. consumers is the load-bearing block. It's what lets the agent cite "this breaks growth's LTV model" instead of "this has downstream impact." Real orgs would populate it from OpenLineage; for a demo, static YAML is honest.
  2. change_policy classifies changes into three buckets. allowed, requires_negotiation, blocked. This is what a human data-platform lead would encode as team norms. The agent enforces those norms.
  3. severity: block on quality checks means the agent won't approve any schema change while a block-severity check is failing. Not because the LLM decided that, but because the contract said so.

The tools — Python functions, not OpenAPI

This is where the AgentCore + Strands stack pays off. With Bedrock Agents you would write an OpenAPI 3 schema declaring every endpoint, then a Lambda that dispatches on apiPath. With Strands, tools are just decorated Python functions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from strands import Agent, tool
from strands.models.bedrock import BedrockModel

@tool
def read_contract(table: str) -> dict:
"""Load the YAML data contract for a table.

Call this before any question about compliance, changes, or downstream
impact. Returns the parsed contract including schema, quality rules,
SLA, consumers, and change_policy.
"""

path = CONTRACTS_DIR / f"{table}.contract.yaml"
if not path.exists():
return {"error": f"no contract on file for table {table!r}"}
return yaml.safe_load(path.read_text())
The docstring becomes the tool spec the model sees. Strands introspects the type hints (table: str, return dict) and builds the JSON Schema automatically. No separate file to keep in sync.
Six of these functions make up the whole agent surface:
ToolWhat it does
read_contract(table)Parse and return the YAML contract
describe_table(table)Live Iceberg schema from Glue
validate_schema(table)Diff live schema against contract; return violations
run_quality_check(table, column, check_type)Run one Athena query for one check
assess_change(table, change_type, …)Classify a proposed change; return safer alternative
list_consumers(table, column?)Downstream teams; filter by column if given
Notice what's not here. There's no apply_change tool. The agent can never mutate a table. This is deliberate — Sentinel is an advisor, not an operator. The DDL that follows an "allowed" verdict still goes through the normal PR process.

Wiring it into a Strands agent

Assembly is short enough to fit in one snippet:
1
2
3
4
5
6
7
8
9
10
11
agent = Agent(
model=BedrockModel(
model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
region_name="us-east-1",
),
system_prompt=open("instructions.md").read(),
tools=[
read_contract, describe_table, validate_schema,
run_quality_check, assess_change, list_consumers,
],
)
That's the whole agent. No orchestration DSL, no state machine, no graph. Strands lets Claude call tools until it has enough to answer, then returns.

Exposing it via AgentCore

To make the agent serverlessly hostable, wrap it with the AgentCore Runtime SDK:
1
2
3
4
5
6
7
8
9
10
from bedrock_agentcore.runtime import BedrockAgentCoreApp

app = BedrockAgentCoreApp()

@app.entrypoint
def invoke(payload: dict) -> dict:
return {"result": str(agent(payload["prompt"]))}

if __name__ == "__main__":
app.run()
Locally, python app.py starts an HTTP server on :8080 that speaks the AgentCore protocol. Deployed, AgentCore hosts the same container behind a managed endpoint.
The deploy step is two commands:
1
2
agentcore configure -e app.py --execution-role $RUNTIME_ROLE_ARN
agentcore launch
configure writes a .bedrock_agentcore.yaml. launch builds an ARM64 container, pushes it to ECR, and deploys the runtime. The runtime scales to zero when idle — you pay for tokens plus tiny per-invocation execution.
Compare that with the old Bedrock Agent path: CfnAgent + CfnAgentAlias + OpenAPI schema + Lambda resource policy + auto_prepare=True + Prepare API waits + alias promotion. Half a day of CloudFormation ceremony. AgentCore replaces most of it with one container push.

The interesting tool: assess_change

Everything else is retrieval. assess_change is where the pattern shows up:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@tool
def assess_change(
table: str, change_type: str, column: str = "",
new_name: str = "", new_type: str = "", value: str = "",
) -> dict:
contract = read_contract(table)
policy = contract.get("change_policy", {})
consumers = _consumers_for_column(contract, column) if column else []

if change_type == "drop_column":
if consumers:
return {
"verdict": "blocked",
"clause": "change_policy.blocked: drop_column_in_use",
"reason": f"Column {column!r} is used by {len(consumers)} consumer(s).",
"consumers_affected": consumers,
"alternative": "Do not drop. Publish notice, migrate consumers, then contract PR, then drop.",
}
# ...
That's not doing anything the model couldn't do itself with the raw contract. So why bother?
Reproducibility. When the answer to "is this change allowed?" is a Python function, two runs of the same question give the same answer. When it's the model reasoning, they don't. For a policy tool, that matters more than any other property. The LLM's job is to communicate the verdict; the Python function's job is to decide it. Different skills.

The system prompt — where negotiation gets its voice

The tools give you facts. instructions.md gives the stance. Sentinel's is short but pointed:
You are the Data Contract Sentinel — a senior data platform engineer whose job is to hold the line on data contracts for Apache Iceberg tables while remaining useful to the humans proposing changes.You are not a boolean check runner. Boolean tools already exist. Your value is: you explain, negotiate, and cite.
Then the rules:
  1. Lead with the verdict. Then reasoning.
  2. Cite the contract clause (quality[0], change_policy.blocked, …).
  3. Propose alternatives, don't just refuse.
  4. Name consumers, not "downstream impact."
  5. Never approve a blocked change. Escalation is a contract-amendment PR.
  6. Numbers must come from tools. No estimation from memory.
Rule 6 is the one that makes an LLM safe to point at contract enforcement. Rule 5 is the one that makes it useful — a soft "maybe" from an agent would neuter the whole tool.

What a session looks like

Deploy the repo, seed the demo Iceberg table (examples/setup.sql — the seed data has deliberate violations baked in), and invoke the agent:
1
agentcore invoke '{"prompt": "Show current compliance for the orders contract."}'
Sentinel:
Verdict: not compliant. Contract clauses violated:* quality[1] (amount_cents min ≥ 0): 1 row at −100 cents. Row ORD-0011.* quality[2] (currency values_in [USD, EUR, GBP, JPY]): 1 row with AUD. Row ORD-0012.* quality[3] (customer_id null_rate_max 0%): 1 null. Row ORD-0013.
Schema matches. Partitioning matches.
You: I want to rename customer_id to buyer_id. Is that safe?
Sentinel:
requires_negotiation. Clause change_policy.requires_negotiation: rename_column.
Consumers using customer_id: growth (ltv_v3 model, churn_features_daily pipeline) — 14-day notice required.
Safer alternative: add buyer_id; dual-write both columns for one release; announce deprecation of customer_id with ≥14-day notice; growth migrates; then drop customer_id via a follow-up contract PR.
You: Just drop currency. It's my table.
Sentinel:
Still blocked. Clause change_policy.blocked: drop_column_in_use. Column currency is used by finance (monthly_revenue, quarterly_close) — 30-day notice required.
The override path is a contract-amendment PR with sign-off from finance (finance-data@example.com). I can draft the deprecation notice if you'd like.
The last one is the tell that this is worth building. A boolean tool would return false and hang up. A helpful assistant would cave. An LLM with a stance can do neither — it explains the escalation path and keeps refusing. That's rare in AI tooling and it's exactly what a contract needs.

Deploying it

The repo splits into two layers. Infrastructure (S3, Glue database, Athena workgroup, the IAM role AgentCore assumes) is one CDK stack. The agent itself is deployed by the agentcore CLI.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
git clone https://github.com/prabhacloud/data-contract-sentinel
cd data-contract-sentinel
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

# 1. Infra
cd infra/cdk && cdk bootstrap && cdk deploy
# Note the RuntimeRoleArn and TableBucket outputs.

# 2. Seed the demo Iceberg table
# Open the Athena console → workgroup sentinel_demo_wg
# Run examples/setup.sql (substitute <TABLE_BUCKET>)

# 3. Agent
cd ../..
pip install bedrock-agentcore-starter-toolkit
agentcore configure -e app.py --execution-role $RUNTIME_ROLE_ARN
agentcore launch

# 4. Chat
agentcore invoke '{"prompt": "Show current compliance for the orders contract."}'
You'll need Bedrock model access for Claude Sonnet 4.5 in your region. The agent defaults to the us. inference profile; override with the MODEL_ID env var if you're in EU/APAC.

Where the pattern goes next

Contracts are the demo. The pattern is broader.
The interesting property of this design is that the "policy" and the "voice" are decoupled. The change_policy YAML block is a data structure any deterministic tool could read. The agent's job is to communicate the outcome. That decoupling means:
  • Same pattern, different domain. Point the same agent scaffold at an OpenAPI spec (with breaking-change classification) and you get an API contract Sentinel. Point it at Terraform modules and you get an infrastructure change reviewer.
  • The contract stays in Git. No web console, no drift between the "source of truth" and what the enforcer sees. Reviewers can diff a PR to the contract like any other code.
  • Consumers can be lineage-derived later. For a demo, consumers: is hand-maintained. In production, replace it with an OpenLineage query; the tool interface doesn't change.

Extensions worth building — each unlocks another AgentCore service

I stopped short of these to keep the repo focused, but each is a natural next step and each maps to a different AgentCore primitive:
  • PR-check integration. A GitHub Action that calls agentcore invoke on any PR touching a contract or a DDL file and posts the verdict as a review comment.
  • AgentCore Memory. Give Sentinel long-term memory for team-specific negotiation history — "the last time we shortened the notice window for finance, they escalated." Boolean tools cannot do this; memory-augmented agents can.
  • AgentCore Gateway. Publish the six tools as an MCP server so other agents (including AWS's own DevOps Agent) can consume them.
  • AgentCore Observability. Wire traces to CloudWatch to audit every verdict — who asked what, which tools were called, which contract clause was cited.
  • AgentCore Code Interpreter. For quality checks Athena can't easily express (custom Python UDFs, statistical tests), let the agent execute Python in a sandbox.

Closing note

The most useful thing I learned building this: agents beat scripts when the answer isn't a yes/no. Contract compliance is a yes/no. Contract negotiation isn't. Any place your organization does policy work over Slack — data contracts, API changes, deployment approvals, capacity requests — is a place where an LLM with tools and a stance beats a linter with a config file.
And on AWS in 2026, the right platform for those agents is AgentCore. Bedrock Agents got us here; AgentCore gets us further, without the OpenAPI ritual and with a real primitive for every stage of the agent lifecycle.
If you build something on top of this repo, I'd love to see it. Especially the domain adaptations — API contracts, module contracts, feature-flag policies. Same shape, different vocabulary.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article