Skip to main content
Glama

Measure customer lifetime value

well_measure_customer_ltv
Read-only

Measure the revenue lifetime value (LTV) of every customer the workspace invoiced over a window of whole months, with the portfolio churn and lifespan it rests on. It draws nothing.

Use it to rank customers by lifetime value, or to state what a customer is worth over its life. Do not use it for revenue in a period (well_sum_invoices), for recurring revenue (MRR), or for what customers still owe (receivables aging). Do not page well_query_records to rebuild this figure: this read covers every invoice in the window, unless partial is true (a month with very many customers can be cut short; see below).

The rule is fixed, and rule echoes it on every call. LTV = average order value x monthly purchase frequency x lifespan in months. It is a REVENUE LTV: no margin or cost is applied, so say "revenue lifetime value" and never present it as profit.

  • Revenue: the billing documents the workspace ISSUED and did not cancel (a draft is not issued, so it is left out), net of tax (items_total), credit notes netted. Payment status plays no part.

  • Per customer and currency: aov = net_revenue / invoice_count; monthly_frequency = invoice_count / active_span_months, where the span runs from the customer's first invoice month to the last month of the window, for every customer including a churned one. net_revenue is what the customer was billed over the window: give it beside the LTV.

  • A churned customer's LTV: A churned customer's LTV can be lower than its billed net revenue, because its frequency counts the months after it churned, and it moves with the window end. Do not present it as what the customer was worth: net_revenue is what it was billed. State that limit when you name a churned customer's LTV (churned: true), and name the window end it depends on.

  • Churned: no invoice for 2 x the customer's usual interval (the median gap between its invoice months), and at least 3 months. A customer invoiced in one month only takes the 3-month floor.

  • Portfolio: monthly_churn = churned customers / observed_customer_months, the months customers were at risk of churning (a customer counts from its first invoice month until it churns, or until the window ends if it has not churned; a churned customer adds no month after its churn); lifespan_months = 1 / monthly_churn, capped at 60. cap_applied: true means the lifespan IS the cap (zero churn, or a churn so low its inverse passes the cap): state that the cap applied, and give the churned count beside it.

  • A window with no invoiced customer gives rows: [] and a null monthly_churn and lifespan_months. Say the window holds no invoiced customers. State no LTV, lifespan or churn for it, and do not call it zero churn.

  • A customer flagged young has an active span under 12 months: its frequency is normalised per month over that short span. Say so when you name one.

The window is whole, COMPLETE months, given one of two ways, never both.

  • By its length: last_complete_months (1 to 60). The server reads that many complete months, ending with the last complete month. Use it for "the last N months" and for the default window, last_complete_months: 60, rather than working the dates out yourself.

  • By its bounds: from and to, both the first day of a month, as YYYY-MM-01; from is inclusive and to is EXCLUSIVE. Use them for a named period, such as a calendar year. The window may not reach the running month, so to is at most the first day of the current month, and it covers at most 60 months. The result's window echoes the from and to read and month_count, the complete months they cover. State the window from that echo: month_count months, from from to the month before to.

Amounts are never converted. Each row is one customer in ONE currency, and currencies summarises each currency apart. Never add rows of two currencies without converting them first at a rate you can state. A customer billed in two currencies has two rows; converted, they add up.

counterparty_alias_sets folds company rows that are one customer. Pass the sets the own-company step confirmed, as arrays of company ids; each set folds into its first id, and alias_company_ids lists what was folded. Without it nothing is folded.

The workspace's own company should be set. Without it Well cannot tell what the workspace issued: the call still returns, but party_scope.side_unknown is true, no side filter was applied and the ranking covers invoices on both sides. Say the side is unknown, never that these are the customers' figures; resolve the own company, then call again for a true ranking.

What the figure cannot reach, stated beside it:

  • unattributed_count: billing documents Well could place on neither side. They may belong to a customer; state the count when it is above zero. null means the count failed, which is not 0.

  • excluded_draft_count: drafts in the window. They are in no row, and a connector or document import can keep a real invoice at draft, so state the count when it is above zero. null means the count failed.

  • excluded_no_customer: issued documents with no identified customer. They are in no row.

  • excluded_malformed: issued documents with no readable net amount or no currency. null means the count failed.

  • credit_note_only_count: customer and currency pairs with credit notes and no invoice in the window.

  • corrected_or_consolidated_count: corrected or consolidated invoices among the rows, which may count billing twice with the invoices they replace.

  • omitted_row_count: rows past the first 100 of a currency. They still count in currencies and portfolio.

  • partial: true means a monthly read was cut short, so some customers' invoices are missing. That can move a customer's churn, the lifespan and every LTV in either direction: the figures are not bounds. Name the truncation in the reply and do not present any figure as a minimum or a maximum.

