Skip to content

Grouping files

So far, all the generated transactions go into a single main.bean file. That works fine for a small number of transactions, but as your accounting book grows, having everything in one file becomes unwieldy. It's common practice in Beancount to split transactions into separate files, such as one file per year, per month, or even per account. The good news is that beanhub-import makes this very easy because the file field in the add transaction action is actually a Jinja2 template. This means you can use template variables from the transaction data to dynamically determine which file a transaction goes into.

Group by year

The simplest and most common approach is grouping transactions by year. Since the date field is available as a variable, you can extract the year from it. Let's update our import rule from the previous step to output transactions into yearly files:

inputs:
  - match: "import-data/connect/chase/*.csv"
    config:
      default_file: "books/{{ date.year }}.bean"
      prepend_postings:
        - account: Liabilities:ChaseCreditCard
          amount:
            number: "{{ -amount }}"
            currency: "{{ currency | default('USD', true) }}"

imports:
  - name: Routines
    common_cond:
      extractor:
        equals: "plaid"
    match:
      - cond:
          desc:
            prefix: "DD *DOORDASH"
        vars:
          narration: DoorDash food delivery
          account: Expenses:FoodDelivery:DoorDash
      - cond:
          desc: "Netflix"
        vars:
          narration: Netflix subscription
          account: Expenses:Entertainment:StreamingService
      - cond:
          desc: "Comcast"
        vars:
          account: Expenses:Internet
    actions:
      - txn:
          flag: "{{ '!' if pending else '*' }}"
          payee: "{{ payee | default(omit, true) }}"
          narration: "{{ narration | default(desc, true) }}"
          postings:
            - account: "{{ account }}"
              amount:
                number: "{{ amount }}"
                currency: "{{ currency | default('USD', true) }}"

Notice how we changed default_file from main.bean to books/{{ date.year }}.bean. Now, when you run the import command, transactions from January 2025 will be written to books/2025.bean, transactions from 2026 will go into books/2026.bean, and so on. The import engine will automatically create those files for you if they don't exist yet.

You can also override the default_file by specifying the file field directly in a particular rule's action. For example, if you want a specific rule to always write to a fixed file regardless of the default:

actions:
  - file: "special/one-off.bean"
    txn:
      narration: "Some special transaction"
      postings:
        - account: "Expenses:Misc"

Group by month

If you have a high volume of transactions, yearly files might still be too large. You can group them by month instead:

inputs:
  - match: "import-data/connect/chase/*.csv"
    config:
      default_file: "books/{{ date.year }}/{{ '%02d' | format(date.month) }}.bean"
      prepend_postings:
        - account: Liabilities:ChaseCreditCard
          amount:
            number: "{{ -amount }}"
            currency: "{{ currency | default('USD', true) }}"

This produces files like books/2025/01.bean, books/2025/02.bean, books/2025/03.bean, and so on. The '%02d' | format(date.month) part ensures the month number is always two digits with a leading zero.

Group by quarter

Some people prefer organizing their accounting books by quarter. You can achieve that using Jinja2's integer division:

inputs:
  - match: "import-data/connect/chase/*.csv"
    config:
      default_file: "books/{{ date.year }}/Q{{ ((date.month - 1) // 3) + 1 }}.bean"
      prepend_postings:
        - account: Liabilities:ChaseCreditCard
          amount:
            number: "{{ -amount }}"
            currency: "{{ currency | default('USD', true) }}"

This produces files like books/2025/Q1.bean, books/2025/Q2.bean, etc.

Group by account

Another common pattern is to group transactions by account. For example, if you have multiple bank accounts and credit cards, you may want each one to have its own output file. You can do this with the loop feature combined with the file template. Building on what we learned from the simplify repetitive rules page:

inputs:
  - match: "import-data/connect/{{ match_path }}"
    config:
      default_file: "books/{{ output_path }}/{{ date.year }}.bean"
      prepend_postings:
        - account: "{{ input_account }}"
          amount:
            number: "{{ -amount }}"
            currency: "{{ currency | default('USD', true) }}"
    loop:
      - match_path: "chase/*.csv"
        input_account: Liabilities:ChaseCreditCard
        output_path: chase
      - match_path: "Wells Fargo/SAVING ...5678/*.csv"
        input_account: Assets:Bank:US:WellsFargo:Saving
        output_path: wells-fargo
      - match_path: "American Express/Blue Cash Everyday®/*.csv"
        input_account: Liabilities:CreditCard:US:AMEXBlueCashEveryday
        output_path: amex

With this setup, Chase transactions from 2025 go into books/chase/2025.bean, Wells Fargo transactions into books/wells-fargo/2025.bean, and so on. Each account gets its own folder with yearly files.

Don't forget the include statements

There is one important thing to keep in mind when grouping transactions into different files. Beancount needs to know about all the files to load. If you're using a single main.bean as the entry point, you need to add include statements so that Beancount picks up the generated files.

Beancount supports wildcard patterns in include statements. This is critical because beanhub-import dynamically creates new files as new transactions come in. If you use a hardcoded list of include statements, you would have to manually add a new include every time a new file is created. With wildcard includes, new files are picked up automatically.

For example, if you're grouping by year:

include "books/*.bean"

If you're grouping by month or quarter within year folders:

include "books/*/*.bean"

If you're grouping by account and then by year:

include "books/**/*.bean"

The ** pattern matches any number of nested directories, so it works no matter how deep your folder structure goes.

One caveat to keep in mind: if a glob pattern matches no files at all, Beancount will report an error. So if you add an include statement like include "books/**/*.bean" before any transaction files have been generated, you will get a "File glob does not match any files" error. To avoid this, you can either run the import command first to generate the initial files, or create an empty placeholder .bean file in the target directory.

Make sure you have the appropriate include statement in your main Beancount entry file. Otherwise, Beancount won't be aware of the generated files, and tools like bean-check, fava, or BeanHub won't see those transactions.