Skip to main content
Rules are the workhorse of any Breadbox setup. A well-tuned set of rules can categorize the overwhelming majority of your transactions during sync, leaving only the genuinely ambiguous ones for a human or agent to look at. This guide walks through the DSL by example — four rules you can adapt and drop into your instance today. If you haven’t yet, skim Breadbox in a nutshell first for the vocabulary. The full specification lives in Auto-categorize transactions with rules and the rules API reference.

The DSL, in one screen

A rule is a JSON document with three important pieces:
  1. A condition — a recursive tree of leaves (field/op/value) and combinators (and / or / not).
  2. One or more actionsset_category, add_tag, remove_tag, or add_comment.
  3. A trigger and stage — when the rule runs (, always, on_change) and where in the pipeline (baseline, standard, refinement, override).
Amounts use Plaid convention: positive = money out (purchases, payments), negative = money in (refunds, paychecks). Every example below respects that.

Example 1 — Amazon purchases → Shopping

The canonical “merchant name contains a substring” rule. Two conditions combined with and: the description must contain AMAZON, and the amount must be positive (so we don’t catch Amazon refunds).
{
  "name": "Amazon purchases",
  "conditions": {
    "and": [
      { "field": "name", "op": "contains", "value": "AMAZON" },
      { "field": "amount", "op": "gt", "value": 0 }
    ]
  },
  "actions": [
    { "type": "set_category", "category_slug": "shopping" }
  ],
  "trigger": "on_create",
  "stage": "standard"
}
Create it via the API:
curl -X POST \
  -H "X-API-Key: bb_your_key" \
  -H "Content-Type: application/json" \
  -d @amazon-rule.json \
  http://localhost:8080/api/v1/rules
Before saving, dry-run against your history to sanity-check the match count:
curl -X POST \
  -H "X-API-Key: bb_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "conditions": {
      "and": [
        { "field": "name", "op": "contains", "value": "AMAZON" },
        { "field": "amount", "op": "gt", "value": 0 }
      ]
    }
  }' \
  http://localhost:8080/api/v1/rules/preview

Example 2 — Uber / Lyft / Waymo → Transportation

When a single condition isn’t enough, an or group handles the “any of these merchants” case. Here we use the in operator on merchant_name to check against a list in one shot.
{
  "name": "Ridesharing → Transportation",
  "conditions": {
    "and": [
      { "field": "merchant_name", "op": "in", "value": ["Uber", "Lyft", "Waymo"] },
      { "field": "amount", "op": "gt", "value": 0 }
    ]
  },
  "actions": [
    { "type": "set_category", "category_slug": "transportation_taxi_and_ride_share" },
    { "type": "add_comment", "value": "Auto-categorized as rideshare by rule." }
  ],
  "trigger": "on_create",
  "stage": "standard"
}
merchant_name is only populated for providers that enrich the raw description (Plaid does this; Teller and CSV imports often don’t). If your data comes primarily from Teller, prefer name with contains and match on a substring of the raw description.

Example 3 — Threshold tag: flag anything over $500

Not every rule has to change the category. This one only tags — anything over $500 gets the high-amount tag so you can scan the queue for big-ticket items before the rest.
{
  "name": "Flag transactions over $500",
  "conditions": {
    "and": [
      { "field": "amount", "op": "gt", "value": 500 },
      { "field": "pending", "op": "eq", "value": false }
    ]
  },
  "actions": [
    { "type": "add_tag", "tag_slug": "high-amount" }
  ],
  "trigger": "on_create",
  "stage": "standard"
}
You’d pair this with a tag you’ve pre-created in the dashboard (TagsNew tag, slug high-amount). Then filter to /transactions?tags=high-amount in the UI, or query_transactions(tags=["high-amount"]) from an agent.

Example 4 — A not rule: auto-categorize groceries, but not Whole Foods prepared food

Sometimes the cleanest way to express a rule is “match this except for these cases.” The not combinator wraps a sub-condition and inverts it.
{
  "name": "Grocery stores → Groceries (except prepared food)",
  "conditions": {
    "and": [
      { "field": "merchant_name", "op": "in", "value": ["Whole Foods", "Trader Joe's", "Safeway"] },
      { "not": { "field": "name", "op": "contains", "value": "PREPARED" } }
    ]
  },
  "actions": [
    { "type": "set_category", "category_slug": "food_and_drink_groceries" }
  ],
  "trigger": "on_create",
  "stage": "standard"
}
You can freely nest and, or, and not up to 10 levels deep. For most workflows you’ll keep it under three.
If a transaction gets the wrong category anyway, set it manually from the dashboard. That flips , which the rule engine honors forever — your deliberate choice won’t be undone by a later rule.

Applying rules retroactively

Rules only fire at sync time by default. To run a newly created rule against your full history:
# Apply one rule
curl -X POST \
  -H "X-API-Key: bb_your_key" \
  http://localhost:8080/api/v1/rules/rule_abc123/apply

# Apply every active rule
curl -X POST \
  -H "X-API-Key: bb_your_key" \
  http://localhost:8080/api/v1/rules/apply-all
Retroactive apply follows the same pipeline order and the same category_override protection as live sync. One caveat: add_comment actions are skipped during retroactive apply (they’re designed to narrate a specific sync event).

Where to go next