| name | connect-tools |
| requires | ["define-workspace"] |
| description | Check which data sources a Well workspace has connected — bank accounts, accounting software, invoicing and payment portals — get the missing ones connected with Well's one-click install links, and hand off a typed coverage result to the flow that follows. Use when the user asks to connect a bank, connect their finance tools, link an accounting tool (Pennylane, QuickBooks, Xero…), add Stripe or Shopify, asks "which tools are connected", "what can I connect to Well", or when a Well skill needs bank / accounting / invoicing data present before it continues. Do not use to compute figures, to trigger a sync, to disconnect a tool, or to run a connector's own actions. |
Connect Tools with Well
Purpose
Answer "does this workspace have the connections this job needs?" and close the gap. One tool does the whole job: well_list_connectors returns Well's connector catalog with every one of the workspace's connections represented on its own catalog row, and in an MCP-Apps host that result renders as the connect picker card. Read the state per kind — bank, accounting, invoicing — from those rows, let the card carry the connect links, and hand a typed coverage result to the calling flow. In flow_step mode — the default, and what a run made for its own sake gets — this is a user stop: the card renders — green or not — as a multi-select picker. In internal_check mode it is not a stop at all; the caller asked for coverage, not for a connect step, and the read hands back in the same turn. The tiles tick on and off, and the is_preselected rows arrive pre-checked. One Connect button opens an install tab per ticked provider, and one Continue button ends the step. The Continue click writes an acknowledgment server-side (well_switch_workspace with ack: "connectors") and prefills "Continue" in the user's composer; the user sends it, and that message is how the flow moves on. On a run scoped to kind bank — the bank-only fallback below, for when connect-bank is not installed — the result reports that scope. The card takes its wording and its ack kind from that field, so it writes ack: "bank" instead. Step two of Well's fetch-missing-invoices flow, ahead of the dedicated connect-bank step where that skill is installed.
The card names its own scope. A call scoped with kind gets that scope's wording — the bank, the accounting tool, or the invoicing tool by name — and an unscoped call keeps the generic wording for the whole catalog. Both states carry the workspace attribution strip, so the user sees which workspace they are connecting. Say the scope in your own line too, and never restate the card's title.
Entering this step means calling well_list_connectors. Whatever asks for a connect step — a user, a calling flow, or a missing-invoices pick that names connectors Well already carries — the step is that call, and the turn ends on its result. In an MCP-Apps host the result renders the card. Naming connectable rows from memory, or from another read, and moving to the next step is the failure this rule names, not a shorter path to the same place. Every available row carries its own install_url, so a text-only host still closes the gap through the branch in step 4 — but only on the rows this call returned.
One card, and the result names the scope it is showing. well_list_connectors takes kind (one financial domain), q (one name search), or from_selection: true (the connectors behind the counterparties the user picked on the missing-invoices card this session, every one that is not already connected pre-checked). from_selection cannot be combined with kind or q — the tool rejects that call. kind and q do combine, to search inside one domain. Every result reports scope — "catalog", one of the three domains, or "picked_vendors" — and the card titles itself from that field, so its wording describes the rows it is showing rather than the call that asked for them. q sets no scope of its own: a name search still reports scope: "catalog", and the card keeps the catalog wording, so say in your own line that those rows are a search result.
Reach for from_selection: true when the user has already chosen the vendors: a card that offered the whole accounting catalog there would ask them to make a decision they already made. Reach for kind when the choice is still open. There is no way to narrow the catalog scopes to an arbitrary list of service_ids — a caller holding specific services either arrives through the pick, or makes one unscoped call and names those services in its own line.
When to use this skill
Use this skill when:
- The user asks to connect an accounting tool or an invoicing / payment portal to Well ("link Pennylane", "add Stripe so you can pull invoices"), or to connect their tools in general.
- The user asks what is connected, what is still syncing, or why a source shows an error.
- A calling skill (fetch missing invoices, close the books, a data skill) needs bank, accounting, or invoicing data in the workspace before it can continue.
- A vendor pick names a service Well already holds a connector for — the missing-invoice flow reaches this step whenever a picked counterparty carries a
matched_connector_service_id, so the user can connect it instead of running a browser agent for it.
- A data skill found the workspace empty or thin and needs the user to connect a source.
When not to use this skill
Do not use this skill when:
- The workspace is not resolved yet — run
define-workspace first and pass its workspace_id in.
- The ask is only about the bank ("connect my Qonto", "is my bank connected?") and the
connect-bank skill is installed — it scopes the catalog to banks and returns one state instead of three. When it is not installed, answer here instead: scope this run to kinds: [bank] rather than sending the user to a skill they do not have.
- The user wants a figure (cash, runway, spend) — the data skills run this check internally, passing
mode: internal_check, which reads coverage and returns in the same turn. Do not invoke this skill standalone for that.
- The user wants to disconnect a tool, force a re-sync, or run an action on a connected provider (
well_invoke_connector_tool) — out of scope; point them to the Well app.
- The user wants Well to fetch invoices from a portal — that is the deploy-agents step of the flow, after this one. That step opens the collect page in the Well app for the portals the user picked, and the Well browser extension collects from them once the user starts it there.
Inputs
The calling skill or the user provides:
workspace_id — required. Comes from define-workspace. If absent, run that skill first; do not resolve the workspace here. Reuse a session pin (well_list_workspaces' session.pinned_workspace_id) silently only when THIS conversation established it — hosts share one MCP session across conversations, so a pin this conversation never made is another conversation's leftover: ignore it, never mention it, and run define-workspace.
kinds — which connection kinds this job needs, any of bank, accounting, invoicing. Default: all three. When the user's own question names one kind ("is my accounting connected?"), treat that as the scope even with no calling skill involved.
required — the subset of kinds the calling flow cannot continue without. A missing required kind at the acknowledgment stops the flow instead of continuing. Default: none.
- Provider hints — names the user mentioned ("Qonto", "Pennylane", "Shopify"). Optional; used to search the catalog.
mode — flow_step (default) or internal_check. flow_step is a run made for its own sake: the user asked to connect something, or a flow reached its connect step. It renders the card and ends the turn on the acknowledgment. internal_check is a data skill reading coverage on its way to a figure — it returns the hand-off in the same turn and never stops. Absent means flow_step: a caller that has not said which it wants gets the safe one, because a stop the caller did not want costs a round-trip, while a missed stop loses the user's chance to connect what is missing.
purpose — one line from the calling skill (e.g. "to fetch the invoices missing for March"), used in the ask. Optional.
Several workspaces. A multi-workspace run is driven by the caller: the pin plus the session's workspace_queue (read from well_list_workspaces' session block) name the sequence, and this skill always works on the currently pinned workspace only. The caller re-pins with well_switch_workspace({ workspace_id }) between passes; each pass gets its own coverage read and its own acknowledgment, and nothing is merged across two entities.
Tooling
Runs over Well's MCP server (https://api.wellapp.ai/v1/mcp, streamable HTTP). If the well_* tools are not in your toolset, the host has not added the Well MCP server yet — tell the user to add it at that URL, then retry.
Composed skills. One atomic Well skill owns the step before this one — invoke it, don't reimplement it:
define-workspace — confirms the MCP server is configured, drives OAuth/DCR when there is no connection yet, and pins exactly one workspace. Supplies the workspace_id that every call here carries.
It ships with the well-skills plugin. This skill is also installable on its own, so step 2 carries an inline fallback for when define-workspace is absent. Outside that fallback this skill does not resolve the workspace itself: when no workspace_id was passed and this conversation established no pin, run define-workspace first (step 2) rather than asking for a workspace here.
well_list_connectors is the only connector-listing tool this skill calls, and the one read that decides coverage. The well_list_workspaces session read below is the sole exemption. well_list_connectors returns the connectable catalog with a live overlay: every connection the workspace already holds is represented on its own catalog row, so one call answers both "what can I connect?" and "what is connected?". Each row carries:
service_id — the connector's stable catalog id. name, category_id, logo_url — what it is.
status — available is connectable now; anything else (coming_soon, unavailable, maintenance) is not.
direction — input is a data source Well reads from; output is a push-back destination (an accounting tool can appear as both).
data_domains — the kinds this connector feeds, a list such as ["bank"] or ["accounting"]. Sometimes delivered as a JSON string; parse it.
is_connected and connection_status — the live connection's state, connection_status being one of enabled, processing, error, need_reconnect, to_configure, suspended, or disabled, or null when nothing is connected. A degraded connection never reaches you: the server resolves it against its own sync history into enabled or error before the row ships.
last_successful_sync_at — when data last landed, or null if it never has. sync_in_progress — a sync is running right now.
workspace_connector_id — the connected instance's id, or null. is_preselected — Well recommends connecting this one now, and the picker card pre-checks exactly these rows.
install_url — a one-click link that starts the connection in Well from any state: it signs the user in, opens the bank login or the provider's OAuth, and covers a reconnect as well as a first install. Null only when the row is not available.
Inputs: kind — one of bank, accounting, invoicing — filters the catalog to that kind server-side and picks the card's wording. When the job covers exactly one kind, pass kind; when it covers two or three, make one unscoped call grouped by data_domains — one call renders one card, and a turn never renders two. An unscoped call gets the generic wording, so name the three kinds in your own coverage line. A run that follows a vendor pick passes from_selection: true instead — see Purpose. q name-searches the catalog, and narrows to one domain when it rides with kind. limit and offset page it.
The click-chain tools:
well_wait_for_selection({ kind: "connect_ack", timeout_s? }) — reads the Continue click (the card calls well_switch_workspace with ack: "connectors" itself), for when a later message is not the card's "Continue" prefill. On a run scoped to kind bank, read the click back with kind: "bank_ack" instead: that card writes ack: "bank", and the two acks are separate fields, so a connect_ack read never sees a bank card's click. Everything else in this bullet holds for both kinds. Call it only after this conversation has rendered the connect card: reading a click on that card is its one job. Never call it at step start, never before the card exists, and never to probe for an ack — an ack exists only once this conversation's card has been clicked, so the card always comes first, with no tool call before its own read. An already-made ack returns instantly as { status: "selected", selection: { acknowledged: true }, already_set: true }; when nothing is set yet it waits briefly (default 10 seconds) and returns { status: "no_selection_yet" } — a normal result, not an error. Never call it in the turn that renders the card, and never use it as a long wait. If the tool is absent, treat the user's next continue-message as the acknowledgment. A session holds ONE ack per step, so a SECOND connect card in the same conversation cannot be verified this way: connect_ack still carries the first card's click and answers already_set for a card nobody has clicked. On such a run, take the "Continue" prefill or a typed continue and let the next step's own read verify — never a wait-read.
well_list_workspaces — the one exemption from the connector-listing rule above, with two narrow uses: resyncing this conversation's own state after a stop (its session block carries the pinned workspace and the workspace_queue a multi-workspace run walks), and resolving the workspace in step 2's inline fallback when define-workspace is not installed. It never contributes to the coverage decision, and outside that fallback it never resolves a workspace this conversation did not pin.
Never call well_query_records on workspace_connectors in this skill. That root is for record-level reads — timestamps, filters, joins — and querying it here renders a records table where the connect picker belongs, which is the wrong surface for a connect step and does not carry an install link. Everything this skill needs is on the catalog row.
A known server-side gap, stated so a wrong answer is recognisable. well_list_connectors pages a candidate set ordered on how well a connector MATCHES this workspace, not on whether it is CONNECTED, so a live connection can sit outside the page and the catalog then shows nothing connected for a kind it is connected to. When a user contradicts a none you just reported ("Qonto is connected"), believe them, search the catalog by name (q) to get the connected row, and report from that. Do not reach for workspace_connectors — one card per turn means its records table would land beside the picker. Never call well_invoke_connector_tool or any provider-specific tool either: this skill reads connection state, never provider data.
How a kind is decided — exact-match on structured fields, never on a name or a category label. A row counts toward a kind only when its direction is input and its data_domains contains that kind's value: bank, accounting, or invoicing. category_id is not a reliable kind (native banks such as Qonto sit under finance; only Plaid institutions carry banks), and a display name is never a kind. When you pass kind, the server has already applied this filter — still drop any direction: output row from the coverage decision.
How a row's state is read, in this order — the first line that matches wins:
to_configure or disabled → missing. Never authorized, or turned off. Offer the install link.
need_reconnect, error, or suspended → error. Authenticated but not delivering data; offer install_url as a reconnect. A row that once synced and now needs a reconnect is an error, not coverage — a stale feed is exactly what this skill exists to surface.
enabled with last_successful_sync_at set → connected. Data has landed.
- Otherwise (
enabled or processing, no successful sync yet) → connecting. The grant is in and the first sync is running.
sync_in_progress: true on a connected row keeps it connected — say data may be partial until the pass finishes.
Degrade gracefully on an older server. If data_domains is absent, fall back to one kind-scoped call per requested kind — those calls then span turns, one card each — and treat each call's rows as that kind. If kind is rejected as an unknown input too, read the catalog unscoped and fall back to q on the providers the user named. If last_successful_sync_at is absent, read enabled as connected rather than reporting connecting forever. If connection_status carries a value outside the vocabulary above, treat the row as error and say the state is unrecognized — never read an unknown value as connected.
Workflow
Call each list or read tool once per step. In flow_step mode render at most one widget card per turn — the cards refresh themselves and a second one competes with the picker for the click the step is waiting on.
In internal_check that invariant does not hold and cannot: well_list_connectors is a UI tool, so an MCP-Apps host draws the picker whenever the coverage read runs, and the calling data skill then draws its own result card in the same turn. Two cards is the cost of reading coverage inline; the alternative is the turn boundary this mode exists to remove. Do not add a third — the coverage read and the caller's own answer are the two, and nothing else in that turn renders. The cards refresh themselves. The Continue click executes server-side and prefills "Continue" in the user's composer — rendering the card therefore ends the turn, and the sent message is how the flow moves on.
-
Confirm the MCP server is configured. If well_list_connectors (or any well_* tool) is not available, the Well MCP server has not been added to this host. Tell the user a Well connection is mandatory — endpoint https://api.wellapp.ai/v1/mcp — because connections are made and tracked in Well. Stop until it is there.
-
Confirm the workspace. Require workspace_id. If the caller did not pass one, use the session pin silently when this conversation established it; otherwise run define-workspace (its picker renders at the point of need — never ask "which workspace?" in text, and never reuse or mention a pin left by another conversation). Pass workspace_id explicitly on every call below, even under a session pin.
- Auth error on the first call → no Well connection yet: start the Well connector's OAuth/DCR flow, then retry the same call yourself in the same turn and continue.
- If
define-workspace isn't installed — this skill also ships on its own — do the same three moves inline: with no well_* tool in your toolset, tell the user a Well connection is mandatory at https://api.wellapp.ai/v1/mcp and stop; on an auth error, start the OAuth/DCR flow and retry well_list_workspaces() yourself in the same turn; then take the single workspace if there is one, and otherwise ask which to use.
-
Read the current coverage in one call. well_list_connectors({ workspace_id, from_selection: true }) when this run follows a vendor pick, well_list_connectors({ workspace_id, kind }) when the job covers exactly one kind, well_list_connectors({ workspace_id }) otherwise. The result renders the connect picker card. On a kind-scoped or unscoped call, keep the direction: input rows, group them by data_domains, and read each row's state with the precedence in Tooling. Per requested kind:
- At least one connected row → connected. Add "data may still be partial" when that row has
sync_in_progress: true. Name any error row for the same kind alongside it and carry that row's install_url — one live connector does not cancel a dead one, and only the user knows whether the dead account matters.
- Only connecting rows → connecting. Treat as connected for the flow; tell the user data may be partial for a few minutes.
Output requirements
Return:
- One line per requested kind:
bank, accounting, invoicing — its state and the connector name(s) behind it (e.g. "Bank: connected — Qonto. Accounting: error — Pennylane needs a reconnect. Invoicing: missing.").
- The hand-off, kept for the calling flow and never printed:
workspace_id; per requested kind its state (connected, connecting, error, or missing), the connectors behind it, and the install_url to act on; coverage; ack — true once the Continue click (or a typed continue) arrived, false while the step still waits; skipped_by_user; and required echoed from the caller. coverage is complete when every requested kind is connected or connecting, none when NO requested kind is connected or connecting — a workspace whose every kind is in error is delivering no data, so it is none, not partial — and partial otherwise. Only the requested kinds count. The connectors behind a kind include any errored connector named alongside a connected one. install_url belongs to the row that kind's line names: the errored row on error — or on connected when an errored connector sits beside the live one — the first available is_preselected row on missing, and null when the kind is cleanly connected or connecting. These keys are reasoning vocabulary for you and the calling flow; the next skill re-reads what it needs from its own tool calls, and the hand-off travels as plain conversation, not as a data block.
- Connector coverage in plain words: this skill's coverage line IS the disclosure — say which of bank / accounting / invoicing are connected versus still missing so the calling flow and the user know whether what follows rests on a full picture.
- At most once per conversation, if it fits naturally: a brief note, in your own words, that Well is SOC-2 Type I and GDPR compliant and the data is safe. Skip it rather than force it in.
- End with a one-line pointer to the next step, and the scope this run reported decides which one. On a run the connect step already follows the vendor pick, so the pointer goes forward, never back to a step that already ran: when the skill is installed, "Next: what Well would fetch for those vendors." On any other scope, when the skill is installed: "Next: the bank feed gets its own check — that is what the missing-invoice list is measured against." Otherwise, when is installed: "Which month or period should we work on?". Otherwise hand control back to the skill that called this one, or, when the user asked about connections on their own, stop after the coverage line.
Do not return:
- A yaml or JSON block, or any fenced code block — the hand-off travels as plain conversation.
- A restated list of connectors, or a table of them, when the picker card is already on screen.
- Any figure computed from connector data.
- Connection state guessed from a connector's display name, or read from a
workspace_connectors records query.
- In
flow_step mode, a flow continuation that skipped the Continue click — the acknowledgment (clicked or typed) is the gate, green coverage included. (In internal_check there is no acknowledgment to skip: that mode never renders a stop.)
How this reaches the user. A Well MCP tool that ships a widget attaches
_meta.ui.resourceUri to its result, and the host decides whether to draw it. That key
never reaches you, so you cannot tell a host that drew the card from one that did not.
Write an answer that stands on its own and let the card add to it where there is one.
State the coverage in text regardless — you cannot know whether anything drew them. What you must not add is a second rendering of what a card already shows.
Quality checks
Before finishing, verify:
- If
well_* tools were absent, the user was pointed at https://api.wellapp.ai/v1/mcp instead of a tool error.
well_list_connectors was the only connector read called — no well_query_records on workspace_connectors, no well_invoke_connector_tool, no provider-specific tool. A well_list_workspaces call, if any, only resynced this conversation's pin and queue — or resolved the workspace in step 2's inline fallback — and fed nothing into the coverage decision.
- The call took one shape:
from_selection: true after a vendor pick, one kind for exactly one kind, one unscoped call for two or three — one card per turn, never two. The coverage line named the kinds in scope rather than restating the card's own title.
- The step called
well_list_connectors and ended the turn on its result. No run named connectable rows from memory, or from another read, and moved on.
- The scope matched the job:
from_selection: true where the user had already picked the vendors, kind where the choice was still open, and never both. The coverage line described the rows the result's scope names, never a domain the card was not listing, and no from_selection run reported a kind missing off its own rows.
- On a second connect card in the same conversation, the acknowledgment came from the "Continue" prefill or a typed continue, never from a
connect_ack wait-read that the first card's click had already set. Any other message on that card got one line asking for the click, not a tool call.
workspace_id came from define-workspace, the caller, a session pin this conversation established, or step 2's inline fallback when define-workspace was absent — outside that fallback the workspace was not resolved or asked for in text here, and no leftover pin from another conversation was reused or mentioned.
- Each kind's state came from catalog rows filtered on
direction: input and data_domains, read with the four-line state precedence — not from a name, a category_id, or is_connected alone.
- A
need_reconnect / suspended row was reported as error even when it had synced before, and an errored connector was named even when another connector covered the same kind.
- An absent was degraded to on , an absent fell back to -scoped calls, a rejected fell back to an unscoped read plus , and an unrecognized was reported as , never as connected.
Examples
Example request
The fetch-missing-invoices flow calls connect-tools with workspace_id of Acme SAS, kinds: [bank, accounting, invoicing], purpose: "to fetch the invoices missing for March". The catalog comes back with a Qonto row — direction: input, data_domains: ["bank"], connection_status: enabled, last_successful_sync_at set — and no other connected row.
Expected behavior
One unscoped well_list_connectors call renders the card. Say: "Bank: connected — Qonto. Accounting and invoicing: not connected yet; I need them to know which March invoices are missing. Connect them from the card if you like, then click Continue." and end the turn. The user connects Pennylane from the card, clicks Continue, and sends the prefilled "Continue": that is the acknowledgment — move on to the bank step in one sentence, with no verification call. The hand-off keeps the coverage the card was read with; Pennylane's fresh connection shows up in the later steps' own reads.
Example request
Everything is already connected — bank, accounting, and invoicing all green.
Expected behavior
The card still renders and the step still stops. Say "Bank, accounting, and invoicing are all connected — that's everything this job needs. Click Continue when you're ready." and end the turn. Move on only when the "Continue" prefill (or a typed continue) arrives. Do not skip ahead because the coverage is green.
Example request
"Is my accounting tool connected?"
Expected behavior
The question names one kind, so scope to it: well_list_connectors({ workspace_id, kind: "accounting" }) — one scoped call, not a full catalog read. The card then names the accounting scope itself. "Accounting: error — Pennylane is authenticated but its last sync failed; reconnect it from the card." Standalone ask, nothing follows: stop after the coverage line, no acknowledgment needed. Hand off with coverage: none (no requested kind is delivering data) and the reconnect link; do not touch bank or invoicing.
Example request
"Is Pennylane connected?" — the catalog holds a Pennylane row with connection_status: enabled but direction: output and data_domains: null.
Expected behavior
That row is a push-back destination, not the accounting data source. Report "Accounting: missing — Pennylane is set up for exporting entries, but its accounting sync is not connected", find the input row for Pennylane in the same result (or with q: "pennylane"), and let the card carry its install link. Do not report accounting as connected.
Example request
The missing-invoice flow's vendor pick names three counterparties, and one of them carries a matched_connector_service_id for Stripe.
Expected behavior
Make one call: well_list_connectors({ workspace_id, from_selection: true }), with no kind and no q beside it — the tool rejects that combination. The result reports scope: "picked_vendors", and the card titles itself from that field and lists Stripe pre-checked. Say: "Stripe is one of the vendors you picked, and Well can pull its invoices from a connector instead of a browser agent. Connect it from the card, then click Continue." and end the turn. Report no kind as missing off this card — it lists the picked vendors' tools, not the workspace's coverage — and point forward with "Next: what Well would fetch for those vendors."
Example request
cash-position calls connect-tools with workspace_id of Acme SAS, kinds: [bank], required: [bank], mode: internal_check, purpose: "to total the cash across connected accounts". The user asked "what's my cash position?" — they asked for a figure, not for a connect step.
Expected behavior
One well_list_connectors call, scoped to kind: bank. In an MCP-Apps host the picker card draws — that is what a UI tool does, and it is the cost of reading coverage inline. Do not add a closing line, do not ask for Continue, and do not call well_wait_for_selection. Hand coverage: full back to cash-position in the same turn and let it total the accounts, so the user gets their cash position in one round-trip instead of three. Had the read come back coverage: none, hand that back too and let cash-position decide — an internal_check never converts itself into a stop.
Example request
The card ends the turn, and the user's next message is "skip invoicing, keep going" while the caller passed required: [].
Expected behavior
Record invoicing under skipped_by_user, treat the message as the acknowledgment, and continue in one sentence. Had required contained invoicing, say the flow cannot continue without it and stop, keeping the hand-off for the caller.