wellapp-ai/skills · Archived

rank-clients-by-ltv

Rank customers by total realized revenue paid to date — sum of paid invoices per customer — using Well's MCP financial graph, backed by real invoice data rather than guesswork. Use when the user asks "rank our clients by lifetime value", "who are our best customers", "rank clients by revenue", "biggest customers", "customer lifetime value", or "which customers have paid us the most". This is a realized-revenue ranking (paid invoices to date), not a predictive churn/retention-based LTV model. Re…

First seen Jul 31, 2026

Installation

$ npx skills add wellapp-ai/skills --skill rank-clients-by-ltv

Summary

  • Rank customers by total realized revenue paid to date — sum of paid invoices per customer — using Well's MCP financial graph, backed by real invoice data rather than guesswork.
  • Use when the user asks "rank our clients by lifetime value", "who are our best customers", "rank clients by revenue", "biggest customers", "customer lifetime value", or "which customers have paid us the most".
  • This is a realized-revenue ranking (paid invoices to date), not a predictive churn/retention-based LTV model.
  • Requires a connected Well workspace with invoicing data and a resolvable `own_company`; if either is missing, this skill walks the user through connecting one or confirming their company first.

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from wellapp-ai/skills.

npx skills add wellapp-ai/skills

Browse all from wellapp-ai/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 1
License LICENSE
Default branch main
Open issues 0
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 34,628 B
  • docs SUMMARY.md 721 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Rank Your Clients by Lifetime Value with Well

Purpose

Use Well's MCP tools to answer "who are our best customers?" by ranking customers on total realized revenue — the sum of every invoice this workspace has issued and been paid for, grouped by customer, to date. This computes cumulative paid-invoice revenue per customer, backed by Well's synced invoice data, not a guess.

This is not a predictive customer-lifetime-value model. A true forward-looking CLV needs churn, retention, and cohort data that Well's invoice graph doesn't carry. What this skill delivers is a realized-revenue ranking — "who has paid us the most so far" — even though users typically reach for "lifetime value" phrasing to ask for it. Always frame the output that way.

When to use this skill

Use this skill when the user asks things like:

  • "Rank our clients by lifetime value" / "customer lifetime value"
  • "Who are our best customers?" / "Biggest customers"
  • "Rank clients by revenue"
  • "Which customers have paid us the most?"

When not to use this skill

Do not use this skill when:

  • The user wants to know who currently owes money (unpaid invoices) — use accounts-receivable-aging instead; this skill only counts paid invoices (realized revenue), not outstanding balances.
  • The user wants a deep dive on one specific customer's full history, not a ranking across all customers — use the sibling company-profile skill instead.
  • The user wants spend/expenses (money going out, not coming in) — use cost-structure instead.

Inputs

The user may provide:

  • A workspace hint — an id, a workspace name, or the company behind it — if they manage more than one. Passed straight through to workspace resolution, which is what resolves it; this skill never picks a workspace itself.
  • A time window (e.g. "this year", "last quarter") — default to all-time since this is a cumulative "to date" ranking, not a period-bound one. State clearly which window was used.
  • How many top customers to show — default to 10.

Tooling

This skill runs entirely over Well's MCP server (https://api.wellapp.ai/v1/mcp, streamable HTTP). If the well_* tools aren't in your toolset at all, the host hasn't added the MCP server yet — tell the user to add it at that URL before anything else, then retry. Required tools once it's added:

  • welllistworkspaces — how the workspace step resolves the workspace.
  • wellqueryrecords — read invoices, workspaces (for owncompany), exchangerates.
  • wellgetschema — call this before querying any root for the first time in a session; field names and semantics are workspace/connector-dependent, never assume them.
  • welllistconnectors — how the connections step surfaces install links.
  • Well's OAuth / Dynamic Client Registration (DCR) flow — driven by the workspace step, not here. Most hosts trigger it automatically when the Well MCP server is added; if your host exposes a dedicated authenticate tool for the Well connector, that step calls it.

Workflow

  1. Pin the workspace.

