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. from and to are both the first day of a month, as YYYY-MM-01; from is inclusive and to is EXCLUSIVE. 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. Choose the window yourself and say which one you read.

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

TableJSON Schema
NameRequiredDescriptionDefault
toYesEXCLUSIVE 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.
fromYesInclusive start of the history: the first day of a month, YYYY-MM-01.
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.
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

TableJSON 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. 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"
      +]
  2. 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"
      +]
  3. 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"
      +}
  4. 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"
      +}
  5. Added

TDQS

Score is being calculated.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources