Skip to main content
Glama

Sum transactions over a window

well_sum_transactions
Read-only

Sum a workspace's transactions over a date window, grouped how you ask. Arithmetic only — this tool holds no definition of burn, spend, or runway, and returns no figure the app renders.

Use it when you are computing a figure whose RULES you are stating yourself: a burn over a window you chose, a total that excludes categories the user named, a per-month series behind a trend you are about to describe. The server derives no burn of its own, so a burn figure starts here: state the rules, sum exactly those rows, then put the result on a card with well_render_burn.

from is inclusive and to is EXCLUSIVE, so a window of whole months passes the first instant of the month after the last one you want. Both are required: a window you cannot state is a decision you have not made, and this tool will not pick one for you.

axes groups the result (any of month, currency, category, ledger_account, category_label, transaction_type, comma-separated) and names what to group in ADDITION to currency. Every axis appears on every row: the ones you did not group by come back null, so the row shape never depends on what you asked for.

On an axis you DID name, a null is a group, not a gap. It is the rows whose column is empty, and it carries its own sums and counts like any other group. A group's rows are count_negative + count_positive, so the null group's share of that total is the part that axis cannot label. Both counts cover readable, non-zero rows only: a zero amount is in neither branch, and unreadable ones are in excluded_malformed. So the share is a share of the rows this tool could sum, not of every row in the window. What that share means, and which grouping is worth using, is yours to decide: this tool holds no view on it.

What each labelling axis IS:

  • ledger_account: the name on the workspace's own chart of accounts. It may have been written by an accounting sync rather than chosen by a person, so do not call it the user's own categorization without checking the ledger_accounts root for the connector that wrote it. A blank label reads as null. A soft-deleted or inactive account still carries its name, because this axis reports what the row was labelled at the time, not what the current chart of accounts holds. It groups on the NAME, and a chart of accounts is unique on the account number rather than the name, so two accounts sharing one name arrive as a single group carrying both their sums. That is one slice per label, which is what a breakdown by label means, but it is not one slice per account: do not read a group here as an account.

  • category: the typed catalog key, and the ONLY value exempt_categories accepts. A key the catalog no longer carries is still populated here, so it groups under a key that names nothing a reader would recognise.

  • category_label: the stored display label, which a connector may have written in its own language. Never pass one to exempt_categories; it is not a key.

  • transaction_type: the transaction's own type. Each value is a full sentence rather than a code, and almost every row carries one.

At most 500 groups come back, biggest first. Past that the smallest are dropped and rows_truncated is true, which is NOT partial: everything here was measured exactly and only the tail is missing. A total over a truncated result is a floor, and an axis's coverage cannot be read off one at all, because the null group may be among the dropped. Group on fewer axes, or over a shorter window, and ask again.

Currency is always grouped, named or not, so a row never mixes two. Adding EUR to USD gives a number denominated in nothing, and no field on the result would tell you it happened. Omitting axes therefore returns one row PER CURRENCY over the window, not one row. Convert the per-currency subtotals yourself, at a rate you can state, before you add them — and if there is more than one row and you report a single total without converting, the total is wrong.

Each row carries BOTH sign branches, and choosing between them is your job. sum_negative is the magnitude of the rows whose amount is negative; sum_positive is the magnitude of the rows whose amount is positive; count_negative and count_positive say how many rows are behind each. Which one is money leaving depends on the FEED, not on the query: most connectors store outflows as negatives, some store them as positive magnitudes. Read the counts to decide, and decide ONCE over the whole window rather than per row or per group: a single category or month can be all-positive on a signed feed, so electing per group flips the convention mid-answer and totals two different things together. A window whose rows are overwhelmingly negative is a signed feed, and outflow is sum_negative. Almost no negatives means the feed stores magnitudes and keeps direction in a field this tool does not group on — so it cannot separate outflow from inflow, and sum_negative + sum_positive is gross movement, not spend. Say so rather than reporting it as an outflow.

A substantial share of BOTH is a third answer, not a close call between the first two. A workspace connected to a signed feed and a magnitude feed at once pools them here, and no combination of the two subtotals is its outflow: sum_negative misses the magnitude feed's spend entirely, and adding sum_positive pulls in the signed feed's income. There is no grouping that separates them, because the axes carry no connector. Report that the window mixes conventions and that a single outflow cannot be derived from it, rather than electing whichever branch is nearer. State which convention you elected and what the counts were, so the reader can check it.