When the token authorizes one workspace, call this directly — no workspace call is needed first. When it authorizes several, this read will not guess which one you mean: pass workspace_id on the call.

Input Schema

NameRequiredDescriptionDefault
toNoEXCLUSIVE end of a window given by its bounds: the first day of the month after the last complete month you read, YYYY-MM-01. At most the first day of the current month. Send it with `from`.
fromNoInclusive start of a window given by its bounds: the first day of a month, YYYY-MM-01. Send it with `to`, without `last_complete_months`.
workspace_idNoTarget workspace. Omit when the token authorizes one workspace. Required when it authorizes several: this read reports one workspace's own figures and will not choose which.
conversation_idNoThe conversation id returned by the previous Well result, in its meta under well/conversation_id, in its structuredContent, or in its JSON text block. Pass it back on every call in the same conversation, including a call a card makes, so the chosen workspace and the earlier answers still apply. It decides the conversation on its own: nothing the host states about the session replaces it. Omit it only on the first call of a conversation.
workspace_scopeNoRestrict this call to the family: the token's primary workspace plus the company workspaces under it that you are a member of. Unrelated authorized workspaces drop out of the read; row reads merge across the family, while a per-workspace figure still reports exactly one workspace.
last_complete_monthsNoThe window by its length: the server reads this many complete months, ending with the last complete month. Send it alone, without `from` and `to`. The default window is 60.
counterparty_alias_setsNoSets of company ids that are one customer, as the own-company step confirmed them. Each set folds into its first id. Omit when there are none.

Output Schema

