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.