scope is required, and it decides which rows are this workspace's. own_and_adopted is the population the burn tile counts: the workspace's own transactions plus any a parent workspace shared with it through an adoption grant, with legs tested against the parent's accounts too. own is the workspace's own transactions only, tested against its own accounts — the rows its balances move with. Use own when the sum is reconciled against the workspace's own balances, as a cash-flow bridge is, and own_and_adopted for a burn. On most workspaces the two agree; on a child workspace they do not, which is why neither is a default.

exclude_internal_transfers: true keeps only the rows with EXACTLY ONE leg on an account the workspace owns. Two legs is a movement between your own accounts and drops, which is the rule's purpose. Zero legs also drops, and that is the part worth knowing: a card purchase sits against a liability account, so on a card-heavy workspace this removes card spend along with the transfers. excluded_zero_leg and excluded_multi_leg count the two populations separately, so read them before describing what the figure covers. Read excluded_zero_leg as "this many rows carried no asset movement" and nothing narrower: a card charge lands there, and so does a row whose payer and payee resolved to no account at all. excluded_no_owned_leg is that second part on its own: rows with no leg on ANY owned account, liabilities included, so card spend is never in it. Those rows could not be attributed to an account and may have moved a balance the sums cannot see, so a figure reconciled against balances treats a non-zero count as flows that are incomplete. A large excluded_zero_leg is a reason to look, never a spend total to quote. Any of the three counts comes back as null when it could not be measured, which is NOT 0: zero says the rule removed nothing, null says nobody counted. On a null, say the exclusion is unmeasured rather than reporting none — the sums themselves are unaffected, and partial is what speaks for those. For reproducing the burn tile that is exactly right — it is the conservation law the cash-flow bridge rests on. For "total spend excluding transfers between our own accounts" it is not what the words promise, so say what fell out or leave the flag off.

The rule is structural: it counts legs, so no label, category, or type on the row affects it, and a user recategorizing something does not change it.

exempt_categories takes category keys that should not count. A transaction with no category at all is never matched by an exemption and always stays in the sum; if you want those excluded too, that is a different question and you must say so.

