Read 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:
1. Call well_get_schema(root) FIRST to discover the available fields.
2. Name in `fields` ONLY the extra values you need (5-15 typically). They are ADDED to the root's default projection in the payload you read.
3. Filter with `whereClause` so 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 ids `well_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": "<company_id>" } } }
- 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 `partyScope` instead: 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 no `partyScope` and 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": "<company_id from well_get_own_company>" } } }
SORTING (orderBy):
- Sort by any field: { field: "grand_total", direction: "desc" }
- Default sort is by primary key ascending.
⚠️ RULES:
- `fields` is ADDITIVE — it widens the data you receive on top of the root's default projection
- Omitting fields (default view) or naming a few extras both beat allFields
- Field paths from schema: "invoices.issuer.name" → ["invoices", "issuer", "name"]
- NEVER guess a field name. Common misses: a transaction has no `amount` column (read ["transactions", "instructed_amount", "amount"] and its "currency"), an account has no `name` (read ["accounts", "account_name"]). An unknown field fails the call and the error lists the valid fields of the root: retry once with one of them.
- Default 50 records per request, max 500.
- Reading whether ANYTHING matches is one call at `limit: 1`: read `totalCount`, 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 `nextCursor` is NOT a to-do. It means more rows exist, which
`totalCount` already 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
`nextCursor` as `cursor`; `nextCursor: null` is the last page.
Returns { rows, totalCount, nextCursor, success }.