Skip to content

Input files

Your input files will probably be CSV files with a line for each transaction, although other file types will work fine if you use a preprocess or a construct script that can read them. These scripts are explained later.

We mostly use conventions based on a predefined directory structure for your input statements.

For example, assuming you have a savings account at mybank, you’ll put your first CSV statement here: import/john/mybank/savings/1-in/2018/123456789_2018-06-30.csv.

An owner, bank, or account can itself be named import. For example, import/john/mybank/import/1-in/2018/april.csv belongs to John’s account named import at mybank. Run hledger-flow import and hledger-flow status from the ledger root to use that layout.

Some people may want to include accounts belonging to their spouse as part of the household finances: import/spouse/otherbank/checking/1-in/2018/987654321_2018-06-30.csv.

Parallel import converts one statement per core at a time, and starts another as soon as a worker finishes. Use --batch-size N to set how many run at once or --sequential to run one at a time.

All files and directories under the import directory are related to the automatic importing and classification of transactions.

The directory directly under import is meant to indicate the owner or custodian of the accounts below it. It mostly has an impact on reporting. You may want to have separate reports for import/mycompany and import/personal.

You can create an owner directory before its first statement arrives. hledger-flow report skips that owner until import creates its all-years.journal and continues generating reports for the other owners.

Below the directory for the owner we can indicate where an account is held. For a bank account you may choose to name it import/john/mybank.

If your underground bunker filled with gold has CSV statements linked to it, then you can absolutely create import/john/secret-treasure-room.

Under the directory for the financial institution, you’ll have a directory for each account at that institution, e.g. import/mycompany/bigbankinc/customer-deposits and import/mycompany/bigbankinc/expense-account.

Next you’ll create a directory named 1-in. This is to distinguish it from 2-preprocessed and 3-journal which will be auto-generated later.

Under 1-in you’ll create a directory for the year, e.g. 2018, and within that you can copy the statements for that year: import/john/mybank/savings/1-in/2018/123456789_2018-06-30.csv

Keep the filenames your bank provides. For example, April [1].csv and April 1.csv can sit in the same year directory. Import treats the brackets literally and includes each statement’s transactions once. If an earlier import duplicated one statement and omitted the other, run hledger-flow import to rebuild the journals from both files.

hledger-flow import --new-files-only compares each generated file’s modification time with its source. It rebuilds a preprocessed CSV or journal when its source is newer, even if the two timestamps differ by only a fraction of a second. Equal timestamps count as up to date. If you replace a statement and preserve its timestamp, run hledger-flow import to force a rebuild.

When an import or report fails, the command finishes writing its queued diagnostics before it exits, even if a pipe consumer reads slowly. To save the failure message for inspection, capture standard error, for example with hledger-flow import 2>import-errors.log.

1-in is the source of truth. Both 2-preprocessed and 3-journal contain generated output; user-supplied files in those stages are not supported. Use manually managed journals for transactions without an input statement.

Import writes each rules-generated journal and each changed include file to a temporary file before replacing the destination. If a write fails, the previous complete copy of that file stays in place. Correct the cause of the failure and rerun the same import. Files written successfully earlier in the run may already contain the new data.

After all selected inputs have been converted successfully, import moves orphaned 2-preprocessed/YYYY/*.csv and 3-journal/YYYY/*.journal files into the account’s .trash directory. A corresponding input must be in 1-in for the same account and year, with a filename that produces that output when its extension is replaced. An old preprocessed CSV does not keep an orphaned journal active. If two inputs would produce the same output, import stops before converting or moving files and names the conflicting inputs. If a symlink points to a source-backed journal, import keeps the link and includes the journal once, preferring the generated filename.

For example, removing or renaming 1-in/2026/march.csv retires its old outputs to:

.trash/2026/2-preprocessed/march.csv
.trash/2026/3-journal/march.journal

Each move is reported. Existing trash entries are preserved, with .1, .2, and so on appended to subsequent filenames. The trash is never imported or emptied automatically. Restore the input and run import again to regenerate its outputs, or recover handwritten transactions from a trashed journal into _manual_. Restoring just the generated file without its input will cause it to be retired again.

If you keep your base directory in Git, add .trash/ to your .gitignore so retired outputs stay out of version control.

Trash moves use an account-local .trash/.move-lock directory to prevent concurrent imports from overwriting recovery files. If an interrupted process leaves this lock behind, the error explains how to remove the empty lock directory once no import is running and retry. A failed run may have moved some outputs already; they remain recoverable in .trash.

Running hledger-flow import from anywhere below an account selects the whole account for statement conversion and orphan cleanup. For example, running it from import/john/mybank/savings/1-in/2018/archive/ selects savings across its statement years and leaves sibling accounts’ statements and generated journals alone. The same scope applies when you pass that directory to import. Use --start-year 2024 to limit conversion and cleanup to 2024 and later years, including years whose last input was removed. Supply exactly four ASCII digits or use --start-year current for the current year. An invalid value such as 24 stops the command before import begins.

Include files are rebuilt globally from source-backed journals and supported manual files, so orphaned journals outside the cleanup scope are also excluded from the ledger. Obsolete generated year and account indexes are rewritten as empty ledgers. Manual-only and empty imports succeed without dummy input files.

Only the documented stage/year layout is processed. Unrelated files such as PDFs are left untouched; hidden files and directories are ignored. Deleting an input can therefore remove its transactions from the ledger, but does not permanently delete the generated files.

The basic owner/bank/account/year structure has been used and tested fairly extensively, I don’t expect a need for it to change.

I’m open to suggestions for improvement though.