Databases
A Nirvai table is a spreadsheet with rules. It looks like rows and columns, but each column knows
what it is willing to hold: a status cell accepts four words and no others, an owner cell accepts
people who actually work with you, a link cell accepts one specific row of one specific other table.
Your assistant does not get to type whatever it likes into a cell — it asks the column first.
Everything below runs through nirvai_execute under the db area; see
the MCP reference for the alias grammar.
You almost certainly have two tables called Leads — one from last year, one you actually use. They are different tables with the same label. Your assistant resolves a table to its stable id first, and every verb below is addressed by that id, never by the name it saw in the conversation.
The verbs
| Verb | Scope | What it does |
|---|---|---|
db.select | read | Read rows, with a raw SELECT-only tail — see below. |
db.get_rich | read | Read a record's page body — the document that lives inside the row. |
db.insert | write | Add rows. Per-row partial success: good rows land, bad rows come back with reasons. |
db.update | write | Change rows matching a condition. All-or-nothing — one invalid value and nothing is written. |
db.update_rich | write | Replace a record's page body. Guarded so it can't silently drop attached media. |
db.edit | write | Change the table's structure — add, drop, retype or rename columns. |
db.create_database | create | Make a new space to hold tables. |
db.create_table | create | Make a table inside a space, with its columns and views. |
db.propose | create | Author a reviewable multi-table setup — a space, its tables, columns and views. You click Create. |
db.delete | destructive | Withheld. Refused for every token — deletion is not possible over the connection. |
Discovery comes from the master tools: nirvai_list_resources for "which tables do I have", and
nirvai_describe for "what exactly is this one" — its columns, their allowed values, who can be
assigned, which tables it links to, and your permission tier. That is read live, every time.
Where you finish this
db.propose builds nothing. It hands you a page at /external/databases/…, and one proposal can
carry a whole multi-table setup — a space plus several tables, their columns and their views. The
page renders one card per package, each with its own Create button.
- It's a form, not a receipt. Change a column's type or its name, drop a table you don't want, then create — package by package, in whatever order you like.
- You have to be signed in, and only you can open it. A proposal link is not shareable.
db.insertanddb.updatehave no review page. Rows land directly; the review step guards creating structure.- Once created, re-opening shows Created ✓ and will not make a second copy — see the review page.
Reading: db.select
condition is a raw SELECT-only tail — a WHERE, an ORDER BY, a LIMIT. Anything that would
change data is rejected outright.
nirvai_execute(
alias = "db.select",
args = { table_session: "…",
condition: "WHERE status = 'Open' ORDER BY id DESC LIMIT 50",
columns: ["id", "name", "status", "owner"] },
description = "List the open deals, newest first"
)
→ { rows, row_count, truncated }
A row ceiling always applies. truncated: true means there was more — your assistant pages
through it with its own LIMIT/OFFSET rather than assuming it saw everything.
Writing values: the tagged-value grammar
Simple columns take a plain value. Structured columns take a tag — a small object whose single
key starts with $, naming what kind of thing you mean.
| Column type | How your assistant writes it |
|---|---|
| text, number, date, checkbox, email, url, phone, rating, currency, progress, colour | The plain value — "Acme", 42, "2025-03-01", true |
| single choice / status | {"$select": "Open"} (or the bare option string) |
| many choices | {"$multi_select": ["Urgent", "Renewal"]} |
| person | {"$person": "jane@company.com"} |
| link to another record | {"$relation": {"table_session": "…", "record_id": 42}} |
| file | {"$file": "quote.pdf"} |
| agent | {"$agent": "…"} |
Two rules make this safe. Options are closed — the words a choice column accepts come from
nirvai_describe, not from your assistant's imagination, and an unlisted value is refused rather than
invented; the same goes for an assignee the table can't actually assign or a link to a row that
doesn't exist. And errors are structured, never raw database errors: you get a named reason —
INVALID_OPTION, PERSON_NOT_FOUND, UNKNOWN_COLUMN, PROTECTED_COLUMN, OUT_OF_RANGE,
RELATION_RECORD_NOT_FOUND — with the column and, where it applies, the allowed set. A failed write
tells your assistant how to fix itself.
A record's id, its created and updated timestamps, its page body and icon, and anything managed by
the agent-task system are never writable through the record path and never appear as writable
fields. The page body is read and written with db.get_rich / db.update_rich, which replaces the
whole body — so your assistant reads it first and refuses to drop media unless told to.
Worked example: find, learn, read, write
nirvai_list_resources(kind = "databases")
→ the tables you have, each with its stable id
nirvai_describe(kind = "databases", ref = "<table id>")
→ columns, allowed options, assignees, relations, views, your permission tier
nirvai_execute(
alias = "db.select",
args = { table_session: "…", condition: "WHERE stage = 'Qualified' LIMIT 20" },
description = "See which leads are already qualified"
)
nirvai_execute(
alias = "db.insert",
args = { table_session: "…",
records: [{ "Company Name": "Acme",
"Stage": {"$select": "Qualified"},
"Owner": {"$person": "jane@company.com"},
"Value": 48000 }] },
description = "Add Acme to the pipeline as a qualified lead"
)
→ { inserted_ids, inserted_count, row_errors }
The describe step is not optional politeness — it is where "Qualified" is confirmed as a real
option and jane@company.com as a real assignee, before anything is written.
Changing structure: db.edit
Adding a column is safe. Dropping, retyping or renaming one is destructive — the data in it goes.
So db.edit runs in two beats: a dry run first, which returns the change plan (what would be
added, dropped, retyped, renamed, and which saved views would be affected) and touches nothing; then
the real edit, which for a destructive change requires an explicit acknowledgement in the call
and is refused without it. Your assistant is expected to show you the plan before it acknowledges
anything.
Limits & guarantees
- Your permission tier comes from ownership, not from the token. You own the table → full access;
you are an org admin → write; you are an org member → read. A read-tier table refuses a write even
when the token carries the
writescope. The tier is shown innirvai_describe. insertis partial,updateis total. Inserting ten rows may land eight and report two; updating either applies completely or not at all.- Nothing is created behind your back.
db.proposewrites a shape — spaces, tables, columns, views — never rows, never data. It becomes real when you click Create, once; re-opening the review page afterwards shows it as already created and will not create it twice. - Deletion is impossible.
db.deleterefuses for every token, always. - Writes show up immediately in your open table view and in the Activity Feed.