Step-by-step example
Now you know how beanhub-import works, let's see an example and show you how to do it step by step. Before that, you need to install BeanHub-CLI first. You probably already did it if you've followed the guide for pulling bank transaction CSV files from BeanHub Direct Connect. If not, it's very simple. You only need to ensure you have Python greater or equal to 3.11 installed. Then, you can run:
pip install "beanhub-cli>=2.1.0"
Define your first importing rule
To make it much easier for you to follow the tutorial below, we have made a GitHub repository containing all the needed files with commits for each step we will perform. You can find the repository here.
Now, let's define the first simple empty beanhub-import rule file at .beanhub/imports.yaml
with content like this:
inputs: []
imports: []
You must also ensure you have at least the main.bean
Beancount file in your current folder.
If not, you can create one with the following content.
2025-01-01 open Assets:Cash
2025-01-01 open Liabilities:ChaseCreditCard
2025-01-01 open Expenses:FoodDelivery
2025-01-01 open Expenses:FoodDelivery:DoorDash
2025-01-01 open Expenses:Friendship
2025-01-01 open Expenses:Internet
2025-01-01 open Expenses:Entertainment:StreamingService
Now, you can run the import command of BeanHub-CLI by:
bh import
And you will see output like this:

What just happened is that the import command reads your import rule file at .beanhub/imports.yaml
and tries to import transactions based on the rule from the input sources.
Because the file contains no input and rules, there is nothing the import engine can do.
Next, here are some example transactions in a CSV file:
date,name,amount,pending,website,datetime,logo_url,account_id,category_id,check_number,account_owner,merchant_name,transaction_id,authorized_date,payment_channel,transaction_code,transaction_type,iso_currency_code,merchant_entity_id,authorized_datetime,pending_transaction_id,unofficial_currency_code,personal_finance_category_icon_url,counterparties__name,counterparties__type,counterparties__website,counterparties__logo_url,counterparties__entity_id,counterparties__phone_number,counterparties__confidence_level,personal_finance_category__primary,personal_finance_category__detailed,personal_finance_category__confidence_level
2025-01-17,DD *DOORDASH SWEETGREE,40.83,False,sweetgreen.com,2025-01-17 18:26:18+00:00,https://plaid-merchant-logos.plaid.com/sweetgreen_986.png,Gl0xgTJpSiAyRPz4igqp6GEaL29NZ8Q,13005000,,,Sweetgreen,1EqoyG9ErL4Kw0LcPiwJ3JQ3bqc80bLy,2025-01-15,online,,place,USD,Do7pjknKqXrQkMr7qdV9JXrQ3Em8nzny5rOE1,2025-01-15 21:59:31+00:00,Anz_8NC87rxlwMSQxO5HEraReGXZc66,,https://plaid-category-icons.plaid.com/PFC_FOOD_AND_DRINK.png,Sweetgreen,merchant,sweetgreen.com,https://plaid-merchant-logos.plaid.com/sweetgreen_986.png,Do7pjknKqXrQkMr7qdV9JXrQ3Em8nzny5rOE1,,VERY_HIGH,FOOD_AND_DRINK,FOOD_AND_DRINK_RESTAURANT,VERY_HIGH
2025-01-17,DD *DOORDASH CHIPOTLEM,35.12,False,chipotle.com,2025-01-17 18:26:20+00:00,https://plaid-merchant-logos.plaid.com/chipotle_mexican_grill_202.png,Gl0xgTJpSiAyRPz4igqp6GEaL29NZ8Q,18021000,,,Chipotle Mexican Grill,hxK1dVXuiogg1NvabkOPBGsLSldqG1Ps,2025-01-16,online,,place,USD,Mjqdpqn3ZgOdrAYpkAKMkArEON7avQ8jV9BX1,2025-01-16 23:03:08+00:00,4QMm8eUYDwHVZTw4WFakxQqQam7Dsv,,https://plaid-category-icons.plaid.com/PFC_FOOD_AND_DRINK.png,Chipotle Mexican Grill,merchant,chipotle.com,https://plaid-merchant-logos.plaid.com/chipotle_mexican_grill_202.png,Mjqdpqn3ZgOdrAYpkAKMkArEON7avQ8jV9BX1,,VERY_HIGH,FOOD_AND_DRINK,FOOD_AND_DRINK_FAST_FOOD,HIGH
2025-01-18,Netflix,15.49,False,netflix.com,2025-01-18 13:48:51+00:00,https://plaid-merchant-logos.plaid.com/netflix_675.png,tO4XeMDNMrgkBnVUjX7JfgMKZjkDr8x,18061000,,,Netflix,OTW6sOZdAoimiPQ4nhE2qCVquhY2UMo,2025-01-17,online,,place,USD,3LEY2bJ6W1vkoaBjgVg3qBgYVEzD6p8d1dQdY,2025-01-17 02:02:03+00:00,,,https://plaid-category-icons.plaid.com/PFC_ENTERTAINMENT.png,Netflix,merchant,netflix.com,https://plaid-merchant-logos.plaid.com/netflix_675.png,3LEY2bJ6W1vkoaBjgVg3qBgYVEzD6p8d1dQdY,,VERY_HIGH,ENTERTAINMENT,ENTERTAINMENT_TV_AND_MOVIES,VERY_HIGH
2025-01-21,DD *DOORDASH SWEETGREE,93.71,False,sweetgreen.com,2025-01-21 20:05:08+00:00,https://plaid-merchant-logos.plaid.com/sweetgreen_986.png,Gl0xgTJpSiAyRPz4igqp6GEaL29NZ8Q,13005000,,,Sweetgreen,0SW5CWrBWETFwH4H0SyheztUaV7LeJMG,2025-01-20,online,,place,USD,Do7pjknKqXrQkMr7qdV9JXrQ3Em8nzny5rOE1,2025-01-20 22:21:52+00:00,5MXac8RO7nm4PPo3X5Y9WrePR6WpngNi,,https://plaid-category-icons.plaid.com/PFC_FOOD_AND_DRINK.png,Sweetgreen,merchant,sweetgreen.com,https://plaid-merchant-logos.plaid.com/sweetgreen_986.png,Do7pjknKqXrQkMr7qdV9JXrQ3Em8nzny5rOE1,,VERY_HIGH,FOOD_AND_DRINK,FOOD_AND_DRINK_RESTAURANT,VERY_HIGH
2025-01-22,DD *DOORDASH SWEETGREE,30.21,False,sweetgreen.com,2025-01-22 18:40:31+00:00,https://plaid-merchant-logos.plaid.com/sweetgreen_986.png,Gl0xgTJpSiAyRPz4igqp6GEaL29NZ8Q,13005000,,,Sweetgreen,zGsuwN4RK4RtCN6Y9MPTKKf4E7gpRgJq,2025-01-21,online,,place,USD,Do7pjknKqXrQkMr7qdV9JXrQ3Em8nzny5rOE1,2025-01-21 22:45:06+00:00,9O8UoDkGLStAMleZeSke14fDH8yUQEmm,,https://plaid-category-icons.plaid.com/PFC_FOOD_AND_DRINK.png,Sweetgreen,merchant,sweetgreen.com,https://plaid-merchant-logos.plaid.com/sweetgreen_986.png,Do7pjknKqXrQkMr7qdV9JXrQ3Em8nzny5rOE1,,VERY_HIGH,FOOD_AND_DRINK,FOOD_AND_DRINK_RESTAURANT,VERY_HIGH
2025-01-25,Comcast,50.00,False,comcast.com,2025-01-25 13:08:59+00:00,https://plaid-merchant-logos.plaid.com/comcast_226.png,tO4XeMDNMrgkBnVUjX7JfgMKZjkDr8x,18009000,,,Comcast,muJHvccd9out4Xts2l60NpzKTPmFsCWU,2025-01-25,online,,special,USD,edRWya4NWjyZqeJbjQVAB2qZakY1XvpVN023g,2025-01-25 02:02:03+00:00,,,https://plaid-category-icons.plaid.com/PFC_RENT_AND_UTILITIES.png,Comcast,merchant,comcast.com,https://plaid-merchant-logos.plaid.com/comcast_226.png,edRWya4NWjyZqeJbjQVAB2qZakY1XvpVN023g,,VERY_HIGH,RENT_AND_UTILITIES,RENT_AND_UTILITIES_INTERNET_AND_CABLE,VERY_HIGH
2025-01-25,CREDIT BALANCE REFUND,689.39,False,,2025-01-26 00:47:48+00:00,,tO4XeMDNMrgkBnVUjX7JfgMKZjkDr8x,21006000,,,Credit Balance,loJ0nJ8BbaL2c3_haIP4JdDoKublBfrJ,2025-01-25,in store,,special,USD,,2025-01-25 02:02:03+00:00,,,https://plaid-category-icons.plaid.com/PFC_TRANSFER_OUT.png,Credit Balance,merchant,,,,,LOW,TRANSFER_OUT,TRANSFER_OUT_OTHER_TRANSFER_OUT,LOW
2025-01-31,SP CORSAIR GAMING INC.,768.26,False,,2025-01-31 13:47:50+00:00,,tO4XeMDNMrgkBnVUjX7JfgMKZjkDr8x,19025000,,,Corsair Gaming Inc.,Vvxn3BBL3Ef41V9es5ruo4_ksQoIcQEb,2025-01-30,in store,,place,USD,,2025-01-30 02:02:03+00:00,,,https://plaid-category-icons.plaid.com/PFC_GENERAL_MERCHANDISE.png,Corsair Gaming Inc.,merchant,,,,,LOW,GENERAL_MERCHANDISE,GENERAL_MERCHANDISE_ELECTRONICS,LOW
2025-02-18,Netflix,15.49,False,netflix.com,2025-02-18 13:00:44+00:00,https://plaid-merchant-logos.plaid.com/netflix_675.png,tO4XeMDNMrgkBnVUjX7JfgMKZjkDr8x,18061000,,,Netflix,fzzYu8A7xUs5U5QuJminpDUHVHzJpG1t,2025-02-17,online,,place,USD,3LEY2bJ6W1vkoaBjgVg3qBgYVEzD6p8d1dQdY,2025-02-17 02:02:03+00:00,,,https://plaid-category-icons.plaid.com/PFC_ENTERTAINMENT.png,Netflix,merchant,netflix.com,https://plaid-merchant-logos.plaid.com/netflix_675.png,3LEY2bJ6W1vkoaBjgVg3qBgYVEzD6p8d1dQdY,,VERY_HIGH,ENTERTAINMENT,ENTERTAINMENT_TV_AND_MOVIES,VERY_HIGH
2025-02-25,Comcast,50.00,False,comcast.com,2025-02-25 13:03:00+00:00,https://plaid-merchant-logos.plaid.com/comcast_226.png,tO4XeMDNMrgkBnVUjX7JfgMKZjkDr8x,18009000,,,Comcast,C0i2e8XpjdAdglv5dqPZYiYxwMuj0Eo,2025-02-25,online,,special,USD,edRWya4NWjyZqeJbjQVAB2qZakY1XvpVN023g,2025-02-25 12:48:01+00:00,,,https://plaid-category-icons.plaid.com/PFC_RENT_AND_UTILITIES.png,Comcast,merchant,comcast.com,https://plaid-merchant-logos.plaid.com/comcast_226.png,edRWya4NWjyZqeJbjQVAB2qZakY1XvpVN023g,,VERY_HIGH,RENT_AND_UTILITIES,RENT_AND_UTILITIES_INTERNET_AND_CABLE,VERY_HIGH
2025-03-18,Netflix,15.49,False,netflix.com,2025-03-18 16:16:46+00:00,https://plaid-merchant-logos.plaid.com/netflix_675.png,tO4XeMDNMrgkBnVUjX7JfgMKZjkDr8x,18061000,,,Netflix,E9grCH5HzuhBfOcm9R8_WFjuti9kEYm6,2025-03-17,online,,place,USD,3LEY2bJ6W1vkoaBjgVg3qBgYVEzD6p8d1dQdY,2025-03-17 02:02:03+00:00,,,https://plaid-category-icons.plaid.com/PFC_ENTERTAINMENT.png,Netflix,merchant,netflix.com,https://plaid-merchant-logos.plaid.com/netflix_675.png,3LEY2bJ6W1vkoaBjgVg3qBgYVEzD6p8d1dQdY,,VERY_HIGH,ENTERTAINMENT,ENTERTAINMENT_TV_AND_MOVIES,VERY_HIGH
2025-03-25,Comcast,50.00,False,comcast.com,2025-03-25 13:14:07+00:00,https://plaid-merchant-logos.plaid.com/comcast_226.png,tO4XeMDNMrgkBnVUjX7JfgMKZjkDr8x,18009000,,,Comcast,KFuM7W0zGjm4PO8s1wxg5_eSAJqSC2HV,2025-03-25,online,,special,USD,edRWya4NWjyZqeJbjQVAB2qZakY1XvpVN023g,2025-03-25 02:02:03+00:00,,,https://plaid-category-icons.plaid.com/PFC_RENT_AND_UTILITIES.png,Comcast,merchant,comcast.com,https://plaid-merchant-logos.plaid.com/comcast_226.png,edRWya4NWjyZqeJbjQVAB2qZakY1XvpVN023g,,VERY_HIGH,RENT_AND_UTILITIES,RENT_AND_UTILITIES_INTERNET_AND_CABLE,VERY_HIGH
2025-04-18,Netflix,15.49,False,netflix.com,2025-04-18 13:18:27+00:00,https://plaid-merchant-logos.plaid.com/netflix_675.png,tO4XeMDNMrgkBnVUjX7JfgMKZjkDr8x,18061000,,,Netflix,2BgLmh3XkdWT5tsOBjBFweJVy5tzsLX,2025-04-17,online,,place,USD,3LEY2bJ6W1vkoaBjgVg3qBgYVEzD6p8d1dQdY,2025-04-17 02:02:03+00:00,,,https://plaid-category-icons.plaid.com/PFC_ENTERTAINMENT.png,Netflix,merchant,netflix.com,https://plaid-merchant-logos.plaid.com/netflix_675.png,3LEY2bJ6W1vkoaBjgVg3qBgYVEzD6p8d1dQdY,,VERY_HIGH,ENTERTAINMENT,ENTERTAINMENT_TV_AND_MOVIES,VERY_HIGH
2025-04-25,Comcast,50.00,False,comcast.com,2025-04-25 13:06:58+00:00,https://plaid-merchant-logos.plaid.com/comcast_226.png,tO4XeMDNMrgkBnVUjX7JfgMKZjkDr8x,18009000,,,Comcast,ZVMmCjpMTgXM7E9vN1AYen9ZWxA2KmfY,2025-04-25,online,,special,USD,edRWya4NWjyZqeJbjQVAB2qZakY1XvpVN023g,2025-04-25 02:02:03+00:00,,,https://plaid-category-icons.plaid.com/PFC_RENT_AND_UTILITIES.png,Comcast,merchant,comcast.com,https://plaid-merchant-logos.plaid.com/comcast_226.png,edRWya4NWjyZqeJbjQVAB2qZakY1XvpVN023g,,VERY_HIGH,RENT_AND_UTILITIES,RENT_AND_UTILITIES_INTERNET_AND_CABLE,VERY_HIGH
Or you can also download it from here.
Please put it at input-data/connect/chase/2025.csv
in your Beancount repository folder.
Now, we have our first input file.
But beanhub-import knows nothing about it, and you need to define it as an input file.
You modified the import rules like this:
inputs:
- match: "import-data/connect/chase/*.csv"
imports: []
Now, let's rerun the import command with bh import.

