mcpbeat Sign in

Well MCP Server

by wellapp-ai Your server? Claim it
answering

Well is answering right now. Last checked 12 min ago. It exposes 82 tools.

Connect your AI to your Well financial data - invoices, companies, contacts.

The linked repository no longer exists on GitHub — it was deleted or made private.

Uptime history 47 days of history · worst day 99%
47 days agonow
100.0%
Uptime 24h
91 of 91 checks
82
Tools
read from the server
575 ms
Response time
average over 24h
open, no key
Access
streamable-http

What changed 260

Every tool that appeared, vanished or quietly changed what it asks for. Recorded since 11 August 2026. No other catalogue keeps this.

17 Sep 12 tools changed the parameters they ask for well_add_contact_channel, well_create_company, well_create_invoice_from_data and 9 more
17 Sep 7 tool descriptions were rewritten well_get_connector_coverage, well_get_worklist_status, well_list_account_balances and 4 more
17 Sep 3 tools appeared well_claim_statement_draft, well_get_customer_einvoicing_details, well_render_cash_forecast
17 Sep a tool disappeared well_get_cash_forecast
16 Sep 13 tool descriptions were rewritten well_get_connector_coverage, well_list_account_balances, well_list_burn_exemptions and 10 more
16 Sep 6 tools changed the parameters they ask for well_create_company, well_get_connector_coverage, well_list_connectors and 3 more
16 Sep 2 tools appeared well_search_context, well_upload_document
15 Sep 15 tool descriptions were rewritten well_get_connector_coverage, well_get_own_company, well_list_accounts_needing_company and 12 more
15 Sep 12 tools appeared well_create_company_candidate, well_create_company_workspace, well_get_accounting_settings and 9 more
15 Sep 12 tools changed the parameters they ask for well_create_person, well_get_connector_coverage, well_get_worklist_status and 9 more
and 177 more, back to 11 August 2026

Well missed 2 checks this week

Everything else answered, so this is steady rather than shaky. We check every 15 minutes, which is how a one-off gets told apart from the start of a pattern, and how you hear about the next one within the hour instead of from your users.

Three servers free · no card

Connect this server

Endpoint below is the one we actually reach during checks — not the one copied from a README. Last verified 12 min ago.

run in your terminal
claude mcp add well-mcp --transport http https://api.wellapp.ai/v1/mcp
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "well-mcp": {
      "url": "https://api.wellapp.ai/v1/mcp"
    }
  }
}
~/.codex/config.toml
[mcp_servers.well-mcp]
url = "https://api.wellapp.ai/v1/mcp"
.cursor/mcp.json
{
  "mcpServers": {
    "well-mcp": {
      "url": "https://api.wellapp.ai/v1/mcp"
    }
  }
}
.vscode/mcp.json
{
  "mcpServers": {
    "well-mcp": {
      "url": "https://api.wellapp.ai/v1/mcp"
    }
  }
}

Available tools 82

Read directly from the server with tools/list, grouped by what they act on. If a tool disappears, we record the date.

