Skip to main content

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.

Always by id, never by name

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

VerbScopeWhat it does
db.selectreadRead rows, with a raw SELECT-only tail — see below.
db.get_richreadRead a record's page body — the document that lives inside the row.
db.insertwriteAdd rows. Per-row partial success: good rows land, bad rows come back with reasons.
db.updatewriteChange rows matching a condition. All-or-nothing — one invalid value and nothing is written.
db.update_richwriteReplace a record's page body. Guarded so it can't silently drop attached media.
db.editwriteChange the table's structure — add, drop, retype or rename columns.
db.create_databasecreateMake a new space to hold tables.
db.create_tablecreateMake a table inside a space, with its columns and views.
db.proposecreateAuthor a reviewable multi-table setup — a space, its tables, columns and views. You click Create.
db.deletedestructiveWithheld. 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.insert and db.update have 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 typeHow your assistant writes it
text, number, date, checkbox, email, url, phone, rating, currency, progress, colourThe 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.

Protected columns can never be written this way

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 write scope. The tier is shown in nirvai_describe.
  • insert is partial, update is 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.propose writes 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.delete refuses for every token, always.
  • Writes show up immediately in your open table view and in the Activity Feed.

What's next