AI agents are great at SQL. Give them a schema and a question, and they'll write a reasonable query. But complex operations — the ones that span multiple tables, require specific ordering, or have business rules baked in — often trip them up.
The fix isn't more prompting. It's giving agents a place to look up how things work in your database.
The problem with one-shot queries
Consider a "process refund" operation. A human knows it means:
- Check the order exists and is refundable
- Calculate the refund amount (minus restocking fee if opened)
- Update the order status
- Credit the user's balance
- Insert an audit log entry
- Notify the payments service
An agent sees "process refund" and writes a single UPDATE. Maybe it gets the order status right. But the audit log? The balance credit? The business rules? Gone.
You can stuff all this into a system prompt, but prompts get stale. They drift from reality. And when you have dozens of complex operations, your prompt becomes a novel nobody maintains.
A skills table
Here's a pattern that works: store the knowledge in the database itself.
CREATE TABLE _agent_skills (
name TEXT PRIMARY KEY,
description TEXT NOT NULL,
tables_involved TEXT[] NOT NULL,
instructions TEXT NOT NULL,
example_sql TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);Now populate it with your domain knowledge:
INSERT INTO _agent_skills (name, description, tables_involved, instructions, example_sql)
VALUES (
'process_refund',
'Process a refund for an order. Handles partial refunds, restocking fees, and audit logging.',
ARRAY['orders', 'users', 'audit_log', 'inventory'],
'To process a refund:
1. Verify order exists: SELECT * FROM orders WHERE id = $order_id
2. Check refund eligibility: status must be "completed" or "shipped"
3. Calculate refund: full amount if unopened, minus 15% restocking if opened
4. Update order: UPDATE orders SET status = "refunded", refunded_at = now()
5. Credit user: UPDATE users SET balance = balance + $refund_amount WHERE id = $user_id
6. Log it: INSERT INTO audit_log (action, entity_type, entity_id, details, performed_by)
7. If physical product, update inventory: UPDATE inventory SET quantity = quantity + 1
IMPORTANT: All steps must succeed or none. Wrap in a transaction.',
'BEGIN;
UPDATE orders SET status = ''refunded'', refunded_at = now() WHERE id = 123;
UPDATE users SET balance = balance + 49.99 WHERE id = 456;
INSERT INTO audit_log (action, entity_type, entity_id, details, performed_by)
VALUES (''refund'', ''order'', 123, ''{"amount": 49.99}'', ''agent'');
COMMIT;'
);Teaching agents to use it
Add one line to your agent's system prompt:
Before performing any operation that modifies data, query
_agent_skillsto check if instructions exist:SELECT * FROM _agent_skills WHERE name ILIKE '%keyword%' OR description ILIKE '%keyword%'
That's it. The agent now knows to look before it leaps.
What to put in skills
Good candidates for skills:
- Multi-step operations — anything that touches more than two tables
- Business rules — "orders over $500 require manager approval"
- Constraints not in schema — "never delete users, only soft-delete"
- Calculated fields — "total_price = unit_price * quantity * (1 - discount)"
- Timing requirements — "subscription renewals must run before midnight UTC"
Bad candidates:
- Simple CRUD — agents handle these fine
- Anything already enforced by constraints or triggers
- Operations that change frequently (keep skills stable)
Making it discoverable
Help agents find relevant skills by adding searchable metadata:
ALTER TABLE _agent_skills ADD COLUMN keywords TEXT[];
ALTER TABLE _agent_skills ADD COLUMN category TEXT;
UPDATE _agent_skills
SET keywords = ARRAY['refund', 'return', 'money back', 'credit'],
category = 'orders'
WHERE name = 'process_refund';Now agents can search more naturally:
SELECT name, description FROM _agent_skills
WHERE 'refund' = ANY(keywords)
OR category = 'orders';Versioning skills
Skills evolve. Add version tracking:
ALTER TABLE _agent_skills ADD COLUMN version INT DEFAULT 1;
ALTER TABLE _agent_skills ADD COLUMN deprecated BOOLEAN DEFAULT false;
ALTER TABLE _agent_skills ADD COLUMN superseded_by TEXT REFERENCES _agent_skills(name);When business logic changes, increment the version and update the instructions. If a skill is replaced entirely, mark it deprecated and point to the new one.
Skills for read operations too
Don't limit skills to writes. Complex queries benefit too:
INSERT INTO _agent_skills (name, description, tables_involved, instructions, example_sql)
VALUES (
'customer_lifetime_value',
'Calculate customer lifetime value including all order history and subscription revenue.',
ARRAY['users', 'orders', 'subscriptions', 'payments'],
'LTV calculation:
1. Sum all completed order totals
2. Add subscription payments (monthly * months_active)
3. Subtract refunds
4. Exclude churned customers (no activity in 12 months)
Note: Use the payments table as source of truth, not orders.amount',
'SELECT u.id, u.email,
COALESCE(SUM(p.amount), 0) as lifetime_value
FROM users u
LEFT JOIN payments p ON p.user_id = u.id AND p.status = ''completed''
WHERE u.last_active_at > now() - interval ''12 months''
GROUP BY u.id, u.email;'
);The maintenance question
"Great, another table to maintain."
True. But consider the alternative: maintaining the same knowledge in prompt templates, scattered across your codebase, often duplicated, never quite in sync.
A skills table is:
- Queryable — find all skills that touch the orders table
- Auditable — who changed what, when
- Testable — validate skills against actual schema
- Shared — every agent, every framework, same source of truth
Plus, your agents can help maintain it. Ask them to draft new skills based on operations they observe. Have them flag skills that reference missing tables or columns.
One more thing
This pattern works especially well with database proxies that stage mutations for review. The agent looks up the skill, executes the steps, and a human reviews the staged changes — with full context of what the operation should have done.
The skill becomes documentation and guardrail in one.
This is a DIY pattern — not a feature we provide. But it pairs nicely with staged mutations. If you're building agents that need to perform complex database operations reliably, give it a try.