NameRequiredDescriptionDefault
rowsYes
ruleYes
errorNo
windowYes
partialYes
successYes
portfolioYes
currenciesYes
party_scopeYesWhat the receivable side resolved to. `side_unknown: true` means the workspace resolved no own company, so NO side filter was applied (`applied: false`) and the ranking covers invoices on both sides — say the side is unknown, never that these are the customers' figures. `null` when no read reported a scope resolution.
conversation_idNoThe conversation this result belongs to. Pass it back as the conversation_id argument on every later Well call in the same conversation.
newly_connectedNoTools that connected to this workspace since the last result and that no session was told about yet, oldest first. Present only when at least one tool is new. The connected-line rule says when to tell an entry; when it does, one line per entry, in the person's language: "<name> is connected.". Never fold several entries into one sentence. Each entry comes back once, on one result only: never repeat its line later.
omitted_row_countYes
excluded_malformedYes
resolved_workspaceNoThe workspace that answered, when the caller named none and the token authorizes several.
unattributed_countYes
conversation_id_noteNoPresent only when the server opened a fresh lane, stating that no choice recorded earlier was read.
excluded_draft_countYes
excluded_no_customerYes
conversation_id_sourceNoWhere the conversation id came from: the host's own request meta, the caller's argument, or a fresh lane the server opened.
credit_note_only_countYes
corrected_or_consolidated_countYes

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed24 schema fields changed
    • removedOutput schema / properties / corrected_or_consolidated_count / anyOf
      Removed value: -[
      -  {
      -    "type": "number"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / corrected_or_consolidated_count / type
      Added value: +[
      +  "number",
      +  "null"
      +]
    • removedOutput schema / properties / credit_note_only_count / anyOf
      Removed value: -[
      -  {
      -    "type": "number"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / credit_note_only_count / type
      Added value: +[
      +  "number",
      +  "null"
      +]
    • removedOutput schema / properties / excluded_draft_count / anyOf
      Removed value: -[
      -  {
      -    "type": "number"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / excluded_draft_count / type
      Added value: +[
      +  "number",
      +  "null"
      +]
    • removedOutput schema / properties / excluded_malformed / anyOf
      Removed value: -[
      -  {
      -    "type": "number"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / excluded_malformed / type
      Added value: +[
      +  "number",
      +  "null"
      +]
    • removedOutput schema / properties / excluded_no_customer / anyOf
      Removed value: -[
      -  {
      -    "type": "number"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / excluded_no_customer / type
      Added value: +[
      +  "number",
      +  "null"
      +]
    • removedOutput schema / properties / omitted_row_count / anyOf
      Removed value: -[
      -  {
      -    "type": "number"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / omitted_row_count / type
      Added value: +[
      +  "number",
      +  "null"
      +]
    • removedOutput schema / properties / portfolio / properties / lifespan_months / anyOf
      Removed value: -[
      -  {
      -    "type": "number"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / portfolio / properties / lifespan_months / type
      Added value: +[
      +  "number",
      +  "null"
      +]
    • removedOutput schema / properties / portfolio / properties / monthly_churn / anyOf
      Removed value: -[
      -  {
      -    "type": "number"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / portfolio / properties / monthly_churn / type
      Added value: +[
      +  "number",
      +  "null"
      +]
    • removedOutput schema / properties / resolved_workspace / properties / name / anyOf
      Removed value: -[
      -  {
      -    "type": "string"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / resolved_workspace / properties / name / type
      Added value: +[
      +  "string",
      +  "null"
      +]
    • removedOutput schema / properties / rows / items / properties / name / anyOf
      Removed value: -[
      -  {
      -    "type": "string"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / rows / items / properties / name / type
      Added value: +[
      +  "string",
      +  "null"
      +]
    • removedOutput schema / properties / rows / items / properties / usual_interval_months / anyOf
      Removed value: -[
      -  {
      -    "type": "number"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / rows / items / properties / usual_interval_months / type
      Added value: +[
      +  "number",
      +  "null"
      +]
    • removedOutput schema / properties / unattributed_count / anyOf
      Removed value: -[
      -  {
      -    "type": "number"
      -  },
      -  {
      -    "type": "null"
      -  }
      -]
    • addedOutput schema / properties / unattributed_count / type
      Added value: +[
      +  "number",
      +  "null"
      +]
  2. Changed6 schema fields changed
    • changedInput schema / properties / from / description
      Previous value: -"Inclusive start of the history: the first day of a month, YYYY-MM-01."New value: +"Inclusive start of a window given by its bounds: the first day of a month, YYYY-MM-01. Send it with `to`, without `last_complete_months`."
    • addedInput schema / properties / last_complete_months
      Added value: +{
      +  "description": "The window by its length: the server reads this many complete months, ending with the last complete month. Send it alone, without `from` and `to`. The default window is 60.",
      +  "maximum": 60,
      +  "minimum": 1,
      +  "type": "integer"
      +}
    • changedInput schema / properties / to / description
      Previous value: -"EXCLUSIVE end of the history: the first day of the month after the last complete month you read, YYYY-MM-01. At most the first day of the current month."New value: +"EXCLUSIVE end of a window given by its bounds: the first day of the month after the last complete month you read, YYYY-MM-01. At most the first day of the current month. Send it with `from`."
    • removedInput schema / required
      Removed value: -[
      -  "from",
      -  "to"
      -]
    • addedOutput schema / properties / window / properties / month_count
      Added value: +{
      +  "maximum": 9007199254740991,
      +  "minimum": -9007199254740991,
      +  "type": "integer"
      +}
    • changedOutput schema / properties / window / required
      Previous value: -[
      -  "from",
      -  "to"
      -]New value: +[
      +  "from",
      +  "to",
      +  "month_count"
      +]
  3. Changed3 schema fields changed
    • changedOutput schema / properties / newly_connected / description
      Previous value: -"Tools that connected to this workspace since the last result and that no session was told about yet, oldest first. Present only when at least one tool is new. Start the reply with one line per entry, in the person's language: \"<name> is connected.\". Never fold several entries into one sentence. Each entry comes back once, on one result only: never repeat its line later."New value: +"Tools that connected to this workspace since the last result and that no session was told about yet, oldest first. Present only when at least one tool is new. The connected-line rule says when to tell an entry; when it does, one line per entry, in the person's language: \"<name> is connected.\". Never fold several entries into one sentence. Each entry comes back once, on one result only: never repeat its line later."
    • addedOutput schema / properties / party_scope
      Added value: +{
      +  "anyOf": [
      +    {
      +      "additionalProperties": false,
      +      "properties": {
      +        "applied": {
      +          "type": "boolean"
      +        },
      +        "requested": {
      +          "enum": [
      +            "purchase",
      +            "sales",
      +            "intra_self",
      +            "unattributed"
      +          ],
      +          "type": "string"
      +        },
      +        "side_unknown": {
      +          "type": "boolean"
      +        }
      +      },
      +      "required": [
      +        "requested",
      +        "applied",
      +        "side_unknown"
      +      ],
      +      "type": "object"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "description": "What the receivable side resolved to. `side_unknown: true` means the workspace resolved no own company, so NO side filter was applied (`applied: false`) and the ranking covers invoices on both sides — say the side is unknown, never that these are the customers' figures. `null` when no read reported a scope resolution."
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "rows",
      -  "currencies",
      -  "portfolio",
      -  "window",
      -  "rule",
      -  "omitted_row_count",
      -  "unattributed_count",
      -  "excluded_malformed",
      -  "excluded_draft_count",
      -  "excluded_no_customer",
      -  "credit_note_only_count",
      -  "corrected_or_consolidated_count",
      -  "partial",
      -  "success"
      -]New value: +[
      +  "rows",
      +  "currencies",
      +  "portfolio",
      +  "window",
      +  "rule",
      +  "omitted_row_count",
      +  "unattributed_count",
      +  "excluded_malformed",
      +  "excluded_draft_count",
      +  "excluded_no_customer",
      +  "credit_note_only_count",
      +  "corrected_or_consolidated_count",
      +  "party_scope",
      +  "partial",
      +  "success"
      +]
  4. Changed2 schema fields changed
    • addedOutput schema / properties / excluded_draft_count
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "number"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ]
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "rows",
      -  "currencies",
      -  "portfolio",
      -  "window",
      -  "rule",
      -  "omitted_row_count",
      -  "unattributed_count",
      -  "excluded_malformed",
      -  "excluded_no_customer",
      -  "credit_note_only_count",
      -  "corrected_or_consolidated_count",
      -  "partial",
      -  "success"
      -]New value: +[
      +  "rows",
      +  "currencies",
      +  "portfolio",
      +  "window",
      +  "rule",
      +  "omitted_row_count",
      +  "unattributed_count",
      +  "excluded_malformed",
      +  "excluded_draft_count",
      +  "excluded_no_customer",
      +  "credit_note_only_count",
      +  "corrected_or_consolidated_count",
      +  "partial",
      +  "success"
      +]
  5. Changed1 schema field changed
    • addedInput schema / properties / workspace_scope
      Added value: +{
      +  "description": "Restrict this call to the family: the token's primary workspace plus the company workspaces under it that you are a member of. Unrelated authorized workspaces drop out of the read; row reads merge across the family, while a per-workspace figure still reports exactly one workspace.",
      +  "enum": [
      +    "family"
      +  ],
      +  "type": "string"
      +}
  6. Changed1 schema field changed
    • addedOutput schema / properties / newly_connected
      Added value: +{
      +  "description": "Tools that connected to this workspace since the last result and that no session was told about yet, oldest first. Present only when at least one tool is new. Start the reply with one line per entry, in the person's language: \"<name> is connected.\". Never fold several entries into one sentence. Each entry comes back once, on one result only: never repeat its line later.",
      +  "items": {
      +    "additionalProperties": false,
      +    "properties": {
      +      "name": {
      +        "type": "string"
      +      },
      +      "service_id": {
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "name",
      +      "service_id"
      +    ],
      +    "type": "object"
      +  },
      +  "type": "array"
      +}
  7. Added

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare a safe read, but the description goes well beyond them: it discloses the fixed LTV formula, revenue definition (issued, uncancelled, net of tax, credit notes netted), churn and lifespan rules, the 60-month cap, partial-truncation risk, alias folding, and the own-company side-unknown failure mode. No statement contradicts readOnlyHint or destructiveHint.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is front-loaded with purpose and organized under bold headings, and given the tool's complexity most sentences carry necessary caveats. It is still very long and repeats the partial-truncation caveat in two places, and much of the text is response-formatting guidance rather than invocation guidance, costing some tightness.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex analytical tool with seven optional parameters and an output schema, the description covers everything an agent needs: window construction, currency separation, alias merging, own-company prerequisites, and every exclusion counter that can make the figures misleading. No material gap remains for correct invocation or interpretation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning: the mutual exclusivity of last_complete_months and from/to, the 60-month default, inclusive from versus exclusive to, and the requirement that bounds be first-of-month dates. It also explains that counterparty_alias_sets folds into the first id and that workspace_id is mandatory only for multi-workspace tokens.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The first sentence names a specific verb and resource — measure revenue lifetime value per invoiced customer over a whole-month window — and states scope precisely. It explicitly separates itself from well_sum_invoices, MRR, receivables aging, and paging well_query_records, so an agent can route correctly without opening a sibling schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives positive selection criteria (rank customers by LTV, state lifetime worth) and explicit exclusions naming the sibling tools for period revenue, recurring revenue, and receivables. It also tells the agent which window parameterization to pick for 'last N months' versus a named period, and when workspace_id is required.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources