Connecting Your Agent to APIs and Databases
An agent with database write access deleted production rows nobody could recover. This case study shows how to do AI agent API integration without handing over the keys.
# WRONG: raw capability
def run_sql(query: str): ... # can read, write, or destroy
# RIGHT: specific, safe intents
def get_order_count(since_date: str) -> int:
# parameterized, read-only, scoped to one question
return db.query(
"SELECT COUNT(*) FROM orders WHERE ship_date >= %s",
[since_date],
)
def get_orders_by_status(status: str) -> list:
return db.query(
"SELECT * FROM orders WHERE status = %s LIMIT 100",
[status],
)An agent connected to a production database with write access ran a query that deleted rows nobody could get back. It wasn't malfunctioning. It did exactly what its tool allowed — the tool just allowed far too much. The engineer who wired it up had given the agent a general "run SQL" capability, figuring the model was smart enough to be careful. The model was smart. That was the problem.
This is the story behind safe AI agent API integration, and its central lesson is counterintuitive: the danger isn't a dumb agent, it's a capable one wired to a powerful tool.
The Problem: AI Agent API Integration Gone Wrong
The setup seemed reasonable. A data team wanted an agent that could answer questions about their database — "how many orders shipped last week," that kind of thing. So they gave it a tool that executed arbitrary SQL and returned the results. Flexible, powerful, one tool to rule them all.
It answered questions beautifully for a while. Then, handling a poorly phrased request, it generated a SQL statement that modified data instead of reading it. The tool executed it without question, because "run arbitrary SQL" includes destructive SQL. Rows were changed, and the backup was a day old. A read task had been given write power it never needed.
The postmortem was short and uncomfortable. Nobody had decided the agent should be able to modify data — it had simply inherited that power as a side effect of a convenient tool. The failure wasn't a bad decision by a person or the model; it was an un-decided capability sitting there until probability found it. Those are the most dangerous kind, because no one is watching a risk no one chose to take.
The Wrong Approach
The mistake was exposing raw capability instead of specific intent. "Run any SQL" is maximally flexible and maximally dangerous — it hands the agent the full power of the database, including every way to destroy data, to answer questions that only ever needed reads.
The reasoning behind it is seductive: a general tool means you don't have to anticipate every question. But that generality is exactly the risk. Every capability the tool exposes is a capability the agent can invoke, and an agent invoking a destructive capability by accident is far more likely than a human doing so, because the agent generates its actions from probabilities, not intentions.
Put starkly: a human with database access might run a destructive query once in a career, and even then usually hesitates at the keystroke. An agent runs many generated statements, each sampled from a distribution, with no hesitation and no sense of consequence. Over enough calls, a low-probability destructive output stops being unlikely and becomes inevitable. Broad capability plus volume plus no intuition equals eventual disaster.
⚡ Pro tip: Never give an agent a general "execute arbitrary code" or "run any query" tool against anything that matters. The flexibility you gain is dwarfed by the blast radius you accept. Expose specific intents, not raw capability.
The Correct Pattern
The fix was to replace the one all-powerful tool with several narrow, purpose-built ones — each doing exactly one safe thing.
[object Object],
,[object Object], ,[object Object],(,[object Object],): ... ,[object Object],
,[object Object],
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object],
,[object Object], db.query(
,[object Object],,
[since_date],
)
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object], db.query(
,[object Object],,
[status],
)What this does: it exposes a handful of specific, read-only, parameterized queries instead of one arbitrary-SQL tool. The agent can answer the questions it needs to, but it structurally cannot write, delete, or run an unbounded query — those capabilities simply aren't in its hands. Safety comes from the shape of the tools, not from hoping the agent behaves.
The shift in mindset is from "what could the agent want to do" to "what does this task actually require." The old tool answered the first question — give it everything, let it choose. The new tools answer the second — give it exactly these reads, nothing more. Scoping to the task rather than to the capability is the whole move, and it's why the fix is a design change, not a smarter prompt.
⚡ Pro tip: Use parameterized queries in your tools, never string-formatted SQL built from the agent's output. This blocks injection and keeps the agent choosing values, not writing query structure — the agent fills in the blanks, it doesn't hold the pen.
Results and What Changed
The destructive-action risk went to zero, because destructive actions were no longer reachable. You can't accidentally delete through a tool that only counts orders. The class of failure was designed out rather than guarded against.
The agent also got more reliable at its actual job. Narrow tools with clear names and parameters are easier for the model to use correctly than one sprawling tool where it had to construct the entire query. Constraining the agent made it both safer and better — the two goals turned out to align.
The one cost was anticipating the queries. They had to think through what the agent needed and build a tool per intent. But that thinking is exactly what surfaced that the agent never needed write access in the first place — the exercise was the safeguard.
This is a pattern worth naming: the discipline of enumerating what an agent needs is itself a security review. You can't build a tool per intent without confronting exactly which intents are necessary, and that confrontation is where you notice the agent never needed to write, or delete, or reach that other table at all. The narrow-tools approach forces the audit that the broad-tool approach skips.
⚡ Pro tip: Default every data tool to read-only, and make write access a deliberate, separate decision with its own guardrails. Most agents that "need" database access need only to read it; write power should be the rare, carefully gated exception.
How to Apply This to Your Situation
List the specific questions or actions your agent actually needs. Build one narrow tool for each, parameterized and scoped. Resist the urge to add a flexible catch-all "just in case" — the catch-all is the risk.
Default to read-only. If a genuine write is required, isolate it in its own tool with the tightest possible scope, add validation on the inputs, and consider a confirmation step for anything irreversible. Treat write access the way you'd treat a sharp knife: available when needed, never left lying around.
For external APIs, the same principles hold: wrap each endpoint you need in a specific tool, validate the arguments before the call, and never expose a generic "make any HTTP request" tool that the agent could point anywhere.
A useful default posture: assume every capability you expose will eventually be exercised, including in the worst combination of arguments. Design as if the agent will, someday, call each tool with the most damaging inputs its schema permits — because over enough runs, it might. If that thought is alarming for a given tool, that tool is too powerful and needs narrowing.
⚡ Pro tip: Add rate limits and result caps inside your tools (like the
LIMIT 100Next Steps
Safe AI agent API integration comes from a single principle: expose specific intents, never raw capability. Replace "run any SQL" and "make any request" with narrow, parameterized, read-first tools that can only do the safe thing they were built for. The danger was never that the agent is dumb — it's that a capable agent will use whatever power you hand it, so hand it only what the task requires.
None of this makes agents less useful. A support agent that can read orders, a research agent that can query analytics, an ops agent that can check status — all fully capable within safe bounds. Capability and safety aren't in tension here; narrow tools deliver both, because a well-scoped tool is both safer and easier for the model to use correctly.
⚠️ Common mistake: Giving an agent broad database or API access on the assumption that the model is smart enough to be careful, then learning that "smart enough" doesn't prevent a probabilistic system from occasionally generating a destructive action. Safety has to be structural — built into what the tools can do — not behavioral, hoped for in how the agent acts.
The tool definitions and descriptions that make integrations safe are reusable across every agent that touches the same systems. PromptABCD keeps them versioned in one place, so the narrow, read-only tool descriptions you got right once become the standard your next agent starts from — instead of someone reintroducing a "run any SQL" tool in the next project.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