well
well_add_contact_channel
Add a contact channel to a company or person. Wraps the resource-scoped REST endpoints (POST /v1/{companies,people}/:id/{emails,phones,web-links,locations}). channel + the matching value field: - email → value.email - phone → value.e164_number (E.164; a leading "+" is added if missing) - web_link → value.url (+ optional value.platform, default "website") - location → value.city, value.country (+ optional address_line1/2, region, postal_code) value.label is optional (defaults to "work"). NOTE: adding a phone is supported on a PERSON but NOT on a company (no endpoint) — that combination returns a clear error. To READ existing channels, use well_query_records on the parent (companies/people) or the channel root.
well_assign_account
Attach a bank account to a company, and say whether the workspace owns it. Use this when an account carries no company, or when its ownership is still `unknown` — the two states a figure that walks account ownership cannot be computed over. REQUIRED: account_id, plus at least one of company_id or ownership. `ownership` is one of: - "workspace" — the business's own account - "counterparty" — someone else's, seen on an invoice or a payment - "unknown" — not yet classified **This changes figures, not just a label.** An account marked "workspace" puts its transactions inside the internal-transfer rule: a movement with both legs on owned accounts stops counting as money leaving the business. Marking a counterparty's account as the workspace's own therefore removes real spend from the burn, quietly and consistently, with no error anywhere. So do not guess it. An account's owner cannot be read off its name, its bank, or the company that appears most often beside it. Ask, or leave it `unknown` — "not yet classified" is a truthful state and a wrong classification is not. `company_id` must name a company in the SAME workspace as the account; a company from another workspace is refused rather than resolved. Pass `company_id: null` to detach. Returns { success: true, account_id, ownership, company_id } on success.
well_assign_missing_invoice_owners
Set the owner SET of the missing-invoice TRANSACTIONS you name — the only write for missing-invoice ownership. REQUIRED: transaction_ids — the settled lines still missing a supplier invoice, from well_list_missing_invoice_owners. owner_person_ids — the people who together owe those invoices; pass an EMPTY array to clear the owners. Ownership is per TRANSACTION and is a SET, not one owner and not a card rule. The write REPLACES the owner set on every named transaction: the people you send become its owners and anyone not sent is removed. Assigning several people to a (counterparty × month) gap creates ONE proof task per distinct person, and ONE supplier invoice resolves every owner's task for that gap — the fan-out is for accountability, not for N separate collections. Tell the user this plainly. Each person_id must already be a member of the workspace (get them with well_query_records on people). A person outside the workspace is refused (refusal_reason NOT_FOUND), not silently dropped. Closed periods are frozen: a transaction whose fiscal month already closed refuses the whole batch (refusal_reason CLOSE_OWNER_PERIOD_FROZEN) rather than rewriting a committed close. A transaction id the workspace does not own refuses the batch too (refusal_reason NOT_FOUND).
well_claim_statement_draft
Claim a bank statement the user already dropped on Well's website, using the claim token from their message. Use this when the user's message carries one or more statement claim tokens — they dropped the files on wellapp.ai before opening this conversation, and the bytes are waiting on Well's side. It saves them re-attaching the files here. Each token works ONCE and expires an hour after the drop. Call this tool once per token. On success the statement enters the same import pipeline as an in-app upload (detection, dedup, promotion), and the parsed rows and totals arrive via well_get_statement_import_result with the returned document_id. If a token is refused as expired or already claimed, tell the user plainly and ask them to attach the file to this conversation instead — do not retry the token.
well_create_company
Create a new company in the current workspace. Use this tool when the user asks to create, add, or register a new company. REQUIRED: name OPTIONAL: description After creation, enrichment (logo, domain, industry, tax ID, description fill-in) runs asynchronously in the background. The new company is available immediately for follow-up actions, but enriched fields may take a few seconds to populate — re-query after a brief delay to see them. Returns { success: true, company_id, name } on success, or { success: false, error } on failure.
well_create_company_candidate
Mint a company candidate from a registry hit, the step between finding the company and creating its workspace. This is the deliberate pick the confirm-your-company card makes. REQUIRED: registry_ref — the `id` of a well_search_company_registry hit. This tool hydrates that hit and mints the company as a primary (own-company) candidate. Then call well_create_company_workspace with the returned `candidate_id` to make it the company workspace. Only a workspace owner or admin may mint a candidate. A caller without that role is refused, not silently ignored. When the picked company already has a confirmed company workspace, the result carries `linked_to_existing_child: true` and its `workspace_id` — switch into it with well_switch_workspace instead of creating another. Confirm the exact company with the user before calling; never pick one from a name alone.
well_create_company_workspace
Create the company workspace from a candidate, the step that turns a picked company into a workspace the close runs in. This anchors the candidate's company as the new workspace's own company and links it to the membership it was created under. REQUIRED: candidate_id — from well_create_company_candidate. This mints the child workspace, projects its accounting settings from the country defaults, anchors its own company, and writes the lineage row, so well_switch_workspace can move into it in the same conversation. Idempotent: calling it again on the same candidate returns the same child, with already_anchored true. Only a workspace owner or admin may create the company workspace. A caller without that role is refused, not silently ignored. Confirm the company with the user before calling.
well_create_invoice_document
Render an existing invoice as a print-ready PDF and attach it as the invoice's source document. The letterhead carries the issuing company's own mark when Well has one on file, and otherwise sets the issuer's name as text. Never promise a logo. Use this tool when the user asks to generate, render, or attach a PDF for an invoice that already exists in the workspace. This does NOT email or send the invoice anywhere — it only creates and attaches the file. REQUIRED: invoice_id (the invoice must already exist) Refused if the invoice is already linked to a REAL ingested document (an upload, a connector import, or a provider-issued PDF) — that source of truth is never overwritten. Returns { success: true, invoice_id, document_id, reference_number, file } on success, or { success: false, error } on failure. `file` carries the rendered PDF's name and size plus the links to fetch it: `download_url` (saves the file), `signed_url` (opens it), and `app_url` (the document in Well). Hand the user `download_url` when they ask for the PDF itself. Both signed links stop working at `expires_at`; `app_url` does not.
well_create_invoice_from_data
Create an invoice in Well from data you extracted by reading an invoice (your own OCR) — you send the structured fields, not the file. Well persists the invoice + its line items + payment means using the same pipeline as uploaded documents. Fill every field you can read from the document: - issuer / receiver: { name (required), company_id?, domain?, tax_id? } - reference_number, issue_date (YYYY-MM-DD), due_date? (YYYY-MM-DD), currency (ISO 4217) - totals?: { items_total?, tax_total?, grand_total } - line_items[]: { name, quantity?, unit_price, currency?, tax_rate? } - payment_means?[]: { type, iban?, bic?, scheme? } - status?: draft | issued | paid | canceled ONE CALL IS THE WHOLE WRITE. This tool takes the invoice's status and both parties' company ids, so a create never needs a well_update_invoice after it: - The user asked to DRAFT an invoice → pass status: "draft" here. - You already found the company (well_query_records, well_get_entity) → pass its company_id on that party. Naming the party without its id re-resolves it, which can attach the invoice to the wrong company or create a duplicate one. Creating and then patching the same invoice writes twice and shows the user two confirmations for one action. Put the intent in this call.
well_create_person
Create a new person (contact) in the current workspace. Use this tool when the user asks to add, create, or register a new contact, employee, or person. REQUIRED: first_name OPTIONAL: last_name, job_title After creation, enrichment runs asynchronously in the background. Returns { success: true, person_id, full_name } on success, or { success: false, error } on failure.
well_create_statement_upload
Mint a one-time, short-lived upload slot for a bank-statement file. Use this when the user has a statement file (PDF, or a large CSV/XML) to import; the file's bytes do not travel through the model. It returns a single-use upload URL + token; the client (or the user) POSTs the raw file bytes to that URL, and the resulting document enters the exact same import pipeline as an in-app upload (detection, dedup, promotion). This result renders a card in widget-capable hosts right away — do not wait for a poll to make it appear. One statement file per call: mint a separate slot for each file. Once the client has uploaded the file bytes, call well_get_statement_import_result with the document_id below one time to learn the outcome. The card polls the import result itself until it settles, so call that tool again only if the user asks. The token authorizes exactly ONE upload to this workspace and expires in 15 minutes. It is burned on first use — a second upload needs a new slot. It cannot be used for anything other than a statement upload. The response's document_id is PRE-ALLOCATED at mint time — the upload has not happened yet, and this exact id is what the document will carry once it does. A call to well_get_statement_import_result before the upload lands is a normal "not_found_yet", not an error.
well_delete_company
Delete a company from the current workspace (soft delete). Use this tool when the user asks to delete, remove, or archive a company. REQUIRED: company_id This soft-deletes the company and its company_person relationships. Linked people records themselves are NOT deleted. Invoices and documents referencing the company are preserved. Returns { success: true, company_id } on success, or { success: false, error } on failure.
well_delete_invoice
Delete an invoice from Well (soft delete). REQUIRED: invoice_id Soft-deletes the invoice. Linked line items and payment_means rows are NOT cascade-deleted — they remain in the database, orphaned. The delete is reversible only at the database level.
well_delete_person
Delete a person (contact) from the current workspace (soft delete). Use this tool when the user asks to delete, remove, or archive a contact. REQUIRED: person_id This soft-deletes the person and its company_person relationships. Linked companies themselves are NOT deleted. The authenticated user cannot delete their own person record. Returns { success: true, person_id } on success, or { success: false, error } on failure.
well_enqueue_invoice_fetch
Queue invoice collection for named counterparties. This creates one durable backlog task per counterparty. The browser agent (a provider that carries a blueprint or a real portal URL) or the manual-upload route picks up each task later. Call this tool only after the user explicitly confirms the launch. Never call it on your own initiative. Get counterparty_company_ids from well_list_missing_invoices. This tool takes no period argument: a collection task belongs to a counterparty, not a month. A repeat call for a counterparty that already has a non-terminal task reuses that task (already_active: true) instead of creating a second one. provider.has_blueprint and provider.has_portal_url on an enqueued row state which counterparties a browser agent will visit (either one is enough), and which fall back to manual upload (neither). Creating the tasks launches nothing in the browser. Inside Well the tasks page and the chat card start and track them. From outside Well, hand the user the collect_url from well_preview_invoice_fetch to start the runs. That link never covers every enqueued counterparty. One link names at most 25 portals, so a counterparty past that ceiling appears in well_preview_invoice_fetch's collect_url_omits instead of on the link. A counterparty with provider: null has no portal at all and is routed to manual upload. A counterparty with an address the link cannot carry appears in collect_url_unaddressable. Never tell the user the link covers a counterparty it does not name. Use well_preview_invoice_fetch first to see what a fetch would cover — it is read-only and launches nothing. Use well_enqueue_close_invoice_fetch instead of this tool when you are inside a close run: it is the same action, scoped to that run's flow_run_id. Use this tool outside a close run. Report the counts back to the user: how many tasks were enqueued, how many of those were already active, and how many counterparties were skipped, with each skip's reason.
well_get_accounting_settings
Read the workspace's accounting settings and their provenance WITHOUT showing the user anything: country of incorporation, incorporation date, tax ID, fiscal year start, base currency, and accounting framework. Each field carries its value, where the value came from, and the stored "Suggested" fills. This draws nothing on the user's screen and asks for no confirmation. Use it ONLY for a silent CHECK the model acts on itself: the close-books step deciding whether the fiscal year start and the base currency are already present and trusted before it moves on, a step that needs the current framework or start month to compute something. Read the fields and act in the same turn — there is no card and no click to wait on. ⚠️ To have the USER review or CONFIRM the settings, call `well_show_accounting_settings` INSTEAD — that one draws the card the user completes and confirms. This tool cannot draw one, so a confirm step run here leaves the user with nothing to act on.
well_get_connector_coverage
Read what a workspace has CONNECTED and what it can connect. This draws nothing on the user's screen. Use it for every coverage CHECK: a data skill confirming a bank is connected before it measures anything, a step that needs a `workspace_connector_id`, a health read on a connector the user asked about. Read each row's state and hand the answer back in your own words, in the same turn — there is no card to wait on here, and no acknowledgement to ask for. ⚠️ FOR A CONNECT STEP, CALL `well_list_connectors` INSTEAD. Same scope arguments, same rows, and its result draws the card with the install links and the Continue the user clicks. This tool cannot draw one, so a connect step run here leaves the user with prose and no way to act. Do NOT read workspace_connectors records to work out connection coverage; this tool is that answer. Each entry has: - service_id: the connector's stable catalog id (e.g. "stripe"), used in the install link. - name, category_id, direction: what the connector is. - data_domains: the financial domains it serves — any of "bank", "accounting", "invoicing" — or null for a non-financial connector. One connector can serve several domains (Qonto serves all three). "bank" here means the connector delivers cash movements, which a payroll or billing platform also does; do NOT read it as "this is a bank". To list banks, pass kind: "bank", which the server scopes on its own bank classification. - invoice_source: this connector can bring supplier invoices into Well, either because it issues or holds them (an accounting or an invoicing tool) or because invoices arrive through it as files (a mailbox, a messaging app, a file drive). Read it to decide which tools to offer for a missing-invoice hunt. It is a property of the connector, not of this workspace's connection. - reason: why this row is on the card. "catalog" is the list that was asked for. "picked_vendor" is a connector behind a counterparty the user picked. Say which is which; never present a catalog row as one the user chose. - status: "available" connectors are connectable now; "coming_soon"/"unavailable"/"maintenance" are not. - is_matched / is_selected: whether this workspace's detected tools matched this connector / already picked it. - match_score: 0..1 confidence of that match; null when unmatched. A high score is a tool Well is confident the workspace already uses. - is_connected: this workspace has a connection you should REPAIR or MANAGE, not install fresh. True for "enabled", "processing", "error" and "need_reconnect"; false for "to_configure" and "disabled", where a fresh install IS the right next step. - connection_status: the existing connection's state, or null when this workspace has no connection row for the connector at all. One of: - "enabled" — connected and syncing. - "processing" — the grant is in and the FIRST sync is still running; data may be partial. Connected: do NOT ask the user to connect it again. - "error" — authenticated but its last real sync failed. Offer install_url as a reconnect. - "need_reconnect" — the grant is dead and only the user can restore it. Offer install_url as a reconnect, NOT a first install. - "to_configure" — a connect attempt that never completed its handshake. Nothing is connected: offer install_url as a first install, and never claim the tool is connected. - "disabled" — the connection was torn down. Offer install_url as a first install. A "degraded" connector never appears: it is resolved server-side against its own sync history into "enabled" or "error", so you never surface a state that clears itself. A connection whose state this build cannot read also reports null, and there is_connected stays true — read the two fields together, and treat "null status, is_connected true" as an existing connection whose health is unknown. - workspace_connector_id: the connection instance's id, or null when there is no connection row. This is the id well_invoke_connector_tool and well_list_connector_tools need — resolve it HERE, never via well_query_records on workspace_connectors. - last_successful_sync_at: ISO timestamp of the last SUCCESSFUL sync, or null when none has landed yet. An "enabled" connector with null here has a valid grant but has never delivered data. - sync_in_progress: a data sync is running right now. Tell the user to wait rather than to act. - is_preselected: Well recommends connecting this one now (a high-confidence match with is_connected false). The interactive picker pre-checks exactly these. A "to_configure" or "disabled" row can still be pre-checked — installing it IS the fix. On kind: "accounting" at most ONE row carries it — the single highest-confidence accounting tool — because connecting the accounting software is a pick-one step; every other scope pre-checks each high-confidence match. - install_url: a one-click link that STARTS or REPAIRS the connection in Well. It works from any state — it signs the user in if needed, creates their workspace if they have none, then runs the connector's own auth flow — and it covers banks too (a bank opens its bank-login flow pre-selected). Null only when the connector is not "available". Hand this to the user to get started in one click. - countries: the ISO 3166-1 alpha-2 countries this connector serves, or null when none is known. It is what the country scope sorts on; use it to explain why a bank fits the company, never to hide a bank the country field is null on. install_all_url is a TOP-LEVEL field, not a per-connector one. It is ONE link that installs every installable connector in this result that is not already connected, in the order they are listed. Its reach is wider than the per-row links: a connector the catalog holds by service id alone carries a null install_url and is still installed by this link, so never read a null install_url as "cannot be installed". When the answer offers several connectors to install, hand the user install_all_url and do NOT list the per-connector install_url links beside it — the one link IS the whole offer, and a table of links beside it puts the reader back through several sign-ins. One link carries at most 10 connectors, and install_all_omitted names the service ids it left out, so offer those rows their own install_url. install_all_url is null when the result offers nothing to install. It is null too wherever the result names no set the user has chosen: the unfiltered catalog and the whole bank domain never carry the link, and a name search or an accounting or invoicing domain carries it only while the WHOLE result fits in one link and this page holds all of it — past that the link would stand for whichever rows the page happened to carry. from_selection always carries the link, however many vendors were picked, because the user named each one. Where install_all_url is null, the rows' own install_url links ARE the offer: list them, and never announce a batch link the result does not carry. Paging: page with offset and page_count, never with the length of connectors. On the first page of an unsearched browse the workspace's ALREADY CONNECTED connectors are prepended so the catalog's ordering cannot bury them past any page you would ask for — so connectors can be longer than the page it came from, and page_count is the catalog window's own length. Advance by offset + page_count; total counts every matching connector across all pages. Those prepended rows carry is_connected true (or a to_configure/disabled state), so a workspace's live tools are visible without paging for them. Scoping: pass kind ("bank" | "accounting" | "invoicing") to get only the connectors serving that domain — the whole set, server-ordered, including the long tail of bank institutions. Pass kind: "upload_surface" for the places invoices ARRIVE — mailboxes, messaging apps, file drives. Pass kind: "storage" for the drives Well FILES INTO — Google Drive, Dropbox, OneDrive — the step that asks where Well should write the documents it collects. Neither of those two is a financial domain: the server resolves each from the connectors' own display categories, so read the rows it returns and never re-derive the set from category_id yourself. They are opposite DIRECTIONS on the same drives, so a "storage" row carries direction "output" and no data_domains, and its is_connected reports the workspace's own file-drop connection, never the drive's separate invoice-source connection. Pass q to name-search the full catalog. Omit all of them for the curated, matched-first view. Use well_list_connector_tools for a live connection's actions. Country: pass country (ISO 3166-1 alpha-2, e.g. "FR") on a connect-a-bank step so the banks that serve the company's country sort first, then the ones that serve its region, then the rest. It reorders the page only — no row is dropped, and a country the catalog carries no coverage for is left in place — so a bank the user names is still found with q. Take the country from the workspace identity; omit it when the country is unknown, and the order is unchanged. Pass from_selection: true for the connect step that FOLLOWS a vendor pick. It returns the connectors behind the counterparties the user picked on the missing-invoices card this session, and NOTHING else: only the picked vendors' connectors, and of those only the ones that can bring an invoice in (reason "picked_vendor"). It offers no accounting or invoicing tool the user did not pick — that offer belongs to its own step, scoped with kind. When the pick leaves no row, the list is empty and the card is not worth drawing. ⚠️ **This tool draws its card on EVERY call, the empty one included**, so never call it with from_selection to find out whether the pick has a connector behind it. Ask `well_get_worklist_status({ worklist: "invoice_sources_for_pick" })` first: it draws nothing, and it reports how many of the picked vendors carry a connector that can bring an invoice in. Make the from_selection call only when that count is above zero. An ABSENT count is not a zero: the probe answers `success: false` when it could not read the pick at all, so retry it rather than reading its silence as a vendor with no connector. row_count reports the same number back on this result. picked_vendors_filtered counts the picked vendors' connectors that were dropped for bringing no invoices in: when it is above zero, say a filter ran rather than letting a short card read as a pick nobody made. It takes no q and no kind — those browse a catalog, and this names a set already decided. An empty list means the session holds no pick for this workspace, or no picked counterparty matched a connector. Every result carries scope — "catalog", one of the three domains, "upload_surface", "storage", or "picked_vendors" — naming what the list IS. The card words its title from that field, so a caller must not describe the result as a domain the scope does not name.
well_get_cost_structure
Get the workspace's cost structure: outflow for the latest closed month, broken down by category — the exact same computation and numbers the Well app's canvas cost-structure donut chart shows. Use this instead of summing/grouping transactions yourself. Returns `entries` (an array of `{ category, amount, pct }`, sorted by amount descending) and `currency` (the workspace base currency). `amount` is a magnitude (outflow), not signed. An entry also carries `category_key` when `rung` is "category_key", except on the rolled-up "Other" slice; that key is what `well_sum_transactions` accepts in `exempt_categories`, so use it to exclude a slice rather than matching its `category` label. On any other rung no entry carries a key, and there is nothing to exclude by — say so rather than passing a label. `period_start` and `period_end` are the inclusive `YYYY-MM-DD` bounds these amounts cover — always a single month. Read the period from those fields and state it whenever you present the numbers. Never derive it from today's date. Never present the figures as a quarter or a multi-month span. If both fields are absent, say the period is unknown rather than naming one. `rung` names which grouping actually produced these categories — "ledger_account" (the workspace's own chart of accounts), "category_key" (Well's category catalog, the only rung that carries `category_key` on each entry), "category_normalized" (Well's stored category label), "transaction_type" (a technical fallback bucket), or "uncategorised" (no rung qualified — either nothing covered the month, or a rung had the coverage but too few labelled rows). State it when you present the breakdown so the user knows whether they're looking at their own ledger's categories or Well's. `label_provenance` says whether a human owns those labels, which `rung` cannot — "curated" (a person set or confirmed every one), "machine" (none were confirmed by a person), "mixed" (some of each), or "unlabelled" (the breakdown is not grouped on a category, so the question does not apply). When it is "machine" or "mixed", say the categories were assigned automatically and may be wrong on any individual line, rather than presenting them as the user's own categorization. If `hints` are present (e.g. a coverage caveat about uncategorized spend), disclose them rather than presenting the breakdown as unconditionally complete. Pass `year` + `month` to break down a specific past month instead of the latest closed one. Read the month you actually got from `period_start`/`period_end` either way — a requested month with no data still comes back with whatever the endpoint could cover. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_get_customer_einvoicing_details
Get one CUSTOMER's e-invoicing identity — the registry values an invoice to that customer is routed on, or that a period aggregate for it is reported under. Use this when the question is about the party the workspace BILLS: "can we invoice this customer electronically", "what is their SIREN / VAT number / billing address", "what do we still need before we can route this invoice". Read `well_get_own_company` instead when the question is about which company the workspace ITSELF is. Returns `customer_kind` ("company" routes an invoice, "individual" reports a sale, absent when the customer's type is not stated), `customer` (the composite: `company_id`, `name`, `subline`, `identified`), `fields` (one entry per detail the graph can hold, each with its `value` and `provenance` when one is held), `unstorable_fields` (details this flow needs that no column holds yet), `hints` and `connectors_url`. A field listed in `unstorable_fields` is not a gap the user can close — say plainly that Well cannot store it yet, and never ask for it. A `fields` entry with no `value` IS answerable and is what still blocks the route. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_get_design_tokens
Get Well's colours, shape and type vocabulary, so a view you compose for Well data looks like Well rather than a generic page. Call this ONLY when you are about to render something yourself — an HTML artifact, a report, a chart you are drawing. You do not need it to answer in prose or in a markdown table. Do NOT use it to restyle a card a Well tool already drew. Where a tool ships its own card the host renders it, and a second styled copy of the same figures is a duplicate, not an improvement. Returns `colors` (roles, not raw token names — `page_background`, `card_surface`, `text_primary`, `accent`, `positive`, `negative`, ...), `series` (categorical chart colours in the order to consume them), `shape` (corner radius and gap), `fonts`, and `color_scheme`, which tells you which ground to compose against. When it is absent the stylesheet did not declare one — pick a ground from `page_background` rather than assuming. Values come from the same token package the Well app, the browser extension and the tool cards compile against, so they cannot drift from the product.
well_get_entity
Read ONE entity with its sub-resources nested in a single call. Convenience over well_get_schema + well_query_records: resolves the field paths for you and returns the single record with its related data expanded. depth (relation-nesting BOUNDARY, 1-3, default 1): 1 = the entity + its direct sub-resources (emails, phones, locations, …) 2 = + the sub-resources' related scalars 3 = the full level-3 graph (LARGER payload — use when you need the whole picture) Stops at depth 3. Aggregates are excluded. Each child collection is capped at 50 rows; for a full list or to page a large child collection, use well_query_records on that child root instead.
well_get_investment_holdings
Get the live holdings/positions (what's currently held and its value) for a connected Plaid investment account — brokerage, IRA, 401k, etc. WORKFLOW: 1. well_list_connectors() → pick the ENABLED Plaid connector (connection_status: "enabled") and read its workspace_connector_id directly off the row. 2. well_get_investment_holdings({ workspace_connector_id }) → the current holdings, fetched fresh from Plaid on every call (never stored/stale data). Only works on Plaid connectors that support the investments product — not the MCP-transport connector-tool-passthrough tools (well_list_connector_tools / well_invoke_connector_tool), and not for investment transactions (buy/sell/dividend/fee), which are queryable as ordinary rows via well_query_records on the transactions root instead.
well_get_own_company
Get which company the workspace itself is: the confirmed own-company anchor (`anchor`) and any detected companies not yet confirmed as it (`candidates`). Use this whenever a question turns on "mine" versus "theirs" — my payables, my receivables, invoices I owe, what we billed — and then filter by the `company_id` this returns. Never decide which records are the workspace's own by comparing a company NAME: the same legal entity appears under several labels (a registered name, a trade name, a bank-issued label), so a name filter silently drops rows. Returns `anchor` (`company_id`, `registered_name`, `trade_name`) or null when the workspace has not resolved one yet, and `candidates` (each with `company_id`, names, `role`, `confidence_score`, `state`). `anchor: null` means the workspace has no confirmed own company. Say so plainly and do not promote a candidate to the anchor yourself — a candidate is a detection, not a decision, and confirming one is a user action. ⚠️ TO ASK THE USER WHICH COMPANY on a card so they can pick or search for it, call `well_show_company_candidates` INSTEAD: it draws a tile per candidate with a registry search and waits for the click. This read draws nothing. Registry tax ids and registered addresses are deliberately not returned. Call this directly — no other tool call is needed first. Both the anchor and the candidates are read from the same workspace this call is scoped to.
well_get_schema
Discover available data types and fields. USAGE: - well_get_schema() → List ALL available roots, including the accounting graph (ledger_accounts, journals, journal_entries) plus account_balances, tax_rates, exchange_rates — query these for real financial statements (compte de résultat / balance sheet) instead of reconstructing them from raw invoices - well_get_schema({ root: "invoices" }) → List all available fields for invoices WORKFLOW: 1. Call well_get_schema(root) to see available fields 2. Pick the fields you need for your task (typically 5-15) 3. Call well_query_records with those specific fields Returns fields with path, type, and (when documented) semantic context: - { path: "invoices.grand_total", type: "numeric", context: "Total invoice amount incl. tax in the document currency...", enrichment: "AI extraction" } → use _eq, _gt, _lt, etc. - { path: "invoices.local_currency", type: "enum" } → use ONLY _eq, _neq, _in, _nin, _is_null - { path: "invoices.issuer.name", type: "text" } → use _eq, _like, _ilike, etc. - "context" (when present) explains what the field MEANS in the domain and how it's used — read it to pick the right field and write correct filters. - "enrichment" (when present) is the value's provenance (e.g. "Bank sync", "AI extraction", "System generated", "Derived", "Manual"). Use the type to choose the right whereClause operators in well_query_records. To use in well_query_records, convert path to array: "invoices.issuer.name" → ["invoices", "issuer", "name"]
well_get_session_digest
Get everything a returning person's first answer needs, in one call: what happened in the workspace since they last looked, where the workspace stands now, and the Well skills that can take it forward. When the person asks what happened since last time, asks to be caught up, or opens a session, do not call this first: load the `signing-back` skill with well_get_skill and follow it. That procedure greets, reads this digest, and proposes the next steps; calling this tool alone skips the greeting and the proposals. Call this tool directly only when a loaded Well skill says to, or when the person asks for the raw counts and nothing else. Returns `records` (one entry per record type with its created / updated / deleted counts and the connectors those creations came from), `errors` (the pipeline failures worth acting on), `skills_run` (the Well skills this person already ran recently, so you do not propose one they just finished), and `boundary` + `since_at` saying where the window starts. `is_first_session` true means there is no earlier moment to report from: greet the person and skip the recap. `truncated` true means the window stopped at 5000 events and the counts cover part of the tail only. Also returns `situation`, the state behind the recap, so no follow-up read is needed: `connectors` (the tools this workspace connected, each with its `connection_status` and `last_successful_sync_at`, beside `connected_count`; the size of Well's catalog is not carried, because it is never a figure to tell the person), `open_period` (the month Well opens the close on, with its `label`, `is_complete` and `selectable`), and `missing_invoices` (that month's `row_count` of counterparties with settled spend and no invoice, plus its `hints`). Each part is null when its read refused or had nothing to read. A null says the part is UNKNOWN: never report it as an empty connector list, a workspace with no open month, or a month owing nothing. And `skills`: the whole Well skill roster, the same entries well_search_skill returns, so a step a click names is loaded with well_get_skill rather than searched for again. `roster_readable` false says the roster could not be read at all, so `skills` is empty because nothing loaded: propose no next step in that turn, because every slug would be invented. And `suggested_steps`: the five next steps Well ranked for this workspace, each a `skill` from the roster and the `reason` it ranks there. The rubric runs on the server: open gaps first (no bank, a month owing invoices, no accounting tool, a stale sync), then the open month's close, then the analysis skills, and never a skill served in the last day. Hand these five to well_propose_next_steps in this order and rank nothing yourself. The sentence each one is offered as is yours to write, in the language the person is using, from that skill's own quoted utterances in `skills` and the figures above. When the list is empty, nothing was offerable: call no render tool and write no five of your own, say so in one line. Pass `mark: true` to advance this person's read cursor to the end of the window, so the next digest starts where this one ended. That cursor is shared with the app, so marking here also clears what the app shows as unread. Pass `mark: false` (or omit it) to inspect the digest without moving anything. The figures are computed by Well. State them as returned: do not re-count, round, or total them yourself. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_get_skill
Get the Well procedure for a job, written by the Well team, and follow it exactly. Returns ONE markdown document: the instructions for the thing you are about to do. Its content is the instruction, not background reading — do what it says, in the order it says, and do not substitute your own plan for it. A document may tell you to run other Well skills. Load each one with this tool, by the id the document names, at the moment the document says to. Use well_search_skill first when you do not already hold the id. The catalog is fixed for the life of the server, so re-fetching a document you already hold buys nothing.
well_get_statement_import_result
Read the outcome of a bank-statement upload started with well_create_statement_upload, by the document_id that tool returned. well_create_statement_upload already renders a card from its own result — this tool does not create or redraw it. Call it once, shortly after the client has uploaded the file bytes, to learn what happened. The card polls this import result itself until it settles, so call this tool again only if the user asks. - status "not_found_yet": the upload has not landed yet — a NORMAL result right after minting the slot, not an error. Poll again once the file has been uploaded. - status "processing": the file is uploaded and the statement is still being extracted / promoted. - status "imported" | "needs_account" | "duplicate" | "skipped" | "failed": the terminal outcome. On "imported", matched_count / review_count / minted_count / already_present_count report the promotion's own snapshot counts, taken once at import time and covering every promotable line of the file disjointly; null on any of them means the row predates count tracking — treat as unknown, never as 0. `records` lists the minted transactions only — matched or ambiguous lines link an existing transaction and are excluded; `graph` is the frozen record graph for the same snapshot; `records_url` opens the workspace's transactions table. This tool reads only — it changes nothing.
well_get_worklist_status
Ask whether a repair gate is still OPEN, without drawing its card. Call this BEFORE the worklist read whenever you are checking rather than repairing — the first pass of a gate, and every re-check after the reader has cleared one. `open: false` means the gate is settled: carry on, and call nothing else. **Only call the worklist read when this says `open: true`.** Those reads draw a card on every call, empty included, so reaching for one to find out whether there is anything to do puts a picker with no rows and a dead button in front of the reader. The tool that draws each card comes back as `card_tool`. WORKLISTS, and the scope each one needs: - `accounts_needing_company` — the accounts with no company attached, or whose ownership is still unknown. No scope. - `uncategorized_window` — the transactions in a window carrying no category. Needs `from` (inclusive) and `to` (EXCLUSIVE), both `YYYY-MM-DD`. - `unposted_transactions` — a period's categorized rows still missing the ledger account they would post to. Needs `fiscal_year` and `fiscal_period`. - `invoice_sources_for_pick` — how many of the vendors the user picked on the missing-invoices card carry a connector that can bring an invoice in. No scope: the pick is on this session's own lane. Ask it BEFORE any `well_list_connectors({ from_selection: true })` call, and make that call only when this answers above zero — a pick with no invoice source behind it draws a picker with no rows and a dead button. - `counterparties_to_categorize` — the counterparties whose invoices the named months are still missing and that carry no industry category. Needs `periods`, the same `[{ calendar_year, calendar_month }]` list the card takes. A scope field the named worklist needs is REQUIRED. Omit one and this refuses: a gate reported clear over the wrong window cannot be told from one that is genuinely clear, and the figure behind it would be computed on that. **`success: false` means the gate is UNKNOWN, not clear.** `open` is ABSENT on that path, so a failed read can never be mistaken for a settled worklist. Retry once; on a second failure say the gate could not be read and stop, rather than computing a figure on evidence you never obtained. Most worklists report no COUNT. One row answers "is it open", and the count of what is left comes from the worklist read itself — which you are about to call anyway when the gate is open. Two kinds are the exception and carry `count`. `invoice_sources_for_pick` reads its whole set by id in one go, never paged, so the number comes free. `counterparties_to_categorize` reads the whole month population rather than one row, so the number is already in hand, and it is the same number the card reports as `uncategorized_count`. COST: `counterparties_to_categorize` reads each named month's spend — the same reads its card makes, and a scope with work left in it pays for them TWICE, once here and once when the card draws. A clean scope pays once and skips the card entirely, which is what the check buys. Probe the months the user actually named, not a whole year "to be safe". When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call. That holds for every gate but two. `invoice_sources_for_pick` and `counterparties_to_categorize` follow their cards instead: `well_list_connectors` and `well_list_counterparties` both answer from the token's primary workspace when you name none, so those gates answer from the same one. A probe that refused where its card answers would be describing a different workspace from the card it stands in for. The result names the workspace that answered.
well_invite_members
Invite one or more teammates into a workspace, or into a workspace group. Use it after well_list_member_candidates, on the people the user chose. Pass `invites` — 1 to 20 `{ email, role }`, role `admin` or `member` — and a `target`: `{ kind: "workspace" }` for this workspace, or `{ kind: "group", group_id }` for a group you belong to. Only a workspace owner or admin may invite; a caller without that role is refused. Returns one `results` entry per invite: `status` `sent` (a new invitation), `reissued` (an already-pending address got a fresh link), or `refused` — with `refusal_reason` naming why, ALREADY_WORKSPACE_MEMBER when the address already has access and INSUFFICIENT_PERMISSIONS when the caller may not invite. `invitation_email_sent` is false when the invite persisted but the email did not leave, so offer a resend. Never invite an address already active in the workspace.
well_invoke_connector_tool
Run one tool on a connected provider's own MCP server, on behalf of this workspace's connection: an action the user asked to take there (create a record in Attio), or a read of content Well does not sync (a page in a docs tool, a note in a CRM, a file the user pasted a link to). It is NOT a way to read financial data: Well already syncs invoices, transactions, accounts and the accounting graph from every connected provider — read those with well_query_records instead of calling a provider's own list/read tools. WORKFLOW: 1. well_list_connectors() → pick the ENABLED provider (connection_status: "enabled") and read its workspace_connector_id directly off the row. A workspace_connector_id the user pasted is fine to use as-is: it is resolved inside this workspace, and an id that does not belong here fails server-side. 2. well_list_connector_tools({ workspace_connector_id }) → the live tool names + input schemas that connection actually exposes right now. 3. well_invoke_connector_tool({ workspace_connector_id, tool: "<one of the names from step 2>", args: { ... } }). Only works on connectors that expose an MCP server (e.g. Attio, Notion, Linear) and whose connection is enabled. Returns the provider's tool result, or { success: false, error } if the tool failed / is not granted.
well_list_account_balances
List every account on the workspace with its stored balance. Rows only — this tool holds no definition of cash, and returns no figure the app renders. Use it when you are computing a cash figure whose RULES you are stating yourself: which accounts belong to the business, which account types count as cash, which stored field is "the balance", what each currency converts at. The server derives no cash position of its own from this call, so a cash figure starts here: state the rules, keep exactly the rows they admit, then put the result on a card with `well_render_cash_position` — or, when the answer is cash month by month rather than one total, with `well_render_cash_forecast`. **It applies no scope.** Every active account comes back, including ones you will almost certainly exclude. `ownership` is `workspace`, `counterparty` or `unknown`, and it decides membership together with `company_id`: - `workspace` — the business's own, EXCEPT when `own_company_id` is set AND the row names a different company. A row with no `company_id` is trusted, because a connector tags a row before any holder is known; so is a row naming a company while `own_company_id` is still `null`, because nothing has disproved the pairing yet. Only a tag contradicting a resolved anchor is stale, and counting that one widens the owned scope and overstates the figure. - `counterparty` — not the business's, unconditionally. - `unknown` — unsettled, and settled ONLY by the anchor: own when `company_id` equals `own_company_id`, a counterparty's when it names a different one. `own_company_id` is `null` when the workspace has not set one. Nothing is settled against it then — no `unknown` row, and no `workspace` row's company pairing either — so say so rather than counting or dropping on a guess. This is the same three-way rule the app's own canvas account scope applies, and a figure that departs from it disagrees with the number the product shows. **It applies no type filter.** `account_type` is one of deposit, credit, loan, investment, payroll, other. A credit or loan account is a liability, so its balance normally nets out of cash rather than adding to it — but that is your decision to state, not a fact about the row, and the sign stored is the sign the provider sent. **It chooses no amount.** `closing_booked` is SETTLED cash; `closing_value` includes pending and uncleared movements. The two differ by every initiated-but-unsettled payment, so which one you total is the single most consequential choice a cash figure makes: state it. `opening_booked` is the fallback for a freshly-opened balance with no settled activity yet. Any of them is `null` when the stored value was absent or not a finite number, which is not a zero balance. **It converts nothing.** Each reading carries its own `currency`, which can differ from the account's own `account_currency`. Convert per row at a rate you can state, then total — a sum across currencies is denominated in nothing and no field here would say it happened. **It lists each copy of an account.** One physical account can arrive once per connector that syncs it. `duplicate_of_account_id` names the account a row is a second copy of, and is `null` on every other row. Leave a marked row out of every total and every count: the balance that counts is the named account's, which is the one the app's own figure reads, and the transactions of both copies are counted once. Total both copies and the figure holds that money twice. When the named account has no readable balance, report it as having none rather than taking the copy's reading in its place, or the figure departs from the app's. Marking follows the ownership rule above: only the business's own accounts are marked, and an `unknown` row only once `own_company_id` settles it. `balance` is `null` when no row was selected for that account. Two different situations produce it and they must not be reported the same way: `verification_rejected: true` means the newest balance failed verification and the bounded walk back found no verified one, so the data is repudiated; `false` means the account simply has no history yet. `months_back` adds `month_ends` to every row: one reading per closed month end, oldest first, keyed `YYYY-MM`, ending on the last COMPLETE month. This series is where a cash forecast starts — "what will our cash look like", "project our cash forward", "when do we hit zero" — and it is the settled half of `well_render_cash_forecast`; the projected half is that series' last settled month minus the burn you measure with `well_sum_transactions`. A `null` reading is a month no stored row covered — not a zero balance, so never plot it as one and never interpolate between two real points. Omit `months_back` for the current reading alone; the series is a second query and is not free. `partial: true` means the read was cut short BEFORE RETURNING ANYTHING, so it always arrives with an empty `rows` — it is a fact about the call, never a coverage figure over rows you received. Nothing is known about what is there, so derive no figure from it: say the read was cut short and offer to try again. `unreadable_rows` is the separate case and the only one that continues: the read finished, and that many rows carried a stored balance that could not be parsed. They hold a `null` balance, sit in no figure, and make any total a floor, by up to their count: the count covers every row, whatever its owner or type. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_list_accounts_needing_company
List the workspace's accounts that cannot yet be placed on either side of a transfer, so a figure that depends on account ownership can say exactly what is missing before it is computed. Two states, ONE worklist, because they answer one question — whose account is this: - No company attached. Nothing can place the account on either side of a transfer. - `ownership: "unknown"`. The account has a company, and whether the workspace owns it is unanswered. The second is not the lesser case. An account left `unknown` sits outside the internal-transfer rule exactly as an unattached one does. **This is a gate on a FIGURE, not a tidiness list.** `well_sum_transactions` with `exclude_internal_transfers` keeps the rows with exactly one leg on an account the workspace OWNS, and drops the two-leg ones. So an account's ownership decides whether its movements count as money leaving the business. An account wrongly marked as the workspace's own removes real spend from the figure, quietly, with no error anywhere. **Do not propose an owner of your own.** You cannot read one off an account's name, its bank, or the company that appears most often beside it — a name-shaped match proposes the company minted FROM that name, and the bank that issues an account is not its owner. Where the system HAS a grounded proposal it rides on the row as `company_suggestion`, and the card is where a reader accepts it. `unknown` is a truthful state and a wrong classification is not. Each row carries `account_id` (pass it to `well_assign_account`), `account_name`, `iban`, `currency`, the `company_id` and `company_name` already attached when the gap is the ownership rather than the link, and `ownership`. `own_company_id` names the company that IS the workspace. It is what settles ownership without guessing: an account attached to that company is the business's own, and one attached to any other company belongs to a counterparty. When it is null the workspace has set no anchor, so nothing here settles ownership and the account stays `unknown` until a reader says otherwise. The companies a reader can pick ride alongside the rows, capped. When the workspace holds more than the cap, narrow them with `company_search` rather than assuming the card carries every company. A row whose ownership is already `workspace` carries `company_suggestion`: the company that IS the workspace, which is what such an account belongs to by definition. The field is ABSENT on a `counterparty` or still-`unknown` row, and on a workspace with no anchor set — absent means nothing grounded a guess, never that the row was checked and has no owner. It is a proposal for a reader to accept, not a decision: ownership decides whether an account sits in the workspace's own set at all, so never write it without the reader choosing it. `truncated: true` means the page filled and more accounts exist, so report the count as a floor rather than as the total. **`success: false` means the worklist is UNKNOWN, not empty.** The read failed, so no count exists. An empty `records` on a failed read is not "every account is settled" — treating it that way lets a figure be computed on evidence it never obtained. When the user asks to see, list or fix these rows, call this tool directly: its card is the answer. ⚠️ **This tool draws its card on EVERY call, the empty one included.** So when nobody asked for the list and you only need to CHECK whether anything is left (the first pass of a gate, or a re-check after a repair), call `well_get_worklist_status({ worklist: "accounts_needing_company" })` first. It draws nothing. Call this tool after it only when it answers `open: true`. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_list_burn_exemptions
List the categories a reader can exempt from burn over one window, each with the spend exempting it would remove. This is what the exemption card offers; it measures nothing the sum did not already measure. Each entry in `groups` is one category's OUTFLOW in the window: `category_key` (the id an exemption is matched on), `label` (the category as the product writes it), `amount` (a magnitude, never signed) and `count` (the rows behind it). Sorted by amount descending, so the biggest decision reads first. A category with no outflow in the window is NOT listed — exempting it would remove nothing, so it is not a choice. `total` is the sum of `groups[].amount` and nothing else. `unclassified_amount` and `unclassified_count` are the outflow this list cannot offer as a choice: rows carrying no category, which no exemption ever matches, and rows whose category is outside the shared vocabulary, which this read cannot name for a reader. The two are reported together because both leave the reader's choices unable to touch that money — not because the same thing is true of them downstream. `total + unclassified_amount` is the window's whole outflow, so a reader can see what the choices do not cover. State the unclassified figure whenever it is not zero rather than presenting `total` as the whole window. Pass the `convention` you elected for the burn figure itself. The list and the figure have to sit on one election, and this read deliberately does not make a second one. `partial: true` means the underlying sum measured nothing, so `groups` is empty and nothing is known about the window's spend. Say so and offer to try again, rather than presenting an empty list as a decision. `unreadable_rows` counts rows whose amount could not be read at all; they are in no figure here. `from` is inclusive and `to` is EXCLUSIVE, so a window of whole months passes the first instant of the month after the last one you want. `window` echoes both back exactly as you sent them. Internal transfers are already out. This read keeps only the rows with exactly one leg on an account the workspace owns — the same rule the burn applies — so a movement between the workspace's own accounts never appears here. `currency` is the one currency every row in the window shares. A window holding more than one, or holding a row that carries none, is REFUSED with `success: false` and an `error` saying which: adding two currencies gives a number denominated in nothing, and no field on this result could say it happened. `currency` is the EMPTY STRING only when the window held no row at all, and then `groups` is empty and both totals are zero. The direction convention is elected ONCE over the whole window, never per category. A window whose rows are overwhelmingly negative stores an outflow as a negative amount, and these figures are that branch. A window that stores outflows as positive magnitudes keeps the direction in a field this grouping does not read, and a window that pools both kinds of feed has no single outflow at all — both are REFUSED with an `error` naming the counts behind the decision, rather than reported as spend. Take the reader's answer from the card, record it with `well_switch_workspace` as `exempt_categories`, and read it back with `well_wait_for_selection` (kind "exemptions"). Then pass the same keys to `well_sum_transactions` as `exempt_categories` to compute the burn without them, and name the exemptions beside the figure so it can be read back. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_list_cash_scope
List the account types a reader can count as cash, each with what it holds. This is what the cash-scope card offers; it measures nothing `well_list_account_balances` did not already read. Each entry in `groups` is one account type: `account_type`, `account_count`, and `subtotals` — one native amount per currency, never converted and never blended. Sorted with the largest holdings first, so the biggest decision reads first. A type with no accounts is NOT listed: counting it would change nothing, so it is not a choice. **Only accounts the workspace owns are folded here.** Ownership is settled by a fact about the account, not by a preference, so it is never offered as a choice on this card. `excluded_not_owned` counts what that removed, and `unsettled_ownership` counts accounts whose owner is unanswered — those are NOT in any group, and a non-zero count means the reader has a repair to do before any total is trustworthy. Say it rather than presenting the groups as the whole picture. `unreadable_balances` counts owned accounts whose stored balance could not be read at all; `unreadable_currency` counts those carrying an amount with no currency code anywhere. Both are in no subtotal, so state them beside any figure rather than presenting one that silently skipped them — and keep them apart, because they are different repairs: a balance that did not arrive against a row that arrived incomplete. `folded_duplicates` counts rows left out because they are a second copy of an account already listed, synced once per connector. They are in no group and no count, since the account they copy is counted once. `partial: true` means the underlying read was cut short before it returned anything, so `groups` is empty and nothing is known about what the workspace holds. Say the read was cut short and offer to try again, rather than presenting an empty list as a decision. Take the reader's answer from the card. Nothing is recorded server-side, because nothing server-side consumes it — YOU apply the scope when you total the balances, so the answer travels back to you as the reader's own message. Then call `well_render_cash_position` with the types you counted in `scope.account_types`. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_list_connector_tools
Discover the actions a connected provider exposes (e.g. "what can I do with Attio?"). WORKFLOW: 1. well_list_connectors() → pick the ENABLED provider (connection_status: "enabled") and read its workspace_connector_id directly off the row. 2. well_list_connector_tools({ workspace_connector_id }) → the actions that provider offers (name + description + input schema). 3. well_invoke_connector_tool({ workspace_connector_id, tool, args }) → run one, shaping args from the input schema returned here. Use this whenever you don't already know a connector's tool names — never guess them. Every response also carries reconnect_url: a deep link to the connector's setup page in the web app. When success is false or status is "need_reconnect" (the provider's token is stale/revoked, so no tools come back), give the user reconnect_url so they can re-authenticate the connector. Surface it as a clickable link; never invent connector URLs.
well_list_connectors
List the connectors a workspace can install AND everything it has already connected, each with a one-click install deep link. The result DRAWS THE CONNECT CARD the user clicks in. ONE tool answers both halves of the connect question — "what can I connect to Well?" and "what is connected, still syncing, or broken?" — because every existing connection is overlaid onto its catalog row. Do NOT read workspace_connectors records to work out connection coverage; this tool is that answer. ⚠️ FOR A SILENT COVERAGE CHECK, CALL `well_get_connector_coverage` INSTEAD. Same scope arguments, same rows, no card. A data skill confirming a bank is connected before it measures anything must use that one: this tool renders on every call, so a check run here drops a connect picker into a conversation about something else and then waits for a click nobody meant to make. ⚠️ WAIT ON THE CARD IN THE TURN THAT DREW IT. Write your one line for the user FIRST — the wait holds the turn open for up to a minute, and a user looking at a card with no sentence beside it has been given no reason to click — then call `well_wait_for_selection` on the kind this result names in `next_step`. Each entry has: - service_id: the connector's stable catalog id (e.g. "stripe"), used in the install link. - name, category_id, direction: what the connector is. - data_domains: the financial domains it serves — any of "bank", "accounting", "invoicing" — or null for a non-financial connector. One connector can serve several domains (Qonto serves all three). "bank" here means the connector delivers cash movements, which a payroll or billing platform also does; do NOT read it as "this is a bank". To list banks, pass kind: "bank", which the server scopes on its own bank classification. - invoice_source: this connector can bring supplier invoices into Well, either because it issues or holds them (an accounting or an invoicing tool) or because invoices arrive through it as files (a mailbox, a messaging app, a file drive). Read it to decide which tools to offer for a missing-invoice hunt. It is a property of the connector, not of this workspace's connection. - reason: why this row is on the card. "catalog" is the list that was asked for. "picked_vendor" is a connector behind a counterparty the user picked. Say which is which; never present a catalog row as one the user chose. - status: "available" connectors are connectable now; "coming_soon"/"unavailable"/"maintenance" are not. - is_matched / is_selected: whether this workspace's detected tools matched this connector / already picked it. - match_score: 0..1 confidence of that match; null when unmatched. A high score is a tool Well is confident the workspace already uses. - is_connected: this workspace has a connection you should REPAIR or MANAGE, not install fresh. True for "enabled", "processing", "error" and "need_reconnect"; false for "to_configure" and "disabled", where a fresh install IS the right next step. - connection_status: the existing connection's state, or null when this workspace has no connection row for the connector at all. One of: - "enabled" — connected and syncing. - "processing" — the grant is in and the FIRST sync is still running; data may be partial. Connected: do NOT ask the user to connect it again. - "error" — authenticated but its last real sync failed. Offer install_url as a reconnect. - "need_reconnect" — the grant is dead and only the user can restore it. Offer install_url as a reconnect, NOT a first install. - "to_configure" — a connect attempt that never completed its handshake. Nothing is connected: offer install_url as a first install, and never claim the tool is connected. - "disabled" — the connection was torn down. Offer install_url as a first install. A "degraded" connector never appears: it is resolved server-side against its own sync history into "enabled" or "error", so you never surface a state that clears itself. A connection whose state this build cannot read also reports null, and there is_connected stays true — read the two fields together, and treat "null status, is_connected true" as an existing connection whose health is unknown. - workspace_connector_id: the connection instance's id, or null when there is no connection row. This is the id well_invoke_connector_tool and well_list_connector_tools need — resolve it HERE, never via well_query_records on workspace_connectors. - last_successful_sync_at: ISO timestamp of the last SUCCESSFUL sync, or null when none has landed yet. An "enabled" connector with null here has a valid grant but has never delivered data. - sync_in_progress: a data sync is running right now. Tell the user to wait rather than to act. - is_preselected: Well recommends connecting this one now (a high-confidence match with is_connected false). The interactive picker pre-checks exactly these. A "to_configure" or "disabled" row can still be pre-checked — installing it IS the fix. On kind: "accounting" at most ONE row carries it — the single highest-confidence accounting tool — because connecting the accounting software is a pick-one step; every other scope pre-checks each high-confidence match. - install_url: a one-click link that STARTS or REPAIRS the connection in Well. It works from any state — it signs the user in if needed, creates their workspace if they have none, then runs the connector's own auth flow — and it covers banks too (a bank opens its bank-login flow pre-selected). Null only when the connector is not "available". Hand this to the user to get started in one click. - countries: the ISO 3166-1 alpha-2 countries this connector serves, or null when none is known. It is what the country scope sorts on; use it to explain why a bank fits the company, never to hide a bank the country field is null on. install_all_url is a TOP-LEVEL field, not a per-connector one. It is ONE link that installs every installable connector in this result that is not already connected, in the order they are listed. Its reach is wider than the per-row links: a connector the catalog holds by service id alone carries a null install_url and is still installed by this link, so never read a null install_url as "cannot be installed". When the answer offers several connectors to install, hand the user install_all_url and do NOT list the per-connector install_url links beside it — the one link IS the whole offer, and a table of links beside it puts the reader back through several sign-ins. One link carries at most 10 connectors, and install_all_omitted names the service ids it left out, so offer those rows their own install_url. install_all_url is null when the result offers nothing to install. It is null too wherever the result names no set the user has chosen: the unfiltered catalog and the whole bank domain never carry the link, and a name search or an accounting or invoicing domain carries it only while the WHOLE result fits in one link and this page holds all of it — past that the link would stand for whichever rows the page happened to carry. from_selection always carries the link, however many vendors were picked, because the user named each one. Where install_all_url is null, the rows' own install_url links ARE the offer: list them, and never announce a batch link the result does not carry. Paging: page with offset and page_count, never with the length of connectors. On the first page of an unsearched browse the workspace's ALREADY CONNECTED connectors are prepended so the catalog's ordering cannot bury them past any page you would ask for — so connectors can be longer than the page it came from, and page_count is the catalog window's own length. Advance by offset + page_count; total counts every matching connector across all pages. Those prepended rows carry is_connected true (or a to_configure/disabled state), so a workspace's live tools are visible without paging for them. Scoping: pass kind ("bank" | "accounting" | "invoicing") to get only the connectors serving that domain — the whole set, server-ordered, including the long tail of bank institutions. Pass kind: "upload_surface" for the places invoices ARRIVE — mailboxes, messaging apps, file drives. Pass kind: "storage" for the drives Well FILES INTO — Google Drive, Dropbox, OneDrive — the step that asks where Well should write the documents it collects. Neither of those two is a financial domain: the server resolves each from the connectors' own display categories, so read the rows it returns and never re-derive the set from category_id yourself. They are opposite DIRECTIONS on the same drives, so a "storage" row carries direction "output" and no data_domains, and its is_connected reports the workspace's own file-drop connection, never the drive's separate invoice-source connection. Pass q to name-search the full catalog. Omit all of them for the curated, matched-first view. Use well_list_connector_tools for a live connection's actions. Country: pass country (ISO 3166-1 alpha-2, e.g. "FR") on a connect-a-bank step so the banks that serve the company's country sort first, then the ones that serve its region, then the rest. It reorders the page only — no row is dropped, and a country the catalog carries no coverage for is left in place — so a bank the user names is still found with q. Take the country from the workspace identity; omit it when the country is unknown, and the order is unchanged. Pass from_selection: true for the connect step that FOLLOWS a vendor pick. It returns the connectors behind the counterparties the user picked on the missing-invoices card this session, and NOTHING else: only the picked vendors' connectors, and of those only the ones that can bring an invoice in (reason "picked_vendor"). It offers no accounting or invoicing tool the user did not pick — that offer belongs to its own step, scoped with kind. When the pick leaves no row, the list is empty and the card is not worth drawing. ⚠️ **This tool draws its card on EVERY call, the empty one included**, so never call it with from_selection to find out whether the pick has a connector behind it. Ask `well_get_worklist_status({ worklist: "invoice_sources_for_pick" })` first: it draws nothing, and it reports how many of the picked vendors carry a connector that can bring an invoice in. Make the from_selection call only when that count is above zero. An ABSENT count is not a zero: the probe answers `success: false` when it could not read the pick at all, so retry it rather than reading its silence as a vendor with no connector. row_count reports the same number back on this result. picked_vendors_filtered counts the picked vendors' connectors that were dropped for bringing no invoices in: when it is above zero, say a filter ran rather than letting a short card read as a pick nobody made. It takes no q and no kind — those browse a catalog, and this names a set already decided. An empty list means the session holds no pick for this workspace, or no picked counterparty matched a connector. Every result carries scope — "catalog", one of the three domains, "upload_surface", "storage", or "picked_vendors" — naming what the list IS. The card words its title from that field, so a caller must not describe the result as a domain the scope does not name.
well_list_counterparties
List the workspace's counterparty companies and how each one is CATEGORIZED — the company-level industry labels a counterparty carries. Use it for "which suppliers have no category?", "what industries are my counterparties in?", and before categorizing a counterparty so you name real ids instead of guessing. Name a scope, and say whether to keep only the ones missing a category: - `periods: [{ calendar_year, calendar_month }, …]` (1-12): the counterparties whose invoices those months are still missing, categorized ones included, each row tagged with its month and carrying `tx_count`, `base_total_amount` in `base_currency`, and `suggested_retrieval`. Every month must have ended. - `periods` PLUS `uncategorized_only: true`: the same months, keeping ONLY the counterparties that carry no category. Use this whenever the question is which of a period's suppliers still need one, and whenever a step asks the user to categorize them: the categorized ones are not the work, and listing them buries it. - `uncategorized_only: true` alone: a WORKSPACE-WIDE sweep for every counterparty that carries no category, no month involved. Returns 50 rows per page plus `total_count`; `tx_count`, `base_total_amount` and `suggested_retrieval` are null because the call names no period. When `next_cursor` is not null the sweep has more counterparties: call again with `cursor` set to it to read them. It is a POSITION, not a row offset, so categorizing the rows of one page never hides the rows of the next. Only this sweep pages: `cursor` is refused beside `periods`. COST: the period form has no batch endpoint, so each named month is a separate read of that month's spend. Ask for the months the user actually named, not a whole year "to be safe". Every row carries `categories` (`[{ category_id, name }]`) and `is_categorized`. `categorized_count` and `uncategorized_count` count the COUNTERPARTIES OF THE SCOPE, once each however many months they appear in, not the rows returned. Under `uncategorized_only` the result lists the uncategorized ones alone while `categorized_count` still counts the ones it withheld, so the two together are the period's coverage and `uncategorized_count` is the work left. Report both: naming the listed rows as the period's whole counterparty set overstates how much is uncategorized. TO SET a counterparty's categories, call `well_update_company({ company_id, category_ids: [...] })` — that field REPLACES the company's whole set. Read the available labels first with `well_query_records({ root: "categories", whereClause: { category_type: { _eq: "company" } } })`: that is the company-category catalog. It has no curated allowlist — the labels are minted during enrichment — so pass ids from it rather than inventing a taxonomy. `suggested_retrieval` is derived from the PROVIDER match, not from the category. Categorizing a counterparty does not change it; do not tell the user otherwise. This tool only reads. It categorizes nothing, mints no task, connects nothing and fetches no invoice. No workspace read is needed first: the workspace is resolved from the caller's authorized token, same as every other well_* tool. When the user asks to see, list or fix these rows, call this tool directly: its card is the answer. ⚠️ **This tool draws its card on EVERY call, the empty one included.** So when nobody asked for the list and you only need to CHECK whether anything is left (the first pass of a gate, or a re-check after a repair), call `well_get_worklist_status({ worklist: "counterparties_to_categorize", periods })` first, with the same scope you would pass here. It draws nothing. Call this tool after it only when it answers `open: true`.
well_list_member_candidates
List the teammates a workspace can invite, exactly as the Well app's invite card shows them. Use it before well_invite_members, and for "who can I invite to this workspace?". Returns `candidates`, each with `person_id`, `name`, `email`, `avatar_url`, a `state` (`active` already has access, `pending` was invited and has not accepted, `not_member` can be invited), and a `source` (`detected` shares the workspace owner's corporate email domain, `provided` was named in `person_ids` or resolved from the assigned gap owners). Never invite a candidate whose state is `active`. Alongside them it returns `targets` — this workspace plus any workspace group you belong to, each an option for where the invite lands — `roles` (`admin` or `member`, with a hint), and `me_person_id` so you never offer to invite the caller. Three ways to source the candidates: - Default: the detected same-domain teammates who hold no membership. - `person_ids`: resolve specific people you already hold the ids for, with their membership state. Set `include_detected` false to return only those. - `from_assigned_gaps: true`: resolve the owners of the settled expense transactions still missing a supplier invoice for the period, server-side, with their membership state — the invite step of a close or fetch flow uses this so it never depends on remembering who was assigned on the owner card. It returns only those owners (the detected teammates are omitted). Name the period ONE way — `{ calendar_year, calendar_month }` or `{ fiscal_year, fiscal_period }` — or name no period to use the months selected on the period card this session. Every month must have ended. ⚠️ WAIT ON THE CARD IN THE TURN THAT DREW IT. Write your one line for the user FIRST, then call `well_wait_for_selection({ kind: "invite_ack" })`, which this result's `next_step` also states. The card's own footer sends the invitations and writes the acknowledgement, so never call `well_invite_members` yourself after a click. The outcome the click carries says which button it was: "done" sent the invitations, "keep_for_later" set the step aside. Both end the step. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_list_missing_invoice_owners
List the settled expense TRANSACTIONS a past period is still missing a supplier invoice for, one row per line, each with its current owner SET. Use it for "who owes the missing invoices?" and as the input to well_assign_missing_invoice_owners. Each row reports its TRANSACTION owner SET. An empty `owners` set means no transaction owner set was found; it does not prove that no card rule or other legacy owner exists. The `bucket` is `no_owner_set`, `assigned_to_me`, or `assigned_to_others`, computed from that set against the calling person. This lists the SAME missing invoices well_list_missing_invoices shows, but flattened to lines you can assign; there is no per-card grouping and no `scope: "card"`. Name the period ONE way: `{ calendar_year, calendar_month }`, `{ fiscal_year, fiscal_period }`, or `periods: [...]` for several months (1-12) — or name NO period to use the months selected on the period card this session. Every month must have ended. Each row carries `transaction_id` (pass it to well_assign_missing_invoice_owners), `date`, `description`, `counterparty` (name, id, and logo when a provider was matched), `amount`, `currency`, and `base_amount`. Rows with no owner set come first, then the caller's own, then those owned only by others; `no_owner_set_count`, `assigned_to_me_count`, and `assigned_to_others_count` summarize the split over the returned rows. The rows per counterparty are a BOUNDED sample (`sampled: true`), so `row_count` may be fewer than `transaction_count` — the window's true total — and `transactions_omitted` is the difference. Use it to assign owners, not to count a period's total gaps; well_list_missing_invoices carries the full per-counterparty totals. This tool reads the user's data and changes none of it. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_list_missing_invoices
List the supplier invoices a past period is still missing — the settled spend whose invoice has not been collected, one row per counterparty, exactly as the Well app's expense-invoices card shows them. Use it for "which invoices am I missing for <month>?" and as the input to fetching them. Name the period ONE way: `{ calendar_year, calendar_month }` (the calendar month, e.g. June 2026 → 2026, 6), `{ fiscal_year, fiscal_period }`, or `periods: [{ calendar_year, calendar_month }, …]` for SEVERAL months in one call (1-12) — or name NO period at all to use the months the user selected on the period card this session (well_list_periods → the user clicks → well_switch_workspace records them). With no period named and no months selected, the call refuses and tells you to run the period step first. Every month must have ended — a current or future month is refused, and so is the adjustment period (13). Duplicate months are refused. COST: there is no batch endpoint, so each named month is a separate read of that month's spend. Ask for the months the user actually named, not a whole year "to be safe". Returns `rows`, ONE per counterparty for the whole call, never one per month. Each row carries `name`, `tx_count` and `base_total_amount` in `base_currency` SUMMED over the months it covers, its own `months` array naming those months (each with that month's `tx_count`, `base_total_amount`, `proof_task_id`, `acquisition_status` and `refusal_reason`), and the route fields `mode`, `available_modes`, `suggested_action`, `matched_provider_name` and `matched_connector_service_id`, which the provider match resolves once per counterparty. NEVER list a counterparty once per month and never present its months as separate gaps: it is one supplier to chase, and one collection covers every month behind it. Name the months a row spans from its `months` array. The envelope's own `months` carries each month's totals (rows are NOT repeated there), `periods_covered` names the months read, and `transaction_count`, `group_count` and `dropped_groups` are totals across every month read. `row_count` counts the DISTINCT counterparties, so it is never the sum of the months' own `row_count`. `dropped_groups` counts the GROUPS that produced no row — party-less bank operations, unresolved counterparties, unnamed companies — never transactions, and `bank_internal` and `unknown` hold one group per month whatever they contain, so quote neither as a quantity of operations. `unknown` and `unnamed_company` ARE categorized expense spend still missing a supplier invoice, so an empty `rows` over a non-zero count is not a complete period; `bank_internal` alone is, since no supplier can invoice a party-less operation. The single-month fields `calendar_year`, `calendar_month`, `fiscal_year`, `fiscal_period` and `period_label` appear ONLY when the call named exactly one month. Every row also carries `transactions` — the counterparty's own lines behind the row, each with `date`, `description` (the bank's remittance text), `category`, `amount`, `currency` and `base_amount`. `amount` is signed and stays in the transaction's own currency, so never add those together across a row; `base_amount` is the same line in `base_currency`, and the magnitudes of those DO add up to `base_total_amount`. The list is capped at 25 per row and `transactions_omitted` says how many the cap left out — quote that number instead of implying the list is complete. `mode` is the ONE route the card suggests for that row: `agent` (a browser agent can collect it from the supplier portal), `connect` (connect the named service and Well fetches it), `upload` (the user supplies the file). `available_modes` lists every route the row offers instead of only the suggested one — `agent` and `upload` on every row, plus `connect` when the catalog holds a connector for the matched provider, so 2 or 3 entries. Present `mode` as the suggestion and `available_modes` as the choice. Only CATEGORIZED expense transactions are considered — uncategorized spend is not listed, so poor categorization coverage under-reports the gaps; disclose the `hints`. This tool reads the user's data and changes none of it. It does not mint tasks, start a close, connect anything, or fetch any invoice. Call this directly — no other tool call is needed first (workspace is resolved from the caller's authorized token, same as every other well_* tool).
well_list_periods
List the recent accounting months of the workspace, with each month's close status, its invoice-retrieval state, and the counts that describe how much work it holds. Use this to ask the user WHICH month or months to work on before any close, review, or month-scoped read — do not guess a month, and do not derive one from today's date yourself. Each entry carries: - calendar_year / calendar_month: the month itself. - fiscal_year / fiscal_period: the same month in the workspace's fiscal calendar — this is the pair every close endpoint and close tool takes. - label: the month written out, e.g. "March 2026". - is_complete: the calendar month has ended. A still-accruing month is never a valid close target. - selectable: the month can be CLOSED. False for a month that has not ended, one already closed, one with nothing to close, and a December whose year-end close is not supported yet. Read this one for a close pick. - analyzable: the month can be REPORTED ON. True once the month has ENDED and while it remains inside the window the canvas endpoints serve; false for the month in progress, for a future month, and for one too far back. It does NOT ask for a close verdict, because a report reads transactions and an unchecked month still has them. Read this one for an analysis pick. - inspectable: the month can be LOOKED INTO. A reader can open its transactions, its missing invoices and its days. True for EVERY month that has begun, the month in progress included. False only for a month that has not begun. It reads no close verdict and no activity count, so a closed month, an empty month and a workspace with no accounting connector at all still have readable months. An empty month answers with an empty list, which is an answer. Read this one for a retrieval or review pick; every selectable month is also inspectable. - close_status: "closeable" (ready), "not_ready" (work remains), "closed" (already locked), "nothing_to_close" (no activity), or null when the workspace has no verdict for that month. - close_reason: the blocking reason behind the status, or null. - invoice_state: "missing_invoices" (at least one counterparty still owes a supplier invoice), "has_invoices" (checked, and nothing is missing), or "none" (no state: no activity, the month has not begun, or the check could not run). Never read "none" as "nothing missing". - missing_invoice_count: how many counterparties owe an invoice for the month — the rows `well_list_missing_invoices` would return. 0 whenever invoice_state is "none", including when the check did not run. - transaction_count: how many transactions the month holds, dated on the basis this purpose measures on. `analysis` counts on `executed_at`, the same column `well_sum_transactions` ranges, so a month's count and a reporting figure cover the same WINDOW. It is not the same row set, and must never be quoted as the figure's row count: the sum can also drop internal transfers and exempt categories on request, and it widens to a parent's granted transactions where this count does not. Read it as a presence signal for the month. `close` and `collect` count on the books date, `COALESCE(value_date, booking_date)`, which a transaction the bank has not booked does not carry — so a zero under those purposes means no BOOKED transaction, never an empty month. - bank_transaction_count: the subset of transaction_count delivered by a connector the workspace actually BANKS with, meaning a bank, a neobank, or a treasury or spend platform whose product is an account. An accounting platform and a payment processor deliver transactions too, so transaction_count is NOT a bank signal. Only this field answers "has a bank fed this month". A transaction counts as not-bank when its source connector is unknown, or when that connector has since been disconnected, so a zero here never licenses skipping a bank-connection step. - unposted_invoice_count: invoices the month HAS that have not posted to the ledger. This is a posting gap, not a missing invoice — do not present it as one. - uncategorized_transactions: transactions in the month not yet categorized — the "help categorize" errand behind a not-ready close. Dated on the books, so it is ABSENT under `analysis` rather than 0 — that purpose counts on execution and never measures this errand, and a 0 would read as "nothing left to categorize". It is also absent on a month the coverage read did not cover. Never read an absent count as "nothing left to categorize": say the month was not measured, or read it again. - categorized_unposted_transactions: categorized transactions not yet posted to the ledger — part of the "review and book" errand. Dated on the books, absent under `analysis`, and absent on an unmeasured month for the same reason. - invoice_state / missing_invoice_count: the month's invoice-retrieval verdict and the count behind it. Both are ABSENT on a month a `analysis` list skipped — that purpose bounds its invoice read by execution-dated activity while the errand is dated on the books, so the two disagree and a "none" there would be a claim nothing measured. Absent is not "owes nothing"; read it from a `close` or `collect` list. - days: the DAYS of the month that carry a retrieval state, ascending, each `{ day, state }` over the same vocabulary as invoice_state. A day is "missing_invoices" when it holds settled expense spend still missing its supplier invoice, and "has_invoices" when it holds activity and no such gap. Days with neither are OMITTED, so an absent day means "none". `days` is empty for every month whose invoice_state is "none" — an unchecked month has no day the tool can call clean — and it is empty for EVERY month on a `purpose: "analysis"` call, whatever that month's invoice_state, because the reporting axis paints no day. On that purpose an empty `days` therefore says nothing about invoice coverage, and neither does an absent invoice_state. This is calendar detail for a picker to paint; quote the month's own counts, not a day list, when answering in prose. - analysis_days: present ONLY for a `purpose: "analysis"` call — the DAYS and whether a breakdown can name what each holds, ascending, each `{ day, state }` over "categorized" / "uncategorized" / "neutral". An "uncategorized" day holds a transaction with no category. It still COUNTS toward a burn total, which filters on no category at all; a cost breakdown just reports it as uncategorised rather than under a named category. Never say a total is short because of it. Unlike the other two axes a quiet day IS listed, as "neutral". Calendar detail for the reporting picker. - close_days: present ONLY for a `purpose: "close"` call — the DAYS carrying a non-neutral close-readiness state, ascending, each `{ day, state }` over "posted" / "progress". A day absent from it is "neutral" (nothing to close). Calendar detail for the close picker, like `days` is for retrieval. `default_period` is the oldest month that is ready to close, falling back to the oldest still in progress. Offer it as the default choice. It reads `selectable`, so it is null whenever no month in the window can be CLOSED, and a null one does not mean the window is empty: an inspectable month can still be worked on for invoice retrieval. On a `purpose: "analysis"` call it is instead the NEWEST `analyzable` month, because a figure describes the latest ended period and a month outside the reporting window would be refused by the endpoints that serve it. PURPOSE: pass `purpose: "close"` when the user is closing the books, so the picker paints close readiness and each month carries its `close_days` and the categorize / review counts. Pass `purpose: "analysis"` when a REPORTING SKILL is already running and is choosing the month its figure will cover, so the picker offers only `analyzable` months and paints the CATEGORIZATION day axis: neither invoice coverage nor close readiness is the decision being made, but an uncategorized day is one a breakdown cannot attribute. A user who merely mentions a report, a burn figure or a cost breakdown is NOT the trigger — naming one of those is phrasing, and phrasing never sets this field. Example: "What months do you have for me? I'm trying to work out my average burn." is a plain listing request that names a reason — it is NOT a reporting skill calling for its own period pick, so this call OMITS `purpose`. Only a caller that IS the reporting flow itself (an `avg-burn`/`cost-structure`/`cash-flow-waterfall` skill run, already past its own gates, now needing the month to compute against) passes "analysis" — never derive it from words in the user's own message, no matter how closely they match a report. Omit it (or `purpose: "collect"`) for invoice retrieval, the default. This is the calling skill's intent — set it from the flow, never from the user's phrasing. WINDOW: by default the `months` most recent calendar months, ending with the current one. Pass `year` instead to get ONE calendar year in full — all twelve of its months, December back to January — which is how you reach a year the recent window does not cover, backwards or forwards. `navigable_years` reports the range `year` is answered for. A year ahead of today comes back in full and every month of it is `selectable: false` and `inspectable: false` with `close_reason` "period_not_ended": books close on a month that has ENDED, and a month that has not begun holds nothing to read. Show such months when the user asks to look ahead, and say why they cannot be picked. Never omit them. COST: the invoice state is read per month from a separate endpoint, so a wide window costs one extra read for every month that holds activity, plus one day-coverage read per calendar year those months touch. A `purpose: "analysis"` call pays the same day-coverage read, dated on the execution basis, because it paints the categorization day axis. Ask for the months the user needs, not 24 by default. A wholly future or wholly empty `year` is cheap — no month in it can hold a settled gap, so none is read. Call this directly — no other tool call is needed first (the workspace is resolved from the caller's authorized token, same as every other well_* tool).
well_list_recurring_contexts
List the billing contexts a reader can count as recurring revenue over one window, each with what counting it would add. This is what the recurring-contexts card offers; it measures nothing `well_sum_invoices` did not already measure. Each entry in `groups` is one billing context's revenue in the window: `context_key` (the id a selection is matched on), `label` (the context as the product writes it), `amounts` (one entry per currency, each already net of credit notes and never converted) and `count` (the invoices behind it). The named contexts are sorted biggest first in the currency that carries the most invoices, so the biggest decision reads first. A context whose window nets to nothing in every currency is NOT listed — counting it would add nothing, so it is not a choice. A context that nets NEGATIVE stays on the list: its credit notes outweighed its invoices, which is a real state, and hiding it would move the figure by an amount nobody saw. **The last entry may be `context_key: "unclassified"`, labelled "No billing context".** It is the invoices whose `billing_context` is `null`: extraction fills the field rather than a billing system, so on most workspaces it holds most of the revenue. It is a choice like the others. A business that bills only subscriptions can count it as recurring; a business with one-off work usually cannot. When the reader counts it, apply it to the `well_sum_invoices` rows whose `billing_context` is `null` — no row carries the key itself. **State its amount whenever it is listed**, counted or not, because it is the part of the figure extraction could not describe. `totals` is the sum of the groups' amounts per currency: the window's whole readable issued revenue. **This read converts nothing and never adds one currency to another.** The reader decides per context, so the choice needs no single total; convert once, in the arithmetic, at a rate you state. This read takes no view on which contexts ARE recurring, and offers no default, the "No billing context" entry included. What counts as recurring revenue is a fact about the reader's business, not about the vocabulary: a retainer is recurring for one company and a one-off engagement for another. Take the reader's answer from the card: its Continue records it with `well_switch_workspace` as `recurring_contexts`, and you read it back with `well_wait_for_selection` (kind "recurring_contexts"). Keep only the `well_sum_invoices` rows whose `billing_context` is in that answer, reading `unclassified` as the rows whose `billing_context` is `null`, and pass the same keys to `well_render_mrr` as `recurring_contexts` so the figure names what it counted. The window is whole months: `from` and `to` are both the first day of a month, YYYY-MM-01, `from` inclusive and `to` EXCLUSIVE. `window` echoes both back exactly as you sent them. When a comparison will be measured, read the list over BOTH windows, from the start of the earlier one to the end of this one, so a context that stopped billing between them is still offered. `partial: true` means the aggregate was cut short: every amount here is a FLOOR, a context's real share can only be larger, and a choice made because a share looked small may not survive the full read. Say so before presenting the list as a basis for the decision. `unreadable_rows` counts invoices whose net amount or currency could not be read at all; they are in no figure here. It is `null` when that count could not be read, which is not zero: say it is unmeasured. Read `success` before `groups`: a failed read returns no contexts, which looks exactly like a window with nothing to choose. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_list_retargetable_connectors
Read the connectors on this workspace's lineage parent (its membership workspace) that could follow it here, WITHOUT showing the user anything: each candidate's connector, how strongly it was proved to belong to this company, and how much transaction history is behind it. This draws nothing on the user's screen and asks for no confirmation. Use it ONLY for a silent CHECK the model acts on itself: the close-books bank step deciding whether a candidate exists on the parent before it offers the retarget card, a step that needs the candidate count. An empty list is the normal answer for a workspace connected correctly the first time. Read the count and act in the same turn — there is no card and no click to wait on. ⚠️ To have the USER bring a connector across, call `well_show_retargetable_connectors` INSTEAD — that one draws the card the user confirms. This tool cannot draw one, so a retarget step run here leaves the user with nothing to act on.
well_list_uncategorized_window
List the transactions in a date window that carry no category, so a figure that depends on categorization can say exactly what is missing before it is computed. `from` is inclusive and `to` is EXCLUSIVE — for whole months, pass the first day of the month after the last one you want. **These rows are measured on when the movement happened (`executed_at`), not on its accounting date.** That is deliberate and it matters: the two disagree about which MONTH a transaction belongs to for a large share of real data, and many rows carry no accounting date at all. A caller listing rows on one basis while summing a figure on the other ends up with rows it counts but cannot offer to fix. Pair this with a sum measured on the same basis. Returns each row's identity, amount, counterparty and the classifier's pending suggestion where one exists. It lists rows with NO category; a categorized row that has not yet posted to the ledger is a booking question and is not returned here. `meta.truncated: true` means the page filled and more rows exist, so report the count as a floor rather than as the total. `meta.returned` is what came back. **`success: false` means the window is UNKNOWN, not empty.** The read failed, so no count exists and `returned` and `truncated` are absent rather than zero. An empty `records` on a failed read is not "nothing is uncategorized" — treating it that way reports a clean list this read never produced. Say the list could not be read. Do not propose categories from this list. Where the classifier has a proposal it rides on the row, and the assignment surface is where a category is chosen. When the user asks to see, list or fix these rows, call this tool directly: its card is the answer. ⚠️ **This tool draws its card on EVERY call, the empty one included.** So when nobody asked for the list and you only need to CHECK whether anything is left (the first pass of a gate, or a re-check after a repair), call `well_get_worklist_status({ worklist: "uncategorized_window", from, to })` first, with the same scope you would pass here. It draws nothing. Call this tool after it only when it answers `open: true`. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_list_unposted_transactions
List the transactions of a fiscal period that carry a category or a role and have STILL not reached the ledger, so a close can say exactly what is holding it. This is the posting gap, not the categorization gap. A row here already has a category; what it lacks is the ledger account its journal entry would post to. For rows carrying no category at all, use `well_list_uncategorized_window`. Each row carries `transaction_id`, `label`, `amount`, `period_date`, the `current_ledger` already attached where one is, and `ledger_suggestions` — the classifier's proposals, each with the account's `code` (its number, e.g. "6156") beside its name. The `ledger_catalog.accounts` list carries every account this workspace can post to, with the id `well_set_transaction_ledger_account` takes. A row whose `ledger_suggestions` is empty is assigned from that list: the classifier proposed nothing, which is not the same as the row having nowhere to go. **The rows arrive snake_cased** (`period_date`, `ledger_suggestions`, `current_ledger`), unlike `well_list_uncategorized_window`, whose close cousin emits camelCase. A caller reading one shape against the other silently sees empty fields rather than an error. The period is named in FISCAL terms, and a workspace's fiscal calendar need not follow the calendar year — "June 2026" is not reliably fiscal period 6. Take `fiscal_year` and `fiscal_period` from a `well_list_periods` entry, or from the months the user already selected this session; never derive them from a calendar month yourself. Most categories already determine their ledger account: the chart maps each category key to a canonical code, and only a handful abstain because the category alone cannot pick a safe account without the transaction direction. So a long list here usually means the categories are missing, not the accounts. **`success: false` means the period is UNKNOWN, not clear.** The read failed, so no count exists, and an empty `records` on a failed read is not "everything posted". `truncated: true` means the period holds MORE unposted rows than this page carries, so `returned` is a floor rather than the period's total. Narrow the period, or state the count as "at least". When the user asks to see, list or fix these rows, call this tool directly: its card is the answer. ⚠️ **This tool draws its card on EVERY call, the empty one included.** So when nobody asked for the list and you only need to CHECK whether anything is left (the first pass of a gate, or a re-check after a repair), call `well_get_worklist_status({ worklist: "unposted_transactions", fiscal_year, fiscal_period })` first, with the same scope you would pass here. It draws nothing. Call this tool after it only when it answers `open: true`. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_list_workspaces
List the workspaces this connection is authorized to access. This draws nothing on the user's screen. Use this FIRST when a single token may cover more than one workspace, and use it for every case a caller can settle on its OWN: exactly one workspace, a hint that matches one, a pin this conversation already wrote, or none at all. Read the rows and say which workspace you took. ⚠️ TO ASK THE USER WHICH WORKSPACE, CALL `well_show_workspace_picker` INSTEAD. It draws one tile per workspace and waits for a click. Reach for it only when the token authorizes several AND no hint resolves — a chooser over a set of one asks nothing, and a chooser the caller could have answered itself asks a question it already knows the answer to. Use this FIRST when a single token may cover more than one workspace. Each entry has: - workspace_id: pass this as the workspace_id argument on other tools to target one workspace. - workspace_name: human-readable name (null if it can't be resolved). - is_primary: true for the token's default workspace (used when you omit workspace_id on a write). - own_company_id: the public id of the company this workspace is anchored to, or null. A row that carries it is a company workspace: the close flow runs in one. A row without it is a membership workspace, the container a sign-up mints. - lineage_parent_workspace_id: the workspace_id of the membership this workspace was created under, or null when the workspace has no active lineage. A membership workspace (no own_company_id) whose id appears here on other rows is the parent of those company workspaces. - identity: the company behind the workspace (registered name, trade name, registry number, country, website, currency, fiscal year start, where the fiscal year start came from, and the jurisdiction's default fiscal year start), so two similarly-named workspaces can be told apart. Every field is null when the workspace has no accounting settings yet. Tax identifiers are deliberately not included. - has_bank_transactions: whether a connector the workspace BANKS with has delivered any transaction to it, meaning a bank, a neobank, or a treasury or spend platform whose product is an account. An accounting platform and a payment processor deliver transactions too and do NOT count here. Neither does a transaction whose source connector is unknown, whose install has since been disconnected, or whose catalog entry has been retired. Only `true` shows that a bank has fed this workspace: `false` means no such transaction was found and `null` means the signal could not be read, so an absent value is never a zero and neither value licenses skipping a bank-connection step. Read this before any month read when the flow needs to know whether the workspace banks with anything at all. The result also carries `session` — what the user's card clicks have already recorded this session: `pinned_workspace_id` (null when not switched), `workspace_queue` (the workspaces to work through next, empty when none), `selected_periods` (the months picked on the period card, empty when none), and `selected_counterparties` (the counterparties picked on the missing-invoices card, with the workspace their company ids belong to; null when none was picked). Call this any time you need to resync with clicks you may have missed. When the token authorizes a single workspace you can omit workspace_id everywhere; when it authorizes several, read tools fan out across all of them unless you pass a workspace_id, and write tools require one. ⚠️ A row without `own_company_id` is a membership workspace with no company of its own. TO ASK THE USER WHICH COMPANY that workspace IS — to show its detected company candidates and let them pick — CALL `well_show_company_candidates`, never this read: this list never shows the candidates.
well_preview_invoice_fetch
Preview which vendors a past period is still missing supplier invoices from, where each one's invoices are, and which route would obtain them. Use it for "what would happen if I fetched <month>'s missing invoices?" before anything runs. Name the period ONE way: `{ calendar_year, calendar_month }` (the calendar month, e.g. June 2026 → 2026, 6), `{ fiscal_year, fiscal_period }`, or `periods: [{ calendar_year, calendar_month }, …]` for SEVERAL months in one call (1-12) — or name NO period at all to use the months the user selected on the period card this session (well_list_periods → the user clicks → well_switch_workspace records them). With no period named and no months selected, the call refuses and tells you to run the period step first. Every month must have ended — a current or future month is refused, and so is the adjustment period (13). Duplicate months are refused. COST: there is no batch endpoint, so each named month is a separate read of that month's spend. Ask for the months the user actually named, not a whole year "to be safe". Returns `vendors` — EVERY vendor of the rows THIS CALL covers, one entry per supplier portal ACROSS the whole window (one portal is one place to go, however many months it spans), or one per counterparty where no portal matched: `name`, `provider_id`, `domain`, `url` and `url_source`, the `counterparties` it covers (each tagged with `calendar_year`, `calendar_month`, `period_label` and `suggested_route`), `tx_count`, `base_total_amount` in `base_currency`. THE ROUTE NEVER FILTERS `vendors`: a vendor Well has no published flow and no connector for is listed exactly like the rest, with its route on its counterparties. WHAT the call covers is a separate question, and two fields answer it: a counterparty pick narrows the rows to the picked companies (see `scoped_to_selected_counterparties` below), and a `hints` line names any group the projection could produce no vendor for. So `vendors` is every vendor of the rows THIS CALL covers, which is the whole period only when neither of those is present. `upload_rows` (the user must supply the file) and `connect_rows` (connecting the named service fetches it) carry the same counterparties again, split by route, with the same month tags. WHERE A VENDOR'S INVOICES ARE: `url_source` says how much `url` knows. "blueprint" is the page Well's own published flow opens, so it IS the billing page. "enrichment" is the vendor's front door — the catalog entry address or the company's domain — so the user still has to find the invoices on it. "none" means no address at all and `url` is null. Never present an "enrichment" address as the invoice page. `url_source` informs and gates nothing: an "enrichment" vendor is offered for the pick, and carried on the link, exactly like a "blueprint" one. ROUTES DESCRIBE HOW, NOT WHETHER: a counterparty Well holds a connector for is in `connect_rows` AND under its vendor, where its entry reads `suggested_route: "connect"` and `connect_routed_counterparties` counts it. Connecting is the route to suggest; the agent run stays available so the user has a way through when the connector does not work for them. A counterparty on `suggested_route: "upload"` is in `upload_rows` too. Never present the same counterparty as two separate gaps — it is one gap seen twice, so count it once. `counts` covers the rows this call actually read, and every field states its own unit: `vendors` and `agents` count PORTALS, `agent_tx` counts TRANSACTIONS, `upload` and `connect` count COUNTERPARTY ROWS — one counterparty per month. They are not summable with each other: never add them into one total, and `vendors` is never the sum of the other four, because every counterparty reaches the vendor list whichever route it takes. A total over the whole window counts the DISTINCT counterparties named in `vendors`, and a counterparty appearing again in `upload_rows` or `connect_rows` is the same gap seen by its route. Across several months a counterparty counts once per month in `upload` and `connect`, while `vendors` and `agents` count each portal once for the window, so neither is the sum of the months' own. WHEN `scoped_to_selected_counterparties` IS PRESENT, `vendors`, `upload_rows`, `connect_rows`, `counts` AND `months` COVER ONLY THE PICKED COUNTERPARTIES, NOT THE WHOLE WINDOW: for the months the pick bounded, every row and every figure here is built from the picked rows alone, and `selection_scope` says how many counterparty rows it left out. Never report those rows as every vendor the period is missing an invoice from, and never report those counts as the period's own — state the truncation and its size, and point at `well_list_missing_invoices` for a fresh card that drops the pick. Without that field the counts cover the whole window. `months` gives each month's own counts; `periods_covered` names the months. A sum is `null` when any member of it had no FX rate, never a partial figure. The single-month fields `calendar_year`, `calendar_month`, `fiscal_year`, `fiscal_period` and `period_label` appear ONLY when the call named exactly one month. THIS TOOL LAUNCHES NOTHING. It creates no task, starts no run, and fetches no invoice — `mode` is always `"preview"` and `nothing_launched` is always `true`. Launching the agents is NOT available on this surface, so present the preview as information and do not promise to run it. `collect_url` is the ONE link to hand the user: the `/collect` page, which asks the Well browser extension to run these portals. It names each portal by its `provider_id`, and that id is the only field that decides which portal runs — a name or an address in the link labels a row and nothing more. Give the link as returned and never build one or edit its parameters. The page starts nothing until the user acts on it, it reports which portals the extension accepted, and it never reports that an invoice arrived. The link also names this workspace, and that name gates WHO may act on the link: the page starts nothing until the reader is signed in to Well as a member of it. It does NOT choose where the invoices land — the extension files into whichever workspace it is signed in to — so never tell the user the link picks the destination. THE LINK CARRIES EVERY VENDOR THAT HAS AN ADDRESS, whatever its `url_source` and whether or not Well holds a published flow for it. Deciding what a vendor's invoices need once the page opens belongs to the app and the extension, not to this read, so `url_source` labels a vendor and never withholds it. Two things still keep a vendor off the link: no address at all, and no `provider_id` the link can address. `collect_url` is null when the window holds no addressed vendor at all; `collect_url_omits` names the vendors a full window pushed past the 25-portal ceiling, and `collect_url_unaddressable` names the ones the link cannot name. A vendor on either list is still missing its invoice, so say the link cannot carry it, and offer the upload or the connect route from `upload_rows` and `connect_rows` instead. Never say it has nothing outstanding. Only CATEGORIZED expense transactions are considered — uncategorized spend is not counted, so poor categorization coverage under-reports what an agent run would cover; disclose the `hints`. Call this directly — no other tool call is needed first (workspace is resolved from the caller's authorized token, same as every other well_* tool).
well_propose_next_steps
Put five next steps on a card, as five lines the person can send. **This tool ranks nothing.** The five skills and their order come from well_get_session_digest's `suggested_steps`; pass them in that order. The sentence beside each one is yours to write, in the language the person is using, from that skill's own quoted utterances and the figures the digest returned. When that list is empty or shorter than five, do NOT call this tool and do not write five of your own: say in one line that the next steps cannot be proposed this time. The server checks that each line can travel and hands the list to the card. Call it at the END of a skill that tells you to, never to work out what the person should do. Each step is a pair: - `skill` the slug of a Well skill, exactly as well_search_skill lists it. A slug the catalog does not hold is refused, and the refusal names the slugs it does. - `prompt` one natural sentence, 1 to 160 characters, written from that skill's own quoted trigger utterances. Write what the PERSON would say, in their words, not an instruction to yourself. REFUSED rather than rendered: - a step naming a brick a flow invokes (`define-workspace`, `define-period`, `normalize-currency`) or one of the two skills that call this tool (`signing-back`, `whats-next`): nobody sends those, so rank another skill in its place - a prompt that starts with "/": the host reads it as a command, not as a message - a prompt containing "<": the host can read it as markup - a prompt containing a line break: a row carries one line - fewer or more than 5 steps: the card is a fixed list Clicking a line records that pick on this connection. Read it back with well_wait_for_selection({ kind: "next_step", timeout_s: 60 }) in this same turn: on "selected", take selection.next_step.prompt as the person's own message and start selection.next_step.skill at once, loading it with well_get_skill. When no turn is waiting, the card sends the sentence into the conversation itself as the person's own message. That is the recovery, not the plan: it arrives as a fresh turn that starts from nothing you already hold. Every row stays clickable while the card is on screen, and a click changes nothing about the row: a person who takes a second step later finds the same five lines. When the card renders, the five lines are already in front of the person and the card sits where this call sits in the turn: write everything you have to say BEFORE calling. After it, the only thing that follows is the well_wait_for_selection call the result's `next_step` field spells out. Prose in place of that call ends the turn, and the click then has to restart the work from a new message rather than continue this one.
well_query_records
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"] - 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 }.
well_remove_contact_channel
Remove a contact channel from a company or person. Wraps the resource-scoped DELETE endpoints (DELETE /v1/{companies,people}/:id/{emails,phones,web-links,locations}/:channelId). Pass channel_id = the UUID of the specific channel row to remove (NOT the parent). Find it by reading the parent with well_query_records and selecting the channel's id field.
well_render_burn
Put a burn figure YOU computed onto the burn card. **This tool measures nothing.** It takes the figure and its method as input and returns them for rendering. Call it only after you have computed the burn yourself and can state every field below from your own work — never to "get" a burn. The server derives no burn of its own. The figure on the card is the one you state here, which is why every field below is required: the policy behind a number is the only thing that makes it checkable. **Under the number the card draws nothing.** It carries the figure, the window it averages and the trend chip; everything else you state below is REQUIRED and reaches no pixel. All of it comes back to you in this tool's text result, which is what you write the prose from. The card is the measure; the explanation is yours. REQUIRED, because a figure whose method is not stated cannot be checked: - `amount` — the outflow per month, as a POSITIVE magnitude in `currency` - `window` — the months the average divides by, not the months that carried spend - `convention` — "signed", and the counts you elected it from - `months_in_window` and `months_with_data` — a window with dark months reports LOWER than its typical month. When the two differ you MUST say so in prose: how many months recorded an outflow, and that the average still divides by the whole window - `excluded` — what fell out, in named groups. `internal_transfers` is the sum's `excluded_multi_leg`; send `null` when the sum could not count it - `transaction_count` and `unplaceable_count` — how much of the window could be placed inside or outside the transfer rule at all. `unplaceable_count` is the sum's `excluded_no_owned_leg`. Send `null` when the sum could not count it. Never send 0 for that, because zero says every row was placed REFUSED rather than rendered: - a negative `amount` — a burn is a magnitude; a negative one means a signed subtotal was used without taking its magnitude - `convention: "magnitude"` — that feed keeps direction in a field no grouping here reaches, so no outflow was measured - `months_with_data` above `months_in_window`, or a measured `unplaceable_count` above `transaction_count` - one of `unplaceable_count` and `excluded.internal_transfers` `null` without the other: one cancelled count nulls both - a `months_in_window` that disagrees with the months `window` spans — the two state one fact, and a reader cannot tell which is the lie - `signed` elected from ZERO negative rows: whatever the convention was called, that window measured no outflow - `convention_counts` summing past `transaction_count`, or `months_with_data` disagreeing with the months `per_month` shows carrying an outflow — your own prose states both, so a contradiction between them is a sentence that refutes itself - a `window` whose bounds are not each the first of a month, or that fits inside one month: a month average divides by whole months - a `per_month` series that is not the window's own months, in order, averaging to `amount` — a dark month belongs in it as a zero, and a series that disagrees with the figure is not the working behind it - a `currency` outside ISO-4217 — the code is checked against the catalog, not its shape OPTIONAL, and only as a pair: - `baseline` and `change` — the earlier window you compared against, its own average, and the signed percentage between them. Send both or neither: a percentage whose baseline the reader cannot name is exactly the unchecked number this tool refuses everywhere else. The card draws the CHIP alone and never the baseline, so sending the pair obliges you to NAME that comparison in prose: the baseline window and its own average. Compute the baseline the same way you computed the figure, over a window of the same length; the two may overlap, and when they do say so too. `change` is checked against `amount` and `baseline.value` and refused when it does not follow from them, so send the percentage you actually divided. Do not send a direction: down is GOOD for a burn, and the card's green is decided server-side from `change` rather than read off its sign. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_render_cash_flow_bridge
Put a cash-flow bridge YOU computed onto the cash-flow waterfall card. **This tool measures nothing.** It takes the four terms of a bridge and the gap between them as input, draws the waterfall, and returns them. Call it only after you have read the opening and closing positions and summed the window's flows yourself — never to "get" a bridge. A bridge rests on one law: the opening, plus the inflows, minus the outflows, lands on the closing. The closing is measured on its own rather than summed from the flows, so the law is a check rather than a given. State the gap as `unexplained` and the tool verifies the five figures add up; state figures that do not and it refuses. REQUIRED: - `currency` — every figure below is in it, each converted before you stated it - `period_start`, `period_end` — the inclusive calendar days the flows cover - `opening` — `amount` (SIGNED, a workspace can be overdrawn), `as_of` (the day before `period_start`), and `derived` (true only when you solved it from the law because the reading could not be taken) - `inflows`, `outflows` — gross magnitudes, both positive; the direction lives in which bar they are - `unexplained` — the SIGNED gap `closing - (opening + inflows - outflows)`, computed from the figures as you rounded them; zero when they meet - `closing` — `amount` (SIGNED) and `as_of`, the moment the reading was taken - `reconciles` — true when the gap is inside the tolerance below, false when it is past it - `partial` — true when any term is incomplete: an anchor some accounts had no reading for, flows with rows no owned account could be placed against, rows that could not be read, or a currency with no rate. A cut-short read returns no rows and stops the run before this call The tolerance is the product's own: 1% of the closing position's size, never less than 1 in the base currency. A bridge that does not reconcile draws an Unexplained bar between the outflows and the closing; one that does draws none. REFUSED rather than rendered, each because your own figures disagree: - five figures that do not add up to within a cent - `reconciles: true` with a gap past the tolerance, or `false` with one inside it - a negative `inflows` or `outflows`; each is a magnitude, so a negative one was re-signed - an opening not dated the day before `period_start` - a window ending more than a day from the day the closing was read - a window that starts after it ends, or a date that names no real day - a derived opening with any gap, on a partial read, or over a window with no flows - a closing `as_of` in the future When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_render_cash_forecast
Put a cash forecast YOU computed onto the forecast card. **This tool measures nothing.** It takes the settled month-end series, the anchor, the burn and the projection you computed, and returns them for rendering. Call it only after you have computed both halves yourself: the month-end totals under your cash scope, and the burn under your stated policy. Never call it to "get" a forecast. The projection is WORST CASE: no revenue arrives, and cash declines by the burn each month until it reaches zero, where it stops. Take the anchor and the burn each to the cent, then each point is `max(0, anchor − k × burn)` for the k-th month after the anchor. The tool re-derives every point from the anchor and burn you state here, in cents. **The card draws the series, the anchor clause and the worst-case caveat.** The cash scope, the burn policy and `partial` are REQUIRED and reach no pixel. All of it comes back in this tool's text result, which is what you write the prose from. REQUIRED: - `currency`, and `as_of`: the full ISO time of the balances read the series came from - `actuals`: one `{ month, amount }` per month, oldest first, ending on the last month that has ended at `as_of` (UTC). A month no account covered is `null`, never 0, and it stays in the list. - `anchor`: `{ month, amount, basis }`. `closed_month_end` is the latest settled month-end in `actuals`. `current_position` is today's cash when no month has a settled total. It sits on the grid at the last actual month. - `burn`: the POSITIVE monthly magnitude, its currency, `trailing_months`, and the `window` it averaged (`from` inclusive and `to` exclusive, each `YYYY-MM-01`). The window ends with the last actual month. - `months_forward` (at most 12), and `projection`: one `{ month, amount }` per projected month. When the anchor sits before the last actual month, the months between are projected too, so `months_forward` must reach past them. - `cash_scope`: the counted account types, whether unknown ownership was counted, `anchor_missing_accounts` (counted accounts with no reading at a closed-month anchor; 0 under `current_position`), and the four exclusion groups - `burn_policy`: the elected convention and its counts, the exclusions (`internal_transfers` is the sum's `excluded_multi_leg`, `unreadable_rows` its malformed rows), and `unplaceable_count` (the sum's `excluded_no_owned_leg`) - `partial`: the forecast's own floor, which is WIDER than a cash total's `is_floor`. It is checked against `cash_scope` and must be `true` exactly when an account was left out with no readable balance, no rate, OR no reading at the anchor month — that last one is the forecast's alone, and a caller that forwards its cash total's `is_floor` unchanged is refused on it. It never means a cut-short read: a cut-short balances read or sum stops the run before this call. REFUSED rather than rendered, each because your own figures disagree: - a projection point that is not `max(0, anchor − k × burn)` within a cent - a projection that does not start the month after the anchor, skips a month, continues after a zero, or has the wrong length - a projection ending on a month that has already ended. The refusal names which of the three causes fired: the cash ran out (report that), the horizon was too narrow for the gap (widen it), or the gap exceeds every legal horizon (the feed is too far behind to project across) - a cash currency that differs from the burn's - a negative burn, a burn elected "magnitude", `signed` elected from no negative rows, or one of `unplaceable_count` and `internal_transfers` null without the other - a `closed_month_end` anchor that is not the latest settled actual, or whose amount differs from it - a `current_position` anchor beside a settled actual, off the last actual month, or with an account missing at it - actual months out of order, repeated, skipped, or ending on any month but the last one that has ended at `as_of` - a burn window that disagrees with `trailing_months`, or ends on a different month than the actuals - a `partial` that disagrees with the floor your own `cash_scope` implies - an `as_of` in the future This tool renders its own chart card. Do not re-plot the series with a charting tool. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_render_cash_position
Put a cash position YOU computed onto the cash card. **This tool measures nothing.** It takes the figure and its method as input and returns them for rendering. Call it only after you have totalled the balances yourself and can state every field below from your own work — never to "get" a cash position. The server derives no cash figure of its own here, which is why every field below is required: the policy behind a number is the only thing that makes it checkable. **Under the number the card draws nothing.** It carries the total and the moment it was read. Every other field below, required or optional, reaches no pixel. All of it comes back in this tool's text result, which is what you write the prose from. The card is the measure; the explanation is yours. REQUIRED, because a figure whose method is not stated cannot be checked: - `amount` and `currency` — the consolidated total. NEGATIVE is legal: an overdrawn workspace has negative cash, and this tool renders it rather than refusing it. - `as_of` — the moment the reading is valid for - `accounts` — every account that CONTRIBUTED, each with its native amount and currency, the converted amount, and the rate applied (`null` when it was already in `currency`). Carry `institution_name` and `masked_account_number` through from the balances read as well: your breakdown names each account by its bank, its name and its masked suffix, or by its currency when the bank and the name are both `null`, and never by its id. - `scope` — the account types you counted as cash, and whether you counted an account whose ownership is unsettled - `excluded` — what fell out, in four named groups: not owned, out-of-scope type, no readable balance, no FX rate. One merged count hides the difference between a rule the reader chose and a defect in the data. - `partial` — whether the total may be a floor (the skills' `is_floor`), because an account with no readable balance or no rate was left out of it. The result carries the value derived from `excluded`, whatever you state. It never means a cut-short read: that stops before this call. REFUSED rather than rendered, each because the caller's own figures disagree with each other: - a total that is not the sum of the contributions listed — totalling a different set than you disclose publishes a figure nobody can audit - a converted amount that does not follow from its native amount and stated rate - an account already in `currency` that carries a rate other than one, or whose converted amount differs from its native one - an account in another currency that states no rate - the same account contributing twice - a non-zero total with no contributing accounts - an `as_of` in the future - a `scope.account_types` naming nothing - a `balance_history` that repeats a month or runs out of order - a currency outside ISO-4217 — checked against the catalog, not its shape OPTIONAL: - `balance_history` — trailing closed month ends, oldest first. A `null` amount is a month no stored row covered; send it as a gap rather than dropping it or sending a zero, and never interpolate between two real points. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_render_mrr
Put an MRR figure YOU computed onto the MRR card. **This tool measures nothing.** It takes the figure and its method as input and returns them for rendering. Call it only after you have computed the recurring revenue yourself and can state every field below from your own work — never to "get" an MRR. The server derives no MRR of its own. The figure on the card is the one you state here, which is why every field below is required: the policy behind a number is the only thing that makes it checkable. **Under the number the card draws nothing.** It carries the figure, the window it averages and the trend chip; everything else you state below is REQUIRED and reaches no pixel. All of it comes back in this tool's text result, which is what you write the prose from. The card is the measure; the explanation is yours. REQUIRED, because a figure whose method is not stated cannot be checked: - `amount` — the recurring revenue per month in `currency`, net of tax and of credit notes, as the sum returned it - `window` — the months the average divides by, not the months that carried revenue - `months_in_window` and `months_with_revenue` — a window with dark months reports LOWER than its typical month. When the two differ you MUST say so in prose: how many months recorded recurring revenue, and that the average still divides by the whole window - `recurring_contexts` — the billing contexts the reader confirmed as recurring, as the keys the card recorded. `"unclassified"` among them means the reader counted the invoices with no billing context (the sum rows whose `billing_context` is `null`) as recurring. This is the reader's half of the policy and the card cannot show it, so an answer that does not name them leaves the figure unchecked - `invoice_count`, `unattributed_count` and `unclassified_count` — how much of the window the figure could reach at all. `invoice_count` is the issued invoices; `unattributed_count` is the SEPARATE set Well could place on neither side, reported beside it rather than inside it, and it may be larger. An unattributed invoice may still be recurring revenue. An unclassified one carries no billing context: it is in the figure only when `recurring_contexts` holds `"unclassified"`, and when it is, say in prose that the reader chose to count it. Send `null` for a count the sum returned as `null`: it is unmeasured, not zero - `excluded` — what fell out, as three invoice COUNTS kept apart: `one_off` (issued invoices under a context the reader did not count), `credit_notes` (the `credit_note_count` netted into the figure) and `unreadable_rows` (the sum's `excluded_malformed`, `null` when unmeasured) REFUSED rather than rendered, each because your own figures disagree with each other: - a negative `amount` — the credit notes outweighed the recurring invoices, so net recurring revenue fell below zero. That is a finding to report in prose, not a figure to put on a card, and never one to flip to its magnitude - an empty `recurring_contexts` — an MRR with nothing counted as recurring is not an MRR of zero, it is a policy nobody stated. Say the figure has nothing to measure instead - `months_with_revenue` above `months_in_window`, or `unclassified_count` above `invoice_count` - a `months_in_window` that disagrees with the months `window` spans — the two state one fact - a window whose bounds are not month starts — a month-average divides by whole months - `change` with no `baseline`, a `baseline.value` of zero, a baseline that does not start before the window or spans a different number of months, or a `change` whose magnitude or sign its own two figures contradict - a `per_month` series that does not name each month of the window once in order, does not average to `amount`, or disagrees with `months_with_revenue` **Send `baseline` and `change` as a pair or send neither.** Do not send a direction: the card's colour is decided server-side from the figures. Revenue is higher-is-better, which is the opposite of the burn card and exactly why a caller does not get to state it. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_render_runway
Put a runway YOU computed onto the runway card. **This tool measures nothing.** It takes the figure and the two numbers behind it as input and returns them for rendering. Call it only after you have computed the cash and the burn yourself — never to "get" a runway. A runway is one division, so this tool checks the one thing that can be checked: that the headline follows from the two figures you state with it. State them and it renders; state a headline they do not produce and it refuses. **Under the number the card draws nothing.** It carries the months, the moment, the burn's own window when you send one (a bare month count when you do not) and a health badge. The cash amount, the burn amount and `partial` are REQUIRED and reach no pixel. All of it comes back in this tool's text result, which is what you write the prose from, the division included. The card is the measure; the explanation is yours. REQUIRED: - `months` — months of cash left, capped at 36 - `status` — "ok", "capped", "infinite" or "insufficient_data" - `cash` — the dividend, amount and currency. SIGNED: an overdrawn workspace is negative. `null` ONLY under "insufficient_data", so a half you could not measure is reported rather than invented. - `avg_burn` — the divisor as a POSITIVE magnitude, its currency, and the `trailing_months` it averaged - `as_of` — the moment the reading is valid for - `partial` — whether the cash may be a floor (the skills' `is_floor`), because an account with no readable balance or no rate was left out of it. It never means a cut-short read: a cut-short balances read or sum stops the run before this call. OPTIONAL, and send it whenever you are sending a burn: - `window` — the months the burn averaged, inclusive start and EXCLUSIVE end, each on a month start. The card names those months instead of a bare count, so a reader can see which months the figure stands on. Without it the card can only pair the cash moment with the number of months, which reads as a window ending at that moment whatever months you actually averaged. Send NONE when `avg_burn` is `null`. The statuses carry two distinctions worth stating, because both are easy to collapse: - **Zero months is DATA when the cash is gone.** A workspace already underwater has a real runway of zero, status "ok" — not "insufficient_data", which means the inputs could not be measured at all. - **Unbounded is not the same as long.** "infinite" means the workspace is not burning; "capped" means it burns slowly enough that the figure passes the 36-month window the product reports in. REFUSED rather than rendered, each because your own figures disagree: - `months` that is not `cash ÷ avg_burn` — the division is the figure's whole claim - a cash currency that differs from the burn's; one division needs one currency - a negative `avg_burn`; it is a magnitude, so a negative one means a subtotal was re-signed - "infinite" with a non-zero burn, or a zero burn reported as anything else - "capped" whose division lands inside the window, or which reports a number other than 36 - a division past the window reported as "ok" instead of "capped" - non-positive cash reported as anything but a real zero - "insufficient_data" carrying a months figure, or a null figure under any other status - an unbounded runway reporting anything but the 36-month sentinel - an `as_of` in the future - a `window` whose bounds are not each the first of a month, or that spans no month - a `window` that spans a different number of months than `avg_burn.trailing_months` — the two state one fact, and a reader cannot tell which is the lie - a `window` sent with no burn to average — an unmeasured burn averaged no months - a `window` reaching into a month that has not ended yet - a `window` bound naming no calendar month, or an unknown key on the object When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_resolve_reconciliation_task
Approve or reject one or more reconciliation review tasks (from well_run_register_diff or the in-app review queue). - approve: confirms the match — the link is flipped to active. - reject: dismisses the match — the candidate does not silently re-surface. Each task_id resolves independently; a failure on one (already resolved, not found) is returned in errors and does not block the rest of the batch.
well_resolve_register_diff_gap
Post a well_run_register_diff gap (one of missing_in_register_ids' review tasks) into QuickBooks as a Purchase or Deposit. Requires the exact ledger_account_id (a UUID, not a name) for both: - bank_ledger_account_id: the bank/cash account the money moved through (e.g. Checking). - category_ledger_account_id: the expense or income category the gap books against. Look these up first with well_query_records({ root: "ledger_accounts", filters: [...] }) scoped to the register connector — never guess an id or match an account by substring/fuzzy name. Fails with an error (not a silent no-op) if gap posting is disabled for this workspace, if either account doesn't belong to this gap's register connector, or if either account no longer resolves in QuickBooks.
well_retarget_connectors
Retarget (bring across) ledger connectors from this workspace's lineage parent onto this workspace — the write behind the connector-retarget card. For each source connector, a new connector row is created here that borrows the parent's credentials and pulls the item's history in on its own first sync; the transactions are not moved. REQUIRED: source_workspace_connector_ids — the workspace_connector_id of each candidate to bring across, from well_list_retargetable_connectors or well_show_retargetable_connectors. An id that is not a current candidate here is refused; an id whose connector has already been retargeted is reported back under already_retargeted_workspace_connector_ids rather than erroring, so a repeated Confirm is a safe replay. Only a workspace owner or admin may retarget a connector, and the acting person must also hold an active membership on the parent workspace whose credentials the borrow consumes. A caller without that role or that membership is refused, not silently ignored.
well_run_register_diff
Diff a workspace's bank transactions against its accounting-register transactions (e.g. QuickBooks), and persist the result. - Every match — hard evidence (structured reference, IBAN, tax ID) or inference-only (memo/payee reading) — is raised as a review task with the candidate already attached (raised_for_review). Nothing links automatically; resolve with well_resolve_reconciliation_task once a human decides. - Bank transactions with no register counterpart come back as missing_in_register_ids, each also minted as a gap review task (gaps_proposed) — resolve one with well_resolve_register_diff_gap once a human names the two ledger accounts. gaps_already_proposed counts gaps re-surfaced from an earlier run that already have an open, unresolved proposal. - Bank transactions NOT confirmed absent from the register come back as contended_in_register_ids — never minted as a gap. Two cases land here: (1) a plausible match lost to a higher-confidence sibling transaction this run, so the register-side movement is already accounted for by the winner; (2) the matcher couldn't produce a trustworthy answer (an invalid model response or a provider failure), so absence was never confirmed. Re-run the diff later; a genuine gap or duplicate should resolve itself once the winner's review task is handled or the matcher succeeds. - Register entries no bank transaction explains come back as unexplained_in_register_ids. Returns { enabled: false, ... } with all counts 0 if the workspace's register-diff feature is off.
well_search_company_registry
Search the public company registries for a company by name, to find the one a workspace IS before you create its company workspace. This draws nothing on the user's screen. Use it in the zero-company case: a membership workspace has no company attached and no detected candidate, so you search the registry for the user's company. Each hit carries an `id` — the registry ref — that you pass to well_create_company_candidate as `registry_ref` to mint a candidate from that hit, then well_create_company_workspace to make it the company workspace. Pass `country` when the user names one, to scope the search to that jurisdiction. The result carries `degraded: true` when a provider was unreachable and the hits are partial. Confirm the exact company with the user before you create anything from a hit; never pick one from a name alone.
well_search_context
Search the workspace's recorded notes and context (meeting notes, tickets, imported documents) for a query. Returns compact snippets — each result's "snippets" is an array of one or more matched passages from that note, never the full note body — follow up with well_get_entity on the returned note id for the full record. Use this for questions about the business, a company, a person, a process, pricing, or a past decision. Do NOT use this for a question well_query_records already answers (amounts, counts, lists, filters). When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_search_skill
Find the Well procedure for what the user wants to do, when no Well skill is installed in this session. Returns the roster of Well skills with their descriptions; pick the one whose description matches the request, then call well_get_skill with its id and follow the returned instructions exactly. Call this FIRST for any request that asks to DO a finance job with Well — fetch or chase missing invoices, connect a bank or a tool, pick a period, categorize suppliers. Route on the form of the request: a job to carry out ("go chase", "get them collected", "connect", "categorize before I close") comes here, while a question about the state of the data ("what is", "which", "how many", "show me", "preview") goes to the matching well_* read tool. Never call it for a question about the user's data (a cash figure, a runway, a list of records, a period's status): those go straight to the matching well_* read tool. Do not call it when a Well skill is already loaded in the conversation.
well_set_own_company
Set which company the workspace itself IS — the confirmed own-company anchor. REQUIRED: company_id — a company that ALREADY EXISTS in this workspace. Obtain it with well_query_records (companies) or well_create_company; this tool never creates one. This is a deliberate, accounting-critical write, not a convenience. Anchoring the own company overwrites the workspace's legal identity on its accounting settings (including clearing fields when the anchor moves), records a manual-confirm audit row, and syncs the billing customer name. It never re-posts existing journal entries. Confirm the exact company with the user before calling; never guess one from a name. Only a workspace owner or admin may set the own company. A caller without that role is refused, not silently ignored. well_start_close hard-gates on this anchor: a workspace with no own company cannot start a close.
well_set_transaction_category
Set ONE transaction's category — the write that clears a categorization gate. REQUIRED: transaction_id, from well_list_uncategorized_window. category — the LABEL, exactly as that read returned it on the row's suggestion, or another label from the closed list this schema carries. The vocabulary is fixed: there is no free-text category and no way to mint one. **`decision` records HOW the category was chosen, and it changes what the row keeps.** - `accepted_classifier_suggestion` — the user affirmed the label the classifier had already put on the row. The row keeps `category_source: "classifier"` and its confidence score, and the affirmation is stamped as `category_confirmed_at`. Send this ONLY when the label equals the classifier's own stored suggestion. - `user_choice` — the user picked the label themselves. The row records `category_source: "user"` with no score. The server verifies an `accepted_classifier_suggestion` claim against the row it is writing and downgrades it to `user_choice` when the stored suggestion is not that label, so the claim can never manufacture classifier provenance. Omitting `decision` is a `user_choice`. **A row from `well_list_uncategorized_window` never qualifies for the affirmation.** That read returns rows carrying NO category at all, so there is no stored classifier value to affirm and the claim would be downgraded every time. Its `categorySuggestions` are PENDING proposals, not a stored category. Clearing that gate is always a `user_choice`; the affirmation exists for a surface that lists rows the classifier already categorized. Categorizing a row does NOT move it in or out of the internal-transfer rule — that rule counts payment-means legs and no label affects it. What a category DOES change is exemption matching: an uncategorized row can never be matched by an exemption and always stays in a sum. One transaction per call. The rows are decided independently and each one is saved as the user decides it. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_set_transaction_ledger_account
Attach ONE transaction to the ledger account its journal entry should post to — the write that clears a posting gap. REQUIRED: transaction_id, from `well_list_unposted_transactions`. ledger_account_id — an account id from that read's `ledgerCatalog`, or from the row's own `ledger_suggestions`. Pass `null` to DETACH the account rather than to leave it unchanged; omitting the field is not how you clear one, because the field is required here. **Attaching the account does not, on its own, clear the gate.** The worklist selects on posting attempts, not on whether an account is present, so a row you attach and leave will come back on the next read. Posting is what clears it. Set `no_invoice_expected: true` to post the entry in the same call. Send it ONLY for a row whose `expects_supplier_invoice` is false on `well_list_unposted_transactions`, which is the read that carries that field: it asserts that no supplier invoice is coming, which is what makes the transaction bookable on its own. A row still waiting for its invoice must be attached WITHOUT it — the invoice is its blocker, and posting early books an entry the invoice would then contradict. Omit the field and nothing posts: the account is recorded and the row stays on the worklist. Most categories already imply their account — the chart maps each category key to a canonical code — so reach for this for the rows the category alone cannot settle, and for a deliberate override. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_show_accounting_settings
Draw the accounting-setup card so the USER reviews and confirms the workspace's accounting settings: country of incorporation, incorporation date, tax ID, fiscal year start, base currency, and accounting framework. This is the tool for every step that asks the user to complete, review, or CONFIRM the accounting settings — the close-books settings step, an onboarding "set up your books" step. It DRAWS the card, shows each row's provenance and the stored "Suggested" fills, lets the user edit what is wrong, and waits for their Confirm. Reach for it directly on such a step; do NOT read the settings first with the silent tool and then decide to draw — drawing the card IS the step. The card gates its Confirm on the required set (fiscal year start and base currency by default; widen it with `required` when a step needs more). ONLY when a step needs the values WITHOUT the user seeing a card (a silent gate check) call `well_get_accounting_settings` instead.
well_show_company_candidates
Show the user the detected COMPANY candidates on a card and let them pick which company is theirs: a tile per detected company candidate with its confidence, and a company-registry search at the top for the case where none was detected. ⚠️ ONLY for the zero-company case — a membership workspace with no own company attached — when the user must choose or find the company to create the workspace from. For the values alone, read `well_get_own_company`, which draws nothing. ⚠️ WAIT ON THE CARD IN THE TURN THAT DREW IT. Write your one line for the user FIRST — the wait holds the turn open for up to a minute, and a user looking at a card with no sentence beside it has been given no reason to click — then call `well_wait_for_selection({ kind: "company_pick" })`, which this result's `next_step` also states. The card's own footer mints the company workspace and switches into it on the click, so never mint it yourself after the pick. ⚠️ NEVER PICK THE COMPANY for the user, and never infer it from the workspace name.
well_show_records
Put a table of records IN FRONT OF THE USER. Use it when the user asked to SEE rows — "show me my invoices", "list my companies", "which suppliers have no category" — and when the answer you owe them IS the table. The table is ALWAYS the root's display view in the Well web app's column order, trimmed on the widest roots to what fits a chat-width table. You never choose columns for presentation: omit `fields` and the right ones render. ⚠️ FOR A READ THAT IS YOURS RATHER THAN THEIRS, CALL `well_query_records` INSTEAD. Same arguments, same rows, no table. Every gate, count, freshness check and intermediate read belongs there — this tool renders on every call, so using it for an internal check drops a table into a conversation about something else. ⚠️ DO NOT NARRATE THE TABLE. The card already shows these rows; restating them as markdown gives the user the table and a duplicate list under it. Two things the table cannot say for itself belong in your text: `totalCount` when it exceeds what is displayed ("showing the 50 most recently updated of 214"), and the `records_url` link for everything the card truncates. ⚠️ ONE CARD PER TURN. A turn draws at most one table, and never a table beside a card that is waiting for a click. 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: - Omit `fields` to show the user a table — that is what renders the root's own columns - `fields` is ADDITIVE and for values YOU need to reason about: it widens the payload you read and never reorders or trims the columns the user sees - Field paths from schema: "invoices.issuer.name" → ["invoices", "issuer", "name"] - Default 50 records per request, max 500. EXAMPLE - show the user their invoices (no `fields`, ever): well_show_records({ root: "invoices", limit: 50 }) 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). So a request to see a record type is ONE call: the user gets a table of the first page, the count tells them how many there are, and the link takes them to the rest. "Show me all my invoices" is answered by one call plus the link — NOT by fetching 483 rows into this conversation. - 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 }.
well_show_retargetable_connectors
Draw the connector-retarget card so the USER brings a bank (or other ledger) connector across from the membership workspace to this company workspace. A connector connected on the parent (membership) workspace syncs its transactions there, where they cannot post. This card lists each such connector that could follow this workspace, with how strongly it was proved to belong here and how much history is behind it, pre-ticks the strong matches, and on Confirm retargets the ones the user keeps: a new connector row is created here that borrows the parent's credentials and pulls the history in on its own first sync. Draw it on the close-books bank step to SHOW the user the connectors they can bring across, let them pick which, and CONFIRM the bring-across, once at least one candidate carries a proof tier other than "no_match" and a transaction count above zero. Drawing the card IS that step, and it waits for the user's Confirm. ONLY when a step needs the candidate count WITHOUT the user seeing a card (a silent gate check that decides whether to offer the card at all) call `well_list_retargetable_connectors` instead.
well_show_workspace_picker
Ask the user WHICH workspace to work in, on a card: one tile per authorized workspace, with its logo and the company behind it. ⚠️ ONLY when the token authorizes SEVERAL workspaces and no hint resolves to one. Every other case is yours to settle with `well_list_workspaces`, which draws nothing: exactly one workspace in the grant, a name or company the user already named, a pin this conversation itself wrote, or no workspace at all. A chooser over a set of one asks nothing, and a chooser you could have answered yourself asks the reader a question you already know the answer to. ⚠️ WAIT ON THE CARD IN THE TURN THAT DREW IT. Write your one line for the user FIRST — the wait holds the turn open for up to a minute, and a user looking at a card with no sentence beside it has been given no reason to click — then call `well_wait_for_selection({ kind: "workspace" })`, which this result's `next_step` also states. The click writes the pin server-side, so never follow it with `well_switch_workspace`. ⚠️ NEVER DEFAULT TO THE PRIMARY WORKSPACE on the user's behalf, and do not restate the workspaces in text under the card. ⚠️ WRITE `reply` IN THE USER'S LANGUAGE, WITH `{picked}` WHERE THE WORKSPACE NAME BELONGS. A click sends that sentence into the conversation as the person's own message, and the card puts the workspace they actually picked in place of the placeholder. A sentence left unwritten sends English to a reader who is not writing in English; a sentence that names a workspace itself is refused, because you are writing it before they have chosen. Use this FIRST when a single token may cover more than one workspace. Each entry has: - workspace_id: pass this as the workspace_id argument on other tools to target one workspace. - workspace_name: human-readable name (null if it can't be resolved). - is_primary: true for the token's default workspace (used when you omit workspace_id on a write). - own_company_id: the public id of the company this workspace is anchored to, or null. A row that carries it is a company workspace: the close flow runs in one. A row without it is a membership workspace, the container a sign-up mints. - lineage_parent_workspace_id: the workspace_id of the membership this workspace was created under, or null when the workspace has no active lineage. A membership workspace (no own_company_id) whose id appears here on other rows is the parent of those company workspaces. - identity: the company behind the workspace (registered name, trade name, registry number, country, website, currency, fiscal year start, where the fiscal year start came from, and the jurisdiction's default fiscal year start), so two similarly-named workspaces can be told apart. Every field is null when the workspace has no accounting settings yet. Tax identifiers are deliberately not included. - has_bank_transactions: whether a connector the workspace BANKS with has delivered any transaction to it, meaning a bank, a neobank, or a treasury or spend platform whose product is an account. An accounting platform and a payment processor deliver transactions too and do NOT count here. Neither does a transaction whose source connector is unknown, whose install has since been disconnected, or whose catalog entry has been retired. Only `true` shows that a bank has fed this workspace: `false` means no such transaction was found and `null` means the signal could not be read, so an absent value is never a zero and neither value licenses skipping a bank-connection step. Read this before any month read when the flow needs to know whether the workspace banks with anything at all. The result also carries `session` — what the user's card clicks have already recorded this session: `pinned_workspace_id` (null when not switched), `workspace_queue` (the workspaces to work through next, empty when none), `selected_periods` (the months picked on the period card, empty when none), and `selected_counterparties` (the counterparties picked on the missing-invoices card, with the workspace their company ids belong to; null when none was picked). Call this any time you need to resync with clicks you may have missed. When the token authorizes a single workspace you can omit workspace_id everywhere; when it authorizes several, read tools fan out across all of them unless you pass a workspace_id, and write tools require one.
well_sum_invoices
Sum a workspace's billed amounts over a window of whole months, grouped by month, currency and billing context. Arithmetic only — this tool holds no definition of MRR or recurrence, and returns no figure the app renders. Use it when you are computing a figure whose RULES you are stating yourself: recurring revenue over a window you chose, a total restricted to the billing contexts a reader confirmed, a per-month series behind a trend you are about to describe. The server derives no MRR of its own, so an MRR figure starts here: state the rules, sum exactly those rows, then put the result on a card with `well_render_mrr`. **The window is whole months.** `from` and `to` are both the first day of a month, as YYYY-MM-01; `from` is inclusive and `to` is EXCLUSIVE, so June to August is `2026-06-01` to `2026-09-01`. A bound inside a month is refused rather than widened, and so is a window longer than 36 months. **Which rows are billed amounts is decided here, and stated so you can say it.** A canceled invoice is left out. Only billing documents count: invoices, debit notes, credit notes and subscription billing statements, so a proforma and the invoice it precedes are not summed twice, and an order, a quote or a payment advice never is. A row with no document type is read as an invoice. Every amount is NET of tax (`items_total`), because tax collected is owed onward rather than earned. **`party_scope` is required, and it decides whose invoice this is.** `sales` is what the workspace ISSUED — its receivables, and the only side revenue can come from. `purchase` is what it received. The two are the same rows read from opposite ends, so no default is offered: a server choosing a side would answer a different question from the one asked. `intra_self` is an invoice between two companies the workspace owns, and `unattributed` is one Well could place on neither side. **Those four scopes partition every invoice exactly once**, which is what makes an incomplete picture visible rather than silent. `unattributed_count` comes back on every call, whatever scope you asked for: it counts the invoices in the window that landed in that fourth bucket. State it beside any total, because an unattributed invoice may still belong in the figure and nothing here can tell you whether it does. **Every row carries ONE month, ONE currency and ONE billing context.** Currency is always a grouping key, named or not: adding EUR to USD gives a number denominated in nothing, and no field on the result would tell you it happened. Convert the per-currency subtotals yourself, at a rate you can state, before you add them. **`sum` is already net of credit notes.** A credit note subtracts its magnitude from its own month-currency-context bucket, whichever sign it was stored with; `credit_note_sum` and `credit_note_count` report what that removed, so you can say what the figure netted. Do not subtract them a second time. A bucket whose credit notes outweigh its invoices nets negative, and that is a real state rather than an error. **`billing_context` is `null` on rows that name no billing arrangement** — none stored, `unknown`, or a value Well has no label for — and that is a third answer rather than a kind of one-off. The field is filled by extraction, not by a billing system, so a workspace can carry real recurring revenue on rows that say nothing about it. `unclassified_count` totals those rows. The recurring-contexts card offers them as one choice, keyed `"unclassified"`, so apply that key to these rows and only these. Counted or not, report the count rather than letting a reader read the remainder as "everything else". `corrected_or_consolidated_count` counts the corrected and consolidated invoices among the rows. Each replaces invoices Well holds no link to, so when those originals fall in the same window the sum counts that billing twice. The rows keep them, because dropping them would lose the revenue whenever the originals fall outside the window. State the count beside any total whenever it is not zero. `excluded_malformed` counts billing documents in the window with no readable net amount or no currency. They are in none of the rows and none of the sums, so state the count beside any total. It comes back `null` when the count could not be read, which is NOT `0`: zero says every row was readable, null says nobody counted. `partial: true` means the aggregate was cut short and every figure is a FLOOR rather than a measurement. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_sum_transactions
Sum a workspace's transactions over a date window, grouped how you ask. Arithmetic only — this tool holds no definition of burn, spend, or runway, and returns no figure the app renders. Use it when you are computing a figure whose RULES you are stating yourself: a burn over a window you chose, a total that excludes categories the user named, a per-month series behind a trend you are about to describe. The server derives no burn of its own, so a burn figure starts here: state the rules, sum exactly those rows, then put the result on a card with `well_render_burn`. `from` is inclusive and `to` is EXCLUSIVE, so a window of whole months passes the first instant of the month after the last one you want. Both are required: a window you cannot state is a decision you have not made, and this tool will not pick one for you. `axes` groups the result — any of `month`, `currency`, `category`, comma-separated — and names what to group in ADDITION to currency. Every axis appears on every row: the ones you did not group by come back `null`, so the row shape never depends on what you asked for. **Currency is always grouped, named or not, so a row never mixes two.** Adding EUR to USD gives a number denominated in nothing, and no field on the result would tell you it happened. Omitting `axes` therefore returns one row PER CURRENCY over the window, not one row. Convert the per-currency subtotals yourself, at a rate you can state, before you add them — and if there is more than one row and you report a single total without converting, the total is wrong. **Each row carries BOTH sign branches, and choosing between them is your job.** `sum_negative` is the magnitude of the rows whose amount is negative; `sum_positive` is the magnitude of the rows whose amount is positive; `count_negative` and `count_positive` say how many rows are behind each. Which one is money leaving depends on the FEED, not on the query: most connectors store outflows as negatives, some store them as positive magnitudes. Read the counts to decide, and decide ONCE over the whole window rather than per row or per group: a single category or month can be all-positive on a signed feed, so electing per group flips the convention mid-answer and totals two different things together. A window whose rows are overwhelmingly negative is a signed feed, and outflow is `sum_negative`. Almost no negatives means the feed stores magnitudes and keeps direction in a field this tool does not group on — so it cannot separate outflow from inflow, and `sum_negative + sum_positive` is gross movement, not spend. Say so rather than reporting it as an outflow. **A substantial share of BOTH is a third answer, not a close call between the first two.** A workspace connected to a signed feed and a magnitude feed at once pools them here, and no combination of the two subtotals is its outflow: `sum_negative` misses the magnitude feed's spend entirely, and adding `sum_positive` pulls in the signed feed's income. There is no grouping that separates them, because the axes carry no connector. Report that the window mixes conventions and that a single outflow cannot be derived from it, rather than electing whichever branch is nearer. State which convention you elected and what the counts were, so the reader can check it. **`scope` is required, and it decides which rows are this workspace's.** `own_and_adopted` is the population the burn tile counts: the workspace's own transactions plus any a parent workspace shared with it through an adoption grant, with legs tested against the parent's accounts too. `own` is the workspace's own transactions only, tested against its own accounts — the rows its balances move with. Use `own` when the sum is reconciled against the workspace's own balances, as a cash-flow bridge is, and `own_and_adopted` for a burn. On most workspaces the two agree; on a child workspace they do not, which is why neither is a default. `exclude_internal_transfers: true` keeps only the rows with EXACTLY ONE leg on an account the workspace owns. Two legs is a movement between your own accounts and drops, which is the rule's purpose. **Zero legs also drops**, and that is the part worth knowing: a card purchase sits against a liability account, so on a card-heavy workspace this removes card spend along with the transfers. `excluded_zero_leg` and `excluded_multi_leg` count the two populations separately, so read them before describing what the figure covers. Read `excluded_zero_leg` as "this many rows carried no asset movement" and nothing narrower: a card charge lands there, and so does a row whose payer and payee resolved to no account at all. `excluded_no_owned_leg` is that second part on its own: rows with no leg on ANY owned account, liabilities included, so card spend is never in it. Those rows could not be attributed to an account and may have moved a balance the sums cannot see, so a figure reconciled against balances treats a non-zero count as flows that are incomplete. A large `excluded_zero_leg` is a reason to look, never a spend total to quote. Any of the three counts comes back as `null` when it could not be measured, which is NOT `0`: zero says the rule removed nothing, null says nobody counted. On a null, say the exclusion is unmeasured rather than reporting none — the sums themselves are unaffected, and `partial` is what speaks for those. For reproducing the burn tile that is exactly right — it is the conservation law the cash-flow bridge rests on. For "total spend excluding transfers between our own accounts" it is not what the words promise, so say what fell out or leave the flag off. The rule is structural: it counts legs, so no label, category, or type on the row affects it, and a user recategorizing something does not change it. `exempt_categories` takes category keys that should not count. A transaction with no category at all is never matched by an exemption and always stays in the sum; if you want those excluded too, that is a different question and you must say so. `excluded_malformed` counts rows in the window whose amount could not be read as a number. They are in none of the sums, so state the count beside any total rather than presenting a figure that silently skipped them. `partial: true` means the aggregate measured nothing: it was cut short, or no asset account is in scope. Either way it arrives with an empty `rows`, so there is no figure, and the empty rows are not a zero. Say so and offer to try again, unless the workspace holds no deposit or other asset account, where a retry changes nothing. When the token authorizes one workspace, call this directly — no other tool call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.
well_switch_workspace
Write this connection's session context — the one place a conversation's standing choices live. This is the tool the widget cards call when the user CLICKS them: the workspace pin and queue, the selected months, the selected counterparties, and the step acknowledgements are all recorded here, and every later tool call defaults to them. Pass any of: - workspace_ids (ordered list): the workspaces to work in. The FIRST entry becomes the pin and the rest the workspace_queue to work through next. Every id must be one this connection is already authorized for — call well_list_workspaces to see them. This grants no new access; it only chooses among the authorized workspaces. - periods: the months the user VALIDATED on the period card ({ calendar_year, calendar_month } each). Period-scoped reads (well_list_missing_invoices, well_preview_invoice_fetch) default to them when called without a period. Send it only for the user's month selection — never to bound a counterparty pick, which would overwrite that selection. - counterparties: the counterparties (vendors) the user selected, each { company_id, matched_connector_service_id }. Copy both ids off the row you listed them from; pass no display name. The selection belongs to the workspace this call is dispatched to, and a switch to another workspace clears it. One session holds one selection, so a new one replaces it; a selection sent for a workspace this connection has switched away from is REFUSED instead, so a card the flow moved past cannot overwrite the pinned workspace's selection. - counterparty_periods: the months the counterparties card listed, sent alongside counterparties. The pick then narrows those months only, and a month it never covered is read in full. With none named, the months this session already holds bound the pick. This never becomes the session's selected months. - exempt_categories: the category keys the user marked as NOT burn on the exemption card, copied off what well_list_burn_exemptions returned. An EMPTY list is a real answer and records that they exempted nothing; omit the field entirely when they have not answered yet. The set belongs to the workspace this call is dispatched to, and a switch to another workspace clears it — but a change of months does not, because a category is or is not burn for the business whatever window is read next. One session holds one set, so a new one replaces it. - recurring_contexts: the billing context keys the user counts as recurring revenue on the recurring-contexts card, copied off what well_list_recurring_contexts returned. An EMPTY list is a real answer and records that they count nothing as recurring. It is scoped, cleared and replaced exactly like exempt_categories. - ack: "connectors", "bank", "categorize", "assign", "deploy", "invite" or "company_pick" — records that the user answered that flow step, in the workspace this call is dispatched to. "company_pick" is the company candidates card's own ack: the card sends it on its Use click alongside the pin to the new company workspace, and on its Keep for later without moving the pin, so you do not send it yourself. "categorize", "deploy", "retarget", "company_pick" and "invite" also take ack_outcome: "done" when the user carried the step out, "keep_for_later" when they set it aside; both end the step. A switch to another workspace clears it, so the next workspace's card asks for its own click. An acknowledgement sent for a workspace this connection has switched away from is REFUSED instead, so a card the flow moved past cannot un-confirm the step the pinned workspace's own card recorded. - next_step: the line the user picked on the next-steps card, as { skill, prompt } copied off that card's row. The prompt is the sentence the model reads as the user's own message, so it travels beside the slug rather than being rebuilt from it. The pick belongs to the workspace this call is dispatched to, and a switch to another workspace clears it. Call it with NO argument at all to pin the workspace this call itself is dispatched to — its universal workspace_id, or the only workspace the token covers. workspace_ids is the PIN write and nothing else: it moves the pin AND replaces the workspace_queue, so a one-entry list ends a run that still had workspaces queued. Send it only to change the workspace. A call carrying periods, counterparties, exempt_categories, recurring_contexts, ack or next_step needs no workspace_ids: its universal workspace_id targets that one call, and the pin and the queue stay where they are. Never re-pin the workspace this connection already holds. Every provided input is applied, and `changed` names the session fields this call wrote. `pickup` says what became of the write: "resumed" or "held_then_resumed" mean the model's own turn carries it on and the caller must send no reply of its own, "unwaited" or "exhausted" mean nothing is watching and the caller's reply is the only thing that moves the flow, and "stale" means the flow already answered this card, so the value is kept as a late edit and the caller says nothing. `resumed` is the boolean half of the first two. After a switch, every later call that omits workspace_id targets the pinned workspace, for reads and writes alike; passing workspace_id on a later call overrides it for that call only. well_list_workspaces reports the current session context, and well_wait_for_selection reads a card click back — instantly when it already landed here, after a short wait otherwise. This changes nothing in the user's data.
well_update_company
Update an existing company in the current workspace. Use this tool when the user asks to change, fix, rename, or edit a company's fields. REQUIRED: company_id OPTIONAL (only include fields the user wants changed): name, description, domain, registered_name, trade_name, tax_id_value, tax_id_type, registry_country (ISO 3166-1 alpha-2, e.g. "FR"), business_type, registered_value, registry_name, locale (ISO 639-1 two-letter language code, e.g. "en", "fr" — not "en_US"). CATEGORIES (a counterparty's industry): pass `category_ids` — the COMPLETE set of category ids the company should carry. It REPLACES the current set: ids you leave out are unlinked, and `[]` clears every category. Omit the field to leave the categories untouched. Read the catalog first with well_query_records({ root: "categories", whereClause: { category_type: { _eq: "company" } } }) and pass ids from it — an id that is not a `category_type = "company"` row is refused, and this tool never creates a category. NOT CHANGEABLE via this tool: emails, phones, locations, linked people, media. Those require dedicated tools (not yet available). PROVENANCE: `decision` says HOW the set was chosen. `accepted_suggestion` — the user let a category the classifier had already proposed stand, without touching it. `explicit` — the user chose the labels. **A request the user typed is always an `explicit` choice, so never send `accepted_suggestion` from a conversation.** The affirmation belongs to the categorization card, where a pre-filled picker the reader leaves alone is the only thing that can be let stand; a user who names a category in words has chosen it, even when they say they agree with a suggestion. Omit the field and the write is `explicit`. The server checks an `accepted_suggestion` claim against the company's own pending proposals and returns `explicit` when the written set matches none of them, so the claim can never manufacture classifier provenance. Returns { success: true, company_id, name } on success — plus category_count, the number of categories the company carries afterwards, and decision, the provenance the server settled on, when the call passed `category_ids`. Returns { success: false, error } on failure.
well_update_invoice
Update an existing invoice in Well. Call well_get_schema("invoices") to discover all available fields. REQUIRED: invoice_id OPTIONAL (only pass fields you want changed): - reference_number, issue_date (ISO date), due_date (ISO date) - status (draft | issued | paid | canceled) - terms, description - grand_total, items_total, tax_total (numbers) - local_currency (ISO 4217 three-letter code, e.g. "EUR", "USD") - document_type_code (UN/CEFACT 1001 code, e.g. "380") - billing_context (e.g. subscription, one_time, project, ...) - issuer_company_id / receiver_company_id (uuid to set, null to clear, omit to leave unchanged) Cannot change line items, payment_means, or document attachment via this tool.
well_update_person
Update an existing person (contact) in the current workspace. Use this tool when the user asks to change, fix, rename, or edit a person's fields. REQUIRED: person_id OPTIONAL (only include fields the user wants changed): first_name, last_name, job_title. NOT CHANGEABLE via this tool: emails, phones, locations, linked companies, media. Those require dedicated tools (not yet available). Returns { success: true, person_id, full_name } on success, or { success: false, error } on failure.
well_upload_document
Upload a document (invoice, receipt, statement) into the workspace by sending its bytes as base64. ⚠️ THIS IS A WIDGET'S WRITE, NOT YOURS. The card's drop zone reads the file the person dropped or chose, encodes it, and calls this tool itself. Do NOT call it: a model holds no file, so a call made from a conversation can only carry bytes nobody supplied. When a person says they have the invoice, point them at the drop zone on the gap card. Send `content_base64` WITHOUT a data-URI prefix — the raw base64 only, no `data:application/pdf;base64,` header. Accepted content: PDF, JPEG, PNG, GIF, HEIC, HEIF, AVIF, WEBP, TIFF, plain text, CSV, XML. The bytes are checked against the declared `mime_type` (file signature, not just the claim), so a PNG announced as a PDF is refused. Size ceiling: 5 MB of file (before base64). A larger file is refused with its actual size — upload it through the web app instead, which accepts up to 15 MB. Pass `source_transaction_id` to anchor the document to the bank transaction it pays. That is what makes a dropped invoice land on the right line instead of in a general inbox. Well extracts the document after upload; the extraction is asynchronous and this call returns as soon as the file is stored. A file already in the workspace is deduplicated by content and returns the existing document rather than a copy.
well_upload_statement_bytes
Upload a bank statement file's BINARY CONTENT (PDF or image) as base64, so the file's real bytes reach Well without any out-of-band HTTP call. Use it for PDF and image statements up to 5 MiB decoded (the base64 text may be roughly a third larger). Base64-encode the file's bytes EXACTLY — never re-encode a screenshot, a transcription, or a summary of the file. Optionally send the file's sha256 (hex); the server decodes, hashes, and rejects a mismatch, proving the bytes arrived intact. The response carries content_sha256 and byte_length of the decoded payload — report them for verification. The parsed rows, totals, and import outcome arrive via well_get_statement_import_result with the returned document_id. Text statements (.csv/.txt/.xml) whose contents are verbatim in this conversation can go through well_upload_statement_content instead. Upload one statement file per call — call this tool once per file. The document enters the same import pipeline as an in-app upload (detection, dedup, promotion).
well_upload_statement_content
Upload a bank statement's TEXT CONTENT (a .csv, .txt, or .xml file) directly, as an alternative to well_create_statement_upload's out-of-band file POST. Use it when the user's statement is a small text file whose contents are verbatim in this conversation (1 MiB decoded limit). Send the content EXACTLY as you received it — never reformat, summarize, transcribe from memory, or reconstruct rows. A mangled relay imports wrong financial data. This path is BEST-EFFORT fidelity: what Well ingests is what you relayed, not a byte-verified copy of the user's file. The response carries content_sha256 and byte_length of what the server received — report them so a corrupted relay is visible. The parsed rows, totals, and import outcome arrive via well_get_statement_import_result with the returned document_id, not in this response. PDFs and images NEVER go here (the model cannot relay their bytes faithfully) — use well_upload_statement_bytes. XML with DOCTYPE/ENTITY declarations is rejected. Upload one statement file per call — call this tool once per file. The document enters the same import pipeline as an in-app upload (detection, dedup, promotion).
well_upsert_accounting_settings
Set the workspace's accounting settings: fiscal year start month, first fiscal year start date, country, base currency, accounting framework, chart-of-accounts confirmation, the incorporation date, and the tax ID (value and type together). Provide only the fields you are changing; omitted fields are left untouched. An empty call (no fields) is refused. tax_id_value and tax_id_type must be provided together. Only a workspace owner or admin may set the accounting settings. A caller without that role is refused, not silently ignored. Changing the fiscal year start month moves the whole fiscal calendar, so it is REFUSED when a period is locked or a close is in progress — the tool surfaces that refusal rather than forcing it. When the change is allowed, it soft-deletes the workspace's regenerable DRAFT journal entries so they re-mint on the new coordinates; VALIDATED and LOCKED entries are never touched. These are accounting-critical values. Confirm each one with the user before calling and never guess them — do not infer a country, currency, framework, start month, or tax ID the user did not state. The tax ID here updates the workspace's anchored company and its settings mirror together, so the two never drift. To set WHICH company is anchored, use well_set_own_company; to set that company's tax ID, use this tool.
well_wait_for_selection
Read the user's card click, holding the turn open until it lands. Call it in the SAME turn, right after the tool whose card asks the user to click: well_list_workspaces (kind "workspace"), well_list_periods (kind "periods"), well_list_missing_invoices (kind "counterparties" — its card is the only one that records a counterparty pick), well_list_burn_exemptions (kind "exemptions"), well_list_recurring_contexts (kind "recurring_contexts"), well_list_connectors (kind "connect_ack" for the connect step, "bank_ack" for the bank step), well_list_counterparties (kind "categorize_ack" — its Continue and its Keep for later both write it), well_list_missing_invoice_owners (kind "assign_ack" — its Continue writes it), well_preview_invoice_fetch (kind "deploy_ack" — its Deploy, its Continue and its Keep for later all write it), well_show_company_candidates (kind "company_pick" — its Use this company mints the company workspace, switches into it and writes the ack in one call; its Keep for later writes the same ack with the outcome the click carried and moves no pin), well_show_retargetable_connectors (kind "retarget_ack" — its Confirm and its Keep for later both write it, with the outcome the click carried), well_list_member_candidates (kind "invite_ack" — its Send and its Keep for later both write it, with the outcome the click carried), or well_propose_next_steps (kind "next_step": a row click writes it; on "selected", take selection.next_step.prompt as the user's own message and start that skill in the same turn, loading it with well_get_skill). It waits up to 60s for the click. "selected" — continue the flow. "no_selection_yet" — call it again at once, at most 5 calls in this turn; after the fifth, end the turn on the card in one line, and the user's click then prefills the reply that resumes the flow. - status "selected": the choice is recorded. `selection` carries it — the pinned workspace_id and workspace_queue, the picked periods, the picked counterparties (each { company_id, matched_connector_service_id } plus the workspace_id they belong to and the `periods` they were listed for), the exempted category keys or the recurring context keys plus the workspace_id they belong to, or the acknowledgement plus the workspace_id it was made in and, on a card whose buttons say different things, the `outcome` the click carried. `already_set: true` means it was recorded since the card was drawn but before this call (the user had already clicked). Continue the flow with it. Only a click recorded SINCE the card was drawn is reported. An answer left over from an earlier conversation stays in the session and is never handed back, so this tool always waits for the click the card in front of the user is asking for. - status "no_selection_yet": nothing has been recorded since the card was drawn and no click landed within the wait (default 60s, clamped 5-60s). This is a NORMAL result, not an error. Call this tool again at once, up to 5 calls in one turn. After the fifth, end the turn on the card in one line; the user's click then prefills the reply that resumes the flow. A counterparty pick belongs to the workspace AND the months it was made against, and it carries those months in `selection.periods`. A switch to another workspace, a change of the selected months, or a fresh well_list_missing_invoices card drops it. So kind "counterparties" never hands back a pick made against another month — with that pick dropped, the call waits for the new click instead. A pick recorded BEFORE this call is reported only when it was made in the workspace this call targets, so pass `workspace_id` to ask about a workspace the connection is not switched to. A pick that lands DURING the wait rides back with the workspace it was made in — compare `selection.workspace_id` before you act on it. A burn-exemption answer belongs to the workspace it was made in and rides back as `selection.workspace_id`, and it carries no months: a category is or is not burn for the business, so the same answer holds over any window. An EMPTY `selection.exempt_categories` on status "selected" means the user exempted NOTHING — act on it, do not re-ask. A switch to another workspace drops it; a change of the selected months does not. A recurring-contexts answer follows the same rules as a burn-exemption answer: it belongs to the workspace it was made in, carries no months, and an EMPTY `selection.recurring_contexts` on status "selected" means the user counts NOTHING as recurring — act on it, do not re-ask. An acknowledgement belongs to the workspace it was made in, and rides back as `selection.workspace_id`. On kinds "categorize_ack", "deploy_ack", "retarget_ack", "company_pick" and "invite_ack" it also carries `selection.outcome`: "done" means the user carried the step out, "keep_for_later" means they set it aside — both end the step, so continue the flow either way and say in half a sentence which one it was. A switch to another workspace drops it. An ack recorded BEFORE this call is reported only when it was made in the workspace this call targets, so pass `workspace_id` to ask about a workspace the connection is not switched to. A click that lands DURING the wait is reported with its own workspace, which can be another card's — compare `selection.workspace_id` before you act on it. This tool reads and waits — it changes nothing.

Tools removed

Tools this server used to expose. Anything built against them stopped working on the day they went.

well_get_burn
removed 7 Sep 2026
well_get_cash_position
removed 10 Sep 2026
well_get_runway
removed 10 Sep 2026
well_get_cash_flow_bridge
removed 11 Sep 2026
well_get_cash_forecast
removed 17 Sep 2026

Endpoints

URLTransportStateLatencyChecked
https://api.wellapp.ai/v1/mcp streamable-http answering 279 ms 12 min ago

Alternatives to Well

same job, measured the same way
Codat
by usefulapi

MCP server for Codat — companies, connections, invoices, bills and financial statements.

28 tools answering
Shelter
by nextauralabs

Connect AI agents to scoped financial context from your own Shelter account

46 installs/wk local only
Buchpilot
by makririch

DACH accounting for AI: connect sevDesk & Lexoffice to create invoices, contacts, vouchers.

36 installs/wk local only
Conta Bemol MCP
by mcp-dir

Connect your Conta Bemol account to AI via Brazil's Open Finance: balances, statements, cards, inves

25 tools answering
ROIC.AI Financial Data
by roic

ROIC.AI financial data: statements, ratios, prices, and earnings transcripts for public companies.

answering
Invoice Data Extraction
by invoicedataextraction

Invoices and other financial documents to rows: upload, say what to extract, read the rows.

answering
Openfinance
by openfinanceorg

Connect your financial accounts to view balances, search transactions, and get spending insights.

35 installs/wk local only
Xero
by mindstone

Xero MCP server for invoices, contacts, payments, quotes, and financial reports.

73 installs/wk local only

Well — questions

Answers built from our own checks of this server.

What can Well do?
It exposes 82 tools, read directly from the server on our last check. Among them: well_add_contact_channel, well_assign_account, well_assign_missing_invoice_owners, well_claim_statement_draft, well_create_company, well_create_company_candidate and 76 more. The full list with descriptions is on this page — we take it from the server itself via tools/list, not from a README. How MCP servers expose tools in the first place →
Is Well working right now?
We send a real MCP handshake every 15 minutes. Over the last 24 hours 91 of 91 checks got a reply (100.0%), average response time 575 ms. The bar chart above shows every period we have measured.
Did Well ever remove tools?
Yes. well_get_burn, well_get_cash_position, well_get_runway, well_get_cash_flow_bridge and 1 more are no longer exposed — we recorded the date each one disappeared. A tool vanishing usually means a breaking change for anything that depended on it.
Is Well still maintained?
The linked repository no longer exists on GitHub — it was deleted or made private. We show this because it changes what you can expect: an unmaintained server may keep answering for months and then stop without warning.
How do I connect Well?
Copy the ready config from this page — we generate it for Claude Code, Claude Desktop, Codex, Cursor and VS Code, each with the file path that client actually reads. It is a remote server, so there is nothing to install — the client connects to the address.
Does Well need an API key?
No. Well completed a full MCP handshake with us as an anonymous client and listed its tools without asking for anything. All 82 of them are readable on this page. This is what we observed, not what the docs claim.
How fast is Well?
It answers our handshake in 575 ms on average, which is faster than 23% of all working MCP servers we measure. That is on the slow side — worth knowing if the tool sits inside an interactive loop. The comparison comes from our own checks across the whole registry, every 15 minutes.