Database
Route SQL database reads and writes through separate SQLAlchemy engines.
DatabaseContextProvider routes database reads and writes through separate SQLAlchemy engines. It exposes query_database for reads and update_database for writes.
Prerequisites
uv pip install -U agno sqlalchemy openai
# Plus your database driver:
uv pip install -U psycopg2-binary # PostgreSQL
uv pip install -U pymysql # MySQLexport OPENAI_API_KEY="your_openai_api_key_here"The PostgreSQL example requires an existing mydb database with an orders table containing order dates, plus separate reader and writer roles. Provision these through your database administrator. Set DATABASE_READ_URL and DATABASE_WRITE_URL to SQLAlchemy URLs with those credentials, for example postgresql://reader:password@localhost/mydb. The example does not create the database, roles or dataset.
Example
Save as database_context.py and run python database_context.py.
import asyncio
import os
from sqlalchemy import create_engine
from agno.agent import Agent
from agno.context.database import DatabaseContextProvider
from agno.models.openai import OpenAIResponses
# Database roles enforce permissions for each engine.
readonly_engine = create_engine(os.environ["DATABASE_READ_URL"])
sql_engine = create_engine(os.environ["DATABASE_WRITE_URL"])
db = DatabaseContextProvider(
sql_engine=sql_engine,
readonly_engine=readonly_engine,
schema="public",
)
agent = Agent(
model=OpenAIResponses(id="gpt-5.4"),
tools=db.get_tools(),
instructions=db.instructions(),
)
async def main() -> None:
await agent.aprint_response("How many orders were placed last month?")
if __name__ == "__main__":
asyncio.run(main())Both engines are required. The provider routes read requests to readonly_engine and write requests to sql_engine. Configure readonly_engine with database credentials restricted to SELECT operations and the allowed schemas. The schema parameter controls table discovery, not authorization for arbitrary SQL.
Provider Params
| Parameter | Type | Default | Description |
|---|---|---|---|
sql_engine | Engine | required | SQLAlchemy engine used by the write sub-agent. Its database credentials determine write permissions. |
readonly_engine | Engine | required | SQLAlchemy engine used by the read sub-agent. Use SELECT-only credentials restricted to the allowed schemas. |
schema | str | None | None | Schema used when listing tables and describing columns. It does not authorize or restrict arbitrary SQL. |
id | str | "database" | Provider ID. Tools become query_<id> and update_<id>. |
name | str | None | None | Display name. Defaults to id ("database"). |
read_instructions | str | None | None | Custom instructions for the read sub-agent. |
write_instructions | str | None | None | Custom instructions for the write sub-agent. |
mode | ContextMode | default | Tool exposure mode. See Architecture. |
model | Model | None | None | Model for the sub-agents. Defaults to Agno's default model. |
read | bool | True | Expose query_database tool. |
write | bool | True | Expose update_database tool. |
Tools Exposed
| Tool | Description |
|---|---|
query_database | Answer database questions through the read sub-agent and readonly_engine. |
update_database | Apply database changes through the write sub-agent and sql_engine. |
Privilege Separation
The read and write sub-agents use separate database connections:
query_database → readonly_engine → read replica / SELECT-only user
update_database → sql_engine → writable connectionThe separate engines route each tool to the intended connection. Database roles enforce the boundary. Grant the reader role SELECT-only access to the required schemas, and grant the writer role only the write permissions it needs. The schema parameter helps the agent discover tables and columns but does not block cross-schema SQL.
Multiple Databases
Configuration fragment: create orders_write_engine, orders_read_engine, inventory_write_engine and inventory_read_engine for your two provisioned databases, using the same credential and permission rules as above. Use different id values:
orders_db = DatabaseContextProvider(
id="orders",
sql_engine=orders_write_engine,
readonly_engine=orders_read_engine,
)
inventory_db = DatabaseContextProvider(
id="inventory",
sql_engine=inventory_write_engine,
readonly_engine=inventory_read_engine,
)
agent = Agent(
model=OpenAIResponses(id="gpt-5.4"),
tools=[*orders_db.get_tools(), *inventory_db.get_tools()],
)
# Agent sees: query_orders, update_orders, query_inventory, update_inventoryRead-only Tool Surface
db = DatabaseContextProvider(
sql_engine=sql_engine,
readonly_engine=readonly_engine,
write=False,
)
# Agent only sees query_databasewrite=False removes update_database from the agent's tools. It does not change the permissions of readonly_engine, so keep that engine restricted at the database level.
Cookbook
Database Context Provider
Read/write with separate engines