---
title: "The agent database"
description: "Every agent gets its own SQLite database, and the workspace gets shared tables. The agent creates the tables; people browse and export them."
url: https://docs.spinrun.ai/build/agent-database
markdown_url: https://docs.spinrun.ai/build/agent-database.md
---

# The agent database

An agent that runs more than once needs somewhere to keep what it learned: the leads it emailed, the prices it saw yesterday, the tickets it already filed. The agent database is that place. It is not Brain — Brain is knowledge you give the agent to read; this is data the agent writes.

## Two scopes [#two-scopes]

| Scope       | Who reaches it                                                                     | Named in the document as                                       |
| ----------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `agent`     | This agent only. Its own SQLite database.                                          | `tools.database: "agent"`                                      |
| `workspace` | Every agent in the workspace granted the shared scope. One database per workspace. | `tools.database: "workspace"` (which includes the agent's own) |

`"off"` (or an absent field) means no database. The switch lives under **Abilities** in the builder — *Own database*, then *Shared workspace tables* — and republishing mints a key that carries `toolkit:data`, plus the `gateway:shared_data` capability for the workspace scope.

The workspace scope is a second decision because other agents read what this one writes. Use it to hand work between agents: a research agent fills `leads`, an outreach agent drains it.

## The four tools [#the-four-tools]

The agent sees them as ordinary tools in its list, under the `data` toolkit. Every call takes an optional `scope` (`"agent"` by default).

| Tool                   | Does                                                                                                                   | Risk tier   |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------- |
| `DATA_DESCRIBE_SCHEMA` | Lists tables, columns, keys and row counts. Free.                                                                      | read        |
| `DATA_QUERY_SQL`       | One `SELECT` or `WITH` statement. Returns up to `max_rows` rows (100 by default, 1000 at most) and a `truncated` flag. | read        |
| `DATA_WRITE_SQL`       | One `INSERT`, `UPDATE`, `DELETE`, `CREATE TABLE / INDEX / VIEW` or `ALTER TABLE`. `RETURNING` is supported.            | write       |
| `DATA_DROP_TABLE`      | Drops one table or view by name.                                                                                       | destructive |

The tiers matter because they are what the agent's approval setting acts on: with `approval: "write"` every write pauses for a person, and a drop pauses for everyone. Inside a sandbox run, where nothing can pause, `DATA_DROP_TABLE` is refused outright.

```json
{ "name": "DATA_WRITE_SQL", "arguments": {
  "sql": "CREATE TABLE IF NOT EXISTS leads (id INTEGER PRIMARY KEY, email TEXT UNIQUE, stage TEXT, last_touch TEXT)"
} }
```

```json
{ "name": "DATA_WRITE_SQL", "arguments": {
  "sql": "INSERT OR IGNORE INTO leads (email, stage) VALUES (?, ?)",
  "params": ["ada@northwind.io", "new"]
} }
```

```json
{ "name": "DATA_QUERY_SQL", "arguments": {
  "sql": "SELECT email FROM leads WHERE stage = 'emailed' AND last_touch < date('now', '-4 days')",
  "max_rows": 50
} }
```

Bind values with `?` and pass them in `params`; never quote them into the SQL. `params` may hold strings, numbers, booleans and `null`, at most 100 of them.

## What the SQL may not do [#what-the-sql-may-not-do]

The dialect is SQLite, one statement per call. The store refuses, whatever the statement looks like:

* `ATTACH`, `DETACH`, `PRAGMA`, `VACUUM` and extensions;
* triggers, virtual tables, temporary objects;
* the engine's own tables (`sqlite_*`);
* a `DROP` anywhere in a write — dropping is its own tool, so it gets its own tier.

A query that somehow writes is rolled back. The refusal comes back to the agent as the tool's error, with the reason, so it can correct the statement.

## Limits [#limits]

|                     |                                                                                     |
| ------------------- | ----------------------------------------------------------------------------------- |
| Rows per query      | 100 by default, 1000 at most; larger results are offloaded like any big tool result |
| Tables per database | Pro and Business 50, Enterprise 200                                                 |
| Database size       | Pro and Business 100 MB, Enterprise 1 GB                                            |
| Credits             | a query 1, a write or drop 2, describe free                                         |

## From a Connect client [#from-a-connect-client]

The same four tools are on the workspace endpoint for a person's own MCP client — a `spr_` key or an OAuth grant from Claude, ChatGPT or Cursor — on any plan that sells the store. The defaults differ, because a person has no agent:

|                                        | Agent key                                                  | Connect client                               |
| -------------------------------------- | ---------------------------------------------------------- | -------------------------------------------- |
| `scope` default                        | `agent` (its own tables)                                   | `workspace` (the shared tables)              |
| `scope: "agent"`                       | its own tables                                             | refused: a client has no database of its own |
| `agent: "<slug or name>"`              | that agent's tables, read-only, with the shared-data grant | that agent's tables, read-only               |
| `DATA_WRITE_SQL` on the shared tables  | with the shared-data grant                                 | any member                                   |
| `DATA_DROP_TABLE` on the shared tables | with the grant, at the destructive tier                    | workspace admins only                        |

Two rules carry the weight. **An agent's own tables are its to write**: a client (or another agent) reads them by naming the agent and can never write or drop them, because an edit behind the agent's back would be invisible to it. **A drop from a client takes an admin**: there is no approval card on the MCP path, so the one irreversible action is fenced by role as well as by any Enhanced Control rule on the destructive tier.

```json
{ "name": "DATA_QUERY_SQL", "arguments": {
  "agent": "outreach",
  "sql": "SELECT email, stage FROM leads WHERE stage = 'replied'"
} }
```

Team endpoints (`/mcp/t/<slug>`) do not list the toolkit: a team endpoint exists to narrow what a client reaches, and one database per workspace cannot be narrowed.

## Where it lives, and what people see [#where-it-lives-and-what-people-see]

Each database is a SQLite store pinned to the EU, with thirty days of point-in-time history and a nightly copy to the blob store. Nothing of it is mirrored elsewhere.

People browse it on the agent's **Data** tab (and the workspace's **Data** page under Resources): the tables, a paged grid of one table, a CSV export. Nobody edits rows from there — a person's edit would be invisible to the agent that owns the table. An admin can drop a table.

Rows read back into the agent are wrapped as untrusted data, the same as any tool result: an agent may have stored text it scraped from anywhere, and persistence must not turn that into instructions.

## Related [#related]

* [The agent document](https://docs.spinrun.ai/build/agent-document.md)
* [Untrusted output](https://docs.spinrun.ai/build/untrusted-output.md)
* [Authorization](https://docs.spinrun.ai/build/authorization.md)