As you can see, the import tool reads the CSV files and their transactions, but we haven't defined any rules, so there is nothing to match or generate yet.
Therefore, you can see all the transactions appear in the Open transactions
section.
It's okay.
We looked at those transactions and realized we have many transactions from DoorDash.
We want to make those transactions go under the Expenses:FoodDelivery
account.
We noticed that the pattern of description (desc
) of DoorDash transactions looks like these:
- DD *DOORDASH SWEETGREE
- DD *DOORDASH CHIPOTLEM
- DD *DOORDASH SWEETGREE
- DD *DOORDASH SWEETGREE
Let's define our first rule in the rules YAML file.
inputs:
- match: "import-data/connect/chase/*.csv"
imports:
- name: DoorDash Food Delivery
match:
extractor:
equals: "plaid"
desc:
prefix: "DD *DOORDASH"
actions:
- file: main.bean
txn:
flag: "{{ '!' if pending else '*' }}"
payee: "{{ payee | default(omit, true) }}"
narration: "DoorDash food delivery"
postings:
- account: Liabilities:ChaseCreditCard
amount:
number: "{{ -amount }}"
currency: "{{ currency | default('USD', true) }}"
- account: "Expenses:FoodDelivery"
amount:
number: "{{ amount }}"
currency: "{{ currency | default('USD', true) }}"
The match
field defined what kind of input transactions we would like to match.
In this case, we would like to match transactions from plaid
because we are only interested in plaid transactions provided by the PlaidExtractor defined in beanhub-extract.
And the desc
with a prefix
value DD *DOORDASH
means we would like to match transactions with this prefix in the desc
field.
Please note that it's AND operations for all the conditions listed under the match
field, i.e., all conditions must met for a transaction to be considered a match.
You can read our document to understand what kind of matching operations are available.
With this first rule, here we run the import command.

