Skip to content

Asking questions ​

Most questions worth asking are not about one list. "Which policies renew next month, and who's the account manager?" touches your policy table, your client table and the link between them. "What tasks came out of meetings about our enterprise clients?" touches four things at once.

mente answers those in one pass. You ask in plain language; your assistant turns it into a single query and hands you the answer — no exporting, no stitching two lists together in a chat window, and no quietly wrong totals.

What you can ask ​

  • Filters over one table — "policies renewing in the next 30 days."
  • Filters that reach across a link — "policies belonging to an enterprise client," where segment lives on the client, not the policy.
  • Columns pulled across a link — "every policy, with its client's name and account manager."
  • Counts and totals, at the grain you asked for — "how many active policies per carrier," "rank clients by total premium."
  • Absence — "clients with no meeting in 60 days," "policies with no account manager." (And it can tell "nobody is assigned" apart from "nobody has filled that field in yet.")
  • Across primitives — "the status of every task created from last week's meetings," "notes mentioning this client, plus the tasks attached to them."
  • Relevance mixed with structure — "notes about renewals, for this client, updated this quarter."

Two things it will not do, on purpose ​

  • It will not do arithmetic you did not ask for. If you want on-time renewals ÷ renewals due, mente gives you both numbers at the same grain and you divide them. A computed column is a place for a wrong answer to hide.
  • It will not compare a record against its own past. mente stores what a record is now, not a version history — so "policies whose premium rose 20% at renewal" is not answerable from the table alone. Keep the prior value as its own column and it becomes an ordinary filter.

Every answer is yours only ​

A query never returns a row you are not allowed to see. The permission check happens inside the query, once per thing it touches — so if you can read the policies but not the clients, you get your policies with the client columns empty, not an error and not somebody else's data.

That means two people can run the same question and legitimately get different numbers. Answers say which check ran where, so the difference is explainable rather than mysterious.

For builders ​

Two tools, and the split matters:

ToolWhat it is for
describe_schemaWhat this workspace contains — your collections, the node types, the links between them. Call it first.
queryThe question itself.

describe_schema is compact by default (names, no shapes) and takes a node type for the detail:

json
{ "type": "collection_item", "collection": { "name": "policies" } }

…which returns that collection's fields, their types, which are unique, which are links and where they point. It only ever names collections you can read.

A query has five parts, and every one of them is optional except from:

json
{
  "from":    { "type": "collection_item", "collection": { "name": "policies" } },
  "where":   [{ "path": ["renewalDate"], "op": "lte", "value": "+30d" }],
  "select":  { "number": true, "premium": true },
  "orderBy": [{ "path": ["renewalDate"], "dir": "asc" }],
  "limit":   25
}
  • from — what one row is. collection_item returns rows; collection returns the tables themselves.
  • where — filters, ANDed. Operators: eq, neq, gt, gte, lt, lte, in, contains, exists, not_exists. in takes a list ("value": ["active", "pending"], up to 1,000 values; an empty list matches nothing). Relative dates (+30d, -60d) are resolved on the server, so "the next 30 days" means the same thing wherever you ask from.
  • select — the keys you want back, named by you. A key may instead be a nested block carrying a hop, which is how you reach a linked record.
  • orderBy / limit — ordinary paging. total is exact and counts the whole match, not the page.
json
"select": {
  "number": true,
  "client": { "hop": { "edge": "client", "to": { "type": "collection_item" } },
              "select": { "name": true, "accountManager": true } }
}

To filter the anchor by its related rows instead, put the hop in where with a quantifier — some, none, every — or with an aggregate and a comparison:

json
"where": [
  { "hop": { "edge": "policies", "to": { "type": "collection_item" } },
    "aggregate": "count",
    "where": [{ "path": ["status"], "op": "eq", "value": "active" }],
    "op": "gt", "value": 5 }
]

A path never crosses a link. ["client", "segment"] is not "the client's segment" — say it with a hop, and the query will tell you so if you forget. That one rule is what keeps filter the children and filter the parent by their children from being confusable.

Grouping ​

groupBy changes what one row is:

json
{ "from": { "type": "collection_item", "collection": { "name": "policies" } },
  "groupBy": { "carrier": true },
  "select":  { "premium": { "aggregate": "sum", "path": ["premium"] } } }

A grouped block returns groups, and total then counts groups — not records. Aggregates: count, count_distinct, sum, avg, min, max. Its select holds only aggregates and nested breakdowns, so the classic spreadsheet mistake (a name printed beside a total it does not belong to) cannot be written.

When you get it wrong ​

Rejections name the alternatives. A misspelled field comes back with the collection's real field names; a link that does not exist comes back with the links that do; a path crossing a hop comes back with both ways to say what you meant. One reply carries every mistake in the query, so a query with three problems takes one retry, not three.

Related

Structured data for the tables themselves, grouping your tables for saved cross-table views, and Questions across your data for a worked set of real business questions.

Docs as of 2026-08-31.