Connecting a Database
A database source lets agents query your relational data — either as plain-English questions turned into read-only SQL, or as indexed rows that appear in ordinary search results. Both are read-only: nothing here ever writes to your database.
Setup spans two pages, deliberately:
- Connections — add the source, test it, rotate its credential.
- Settings → Database sources — tune what gets indexed and who may see which rows. Also where you remove a source.
1. Add the source
On Connections, add a database source and fill in:
| Field | Notes |
|---|---|
| Kind | Which driver, from those compiled into your deployment. An empty list means the build has no database drivers. |
| Connection string | e.g. postgres://user:password@host:5432/database. Stored as an encrypted secret under a derived id — the console never reads it back. |
| Connection secret | Alternative to typing a string: point at a secret you already uploaded. |
| Schema | Defaults to public. |
| Workspace id | Optional. Inherits from the source name if left blank. This is the workspace its rows and schema land in. |
The dialog tests before saving, running the same three checks a reload runs, in the same order. A typo in the host or password fails here rather than three steps later as a puzzling sync error.
Use a database account with read-only grants on only the tables you intend to expose. The engine never issues writes, but the credential should not be able to either.
2. Apply a reload
A saved source is not a running source
It shows Needs a reload and its Test and Sync buttons stay disabled. Open Settings → Database sources, then Dry run → Apply reload. The badge on Connections links straight there.
Once live, Test pings it and Sync now runs the first sync.
3. Decide what gets indexed
Two levels, and the distinction matters for both cost and privacy.
Schema only (the default)
Without row indexing, the engine indexes structure: table names, column names, types, and relationships. That is enough for database_query to write correct SQL against your schema — the tool profiles table statistics and generates queries; it does not need your rows pre-embedded.
This is the cheap option. No row content is embedded, so no per-row token cost, and no row content is stored in the index.
Indexed rows
Turn on Index row content (per source, and per table) when you want rows themselves to appear in semantic search results — a support ticket surfacing next to the code that handles it.
Per table you then choose:
- Embed columns — which columns become the searchable text. Leave blank for all. Naming just the meaningful text columns is both cheaper and more accurate than embedding every numeric id in the row.
- Ontology class — the class assigned to rows from this table, when the source is bound to a vocabulary.
Rows need a single-column primary key
Row indexing pages through the table with a keyset pager, which needs one column to page on. A table with no primary key, or a composite one, reports that it cannot be indexed and says which case it is. Schema indexing still works for those tables.
Nothing syncs on a schedule yet, so run Sync now after adding a source and after changing a row policy.
Rows in search results
- Each matching row is a result of its own. Several rows of one table can come back for one query, each with its own text. A table's schema is a separate result.
- A row carries its own graph node, or none. Where the source builds a graph node for each row (by default, only for Salesforce tables), a row's result carries that row's node and its links: its Salesforce object, the rows it looks up. Elsewhere it carries no node; neither does a table's schema result.
- Its links are capped. A row's node lists at most 50 links each way, so a row that thousands of others point at stays quick to return.
4. Restrict who sees which rows
If more than one person queries this source, set a row policy per table. This is the database equivalent of folder access rules.
| Field | Meaning |
|---|---|
| Owner column | The column holding the value that identifies a row's owner — a Salesforce user id, an account manager's id. |
| Group column | The column holding a group identifier, for rows shared with a team. |
With a policy set, the rows an actor reaches through database_query, and the rows that appear in their search results, are limited to those matching their owner or group values.
There are two ways to give an actor its owner values:
- Per actor. In the Directory, enter that person's owner-column values for the source, one per line.
- By convention. Turn on "An actor id is an owner value" for the source, and an actor reaches rows whose owner column holds their own actor id — nothing to configure per person.
Option 2 is much less work, but only correct when your owner column and your actor ids are the same namespace. Leave it off when the owner column holds values from an unrelated namespace, such as a numeric internal key: an actor id that happened to equal one would otherwise reach rows nobody intended.
Changing a row policy needs a re-sync
A row's labels are derived from the values in its own owner and group columns at sync time. Nothing can re-stamp them in place — refreshing them means re-reading the table. After changing a policy, run Sync now, or already-indexed rows keep their old labels and search will not be filtered the way you just configured. The console warns you about exactly this after such a save.
5. Classify whole tables
Row policies answer whose a row is. A classification level answers how sensitive a table is, and the two apply together. A table takes the level of its path, db://{source}/{schema.table}:
[[access.classification.rule]]
path = "db://crm/public.salaries"
level = "restricted"Use db://crm/** for every table of a source. Write the table name in the case the database reports it, or set case_insensitive = true on the rule — a rule that matches nothing leaves the table unclassified.
The level applies to search and to live SQL. A caller cleared below it cannot query the table, is never grounded on it when the engine writes SQL for them, and does not see its saved queries. Row values are never scanned for markers; a table's level comes only from its path.
A default above your lowest level refuses unclassified callers
Every table with no rule sits at the scheme's default level, and a caller with no clearance is cleared for the lowest level only. With a default above it they can query nothing, and database_query answers no tables admitted rather than returning no rows. Give those callers a clearance, or set the default to your lowest level.
Querying it
Agents reach the source through the database_query tool, which needs the dbsource:<name>:read scope on the key — one scope per source, so a key can be limited to one database.
The tool accepts either SQL or a plain-English question. Generated SQL is read-only and grounded on auto-profiled table statistics. A question is grounded on the tables search finds for it: a table whose rows match counts, as does one whose schema does. Both modes are bounded by the caller's classification ceiling, as above.
Rotating the credential
Use Rotate credential on Connections. It works even on a source that has not been reloaded yet — which is exactly when a mistyped string most wants replacing — and takes effect at the next reload. Until then the source keeps using the previous string.
Removing a source
Remove it in Settings → Database sources. That stops syncing, but rows already indexed stay searchable until you purge them. Stopping a sync and deleting data are deliberately separate actions, so removing a source by mistake does not destroy an index.
A purge takes the source's whole knowledge graph with it: every table, column and row node, and every link they hold, including rows the sync had stopped tracking. See Removing content.