Here you go! We just made our first tiny step to import transactions automatically from CSV files. How exciting is that? If you look at the Git diff of our current repository, you will see the new transactions added to your main Beancount file automatically.

How it works
Here's how it works.
The import engine finds all the matched input CSV files in the defined input sources.
Next, it goes through all the transactions provided by beanhub-extract from the CSV file at import-data/connect/chase/2025.csv
one by one.
It tries to match the transaction with conditions defined in the import rules.
Whenever there is a rule match, it will try to run the action section.
By default, if not specified, the action type will be add_txn,
which means adding a corresponding transaction in your Beancount file.
With the template defined in the action, the Jinja2 template will be defined for each field for the Beancount transaction with the variables from the current transaction.
With the first DoorDash transaction data from our CSV file as an example, {{ amount }}
will be replaced with 61.94
as the amount
value of that transaction is 61.94.
The payee field template {{ payee | default(omit, true) }}
will be replaced as Sweetgreen
.
The transaction is pending or not will determine the Beancount flag
field as !
or *
based on the template {{ '!' if pending else '*' }}
.
So and so on, the whole transaction template like this will be rendered with transaction data:
txn:
flag: "{{ '!' if pending else '*' }}"
payee: "{{ payee | default(omit, true) }}"
narration: "DoorDash food delivery"
postings:
- account: Liabilities:ChaseCreditCard
amount:
number: "{{ -amount }}"
currency: "{{ currency | default('USD', true) }}"
- account: "Expenses:FoodDelivery"
amount:
number: "{{ amount }}"
currency: "{{ currency | default('USD', true) }}"
As a result, you get a Beancount transaction that looks like this:
2025-01-15 * "Sweetgreen" "DoorDash food delivery"
import-id: "1lmOANutKCLWGfAmkkAfFdPvvU4GhQW"
import-src: "import-data/connect/chase/2025.csv"
Liabilities:ChaseCreditCard -61.94 USD
Expenses:FoodDelivery 61.94 USD
The same process happens to all the matched DoorDash transactions.
As you see, we have a file
field in the add transaction action.
It specifies which Beancount file this transaction should be written into.
In this case, we set it to main.bean
, so the generated transaction will be inserted or updated in the main Beancount file.
Update existing Beancount transactions
The beanhub-import importing engine is smart enough to update existing Beancount transactions if you already have one or insert a new one if it doesn't exist yet.
It can even automatically move a transaction from one file to another by adjusting the output file
value.
We will show you in the later sections.
In the meantime, let's try to update the import rules first.
Say we don't want to use Expenses:FoodDelivery
as the account for DoorDash delivery anymore.
We have made a new account, Expenses:FoodDelivery:DoorDash
, and want to use it for DoorDash transactions instead.
You can update your import rule like this:

Then, rerun the import command.

You see, this is the unique superpower of beanhub-import.
We make the import operation idempotent and declarative.
As long as the input data and the rules are the same, the output result is supposed to be the same.
Therefore, you can modify the rule and rerun the import easily and expect it to update all the Beancount files for you.
This feature greatly relies on the import-id
.
You can read the document here to understand more about how it works.