<div class=pagetoc>
<!-- toc -->
</div>
For more workflow docs, see [hledger.org > Workflows](https://hledger.org/workflows.html#more-advanced-workflows).
I'm keeping this one here for easier editing; more updates may come.
Last updated: 2025-07-22
I am in my 20th year of keeping [PTA](https://plaintextaccounting.org) data.
Do my files and process look complicated ? I hope not too much.
In essence it's pretty simple, I have:
- a bunch of .journal files (organised by year and type of content)
- a bunch of .rules files (for importing new transactions from downloaded CSV)
- and some handy scripts in Justfile, and bash aliases in bin/bashrc.
As usual I am using a journal first setup, which means the journal files are primary and version-controlled, CSV files are secondary and keeping them is optional.
This year I am more being more organised about naming CSV and rules files and keeping a few copies of old CSV files for troubleshooting.
I am also using more journal sub files and being more uniform and systematic to reduce friction.
## Tools
- `hledger` 1.41+
- `hledger-ui` used occasionally, `hledger-web` rarely
- `just` for organising task scripts
- `git`, `magit` and/or `jujutsu` for version control
- `emacs`, `ledger-mode`, `flycheck-hledger` for editing
- VS Code for window/context management (with a text emacs in an editor-area terminal tab), and occasionally for editing/search/replace
- `rg` for powerful grepping
- GNU `sed` for powerful multi-file replace
## Finance directory
My hledger files live under the `~/finance` directory.
This directory is also a git repository, which helps preserve all file history and makes it easy to inspect or roll back any change.
Currently I commit all of the files shown below, except for the .csv files.
I use magit and sometimes jujutsu as better UIs for git.
In the past I have used a git pre-commit hook to check my files for errors before I can commit.
## Journal files
My journal files begin in 2006:
```
2006
`-- 2006.journal
2007
`-- 2007.journal
...
2025
|-- 2025.journal main file, transactions
|-- PEUR25.journal prices for EUR
|-- PGBP25.journal prices for GBP
|-- PJPY25.journal prices for JPY
|-- PVCEB25.journal prices for VCEB
|-- accts25.journal account declarations
|-- comms25.journal commodity declarations
|-- forecast25.journal future transactions
|-- iaccts25.journal investment account declarations
|-- inv25.journal investment transactions
|-- payees25.journal payee declarations (unused)
`-- tags25.journal tag declarations
all.journal
```
In the beginning I had just one file per year and kept them all in the main directory.
But as you can see in 2025 I have many files, so now each year gets its own directory to keep things organised.
The year directories are useful for organising other things too, like tax documents.
The main journal file in each year is `YYYY.journal`.
There's an `all.journal` file at top level, which [includes](https://hledger.org/dev/hledger.html#include-directive) all the `YYYY.journal`s, making all-year reports easy.
It also contains account aliases which help normalise my account names over time.
## Journal file organising principles
These are the ideas leading to the 2025 file arrangement above.
Some may not apply for you, or may not make sense yet, especially if you have less data to manage.
- **Simpler is better.**\
Fragmentation adds friction. Complexity is the enemy in PTA. Less is more.
- **Separation of concerns.**\
But separate files, each containing one kind of thing, when practical, can make navigation and maintenance easier.
- **Livestock not pets.**\
Files containing uniform machine-reproducible data are easier to maintain and evolve than files containing unique hand-crafted text. For efficiency, prefer the former style.
(Keeping in mind what `hledger print`, `hledger accounts` etc can preserve and reproduce.)
Some of my files contain "pet" data at the top and "livestock" data below, separated by a marker comment.
- **Partition by time when you need to.**\
Data will sooner or later need to be partitioned by time period, to keep performance high, avoid clutter, allow evolution, and help with data integrity. Yearly is usually the most natural frequency, so I partition by year.
- **Composable periods.**\
Each time slice (each year of data) should be usable by itself, yet ideally also combinable with others. I currently use the [clopen method](https://hledger.org/dev/hledger.html#close) for this, closing AL balances at the end of each year and reopening them at the start of the following year,
so that I can freely work with any, all, or some of the year files. `years` and `eachyear` scripts help with the latter.
- **Partition by topic only if worthwhile.**\
Accounts and transactions sometimes fall into topical groups which are relatively independent, with few transactions connecting them, and which we tend to work on at different times. In such cases it may be worthwhile to separate these, for a net reduction in cognitive load. For me, it felt right to separate out investment-related transactions, so I have:
- general transactions (YYYY.journal, acctsYY.journal, PEUR25.journal..)
- investment transactions (invYY.journal, iacctsYY.journal, PVCEB25.journal..)
Transactions crossing the boundary between groups (hopefully few) are recorded in both groups, using equity:transfer as a third account. While not strictly needed, this keeps each group balanced and self-contained.
- **Standalone subfiles.**\
As with time-based subsets, these topical subfiles should ideally be usable individually, not only when combined. So eg if you are focussed on just the inv25.journal, then reports, `hledger check --strict`, flycheck, etc. should be usable in the usual way. To enable this, invYY.journal includes its own investment accounts and prices files, declares its own investment commodities, and re-declares some general commodities, and the above-mentioned transfer account is used at the boundary.
- **Ergonomic filenames.**\
Recognisable, easily typed file names become increasingly important. So:
- File names are unique, for clarity when you have multiple years' files open in an editor.
- Within each year, file names are unique in their first 1-2 letters, for easy tab-completion and minimal typing.
- Price file names begin with P, to group them together and to help keep prefixes unique.
- Consistency across years is helpful, so I am gradually reorganising and renaming previous years' files to be more like the 2025 style.
## Main journal
I keep the `YYYY.journal` file minimal so that I can clean it up easily.
It contains a block of includes, followed by transactions in date order, as printed by `hledger print` and then aligned by Emacs ledger-mode.
```journal
include accts25.journal
include comms25.journal
include payees25.journal
include tags25.journal
include PEUR25.journal
include PGBP25.journal
include PJPY25.journal
include forecast25.journal
2025-01-01 * opening balances ; clopen:2025
...
```
I avoid other directives or inter-transaction comment lines, which `print` would not preserve.
If I need a comment like that, I give it a date, making a postingless "transaction" that will be preserved and shown in print reports.
## Forecast journal
`forecastYY.journal` is a place to note expected future transactions, one-time or recurring.
These can be helpful for forecasting and planning, even if they're just estimates.
Because I don't want to see them (and have them affect my report dates) all the time, I use (mostly non-recurring) [periodic transaction rules](https://hledger.org/dev/hledger.html#periodic-transactions) instead of ordinary transaction entries.
That means these transactions will appear in reports only when I add the [`--forecast` option](https://hledger.org/dev/hledger.html#forecasting).
Another way would be to use ordinary transactions, and instead of `include`-ing the forecast journal, add it on the command line when needed, like\
`hledger -f $LEDGER_FILE -f 2025/forecast25.journal print -b today ...`
## CSV rules
In the rules subdirectory I keep CSV conversion rules for the institutions I import transactions from.
```
rules
|-- boi-ichecking.csv.rules
|-- common.rules
|-- paypal.csv.rules
|-- unify-checking.csv.rules
|-- unify-savings.csv.rules
|-- unify.rules
|-- vanguard.csv.rules
|-- wf-bchecking.csv.rules
|-- wf-bsavings.csv.rules
|-- wf-common.rules
|-- wf-pchecking.csv.rules
|-- wf-psavings.csv.rules
```
## Importing CSV data
I would like to automate more, but currently it looks like this:
- Go to bank website(s), most active first, and manually download recent csv for each account.
I don't need to care much about which dates I download; the `import` command will ignore overlaps. I just make sure to download enough data so there's no gaps.
- `just csv` gather new csv files from ~/Downloads, and from paypal API, and archive them as `rules/data/BANKNAME.DATE.csv`.
You don't need to archive CSV files, but I find it useful sometimes for troubleshooting or rerunning CSV rules.
If you save complete CSV histories in here you could regenerate your journals from them, in principle.
- `just csv-import-dry-unknown` do a dry-run import of all latest csv files, and show anything not categorised by rules.
- Adjust rules in rules/* to categorise unknowns, where it seems worthwhile.
- `just csv-import` import the new transactions to the main journal.
- In emacs with ledger-mode: select the new transactions and press `M-q` to align the amounts (at decimal mark).
- Review each new transaction; apply manual fixups where needed (sometimes copying old entries, sometimes tab-completing account names);
update rules if needed; then press `C-c C-e` to mark the entry as "final" (`*`).
- Press `C-c ! l` to open a flycheck-hledger window, and fix any `check` errors being shown there, one at a time.
- If the final recentassertions check is failing, `just assertw` to write a balances assertions entry, commented.
- Manually uncomment the required account balance assertions until the check passes, and remove the rest.
- Review changes in magit, and commit ('accts', 'rules', 'txns', 'scripts').
In `Justfile` the recipes look something like:
```just
hledger := 'hledger -n'
DOWNLOADDIR := '/Users/simon/Downloads'
DATADIR := 'rules/data'
IMPORTFILES := '\
rules/wf-bchecking.csv.rules \
rules/wf-pchecking.csv.rules \
rules/wf-bsavings.csv.rules \
rules/wf-psavings.csv.rules \
rules/boi-ichecking.csv.rules \
rules/paypal.csv.rules \
rules/fidelity.csv.rules \
rules/vanguard.csv.rules \
'
# these csv gathering scripts
# remove the downloaded file from DOWNLOADDIR
# archive both original and .clean version if any
# make sure the .clean version has the newest timestamp
#
# Gather main downloaded CSVs to $DATADIR (also download any that can be auto-downloaded).
csv:
#!/usr/bin/env osh
set -euo pipefail
# Save new downloaded data files as clean/dated files for hledger import / archiving.
# In future this will be done by hledger, which knows the source file patterns.
# latest FILEGLOB - list the latest-modified matched file.
# End the glob with *.EXT to accommodate version numbers added by browsers.
latest() { ls -t $1 2>/dev/null | head -1; }
for f in $(latest $DOWNLOADDIR/Checking1*.csv); do
t="$DATADIR/wf-$(basename $f .csv).$(date -I).csv"
echo "saving $f as $t"; mv $f $t
done
for f in $(latest $DOWNLOADDIR/Checking2*.csv); do
t="$DATADIR/wf-$(basename $f .csv).$(date -I).csv"
echo "saving $f as $t"; mv $f $t
done
...
# boi. file pattern also matches coinbase files above, name them differently
for f in $(latest $DOWNLOADDIR/????????-????-????-????-????????????*.csv); do
t="$DATADIR/boi-$(basename $f .csv).$(date -I).csv"
echo "saving $f as $t"; mv $f $t
done
# keep 13-field records, remove leading space, ensure two decimal places
for f in $(latest $DOWNLOADDIR/History_for_Account_Z????????*.csv); do
t="$DATADIR/fi-$(basename $f .csv).$(date -I).csv"
c="$DATADIR/fi-$(basename $f .csv).clean.$(date -I).csv"
echo "saving $f as $t"; cp $f $t
echo "saving $f as $c"; grep -E '^([^,]*,){12}[^,]*