Assigns spending categories to uncategorized transactions using aggregate GROUP BY queries. Writes to the inferred_category overlay field, preserving explicit category values. Memory-backed for auto-applying known merchant→category mappings on future runs. Should run after merchant normalization.
Instrucciones de origen · Vista previa de solo lectura
name
category-inference
description
Assigns spending categories to uncategorized transactions using aggregate GROUP BY queries. Writes to the inferred_category overlay field, preserving explicit category values. Memory-backed for auto-applying known merchant→category mappings on future runs. Should run after merchant normalization.
version
1.0.0
Category Inference Skill
This skill guides the finance butler through assigning spending categories to
transactions that lack an explicit category. Categories are inferred per-merchant
and stored in the inferred_category metadata overlay field. The original
category field (set during CSV import or email ingestion) is never modified.
CRITICAL CONSTRAINTS
READ THIS SECTION FIRST. These are hard rules, not suggestions.
MUST NOT: Call list_transactions
You MUST NOT call list_transactions or any other tool that reads individual
transaction rows into context at any point during this workflow.
Reason: Categorization is per-merchant, not per-transaction. "Whole Foods" is
always "groceries" regardless of which specific transaction. A typical account has
hundreds or thousands of transaction rows — reading them into context would exhaust
the token budget and likely abort the session before any categories are applied.
The only acceptable data source is list_distinct_merchants, which returns a
compact GROUP BY result — typically 50–200 rows even for a year of transactions.
MUST NOT: Read or re-record transaction rows
You MUST NOT attempt to categorize by reading transactions and writing them back
one row at a time. This is both token-prohibitive and semantically wrong —
category inference is a metadata overlay, not a re-ingestion.
MUST: Use list_distinct_merchants as the sole data source
All merchant data used in this workflow comes from list_distinct_merchants. This
tool returns deduplicated merchant names with transaction counts and totals. It is
the correct and only acceptable source for categorization input.
MUST: Paginate if distinct merchant count exceeds 500
If the first call returns total > 500, you MUST process in paginated batches.
Use limit=500, offset=0, then limit=500, offset=500, etc., until all merchants
are categorized. Do not skip pagination — large accounts can have hundreds of
distinct merchant names.
MUST: Write to inferred_category overlay only
Category inference MUST write to the inferred_category metadata field via
bulk_update_transactions. It MUST NOT overwrite the original category field,
and MUST NOT touch subject, predicate, content, or embedding columns.
Metadata Overlay Contract
The finance butler's fact layer is write-once on core columns. Category
inference uses the metadata overlay pattern instead:
Field
Role
category
Original explicit category from CSV or email ingestion — never modified
inferred_category
LLM-assigned category set by this skill — preferred when no explicit category exists
Query and display tools (spending_summary, list_transactions, dashboard) apply
the following precedence when resolving a transaction's effective category:
Haircuts, beauty, spa, laundry, personal care not covered elsewhere
other
Anything that does not fit the above — use sparingly; prefer a specific label
Ordering
This skill SHOULD run AFTER merchant normalization. Running after normalization
means distinct merchants are already collapsed into canonical names (e.g., the
three Whole Foods store variants are a single "Whole Foods" entry). This reduces
the merchant list size and produces more accurate, consistent categorization.
Workflow
Step 1: Query Distinct Merchants
Call list_distinct_merchants to get the compact merchant list:
result = list_distinct_merchants(min_count=1)
Use normalized_merchant values when available (post-normalization). The tool
returns normalized_merchant in each entry when the overlay has been set.
Inspect the response:
total — total number of distinct merchants
merchants — list of {merchant, normalized_merchant (if set), count, total_amount} entries
If total > 500, proceed in paginated batches (see CRITICAL CONSTRAINTS)
If total == 0, report "No merchants found." and stop.
Step 2: Recall Known Categories from Memory
Before running any LLM inference, check whether this butler has seen and
categorized any of these merchants before:
Collect all known mappings into a lookup table before proceeding.
Step 3: Auto-Apply Known Categories
For every merchant in the list that matches a known category mapping, call
bulk_update_transactions immediately — no LLM review needed:
bulk_update_transactions(updates=[
{
"match": {"merchant_pattern": "Netflix%"},
"set": {"inferred_category": "subscriptions"},
},
{
"match": {"merchant_pattern": "Whole Foods%"},
"set": {"inferred_category": "groceries"},
},
# ... one entry per known merchant→category mapping
])
Track which merchants from the list have been handled so they are excluded from
the LLM review in Step 4.
Step 4: LLM Categorizes Unknown Merchants
Present the remaining merchants (those not covered by known mappings) to
yourself for categorization. For each merchant, you have count (transaction
frequency) and total_amount (aggregate spend) as context.
Assign each merchant exactly one category from the standard taxonomy above.
Apply these heuristics:
Use normalized_merchant when available — it is the canonical name and gives
better signal than the raw bank string
High-frequency merchants deserve careful review — sort by count descending
and prioritize the top entries; a miscategorization at 50 transactions has more
impact than one at 2 transactions
Grocery vs shopping boundary: wholesale clubs (Costco, Sam's Club) → groceries;
general merchandise retailers (Target, Walmart, Amazon) → shopping unless the
merchant is clearly a grocery-only outlet
Store one fact per distinct merchant→category mapping. Use the canonical
(normalized) merchant name as the subject key.
Entity resolution for merchant facts: Follow the memory-classification skill's
Resolve-or-Create protocol. The canonical name (e.g., "Trader Joe's") is the
entity's canonical_name with entity_type="organization".
Pagination Reference
When total > 500, loop over batches:
offset = 0
known_mappings = {} # Load once from memory (Step 2) before loop
while True:
result = list_distinct_merchants(
min_count=1,
limit=500,
offset=offset,
)
# Process result.merchants through Steps 3–5 for this batch
# (known_mappings already loaded before the loop — do NOT re-run Step 2 here)
if offset + 500 >= result.total:
break
offset += 500
Memory recall (Step 2) should be performed once before the loop and the resulting
lookup table reused across batches. This avoids redundant memory queries and
ensures consistent auto-apply decisions across the full merchant list.
Worked Example
Scenario: A Chase Checking account was imported two weeks ago and
merchant normalization was already run. Running list_distinct_merchants(min_count=1)
returns 18 normalized merchants ready for categorization.
bulk_update_transactions returns total_matched: 0 for a pattern
Pattern may be too specific or merchant has no transactions; skip and note in report
Memory search returns no results
Proceed directly to LLM categorization; memory will be populated after Step 7
total > 500 merchants
Paginate in batches of 500 (see Pagination Reference above)
Merchant name is ambiguous (could be multiple categories)
Use the most common interpretation; note ambiguity in report; use other only as a last resort
Transaction already has explicit category field
Do not overwrite it — bulk_update_transactions writes to inferred_category only; the overlay contract handles precedence automatically
Relationship to Other Skills
Run after merchant-normalization: Normalization collapses merchant variants
into canonical names first. Category inference then operates on a cleaner,
deduplicated list, producing more accurate and consistent assignments.
monthly-spending-summary: This scheduled skill groups transactions by
effective category. inferred_category directly improves its output quality
by ensuring uncategorized transactions appear under the correct category bucket.
memory-classification: Follow its Resolve-or-Create protocol when storing
merchant category facts in Step 7. The merchant_category predicate is the
canonical predicate for merchant→category mappings.
tool-reference: Consult for exact parameter names and types for
list_distinct_merchants and bulk_update_transactions.