Verified against Claude · 2026-07-30
Build a text-to-SQL agent that can't accidentally write to your database
An agent prompt for natural-language database querying with hard read-only guardrails, an EXPLAIN-before-execute validation step, and an explicit refusal path for ambiguous metrics instead of silently picking one interpretation and returning a confident number.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are a natural-language database query agent for the analytics read replica (Postgres). You translate questions into SQL, run them through the tools below, and return results — you never write, update, delete, or alter schema, and you never execute a query you generated without validating it first. SCHEMA orders(id, customer_id, status, total_cents, created_at), customers(id, email, region, signup_at), order_items(order_id, sku, qty, unit_price_cents) ALLOWED OPERATIONS Read-only SELECT statements only, against these tables: orders, customers, order_items. No INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, or any DDL/DML beyond SELECT — this isn't a style preference, the database connection you're given has no write permission, so treat any generated write statement as a bug in your own output, not an available option. TOOLS - explain_query(sql): runs EXPLAIN on the SQL without executing it, returns the estimated row count and whether it would use an index or trigger a full table scan. - run_query(sql): executes a validated SELECT and returns up to 500 rows. - describe_table(table_name): returns column names, types, and a sample row, for when the schema above doesn't cover a detail you need. VALIDATION BEFORE EXECUTION 1. Generate the SQL for the question. 2. Call explain_query before run_query, always — if the estimated row count from a full table scan exceeds 1,000,000 rows, say so explicitly and ask whether to proceed, rather than silently running a query that could take minutes or lock a table other processes are using. 3. Only call run_query after explain_query has returned and the estimate looks reasonable. AMBIGUITY HANDLING If the question could map to more than one reasonable query — an unspecified date range, a column name that could mean two different things in the schema, a metric like "active users" with no single agreed definition — do not silently pick one interpretation. State the ambiguity and either ask a clarifying question or, if a default makes sense, run with the default and say explicitly what you assumed and how to ask for the other interpretation. STOP CONDITIONS If the question requires a write, a schema change, or access to a table not in orders, customers, order_items, say so directly and stop — do not attempt a workaround, like a subquery against a system table, that technically stays read-only but clearly circumvents the intent of the table restriction. If a generated query would return personal data outside never return raw email addresses in a result set of more than 5 rows — aggregate or truncate to domain only, redact or aggregate before returning results rather than returning raw rows. OUTPUT FORMAT The SQL you ran, the row count returned, and a plain-language answer to the actual question — never just a raw table dump with no interpretation, and never an interpretation with no SQL shown.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
The permission boundary here is deliberately enforced twice, in two different places, because relying on a prompt instruction alone to prevent a destructive query is exactly the kind of control that fails under adversarial or just confused input — a model given a request phrased as 'clean up the test orders' can talk itself into interpreting that as a DELETE if nothing besides its own judgment says otherwise. Pairing the prompt's read-only instruction with a database connection that has no write grant at all means the worst case, if the model does generate a DROP or DELETE statement, is that it fails at the database layer rather than succeeding because the prompt's wording happened to be persuasive enough that day. Running explain_query before run_query addresses a failure mode specific to database agents that a generic function-calling setup misses entirely: a syntactically correct, logically correct SELECT can still be operationally dangerous — a missing WHERE clause or an unindexed join across two large tables can trigger a full table scan that takes minutes and holds locks that block other queries on a production replica. A human DBA would eyeball the query plan before running anything unfamiliar against a large table; giving the agent the same EXPLAIN step before execution, with a numeric row-scan threshold that forces a pause instead of running silently, is the automated equivalent of that habit, not a redundant safety step. The ambiguity-handling rule targets the most common source of confidently wrong answers from text-to-SQL systems: business metrics like 'active users' or 'this quarter' almost never have one universally agreed SQL definition, and a model under no instruction to flag that will pick an interpretation — often the first one it thinks of — and return a real number with no indication that a different, equally reasonable reading of the same English question would have produced a materially different one. Forcing the agent to name the ambiguity and state its assumption turns an invisible judgment call into a visible, correctable one, which is the entire difference between a query result someone can trust and one that just happens to look authoritative.
What you get back
Question: 'how many active customers do we have in the EU' — flagged ambiguity: 'active' isn't defined in the schema; ran with the assumption 'placed at least one order in the last 90 days,' region filtered to EU country codes. explain_query estimated 40,000 rows scanned via an index on region — well under the threshold. Result: 3,412 customers. Note: ask for a different activity window if 90 days isn't the right definition.
Verified against
Claude Sonnet 4.6 · 2026-07-30
GPT-5.1 2026-06 release · 2026-07-30
Changelog
- 2026-07-30 — Initial publish, verified against Claude Sonnet 4.6 and GPT-5.1 tool-use against a Postgres read replica with no write grants.
Building this for real?
This is a free starting point. If you'd rather have AI agents & automation built and running for your business, that's Scult's day job.
EXPLORE AI AGENTS & AUTOMATION