Call each list or read tool once per step, and render at most one card that AWAITS AN ANSWER per turn. The cards refresh themselves. A card whose click executes server-side and prefills a message in the user's composer is what ends the turn, and the sent message is how the routine resumes — so it is the WAITING that a turn may only do once, not the drawing. A read that renders a card and hands its result straight back in the same turn is not waiting on anything and does not consume that budget.

Confirm the Well MCP server is configured — if welllistworkspaces (or any well_* tool) is not available, tell the user a Well connection is mandatory at https://api.wellapp.ai/v1/mcp and stop until it's there.

Call welllistworkspaces().

  • Auth error → no Well connection yet: start the Well connector's OAuth/DCR flow, then retry welllistworkspaces() yourself in the same turn and continue — do not ask the user to confirm they signed in.
  • success: false with a non-auth error → retry once; on a second failure, do not invent a workspace — tell the user and give them <well-app-base-url> to open Well directly.
  • Zero workspaces → the account has no workspace yet. Say so, point the user to Well to finish signing up, and return resolution: unresolved.
  • session.pinnedworkspaceid set, and THIS conversation established it (its own picker click or typed choice earlier in the conversation), and the user is not asking to pick or switch → use it silently, map it to its row, resolution: userpicked, skip straight to the hand-off. A non-empty session.workspacequeue alongside it means a multi-pick is mid-walk — hand off multi_picked with the pin first and the queue behind it.
  • session.pinnedworkspaceid set, but this conversation never rendered the picker nor took a typed choice → it's another conversation's leftover. Ignore it and resolve as if unset. Never mention it — "already pinned" is forbidden phrasing — and never skip the picker because of it.

Resolve without asking when you can:

  • Exactly one workspace → use it, resolution: single. Say which one in one line; do not ask for confirmation and do not call wellswitchworkspace.
  • Several workspaces and a hint (a workspaceid, name, or company behind it) → match it exactly on workspaceid; otherwise case-insensitively on workspacename, identity.registeredname, identity.tradename, or — for a country hint such as "my US entity" — on identity.country (ISO code). Exactly one match → use it, resolution: hintmatched, say which one you matched, and call wellswitchworkspace({ workspace_id }) so a later call can't fall back to a sibling entity. Zero or several matches → fall to the picker below; never pick the closest name.
  • A hint naming several entities ("FR and US", "both my companies") is a sequence, not an ambiguity — split it into fragments, match each exactly as above, keep the user's order. Every fragment matching exactly one distinct workspace, and at least two distinct workspaces matched → call wellswitchworkspace({ workspaceids: [...] }) once, in that order — the first is pinned, the rest become the session's workspacequeueresolution: multi_picked. Any fragment matching zero or several workspaces → fall to the picker; never resolve part of a compound hint and drop the rest silently.

With several workspaces and no usable hint, end the turn on the card: the welllistworkspaces result already rendered the picker (one tile per workspace, multi-select). Don't restate the workspaces under it. End with one short line naming the entities — "to rank your customers by the revenue they've paid you" — and stop. In a text-only host, list each workspace on one line (name, country, base currency, "(default)" on the primary) and ask the same one-line question. Never default to the primary workspace on the user's behalf.

