Read your app's data: schema and SQL
Once an app has a managed database, you can inspect and read its data through the agent surface, without ever putting the connection string in front of the agent. This guide covers the two direct-data-access tools and how they fit together:
describe_database: the schema (shape only, never row values). Free, all plans.query_database: a single read-only SQL query, capped and audited. Paid.
Both work on a prod app or a preview app (a branched preview carries its own managed-database record). Both are also available on the REST /api/v1 surface (paid plans — REST API access is a paid capability).
Heads-up on previews: a
clone-mode preview's branched database contains real production rows copied at branch time. Reading it reads prod-shaped data, so treat it accordingly.
Inspect the shape: describe_database
Start here. describe_database returns the schema only: tables, columns (name, type, nullability), primary keys, indexes, and an approximate row count per table. It never returns a single row of data, so it's free on every plan (including the Explore free tier).
describe_database({ applicationId })
{
"ok": true,
"applicationId": "app_123",
"databaseManaged": true,
"provider": "postgres",
"schema": {
"tables": [
{
"name": "todos",
"columns": [
{ "name": "id", "dataType": "uuid", "nullable": false },
{ "name": "title", "dataType": "text", "nullable": false }
],
"primaryKey": ["id"],
"indexes": [{ "name": "todos_pkey", "columns": ["id"], "unique": true }],
"approxRowCount": 42
}
]
}
}
Recoverable errors (success channel)
errorCode | Meaning |
|---|---|
no_managed_database | The app has no DeployMill-managed database. Add a database block to .deploymill/project.json and reconcile_project. |
describe_unsupported | The backend can't introspect the per-app database from here (e.g. Supabase). Use that provider's console. |
Run a read-only query: query_database
query_database runs one read-only SQL statement and returns capped rows. This is the agent's actual row-data surface. It requires a plan with direct data access (a paid Builder/Enterprise capability, see Upgrading).
query_database({ applicationId, sql: "select id, title from todos order by created_at desc", maxRows: 100 })
{
"ok": true,
"applicationId": "app_123",
"provider": "postgres",
"columns": ["id", "title"],
"rows": [{ "id": "…", "title": "ship it" }],
"rowCount": 1,
"truncated": false
}
- Single statement only:
SELECT/WITH/EXPLAIN/SHOW/TABLE/VALUES. No;-separated batches, no DML/DDL. maxRowsdefaults to 1000 and is hard-capped at 1000. When more rows match,truncated: true, so narrow the query or paginate in SQL.- Read-only is enforced two ways: a parse guard rejects obviously mutating or
multi-statement SQL up front, and the query runs inside a read-only transaction with a server-enforced statement timeout. A write that slips past the guard is still rejected by the database.
- Secret-named columns are redacted: cells in columns whose name contains
password,secret,token,api_key,private_key,access_key, orclient_secretcome back as[redacted]. - Every call is audited (the SQL text and row count, never the row data).
Recoverable errors (success channel)
errorCode | Meaning |
|---|---|
no_managed_database | The app has no managed database. |
not_read_only | The SQL was rejected by the parse guard (mutating or multi-statement). |
query_unsupported | The backend has no clean read-only SQL path (e.g. Supabase). |
query_failed | The database rejected the query (syntax error, write blocked by the read-only transaction, timeout, …). |
upgrade_required | The org's plan doesn't include direct data access. Includes an upgradeGuide pointer (and upgradeUrl when configured). |
data_access_disabled | An org admin turned the direct-data-access governance toggle off, even on a paid plan. |
A typical flow
describe_databaseto learn the tables and columns (free; do this first).query_databaseto read the rows you need (read-only, capped, audited).
Security notes
- The app's real
DATABASE_URLis the canonical "secret the agent never sees".None of these tools return it.
query_databaseis gated by the direct data access entitlement and can beturned off org-wide by an admin governance toggle. It returns coded
upgrade_required/data_access_disabledrather than a silent dead-end.- Reads are attributable: every
query_databasecall lands in the audit trail.