Database Access Patterns for AI Agents
Giving a model a SQL tool is the easiest pattern to build and the hardest to defend. The four patterns in production use are raw SQL, a read-only replica, a parameterised query catalogue, and a policy-enforced ORM layer. They differ in what they can express, what they leak on a prompt-injection, and whether an auditor can reconstruct what the agent saw. This post compares them on those three axes and is explicit about which one is over-engineered for an internal prototype.
The first version of every agent that touches data is a tool called run_sql that takes a string. It works immediately, demos well, and has a threat model nobody has written down. The question is not whether to let an agent read from a database — that is usually the whole point — but which of four access patterns you are choosing, and what each one costs when the input is adversarial.
The threat model, stated plainly
An agent with database access has three distinct exposure surfaces, and conflating them is why the conversation usually goes nowhere.
The first is the model’s own error. It writes a join that scans a billion rows, or an UPDATE without a WHERE, because it misread the schema. This is not malice, it is ordinary wrongness, and it is the most frequent incident by a wide margin.
The second is untrusted content in the context window. A support ticket, a scraped page, a PDF, a row in the very table being queried — anything the agent reads can contain instructions. Prompt injection is not a theoretical attack against a data-connected agent; it is the expected case once the agent reads anything a stranger wrote.
The third is over-collection. The agent legitimately queries a table it is allowed to query, and the result set contains a column of personal data that then lands in a log, a vector store, or a model provider’s request body. Nothing was breached. The data still left the boundary.
A pattern that defends the first two but not the third is common, and is usually the one that causes a disclosure problem.
The four patterns
| Pattern | Expressive power | Prompt-injection blast radius | Audit quality | Build cost |
|---|---|---|---|---|
| Raw SQL tool | Total | Whatever the credential can reach | Query text only | Hours |
| Read-only replica | Total, reads only | Full read of the replica | Query text only | Days |
| Query catalogue | Only what you pre-wrote | Bounded by the catalogue | Named operation plus arguments | Days to weeks |
| Policy-enforced ORM | Model-level, filtered | Bounded by policy, per field | Operation, fields, rows, policy decision | Weeks |
Raw SQL. The agent emits a query string and you execute it. Everything is expressible, which is exactly the problem: the credential’s grants are the only boundary, and a model that has been talked into SELECT * FROM users has a valid query. It is the right choice for a local analysis tool against a scratch copy, and the wrong choice for anything holding customer records.
Read-only replica. Point the credential at a replica with no write path. This removes an entire incident class — the destructive accident — for a day of infrastructure work, and it is the highest-value single change most teams can make. It does nothing for reads. An injected instruction can still exfiltrate every row the replica holds, one page at a time, through a channel that looks like normal tool use.
Query catalogue. Instead of a SQL string, the agent calls named operations you wrote by hand: orders_for_customer(customer_id, since). The model chooses which to call and with what arguments; it cannot compose new access. The security properties are excellent because the attack surface is a list you can read in an afternoon. The cost is that the agent can only answer questions you anticipated, and the catalogue grows a new entry every time someone wants a slightly different cut.
Policy-enforced ORM. The agent works against your existing ORM models, and a runtime sits between the model and the database evaluating every operation against a policy: which fields are visible, which rows are in scope for this tenant, how much a single query is allowed to cost, and what gets written to the audit log. This is the pattern ormai implements, wrapping SQLAlchemy, Tortoise and Django on the Python side and Prisma, Drizzle and TypeORM on the TypeScript side. The comparison against hand-rolled SQLAlchemy access is written up at ormai vs SQLAlchemy.
Field-level redaction is the part people skip
Row-level scoping — this tenant sees this tenant’s rows — is well understood and usually implemented. Field-level control is not, and it is where the over-collection problem lives.
Consider a support agent that needs to confirm a customer’s plan tier. The natural query returns the customer row. That row also contains an email address, a billing address, and possibly a payment token. None of it was needed. All of it is now in the context window, which means it is in the provider request, in your trace store, and in whatever the agent writes next.
Redaction at the field level fixes this by making the projection a policy decision rather than a query-authoring decision. The agent asks for a customer; the runtime returns the fields the policy allows for this caller and this purpose, with the rest absent rather than masked. Absent matters: a masked value still tells the model the field exists and invites it to ask again.
Budgets stop the expensive accident
The other control that pays for itself immediately is a per-query cost budget — a cap on rows examined, rows returned, or execution time, enforced before the query runs rather than after the database is already busy.
This is not primarily a security control. It is the thing that stops an agent in a retry loop from taking down the reporting replica at 02:00 because it kept rephrasing a query that needed an index nobody added. Treat it as an availability control and it will be easier to get approved.
The audit trail is the real deliverable
Six months after the agent ships, someone will ask what it saw. The answer needs to be reconstructable, and a log of SQL strings is a poor substitute for one.
A usable trail records, per operation: who the agent was acting for, which named operation ran, which fields the policy permitted, how many rows came back, and which rule allowed or denied the access. That is enough to answer both “did the agent have access to this record” and “why was it allowed.” A pile of query text answers neither without a reconstruction exercise.
There is a structural point here too. If the log is append-only, the record of what happened survives the process that produced it — the same property that makes an event log the right substrate for agent coordination, as described in the agentic infrastructure stack.
When each pattern is the right call
Use raw SQL when the data is synthetic or already public and the agent runs on a developer’s laptop. The controls above are overhead you do not need.
Use a read-only replica as the default floor for anything internal. It is cheap, it removes the worst outcome, and it does not constrain what the agent can answer.
Use a query catalogue when the set of questions is genuinely small and stable — a status bot, a pricing lookup, a compliance check. The rigidity is a feature in that shape.
Use a policy-enforced layer when the agent touches customer data, when more than one tenant shares the database, or when someone will eventually audit it. Below that bar it is more machinery than the problem justifies, and saying so is more useful than pretending every prototype needs a policy engine.
Limitations
None of this addresses what happens after the data reaches the model. Once a value is in the context window it can appear in a response, a tool call, or a trace. Access control constrains what enters; it cannot constrain what the model does with it. Output filtering and human approval on consequential actions are separate controls, and a governed workflow like closegate sits at that second boundary rather than this one.
Nor does a policy layer remove the need for database grants. Defence in depth means the credential should still be scoped to the tables the agent needs, so a bug in the policy runtime is not the only thing standing between the model and the rest of the schema.
Frequently asked questions
Is a vector store safer than a database? No. It is a different store with the same exposure: whatever was indexed can be retrieved, and an injected instruction can steer retrieval. The controls transfer.
Can I just tell the model not to query sensitive tables? An instruction in the system prompt is a request, not a boundary. It competes with every other instruction in the context, including hostile ones.
Does this slow the agent down? Policy evaluation is microseconds against a query that takes milliseconds. The latency cost is not where the objection usually is; the build cost is.
The bottom line
Pick the pattern by what the data is, not by what the demo needs. A read-only replica plus field-level redaction covers most of the realistic risk for most internal agents, and the full policy layer earns its cost once customer records or multiple tenants are in play. The catalogue has implementations of the last two patterns; the first two you can build yourself this week, and for a lot of systems that is the correct answer.
Related Products
Related Articles
The Agentic Infrastructure Stack
A reference architecture for an agentic stack: workspace runtime, memory, ORM policy, orchestration and payments, built from six Neul Labs primitives.
Cost Control for Production AI Agents
Where the money goes in an agent run, how to attribute spend per task rather than per month, and which of caching, batching and budgets to build first.
Running AI Coding Agents in Parallel
brat is an open-source multi-agent harness that coordinates Claude Code, Aider and Codex on one codebase with crash-safe state, merge queues and a dashboard.