vault

Tags

Tags are how millfolio turns a pile of transactions into something you can slice — groceries, travel, phone. They make category questions fast and exact: a tagged total is a filter over stored data, not a per-transaction guess.

Every tag is a rule, and all the rules live in one editable text file — ~/.config/millfolio/categories.txt — which is the source of truth. millfolio ships sensible defaults (phone, travel, restaurant, groceries, health); you extend them or add your own. There are two kinds of rule, plus a way to combine them.

The format at a glance: one rule per line. tag (scope note) = keyword, keyword… is a keyword tag; tag (scope note) : yes/no question is an AI tag. The parenthetical scope note is optional. Lines starting with # are comments.

Keyword tags

A keyword tag matches when any of its keywords appears in a transaction's description. Matching is case-insensitive and a plain substring match — no wildcards needed. Write it with an =:

# <tag> (optional scope note) = keyword, keyword, …
pets (vet, pet food, pet supplies) = chewy, petco, the vet
subscriptions (recurring digital services) = netflix, spotify, hbo max
phone = my local carrier        # extends the built-in phone tag

Keyword tags are deterministic and instant — they're pure text matching, applied the moment a transaction is indexed and re-applied whenever you edit the rules, with no model involved. They're also multi-valued: an airport restaurant can be both travel and restaurant. Because a rule only fires on a keyword you chose, a credit-card payment with a long account number is never mistaken for a phone bill — it matches no carrier keyword, so it gets no tag.

Keep keywords specific enough to avoid collisions — prefer at&t over a bare att that would also hit mattress. Adding keywords to a built-in tag (like phone above) extends it; the defaults are never removed.

The scope note

The parenthetical after the tag name — pets (vet, pet food…) — is an optional one-line scope note. It's a short description of what the tag means. Your keyword rules never leave your Mac, but the tag name and its scope note are shown to the model that writes your query programs, so a good note helps it pick the right tag instead of guessing from the name alone (it's why the built-in health tag notes "NOT gyms or fitness").

AI tags

Some categories can't be spelled as a keyword list. "Is this a gym?" isn't a merchant you can enumerate — every studio has a different name. For those, a tag can instead be an AI tag: a plain-English yes/no question the on-device model answers for each transaction. Write it with a : instead of an =:

# <tag> (optional scope note) : <yes/no question>
gym : is this a gym or fitness studio?
coffee shop (independent cafes, not chains) : is this an independent coffee shop?

The model reads each transaction's description locally and answers yes or no; a yes assigns the tag. Nothing about the transaction or the verdict ever leaves your machine. Once assigned, an AI tag behaves exactly like a keyword tag — "how much on the gym?" is the same fast, exact filter.

There's one practical difference: a model call per transaction is far too slow to run at query time, so AI tags are classified ahead of time and cached. New transactions are handled at index time; the ones you already have (and everything from before you added the rule) are filled in by the backfiller.

Tag groups (references)

A tag group is a rollup — a tag whose rule references other tags instead of (or alongside) keywords. In a = rule, a term prefixed with @ is a reference to another tag. A transaction gets the group tag when any term matches, and references mix freely with plain keywords:

# a pure rollup — essentials = anything already tagged groceries OR utilities
essentials (everyday necessities — rollup of groceries + utilities) = @groceries, @utilities

# mixed: references AND a keyword in the same rule
essentials = @groceries, @utilities, costco

A group tag is just a normal tag that happens to reference others. It's computed automatically from the tags a transaction already has — the referenced tags are unioned — so essentials covers everything tagged groceries or utilities without you re-listing a single merchant. Groups can reference other groups, so you can build a small hierarchy (e.g. a fixed costs group that references essentials).

Don't double-count. A rollup overlaps its members — every essentials transaction is also a groceries or utilities one. So don't sum a group together with its members in the same total; that counts those transactions twice. A group is an alternative view, not an additional bucket.

Tags follow the money direction

Tags are applied to your spending. Every category — the built-in ones, your keyword tags, and your AI tags — only lands on debits (money out). So an ACH deposit, a card payment, or a transfer — anything that's money in (a credit) — never picks up an expense category. A payroll deposit whose description happens to mention a café won't be tagged coffee shop; a refund isn't counted as a purchase.

The two built-in income tags are the exception, and they work the other way around: transfers (ACH, Zelle, Venmo, wires — money moved between accounts or people) and rewards (credit-card cash back and rewards credits) apply to your credits. That keeps the split clean — expense categories describe what you spent, and these two describe money coming in.

Tagging income beyond those two is planned: a future release will let you mark any tag as an income tag, so you can build your own credit-side categories (say, salary or refunds). For now the income side is just transfers and rewards.

Editing your tags

categories.txt is the source of truth, and you can edit it directly — but the easiest place is the Tags panel in the millfolio web app, under Vault. It shows three views — Records, Tags, and Files — and the tag registry is editable right there as text.

Adding or changing a rule re-tags the transactions you've already stored: keyword and group tags apply instantly, and AI tags are queued for the backfiller. millfolio auto-refreshes the built-in defaults on upgrade only while you haven't touched them — once you edit any rule, the file is yours and won't be overwritten.

One naming rule: a tag name can contain spaces but not a comma, =, :, or parentheses — those are the separators the format reserves.