Nerd Adventures: Replicating YNAB's Zero-Based Budget in hledger
Table of Contents
Periodically, I like to go through everything in my budget and evaluate whether I really still need to be paying for that thing or I’m just doing it because I don’t want to take the time to look for cheaper alternatives. One of the charges on the chopping block recently was YNAB (You Need a Budget) - the software I’ve been using to track my budget for years (oh, the irony!). It is a wonderful tool, and I have no complaints about the platform, the method, or the company. But it does cost $98.99 a year, and I do love to hack my own solutions…so here we are.
Research #
I did a pretty thorough web search, including combing through Reddit, for examples of other people attempting to do the same thing. There are several excellent blog posts describing how people did something similar to what I was imagining, so it really just took a bit of Frankensteining to develop what was going to be my strategy. Many of them were way more in-depth than what I was looking for - Michael Walker’s personal finance memo is incredible, for example, but goes into a level of reporting I do not aspire to. And many of them were using sub-accounts of real-world accounts to track budget categories, which truly broke my brain after being used to working in YNAB for so long (YNAB doesn’t care where your money is physically located, only what its job is).
What I settled on was using a command line tool called hledger, a free and open-source application that has a thriving community and is incredibly feature-rich. Its predecessor ledger isn’t very actively developed anymore, and its cousin beancount has lots of flashy features but loses some of the flexibility that the *ledger tools offer. The hallmark feature I was looking for was something called virtual postings, which would allow me to keep track of my money in real-world accounts while still managing arbitrary envelopes of dollars assigned to specific purposes.
I took a lot of my inspiration from this post about envelope budgeting. I had to learn a few principles of double-entry accounting to understand how I was going to need to set it all up, but after a few readings and re-readings I was ready to get started.
Setup #
Installing hledger was easy. I’m already a homebrew user, so running brew install hledger
was all I had to do. From there, I cd
ed into a the directory I wanted to work in and initiated a git repo (git init
), since knew I was going to want my financials under version control.
I started off by declaring all of my accounts. You don’t have to do this, but I wanted to be able to catch typos easily now that I was working in plain text. I actually decided to declare my accounts in their own file, then include that file in my main journal file. But I just as well could have done it directly in my main journal file. This is how I did it (yes, still right in the terminal!):
$ cat -> accounts.journal
account assets ; type: A
account assets:investments:401k
account assets:investments:ira
account assets:hsa
account assets:mybank ; type: C
account assets:mybank:checking
account assets:mybank:savings
account assets:cash ; type: C
account assets:cash:petty
account liabilities ; type: L
account liabilities:credit:visa
account liabilities:credit:discover
account liabilities:loan:auto
account liabilities:loan:mortgage
^D
Notice the type
declarations? Those were purposeful - A is for Assets, C is for Cash (a type of asset), and L is for Liabilities. The accounts declared as type: C
are the ones that are going to feed into my envelopes - those are my “on budget” accounts, in YNAB terms. Those sitting in type: A
are just for tracking purposes, since money in those accounts doesn’t currently feed into my YNAB budget.
Next up was creating the main journal file - the one I was going to be logging all of my transactions in. I also knew I wanted to include that accounts file I’d just finished setting up, and that I wanted to create a persistent environment variable for my main journal file so hledger would know where to put things by default. Once again, just a few terminal commands!
$ cat -> main.journal
include accounts.journal
^D
$ export LEDGER_FILE=main.journal
$ echo 'export LEDGER_FILE=main.journal' >>~/.profile
The final step of setup, which was also the first step of actually transacting in hledger, was declaring all my opening balances. This part made me a little lightheaded at first, because one of the principles of double-entry accounting is that all postings have to balance to zero, including opening balances. I trust the method, though, so I just went ahead and added account Equity ; type: E
to my accounts.journal
file and figured I’d use equity to balance it, since that’s what the internet told me to do.
$ cat -> main.journal
2023-01-01 opening balances
assets:investments:401k $55000.00
assets:investments:ira $35000.00
assets:hsa $2400.00
assets:mybank:checking $5000.00
assets:mybank:savings $10500.00
assets:cash:petty $350.00
liabilities:credit:visa -$200.00
liabilities:credit:discover -$325.00
liabilities:loan:auto -$5000.00
liabilities:loan:mortgage -$85000.00
equity
^D
That last line is where equity just kind of eats whatever amount is required to get the transaction to balance. Balancing postings makes so much sense to me in general, except where opening balances are concerned, so I try not to think about it very hard.
And now we YNAB! #
Setting the journal file up to accommodate a zero-based budget requires some additional work that normal hledger users won’t need to deal with. That’s where virtual postings enter the picture. There are two types of virtual postings, balanced and unbalanced. Unbalanced virtual postings break the main rule of double-entry accounting (that all postings in a transaction must sum to zero), so although they would be significantly easier to deal with, I chose to use balanced ones instead.
Budgeting #
Back to my accounts.journal
file for a minute - I realized I needed to declare all of the envelopes I was going to be putting money in. Essentially, I needed to recreate my categories and category groups in this journal file, then start allocating money to them. I created a new account subcategory under assets that I just called budget
, and nested all of my category groups under there. Here’s an example of what I mean (I won’t list out every single category here because I have way too many).
Also, remember that declaring accounts is an optional step! You can just create them in transactions on the fly, if you prefer. I happen to like the definitiveness of declaring the types, and I wanted to be able to run commands in --strict
mode to check for typos.
account assets:budget ; type: C
account assets:budget:to-be-budgeted
account assets:budget:bills:mortgage
account assets:budget:bills:water
account assets:budget:variable:groceries
account assets:budget:true expenses:registration
account assets:budget:goals:vacation
account assets:budget:cards:visa payment
And with that, it is finally time to give jobs to our dollars! Open up that main.journal
file again and let’s get to budgeting.
When we declare our opening balances in YNAB, it very kindly puts all of that money in To Be Budgeted (now it’s called “Ready to Assign”, but I prefer the ‘TBB’ acronym to the ‘RTA’ one). With hledger, we need to do that manually. So my first step was to virtually move all of the money from the cash accounts I just opened into my TBB budget account. That looks like this:
2023-01-01 populate TBB
[assets:mybank:checking] -$5000.00
[assets:mybank:savings] -$10500.00
[assets:cash:petty] -$350.00
[assets:budget:to-be-budgeted]
Virtual postings are denoted with [square brackets] around the name of the account, as you see above. A transaction can have both virtual and real postings, but each individual set must sum to zero (meaning the real postings must balance, and the virtual postings must balance).
The magic of these virtual transactions? If I run any hledger command and append -R
(meaning “real”), it will show me the balance of my real-world accounts. Without -R
, it will show me the balance of my envelopes. If I grab my cash account balances right now, here’s what they look like:
$ hledger bal ^C
$15,850.00 assets:budget:to-be-budgeted
Virtually, all the money is gone from my cash accounts - so they don’t even show up on this list. (If I wanted to include them, I could include the --empty
command, which includes even accounts with a $0 balance.) But with the -R
flag, they’re back with their correct balances!
$ hledger bal ^C -R
$5,000.00 assets:mybank:checking
$10,500.00 assets:mybank:savings
$350.00 assets:cash:petty
Everything is in TBB right now, but it just takes one more virtual transaction to get that money assigned to the right categories.
2023-01-01 budgeting session
[assets:budget:bills:mortgage] $1300.00
[assets:budget:bills:water] $50.00
[assets:budget:variable:groceries] $400.00
[assets:budget:cards:visa payment] $200.00
[assets:budget:to-be-budgeted]
Et voilá! I can now use that same hledger bal ^C
command to see an overview of where money is allocated in the envelope world, and add -R
when I want to double-check my account balances.
Transacting against the budget #
It’s time for Income and Expenses - the inflows and outflows that will add or remove money from our real-world accounts. Again, I took the optional step of declaring all of my income and expense accounts, and in the case of expenses matching them up with the budget accounts I’d declared previously. You don’t actually have to match your expense accounts precisely with your budget envelopes - I do because it’s easier, but I may experiment with it if I think it would be meaningful to track spending in a different set of categories than budgeting.
account income ; type: I
account income:companyA:salary
account income:companyB:salary
account income:gifts
account expenses ; type: X
account expenses:bills:mortgage
account expenses:bills:water
account expenses:variable:groceries
account expenses:true expenses:registration
Using virtual accounts for everything means our transactions are going to be chunkier than your average hledger user, which is a tradeoff I’m willing to make since I was already #TeamManualEntry in YNAB. But every transaction will basically need to be entered twice, since every transaction combines a balanced real-world posting and a balanced virtual posting. Allow me to explain!
Credit card transactions #
Buying groceries on a credit card might look like this for a normal hledger user:
2023-01-15 * shoprite | groceries
liabilities:credit:visa -$34.56
expenses:variable:groceries $34.56
But for us, we have to account for the money that has left the groceries envelope and now belongs in our credit card payment envelope. So we add to this transaction:
2023-01-15 * shoprite | groceries
liabilities:credit:visa -$34.56
expenses:variable:groceries $34.56
[assets:budget:variable:groceries] -$34.56
[assets:budget:cards:visa payment] $34.56
Cash transactions #
Honestly, this one is even more confusing. Let’s say I pitched in $20 for a baby shower gift for a coworker:
2023-02-01 * sally | gift for jane
assets:cash:petty -$20.00
expenses:gifts $20.00
In the virtual world, we actually have to return the money from the gifts envelope back into our wallet, so we can use it to pay our coworker. I still have a hard time wrapping my head around this one.
2023-02-01 * sally | gift for jane
assets:cash:petty -$20.00
expenses:gifts $20.00
[assets:giving:gifts] -$20.00
[assets:cash:petty] $20.00
Income #
I actually prefer the way I am handling income in hledger to the way it is handled in YNAB, because I have a much greater sense of my overall compensation and where the money from each paycheck is going. I tried once to track my gross pay in YNAB, but quickly felt like I was pushing the boundaries of the software and quit. In hledger, it’s super easy to track gross and net pay without making the budget itself more complicated.
Here’s a typical income transaction for me:
2023-02-15 * company A | biweekly paycheck
income:companyA:salary -$2700.00 ; my gross pay
expenses:taxes:federal $160.00
expenses:taxes:state $65.00
expenses:retirement $150.00
expenses:health insurance $150.00
assets:mybank:checking $2175.00 ; my net pay
[assets:mybank:checking] -$2175.00
[assets:budget:to-be-budgeted] $2175.00
Here, I can track that my actual income from Company A was $2700.00, but not worry about including all of that in the budget. The only part of that that goes into TBB is my net pay, which means I’m only budgeting the money in my actual account. A couple of extra postings, and now we can repeat our budgeting exercise with all the new money we have available!
Reporting #
I’ve already shared my budget overview command - it’s just hledger bal ^C
. That gives me all of my budget categories and their balances, and if I’m doing it right, nothing else (all my real cash accounts will be “virtually” empty, and won’t show up). This is basically what I see when I open YNAB’s “Money Available” focused view, with the added benefit that it shows me overspent categories as well. And hledger bal ^A ^L -R
is the sidebar view in YNAB, where I can see the real-world balances of all of my asset and liability accounts.
I have always been fairly underwhelmed with YNAB’s reporting capabilities, so it didn’t take long to be able to replicate everything I regularly use. Spending trends by month? hledger bal ^X -M
will do that. Net worth? Just run hledger bs -1
(short for balance sheet) and check the Net amount at the bottom. I’m sure I’m just barely scratching the surface of the reporting that’s possible in hledger, and I’ve already covered what YNAB can do.
What’s next? #
I really am quite happy with what I’ve built in hledger. I have always been a huge advocate of manual entry in YNAB, so having to enter transactions with hledger doesn’t bother me at all (especially since hledger add
gets smarter the more you enter transactions).
However, I am a major scheduled transaction user, and I am missing it terribly since starting this project. I have it on my list to look into the forecasting capabilities of hledger, because I think there is a way to bring in scheduled transactions with the --forecast
command.
I am also intrigued by the automatic posting functionality I keep reading about. It seems like this should offer a way for me to have hledger automatically apply my virtual posting rules to real-world transactions. For example, it looks like I should be able to do something like this:
= income:salary
expenses:tithe -*0.1
liabilities:tithe *0.1
Then, theoretically, when I get paid, this:
2023-02-15 * company A | paycheck
income:salary -$2700.00
assets:checking $2700.00
will turn into this with the --auto
command:
2023-02-15 * company A | paycheck
income:salary -$2700.00
expenses:tithe $270.00
liabilities:tithe -$270.00
assets:checking $2700.00
Again, I haven’t tested this extensively, but I think it could help me automate some of the “extra” postings I’m adding to accommodate my budget envelopes.
Conclusion #
If you’re still here, I’m impressed (and would like to know you, because clearly we have a lot in common). There may be more coming on this as I figure out how to automate and report on my hledger data, but for now I think I’ve said quite enough.
I am still paying for YNAB, by the way. I really do enjoy the software, and I like having it available when I’m frustrated by something I haven’t quite figured out how to do in hledger. I don’t know how long I will stay subscribed, but for now, I’m happy to have two fantastic personal finance tools in my toolbelt.
If this helped you, or if you have suggestions for how I can continue to hack away at this project, let me know! I would love to hear from you (contact [at] gwendo.me). Thanks for reading!