Query records
well_query_recordsRead records from Well's context graph FOR YOUR OWN WORK. This draws nothing on the user's screen.
Use it for every read whose answer is yours rather than the reader's: a gate checking whether a window holds transactions, a totalCount an answer has to quote, a sync log's latest status, a field a later step needs, the rows behind a figure you are about to compute.
⚠️ TO SHOW THE USER A TABLE, CALL well_show_records INSTEAD. Same arguments, same rows, and it renders the root's own table. This tool cannot put one on screen, so a request to "show me my invoices" answered here leaves the user with prose where a table belongs.
⚠️ WORKFLOW:
Call well_get_schema(root) FIRST to discover the available fields.
Name in
fieldsONLY the extra values you need (5-15 typically). They are ADDED to the root's default projection in the payload you read.Filter with
whereClauseso the read answers the question. A count under a filter beats reading rows and counting them yourself.
ROOTS (read-only — all 33): companies, people, connectors, invoices, documents, transactions, accounts, payment_means, workspace_connectors, memberships, cards, checks, ledger_accounts, journals, journal_entries, tax_rates, exchange_rates, invoice_transactions, categories, account_balances, tasks, workspaces, invoice_payment_means, chat_conversations, blueprint_runs, workspace_connector_sync_logs, media, emails, phones, web_links, locations, invoice_items, billing_events (The accounting graph — ledger_accounts, journals, journal_entries — and balances/rates are read-only projections owned by the sync/posting pipelines; query them for financial context, you cannot create/update them here. Sub-resources like emails/phones/locations are usually richer when read via their parent company/person.)
CATEGORY CATALOGS: "categories" holds two independent taxonomies, separated by category_type. Always filter on it — an unfiltered read mixes them:
whereClause: { category_type: { _eq: "company" } }is the COMPANY-CATEGORY catalog: the industry labels a counterparty carries, and the idswell_update_company({ category_ids })accepts. There is no curated allowlist — the labels are minted during enrichment — so read them here rather than inventing a taxonomy.whereClause: { category_type: { _eq: "transaction" } }is the management/transaction taxonomy.
CONNECTED TOOLS: do NOT use this tool to show the user what they have connected — call well_list_connectors instead. It owns that job: connection status, and an install link for anything not connected yet. Query root "workspace_connectors" here only for genuine RECORD-level needs — reading sync timestamps, filtering connections, joining them with other roots. ("connectors" is the installable catalog; "workspace_connector_sync_logs" is per-sync history.)
Well already syncs the providers' data into the roots above — invoices, transactions, accounts, the accounting graph. ALWAYS read it from here. well_invoke_connector_tool and a provider's own tools are for an ACTION the user explicitly asked to take on that provider (e.g. "create this record in Attio"), never a way to fetch data Well already holds.
FILTERING (whereClause):
Uses Hasura-style operators on field names.
Safe operators (work on ALL field types): _eq, _neq, _in, _nin, _is_null
Numeric/date only: _gt, _gte, _lt, _lte
Text only: _like, _ilike
When unsure of a field's type, prefer _eq or _in (they always work).
Combine with _and, _or, _not
For relationship fields, use nested syntax: { "issuer": { "company_id": { "_eq": "" } } }
NEVER select the workspace's OWN records by matching a company name. One legal entity appears under several labels — a registered name, a trade name, a bank-issued label — so a name filter silently drops rows and the total reads as complete. On the invoices root, pass
partyScopeinstead: it resolves the workspace's own side on the server, so this query needs no id lookup and no extra call. Call well_get_own_company for the id only when a root has nopartyScopeand you must filter on issuer_pk / receiver_pk or the nested company_id yourself.Match a counterparty by id too whenever you have one. Reach for _ilike on a name only to DISCOVER candidates to show the user, never to compute a figure you will report. Examples: { "status": { "_eq": "unpaid" } } { "grand_total": { "_gt": 1000 } } { "local_currency": { "_eq": "EUR" } } { "_and": [{ "status": { "_eq": "unpaid" } }, { "grand_total": { "_gte": 500 } }] } { "issuer": { "company_id": { "_eq": "" } } }
SORTING (orderBy):
Sort by any field: { field: "grand_total", direction: "desc" }
Default sort is by primary key ascending.
⚠️ RULES:
fieldsis ADDITIVE — it widens the data you receive on top of the root's default projectionOmitting fields (default view) or naming a few extras both beat allFields
Field paths from schema: "invoices.issuer.name" → ["invoices", "issuer", "name"]
Default 50 records per request, max 500.
Reading whether ANYTHING matches is one call at
limit: 1: readtotalCount, not the rows.
EXAMPLE - does the window hold any transactions at all? well_query_records({ root: "transactions", limit: 1, whereClause: { "executed_at": { "_gte": "2026-06-01", "_lt": "2026-09-01" } } }) // totalCount answers it. One row comes back and you ignore it.
EXAMPLE - answer "how much is still owed on the unpaid invoices?": well_query_records({ root: "invoices", fields: [["invoices", "balance_due"]], whereClause: { "payment_status": { "_in": ["unpaid", "partial"] } } }) // balance_due arrives in the rows for you to total up.
ONE CALL IS THE ANSWER — do not walk the root:
Every response carries totalCount (ALL matches, not just this page) and records_url (the full web-app table, with your filter and sort already applied). Hand the link to the user for anything past this page.
A non-null
nextCursoris NOT a to-do. It means more rows exist, whichtotalCountalready told you and the link already covers.Never paginate to compute a total, count, average or breakdown: aggregate over the filtered set instead. Summing a paginated sample produces a wrong number.
Never paginate to "be thorough". Large roots will exhaust the output limit mid-walk, and the user ends up with nothing legible.
Paginate ONLY for per-row work over every match that no aggregate can express, and tell the user the cost before starting. Then: pass the returned
nextCursorascursor;nextCursor: nullis the last page.
Returns { rows, totalCount, nextCursor, success }.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| root | Yes | The entity type to query — any of the 33 read-only roots (companies, people, connectors, invoices, documents, transactions, accounts, payment_means, workspace_connectors, memberships, cards, checks, ledger_accounts, journals, journal_entries, tax_rates, exchange_rates, invoice_transactions, categories, account_balances, tasks, workspaces, invoice_payment_means, chat_conversations, blueprint_runs, workspace_connector_sync_logs, media, emails, phones, web_links, locations, invoice_items, billing_events). Call well_get_schema(root) first to discover fields. | |
| limit | No | Max records to return (default 50, max 500) | |
| cursor | No | Opaque cursor for the next page. Omit for the first page, then pass nextCursor from the previous response. | |
| fields | No | EXTRA field paths to add to the root's display view, for values you need to reason about. Each path is an array whose first segment is the root's table name — use the paths well_get_schema(root) returns verbatim, which is the root name for every root except people (whose table is peoples); a path opening with any other segment is dropped. Additive only: they widen the payload you receive, and the root's own display projection (the columns the Well web app shows, and the ones a table drawn from this query carries) stays what it is no matter what you pass here. A scalar a composite renders comes back AS that composite — asking for grand_total gets you composite_total_amount_currency, with grand_total inside it — so read `columns` for what was actually materialized. Omit unless you need a value the display view does not carry. | |
| orderBy | No | Sort results by a field. Example: { field: "grand_total", direction: "desc" } | |
| allFields | No | If true, automatically fetches all scalar fields from schema. No need to specify fields. | |
| partyScope | No | Which side of an invoice the workspace itself occupies, resolved from its own company rather than a party name. `invoices` root only. "purchase" = the workspace owes it (payables); "sales" = the workspace is owed (receivables); "intra_self" = both parties are companies the workspace owns; "unattributed" = Well cannot place it on either side. The four partition every invoice, so report the "unattributed" count beside any payable total rather than dropping it — an unattributed invoice may still be owed. Prefer this over hand-writing an issuer/receiver filter. | |
| whereClause | No | Hasura-style filter object. Operators: _eq, _neq, _gt, _gte, _lt, _lte, _like, _ilike, _in, _nin, _is_null. Example: { "status": { "_eq": "unpaid" } } | |
| workspace_id | No | Target workspace. Omit to query every authorized workspace at once; each row comes back tagged with the workspace it belongs to. | |
| conversation_id | No | The conversation id returned by the previous Well result, in its meta under well/conversation_id, in its structuredContent, or in its JSON text block. Pass it back on every call in the same conversation, including a call a card makes, so the chosen workspace and the earlier answers still apply. It decides the conversation on its own: nothing the host states about the session replaces it. Omit it only on the first call of a conversation. |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| rows | Yes | Query results | |
| error | No | ||
| columns | No | The materialized columns in display order, with each composite substituted in place of the source fields it consumed. A row object's key order does not preserve this — the flattener appends reconstructed composites last — so a UI that wants the web app's column order must read it from here. | |
| success | Yes | ||
| returned | Yes | Number of rows returned | |
| columnMeta | No | Per-column field meaning, keyed by the same column paths as the rows. `context` = what the field means; `enrichment` = how the value is sourced (e.g. Bank sync, AI extraction). Only documented columns appear. Read this to interpret the returned values. | |
| nextCursor | No | Cursor for the next page. null means last page. | |
| totalCount | Yes | Total matching records | |
| records_url | No | Login-gated deep link to the FULL web-app records table for this root (real DataTable: composites, inline editing, resize/pin), carrying this call's `whereClause` and `orderBy` so it opens on the same rows. Hand it to the user for everything past this page — it is the answer to 'show me all of them', not pagination. Null when no workspace is in context or no web page serves the root. | |
| conversation_id | No | The conversation this result belongs to. Pass it back as the conversation_id argument on every later Well call in the same conversation. | |
| conversation_id_note | No | Present only when the server opened a fresh lane, stating that no choice recorded earlier was read. | |
| conversation_id_source | No | Where the conversation id came from: the host's own request meta, the caller's argument, or a fresh lane the server opened. |