excluded_malformed counts rows in the window whose amount could not be read as a number. They are in none of the sums, so state the count beside any total rather than presenting a figure that silently skipped them. partial: true means the aggregate measured nothing: it was cut short, or no asset account is in scope. Either way it arrives with an empty rows, so there is no figure, and the empty rows are not a zero. Say so and offer to try again, unless the workspace holds no deposit or other asset account, where a retry changes nothing.

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

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
toYesEXCLUSIVE end of the window, ISO-8601 — the first instant after the last month you want.
axesNoGroup the sums by these, IN ADDITION to currency. Omit for one row per currency over the whole window.
fromYesInclusive start of the window, ISO-8601 (e.g. 2026-06-01).
scopeYesWhich rows are this workspace's: `own` for a sum reconciled against its own balances, `own_and_adopted` for a burn. Required; see the description.
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.
exempt_categoriesNoCategory keys that do not count. An uncategorized row is never matched by one.
exclude_internal_transfersNoKeeps rows with exactly one leg on an owned account; two-leg transfers AND zero-leg rows (card purchases) both drop. See the description.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
rowsYes
errorNo
windowYes
partialYes
successYes
rows_truncatedYes
conversation_idNoThe conversation this result belongs to. Pass it back as the conversation_id argument on every later Well call in the same conversation.
excluded_zero_legYes
excluded_malformedYes
excluded_multi_legYes
resolved_workspaceNoThe workspace that answered, when the caller named none and the token authorizes several.
conversation_id_noteNoPresent only when the server opened a fresh lane, stating that no choice recorded earlier was read.
excluded_no_owned_legYes
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.

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed4 schema fields changed
    • addedInput schema / properties / conversation_id
      Added value: +{
      +  "description": "The 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.",
      +  "type": "string"
      +}
    • addedOutput schema / properties / conversation_id
      Added value: +{
      +  "description": "The conversation this result belongs to. Pass it back as the conversation_id argument on every later Well call in the same conversation.",
      +  "type": "string"
      +}
    • addedOutput schema / properties / conversation_id_note
      Added value: +{
      +  "description": "Present only when the server opened a fresh lane, stating that no choice recorded earlier was read.",
      +  "type": "string"
      +}
    • addedOutput schema / properties / conversation_id_source
      Added value: +{
      +  "description": "Where the conversation id came from: the host's own request meta, the caller's argument, or a fresh lane the server opened.",
      +  "enum": [
      +    "host_meta",
      +    "argument",
      +    "minted"
      +  ],
      +  "type": "string"
      +}
  2. Changed8 schema fields changed
    • changedInput schema / properties / axes / items / enum
      Previous value: -[
      -  "month",
      -  "currency",
      -  "category"
      -]New value: +[
      +  "month",
      +  "currency",
      +  "category",
      +  "ledger_account",
      +  "category_label",
      +  "transaction_type"
      +]
    • addedInput schema / properties / axes / maxItems
      Added value: +6
    • addedOutput schema / properties / rows / items / properties / category_label
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "string"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / ledger_account
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "string"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / transaction_type
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "string"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ]
      +}
    • changedOutput schema / properties / rows / items / required
      Previous value: -[
      -  "month",
      -  "currency",
      -  "category_key",
      -  "sum_negative",
      -  "sum_positive",
      -  "count_negative",
      -  "count_positive"
      -]New value: +[
      +  "month",
      +  "currency",
      +  "category_key",
      +  "ledger_account",
      +  "category_label",
      +  "transaction_type",
      +  "sum_negative",
      +  "sum_positive",
      +  "count_negative",
      +  "count_positive"
      +]
    • addedOutput schema / properties / rows_truncated
      Added value: +{
      +  "type": "boolean"
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "rows",
      -  "window",
      -  "excluded_malformed",
      -  "excluded_zero_leg",
      -  "excluded_multi_leg",
      -  "excluded_no_owned_leg",
      -  "partial",
      -  "success"
      -]New value: +[
      +  "rows",
      +  "window",
      +  "excluded_malformed",
      +  "excluded_zero_leg",
      +  "excluded_multi_leg",
      +  "excluded_no_owned_leg",
      +  "partial",
      +  "rows_truncated",
      +  "success"
      +]
  3. Changed4 schema fields changed
    • addedInput schema / properties / scope
      Added value: +{
      +  "description": "Which rows are this workspace's: `own` for a sum reconciled against its own balances, `own_and_adopted` for a burn. Required; see the description.",
      +  "enum": [
      +    "own",
      +    "own_and_adopted"
      +  ],
      +  "type": "string"
      +}
    • changedInput schema / required
      Previous value: -[
      -  "from",
      -  "to"
      -]New value: +[
      +  "from",
      +  "to",
      +  "scope"
      +]
    • addedOutput schema / properties / excluded_no_owned_leg
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "number"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ]
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "rows",
      -  "window",
      -  "excluded_malformed",
      -  "excluded_zero_leg",
      -  "excluded_multi_leg",
      -  "partial",
      -  "success"
      -]New value: +[
      +  "rows",
      +  "window",
      +  "excluded_malformed",
      +  "excluded_zero_leg",
      +  "excluded_multi_leg",
      +  "excluded_no_owned_leg",
      +  "partial",
      +  "success"
      +]
  4. Added

TDQS

A4.9/5.0
Behavior5/5

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

Even though readOnlyHint and destructiveHint already signal a safe read, the description discloses far more: inclusive/exclusive window semantics, null groups on named axes, truncation at 500 groups, currency always grouped, sign-branch conventions, leg-count exclusion behavior, malformed rows, and partial results. No statement contradicts the annotations.

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?

The description is very long, so it is not concise in raw word count, but it is front-loaded, organized by parameter and bolded semantic warnings, and almost every paragraph carries a distinct behavioral fact. Length is mostly justified by the tool's complexity, though some editorial restatement could be trimmed.

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?

Given eight parameters, six axes, and an output schema, the description is complete enough to call correctly: it explains row shape, null vs zero meanings, truncation flags, partial rows, exclusion counts, sign-branch decisions, and multi-workspace handling. The presence of an output schema covers the remaining return-value structure.

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

Parameters5/5

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

Schema coverage is 100%, but the description adds substantial meaning beyond the schema: from/to inclusivity, axes as additions to currency, scope's reconciliation vs burn meaning, workspace_id selection behavior, conversation_id persistence, exempt_categories' non-matching of uncategorized rows, and the zero-leg drop in exclude_internal_transfers.

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 opening sentence names a specific verb, resource, and scope: 'Sum a workspace's transactions over a date window, grouped how you ask.' It also differentiates from rendering tools by saying 'Arithmetic only' and 'returns no figure the app renders,' which clearly separates it from siblings like well_render_burn and well_sum_invoices.

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?

The description explicitly states when to use it: 'Use it when you are computing a figure whose RULES you are stating yourself,' and explains that a burn figure starts here and should then go to well_render_burn. It also gives scope-selection rules (own vs own_and_adopted), notes when workspace_id is required, and says when no prior tool call is needed.

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