Resolve the next message after the card, in this order, never by re-asking:

  • The message is the card's prefill ("Continue in <name>", or the multi form "— then …") → the click already pinned it server-side. Acknowledge in half a sentence and continue — never re-verify with an extra call, never call wellswitchworkspace for it. A single name → resolution: userpicked; the multi form → resolution: multipicked.
  • The message names one or more workspaces in its own words → map each to its workspaceid from the earlier result — never a guessed id — then call wellswitchworkspace yourself (workspaceid for one, workspace_ids for several, in the user's order). A name matching zero or several rows is asked about, never guessed.
  • The message declines ("later", "not now") → resolution: unresolved. Say nothing was pinned and stop; do not call wellwaitfor_selection, do not run any workspace-scoped call.
  • Any other message that needs the workspace → call wellwaitforselection({ kind: "workspace", timeouts: 10 }) once. selected → continue on selection.workspaceid (an empty selection.workspacequeue is userpicked, non-empty is multipicked). noselectionyet → one line asking to click the card, end the turn.

hasbanktransactions rides the hand-off because a later step needs it and only this one reads the workspace rows. It is true only when a connector the workspace BANKS with has already delivered a transaction — an accounting platform or a payment processor does not count, and neither does a transaction whose connector is unknown, disconnected or retired. 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 no value here licenses skipping a bank-connection step.

Emit the hand-off:

workspace_id: <uuid>
workspace_name: <name or null>
is_primary: <true|false>
identity:
  registered_name: <value or null>
  trade_name: <value or null>
  country: <ISO code or null>
  base_currency: <ISO code or null>
  fiscal_year_start_month: <1-12 or null>
has_bank_transactions: <true|false|null>
resolution: single | hint_matched | user_picked | multi_picked | unresolved
workspaces: [{ workspace_id, workspace_name, identity, ... }, …]  # multi_picked only — pinned entry first, then the queue in order

On unresolved, every other key is null. Pass workspaceid explicitly on every well* call from here on, pinned or not — a pin changes what an omitted argument falls back to, it does not make the argument optional.

On multipicked: the caller runs its whole walk on the pinned workspace first, then calls wellswitchworkspace({ workspaceid: <next> }) on the next queue entry (read from welllistworkspaces' session.workspacequeue, never from chat) and repeats. Each pass carries its own workspaceid explicitly and gets its own recap — nothing is merged across two entities: no shared row, no combined total.

Verify before moving on: exactly one workspace is pinned, or resolution: unresolved — never two, never a merged view; session.pinnedworkspaceid was trusted only when this conversation established it; a hint resolved only on an exact id match or an unambiguous case-insensitive name match; wellswitchworkspace was called exactly once on a hint match or typed pick and not at all for a pick the card itself already made; on multi_picked, the loop rule (one entity at a time, own recap, no merging) was stated in the hand-off.

  1. Confirm the connections this answer needs.

The workspace is already pinned — pass its workspace_id on the call below; do not re-resolve it here.

Read the current coverage in one call: welllistconnectors({ workspaceid, fromselection: true }) when this run follows a vendor pick; welllistconnectors({ workspaceid, kind }) when the job covers exactly one kind; welllistconnectors({ workspaceid }) otherwise (one unscoped call for two or three kinds — one call renders one card, and a turn draws at most one card that AWAITS AN ANSWER).

For each of the requested kinds —

  • invoicing
  • accounting

— keep only rows whose direction is input and whose datadomains contains that kind (never a display name or categoryid), and read each qualifying row's state in this order, first match wins:

  1. to_configure or disabledmissing.
  2. needreconnect, error, or suspendederror — offer installurl as a reconnect, not a first install.
  3. enabled with lastsuccessfulsyncat set → connected (note "data may be partial" if syncin_progress: true).
  4. Otherwise (enabled or processing, no successful sync yet) → connecting.

At least one connected row for a kind → connected, and name any error row for that same kind alongside it (a live connector does not cancel a dead one). Only connecting rows → connecting. Only error rows → error, name the connector, offer the reconnect link. No qualifying row → missing, including a to_configure row the user started but never finished.

This is a coverage read for a data skill, not a connect step: hand the per-kind states straight back in the same turn. No closing question, no wellwaitfor_selection, no card acknowledgment to wait for. When a required kind is missing, say so in the hand-off.

On a transient welllistconnectors failure, retry once; on a second failure, do not invent coverage — say it's unknown, give the user <well-app-base-url>/workspaces/<workspace_id>, and hand the failure back to the caller with no coverage claim.

Hand off, kept for the caller and never printed as a block: per requested kind, its state (connected/connecting/error/missing), the connector(s) behind it, and the installurl to act on; coveragecomplete when every requested kind is connected or connecting, none when none is (an all-error workspace is none, not partial), partial otherwise; skippedby_user; required echoed back.

Verify before moving on: welllistconnectors was the only connector-listing tool called — no wellqueryrecords on workspaceconnectors, no provider-specific tool; each kind's state came from the four-line precedence above, not from a name or isconnected alone; coverage: none was used (not partial) when every requested kind was in error; a transient failure was retried once before the fallback link.

- coverage: none → stop; there is nothing to rank yet. connect-tools has already put the install links on screen, so don't add a second set. - Any kind reported connecting, or a connected connector whose latest sync is still running → carry on, and carry "the data may still be partial" into the answer. - coverage: partial → carry on with what is connected, and keep the missing kinds for the coverage disclosure the Output requirements ask for. - A kind the user chose to skip comes back under skippedbyuser — respect that and don't re-ask for it in this run.

  1. Verify the data itself has landed. connect-tools reports connections, not rows — a connector can be connected and still have delivered nothing this skill can use. Spot-check what this skill actually reads: a 1-row wellqueryrecords read on invoices. Zero rows means the workspace has no invoices synced yet — say so and stop, rather than presenting an empty ranking as a real one.
  1. Resolve your own company.

The workspace is already pinned — pass its workspace_id on every call below.

Read the schema, then the field: wellgetschema({ root: "workspaces" }), then workspaces.own_company for the pinned workspace. Treat all three of these as unresolved, never only the null case: the relation is null; the field is absent from the schema entirely; or it resolves to more than one plausible company. Never infer it from the workspace's name, title, logo, slug, or email domain — a coincidence is not a record, and an inferred pick is indistinguishable from a correct one in the output.

Resolved cleanly → take it. One unambiguous company from the schema field → resolution: schema_field. Say which company in one line and don't ask for confirmation either way.

Unresolved → ask once. Query companies for the workspace and ask which one is theirs, with the list on screen, saying why — "to count only the invoices you issued" and what a wrong pick breaks — "ranks the wrong side of the invoice". Then, on the user's explicit confirmation of one company: The answer holds for this run only → resolution: userconfirmed, persisted: false. If the user wants it set permanently, point them at <well-app-base-url>/workspaces/<workspaceid>, where the picker in the Well app writes it. If the user declines, return resolution: unresolved and restate "state plainly that the ranking can't isolate this workspace's own paid invoices until it's set" so they know what they still get — never fall back to a guess.

Fold in duplicate company records: one legal entity often has several companies rows differing only by a legal-form prefix/suffix, punctuation, or accents. Normalize both sides identically — Unicode NFD, strip combining marks, lowercase, replace punctuation/separators with a space, collapse whitespace, trim — then treat a pair as a candidate when either normalized name contains the other (containment is directional: test both ways, or an alias like an EI- prefix is missed one direction). Propose the candidates, take an explicit yes before treating the confirmed set as one identity, and flag the duplicate as worth fixing in Well. Never merge silently. Run the same both-direction, normalized comparison among the other companies too, and propose those alias sets as well — an unmerged counterparty alias splits one party's invoices across two rows and understates them.

Emit the hand-off:

workspace_id: <uuid>
own_company_id: <uuid or null>
own_company_name: <name or null>
identity_set: [<uuid>, …]
aliases: [{ id: <uuid>, name: <name> }, …]
counterparty_alias_sets: [[{ id: <uuid>, name: <name> }, …], …]
resolution: schema_field | user_confirmed | suggested | unresolved
persisted: <true|false>

identityset is the own company plus every confirmed alias — the key a caller compares invoice ids against. persisted is true whenever the anchor is stored server-side — schemafield/suggested (both read the stored setting), and a userconfirmed answer written with wellsetowncompany in persist mode — false for a userconfirmed answer that held for this run only. On unresolved, every key but workspaceid is null or empty.

Verify before moving on: all three unresolved states were treated as unresolved (null, absent, ambiguous); the own company was never derived from the workspace's name, logo, slug, or domain; alias candidates were found with both-direction containment on identically normalized names and proposed, never merged silently; no write tool (wellupdatecompany, welldeletecompany) was called except wellsetown_company in persist mode on an explicit confirmation; a decline returned resolution: unresolved with no guess substituted.

- resolution: unresolved means the user declined to confirm. Say plainly that the ranking can't isolate this workspace's own paid invoices until it's set, and stop rather than ranking both sides together.

  1. Resolve the time window. Default to all-time (this is a cumulative "to date" ranking). If the user names a window (e.g. "this year"), use it and filter on issue_date. State explicitly which window was used in the output either way.
  1. Query paid revenue by customer. Call wellgetschema({ root: "invoices" }) (always, even if queried earlier in the session for a different purpose — this skill relies on paymentstatus, a separate dimension from lifecycle status, and field behavior can vary by connector). Query invoices where issuercompanyid matches the identityset from the own-company step and paymentstatus is paid (optionally filtered on issuedate to the resolved window). Include receiver.name, grandtotal, localcurrency. Group and sum grandtotal by receivercompanyid/receiver.name, collapsing each set in counterpartyalias_sets into a single row.

- Don't let an equality filter hide rows — and don't over-collect either. A filter on issuercompanyid silently drops invoices where it is null. Query that bucket separately, then split it on the receiver before counting anything as revenue, because a null issuer alone does not make a paid invoice income: - Receiver is the own-company identity → a bill the workspace paid, not revenue it earned. Counting it would inflate every total on the page. Leave it out entirely. - Receiver is an external company → genuinely unresolved, and revenue on the balance of evidence. Report it as a labeled row ("unattributed, issuer not recorded") alongside the ranking, so the user can see how much revenue the ranking couldn't place. - Receiver is null too → nothing places this row. Report it as a separate unsplit line with a count and total, outside the ranking and outside the revenue total. - Paid invoices the workspace issued but whose receivercompanyid is null are real revenue with an unknown customer: keep them in the revenue total as a single "unattributed customer" row rather than dropping them, and never merge them into a named customer's figure. - Invoices whose issuer and receiver are the same company are not revenue. Keep them out of the ranking and out of the total, and note them once as a data-quality issue.

  1. Normalize currency. If results span more than one currency:

The workspace is already pinned — pass its workspace_id on every call below.

Group the input amounts by currency for the rate lookup only — keep every tagged row, since the rate found for a currency gets applied back to each of its rows later, not just to a subtotal.

Settle the target currency: the caller's value if given, otherwise the workspace's identity.base_currency. If both are absent, ask rather than guessing, or fall back to reporting per currency and say why.

Take the single-currency shortcut (report the one total, resolution: singlecurrency, no rate lookup) only when that one currency already equals the target currency, or when the mode is percurrency. A lone foreign currency, with conversion asked for, is not a shortcut — convert it like any other.

Read each non-target currency's rate: wellgetschema({ root: "exchange_rates" }) once per session, then look up the pair as of the as-of date (default today). An exact-date rate → use it. No exact-date rate → use the most recent rate at or before the as-of date, and record that date — never a rate dated after it, and never an arbitrary nearby one. Check pair direction against the schema before dividing rather than multiplying.

A missing rate excludes that one currency — leave it out of the converted total, keep it in the per-currency breakdown, carry it in excluded with the reason, and mark the total partial. Never drop a currency silently.

Convert per row, then total: apply each currency's rate to every tagged row in that currency, not just to its subtotal, then sum the converted rows.

Emit the hand-off:

target_currency: <ISO code or null>
as_of: <YYYY-MM-DD>
converted_total: <number or null>
per_currency:
  - currency: <ISO code>
    native_amount: <number>
    converted_amount: <number or null>
    rate: <number or null>
    rate_date: <YYYY-MM-DD or null>
    rate_is_exact: <true|false>
converted:
  - tag: <caller's row id>
    currency: <ISO code>
    native_amount: <number>
    converted_amount: <number or null>
excluded: [{ currency: <ISO code>, reason: <text> }, …]
partial: <true|false>
resolution: converted | per_currency | single_currency | unresolved

Verify before moving on: the single-currency shortcut was taken only when that currency already equalled the target or the mode was per_currency; every converted figure carries the rate and rate date used, with the fallback date stated when an exact-date rate wasn't available; no rate dated after the as-of date was used; a currency with no available rate was excluded explicitly and the total marked partial; no total blends currencies anywhere in the output.

- Use its percurrency rows for the per-customer figures and its convertedtotal for the ranking, tagged one entry per customer so the ranking is built on converted totals. Report its convertedtotal with those rates, or its percurrency breakdown — never a blended total. Build any per-row figure from its converted entries, matched back by tag, rather than re-applying rates yourself. - partial: true means a currency had no rate in Well. Name it and say the total covers the rest, rather than letting a quietly smaller total read as complete.

  1. Sort and limit. Sort customers descending by total paid revenue. Return the requested count, default top 10.
  1. If any required step errors or returns unusable data, do not guess. If the failure is transient (a network/timeout error on the MCP call itself), retry once before falling back — don't dead-end on a blip. If it errors again or the data stays unusable, the fallback is: (a) state the fallback question plainly in your reply (e.g. "Who are our best customers?"), (b) answer it yourself using whatever partial Well MCP data you already have, clearly caveated, and (c) give the user a direct link to their workspace in Well (<well-app-base-url>/workspaces/<workspace_id>) so they can ask it there directly and get a second opinion from their own AI assistant.

Output requirements

Return:

  • The time window used (all-time by default), stated explicitly.
  • A ranked table: customer name, total paid revenue, currency, and share of total paid revenue across all ranked customers. wellqueryrecords ships its own card, and that card renders these rows — so do not restate them in prose. It draws no chart, and neither do you: this skill has no tool of its own, so the answer is the table and the prose around it.
  • The as-of date the ranking was computed against.
  • An explicit one-line caveat: this is realized paid-invoice revenue to date, not a predictive customer-lifetime-value model.
  • Whether the picture is complete: which relevant connector categories (invoicing/accounting) are connected versus still missing — read off connect-tools' coverage and skippedbyuser hand-off, not an inline connector read of your own — and whether the workspace's own company is set, read off the own-company hand-off, so the user knows whether this ranking reflects their full revenue history or a partial view gated by what's connected today.
  • A one-line pointer to company-profile for a deep dive on any single top customer's full relationship history.
  • 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. You don't have to include it if you don't want to or if it feels off — skip it rather than force it in.
  • If step 9's fallback was used, the caveated answer plus the workspace link, clearly labeled as a fallback.

How this reaches the user. This skill has no Well MCP tool of its own, so no card is drawn for it on any host — the widget-disclosure reasoning the tool-backed skills carry does not apply here. Answer in prose and a markdown table, and state every figure in the text. Do not compose a styled visual: Well's own surfaces own how Well data is drawn, and this answer is not one of them.

Quality checks

Before finishing, verify:

  • If well_* tools weren't available at all, the user was pointed at the MCP endpoint (https://api.wellapp.ai/v1/mcp) instead of erroring silently.
  • The workspace came from the pinned hand-off, and its workspaceid rode every well* call rather than being left off.
  • Connection state came from connect-tools' hand-off, and row presence was spot-checked separately in step 3; a connected connector was never assumed to mean usable data had landed.
  • The own company came from the own-company hand-off's identity_set, not a value resolved here — and on resolution: unresolved the documented fallback ran rather than a guess.
  • Duplicate company records were folded upstream, which proposes them for an explicit yes; none were merged silently here, and no wellupdatecompany/welldeletecompany call was made.
  • Null-issuercompanyid invoices were split on the receiver before counting as revenue: own-company receiver means a bill the workspace paid and was excluded, external receiver reported as a labeled unattributed row, both-null reported as a separate unsplit line outside the revenue total.
  • Invoices whose issuer equals their receiver were excluded from the ranking and the total.
  • wellgetschema was called on invoices before querying it, even if it was queried earlier for a different purpose.
  • Only invoices with payment_status: paid were counted — not unpaid/partial, which would overstate realized revenue.
  • Only invoices where the workspace is issuer were counted — receiving invoices would be spend, not revenue.
  • Multi-currency results are converted (with rate/date noted) or clearly separated, never blended.
  • Every number carries a currency and an as-of date.
  • Which connector categories (invoicing/accounting) are connected versus missing was stated from connect-tools' hand-off, so the user knows whether the picture is complete or partial.
  • The "not a predictive lifetime-value model" caveat is present in the output.
  • Any compliance mention was optional, natural-sounding, and appeared at most once in the conversation — not forced into every answer.

Examples

Example request

"Rank our clients by lifetime value — top 10."

Expected behavior

Pin the workspace, then check connections, and spot-check that rows have landed; resolve owncompany, default to an all-time window, pull all invoices where this workspace is issuer and paymentstatus is paid, sum grand_total per customer, sort descending, and present the top 10 with customer name, total paid revenue, currency, share of total, as-of date, and the realized-revenue-not-predictive-CLV caveat.

Example request

"Who's our biggest customer?" — two separate runs, each against one workspace only: one workspace where a customer paid invoices in EUR and the rest paid in USD, and another workspace where no invoices have been marked paid yet.

Expected behavior

In the multi-currency workspace's run: pass the per-customer totals to normalize-currency tagged by customer and rank on its converted entries — reporting the rate and date it used — or report the EUR customer separately rather than adding their total directly into a USD-only ranking. In the zero-paid-invoice workspace's run: state plainly that no realized revenue exists yet (all invoices are unpaid/partial), do not fabricate a ranking, and offer the same fallback link so the user can ask in Well directly.

Example request

"Who are our best customers?" (workspace whose schema does not expose workspaces.own_company, and whose companies list holds both "Northwind Trading" and "NORTHWIND TRADING, LTD")

Expected behavior

Detect in step 4 that owncompany is unresolved because the field is absent from the schema — not merely null — and ask which company is theirs rather than matching the workspace's name or logo to a companies row. Once confirmed, normalize both sides (punctuation folded to spaces, runs collapsed) so "northwind trading ltd" and "northwind trading" compare as containing one another, and offer the LTD record as a candidate alias for confirmation — on the customer side as well as the own-company side, since an unmerged customer alias splits one client across two rows and understates their rank. Then split the null-issuercompany_id invoices on the receiver before counting anything as revenue: an own-company receiver means a bill the workspace paid, which is excluded outright, while an external receiver is reported as a labeled unattributed row. Say the confirmation holds for this run only, and link to the Well app to set it permanently.

Voice

<!-- voice:begin --> Write like a brilliant, understated operations colleague. Hold the tone professional and casual at the same time, confident but never arrogant, credible but easy to follow, warm but never cute. This governs every message of the run, whichever step produced it. Precedence is fixed: when a step hands you an exact string to write, write it exactly as given, dashes and capitals included; these rules govern the prose you compose yourself.

Lead with the outcome, then the detail behind it. Write short active sentences a non-technical reader understands. Use sentence case for the headings and labels you write yourself. Name a real button or card label exactly as the app renders it, such as Use, Validate, Continue, or Deploy, so the user reads the same word on screen. Prefer a concrete number or a real example over an abstract claim.

Never write an em dash or an en dash. Use a period, a comma, or a colon instead. Never write an exclamation mark or an emoji. Keep an acknowledgement brief and specific, such as "Got it, pulling those invoices now." Skip preamble, superlatives, and self-praise.

Drop the habits that make an answer sound generic:

  • Hedging transitions, such as "Furthermore", "Moreover", "Additionally", or "In today's fast-paced landscape".
  • Buzzwords, such as leverage, delve, harness, foster, revolutionize, revolutionise, streamline, optimize, optimise, seamless, game-changer, cutting-edge, best-in-class, world-class, unparalleled, disruptive, synergy, blockchain, and crypto.
  • Hollow contrast, such as "not just X, but Y".
  • Vague praise, such as powerful, robust, intelligent, frictionless, elegant, or advanced.

Reach for these verbs first: ask, drop, connect, get, surface, compose, share, route, enrich, learn, reconcile, match, flag.

Keep to the house words in what you write to the user. Write "connect", never "integrate". Write "sessions", never "chat". Write "business data", never "financial data". Write "tokens", never "credits". Name every object by its own name, the workspace, the connector, the company, or the invoice, and never show the user a raw id on its own. A Well app address is a link, not an id, so keep it whole even when it carries a workspace id. <!-- voice:end -->