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          # MySQL
export 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

ParameterTypeDefaultDescription
sql_engineEnginerequiredSQLAlchemy engine used by the write sub-agent. Its database credentials determine write permissions.
readonly_engineEnginerequiredSQLAlchemy engine used by the read sub-agent. Use SELECT-only credentials restricted to the allowed schemas.
schemastr | NoneNoneSchema used when listing tables and describing columns. It does not authorize or restrict arbitrary SQL.
idstr"database"Provider ID. Tools become query_<id> and update_<id>.
namestr | NoneNoneDisplay name. Defaults to id ("database").
read_instructionsstr | NoneNoneCustom instructions for the read sub-agent.
write_instructionsstr | NoneNoneCustom instructions for the write sub-agent.
modeContextModedefaultTool exposure mode. See Architecture.
modelModel | NoneNoneModel for the sub-agents. Defaults to Agno's default model.
readboolTrueExpose query_database tool.
writeboolTrueExpose update_database tool.

Tools Exposed

ToolDescription
query_databaseAnswer database questions through the read sub-agent and readonly_engine.
update_databaseApply 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 connection

The 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_inventory

Read-only Tool Surface

db = DatabaseContextProvider(
    sql_engine=sql_engine,
    readonly_engine=readonly_engine,
    write=False,
)
# Agent only sees query_database

write=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