- Set a transaction’s category (
set_category) - Add or remove tags (
add_tag,remove_tag) - Write or delete free-form metadata keys (
set_metadata,remove_metadata) - Link the transaction to a recurring series (
assign_series) - Link the transaction to a counterparty (
assign_counterparty) - Raise or clear the flag that surfaces a charge for human attention (
flag,unflag) - Leave an automated comment explaining the categorization (
add_comment)
Condition structure
Rule conditions use a recursive tree of leaf nodes and combinators. A leaf node tests a single field:{} matches every transaction.
Available fields
Use
category (not provider_category_primary or provider_category_detailed) when you want a condition to react to the category that Breadbox or a prior rule assigned. The provider_* fields always hold the provider’s original values and never change.Match-stability: prefer raw, immutable fields
A rule is only as durable as the fields it matches on. The provider’s raw fields (provider_name, provider_merchant_name, amount, pending, provider, provider_category_*) and the date-parts derived from the immutable posting date (day_of_month, month, day_of_week, day_of_year) are raw-immutable — Breadbox never rewrites them, so a rule keyed on them resolves the same way on the create pass, on every re-sync, and on retroactive apply. The surrogate IDs account_id and user_id are stable-surrogate — also safe.
account_name, user_name, category, tags, series, in_series, counterparty, has_counterparty, and metadata.<key> are mutable-display — they either silently break when something is renamed or depend on what an earlier-stage rule wrote in the same pass. Use them deliberately to chain off another rule’s output, not as the load-bearing condition that decides whether the rule fires.
Operators by field type
String fields (provider_name, provider_merchant_name, provider_category_primary, provider_category_detailed, category, provider, account_name, user_name, series, counterparty):
Numeric fields (
amount, date-parts):
Boolean fields (
pending, in_series, has_counterparty):
Tag field (
tags):
Metadata fields (
metadata.<key>):
Every operator other than
exists / not_exists requires the key to be present — an absent key matches only not_exists. Use or of the two when you mean “missing OR different”.
Pipeline stages
Rules run in pipeline order — lower stage numbers run first. Forset_category, the last matching rule wins, so higher-stage rules have the final say on categorization. For accumulator actions (add_tag, add_comment, set_metadata), every matching rule contributes.
Supply
stage as a string in the request body. You can also supply a raw priority integer (0–1000) for fine-grained ordering within a stage. If you supply both, priority wins.
Rule chaining
Because rules run in pipeline order and share a mutable transaction context, later rules can react to what earlier rules did. A rule that assigns a category at stage 0 makes that category readable viafield: "category" for any rule at stage 10 or higher in the same sync pass. The same holds for tags, series, counterparty, and metadata.<key> — a later rule reads what an earlier one wrote.
Creating a rule
A complete example that categorizes Amazon purchases:trigger specified defaults to on_create, which means it fires only on newly synced transactions. Use always to also run on re-synced changes, or on_change to run only when an existing transaction is modified.
Actions
set_category
set_category per rule. Last-writer-wins across the pipeline — rules, agents, and users all write the same category_id field, so a higher-stage rule’s set_category overrides a lower one. Rules only run on new or changed transactions, so a user’s manual edit on an unchanged row is not continuously re-clobbered.
add_tag / remove_tag
add_tag if the slug doesn’t exist. Both are idempotent. If a single sync pass would add_tag and remove_tag the same slug, they cancel — neither write hits the DB.
set_metadata / remove_metadata
metadata blob, leaving every other key untouched. metadata_value can be any JSON value (string, number, boolean, object, array); keys are ≤128 chars and values must serialize to ≤4 KiB. Repeatable — a rule can write several keys at once. Last-writer-wins per key; a same-pass set-then-remove cancels.
Use it to capture arbitrary household enrichment that isn’t a first-class field: tax_deductible, trip, reimbursable_by, project_code, … A later rule can read it back via metadata.<key> conditions.
assign_series
series_short_id— link to an existing series by its short ID.series_name+create_if_missing: true— mint a series by name if one doesn’t already exist with that live name (surrogate-first; the same name always resolves the same series).
assign_series never steals a charge already in another series.
A series is its governing rules: the membership of every series is exactly the set of charges its assign_series rules match. The admin Recurring detail page makes this explicit by listing the linked charges beside the rules that define them.
assign_counterparty
counterparty_short_id or counterparty_name + create_if_missing: true. Same NULL-fill, last-writer-wins, surrogate-first semantics as assign_series. Like series, a counterparty’s membership is exactly the set of charges its assign_counterparty rules bind.
flag / unflag
flag sets transactions.flagged_at = NOW(); unflag clears it. Last-writer-wins across the pipeline — a higher-priority unflag clears a lower-priority flag. Retrieve flagged rows with the query_transactions(flagged=true) MCP tool or GET /transactions?flagged=true.
Use it to mark anything that needs eyes: large charges, suspected duplicates, foreign-currency outliers, charges from a paused subscription, …
add_comment
add_comment because comments narrate a specific sync event.
Combining actions
A rule can carry multiple actions of different types; they all fire together. Useful combinations:
Only
set_category, flag, and unflag are singleton per rule. The rest can appear multiple times (e.g. add two tags, write two metadata keys).
Previewing a rule
Before saving a rule, you can dry-run its condition against your existing transactions to see what it would match:Applying a rule retroactively
After creating or editing a rule, apply it to your full transaction history without waiting for the next sync:set_category, add_tag, remove_tag, set_metadata, remove_metadata, assign_series, assign_counterparty, flag, and unflag — through both the single-rule and the bulk apply-all paths.
Last-writer-wins, no provenance guard
Rules, agents, and users all write the same underlying fields (category_id, tags, metadata, series link, counterparty link, flag). There is no per-source precedence — the writer who runs last wins. The sync engine still only runs rules on new or changed transactions, so a user’s manual edit on an unchanged row is not silently re-clobbered on the next sync.
Related reading
- Rules API — REST endpoints for creating, listing, updating, deleting, applying, and previewing rules.
- MCP: rules (read) and MCP: rules (write) — the equivalent tools agents use to create and maintain rules on your behalf.
- Categories — how the two-level hierarchy that rules target is structured.
- Tracking subscriptions — how
assign_seriesbuilds your recurring catalog. - Review workflow — how the seeded
needs-reviewrule drives the default triage queue.