thomaszdxsn12 downloadsEdit Beancount ledgers with completion, alignment, outline, and bean-check validation.
Edit Beancount ledgers in Obsidian: syntax
highlighting, account/payee completion, directive snippets, posting auto-indent,
decimal-point alignment, ledger formatting, date insert, outline, account
balances, and bean-check validation on save.
Desktop only — validation shells out to the bean-check executable.
Plugin id: beancount. Source: apps/obsidian-plugin; manifest.json and
versions.json live at the repository root.
Obsidian desktop 1.7.2 or later (minAppVersion).
For save-time validation: Beancount's bean-check:
pip install beancount
Then either leave Bean-check executable empty (uses bean-check on
PATH) or set beanCheckPath to the binary. Only a program named
bean-check is accepted.
Ledger snippets in notes use a beancount or bean info-string (backticks
or tildes; an extra suffix like linenums is ignored):
```beancount
2026-10-04 * "Coffee"
Expenses:Food 12.00 CNY
Assets:Cash
```
Saving a markdown note copies those fence bodies into a temporary .bean
file, runs bean-check, and maps errors back onto the fence lines.
Configured in Settings → Beancount. Keys match data.json.
| Setting | Key | Default | Description |
|---|---|---|---|
| Align amounts on save | alignOnSave |
off | Re-align posting amounts whenever a markdown or beancount file is saved (debounced). |
| Instant alignment | instantAlignment |
on | Typing . in a posting amount aligns that transaction block and leaves the caret after the point. |
| Separator column | separatorColumn |
50 |
1-based display column of the decimal point for instant alignment (wide accounts still push past it). |
| Show separator ruler | showRuler |
on | Dotted vertical line at the separator column in .bean / .beancount files. |
| Bean-check executable | beanCheckPath |
empty | Path to bean-check; empty runs bean-check from PATH. |
| Entry ledger | entryLedger |
empty | Vault path of the ledger entry file (e.g. main.bean). |
| Complete payees | completePayee |
on | Suggest historical payees in the first quoted field. |
| Autofill payee postings | payeeAutofill |
on | Picking a payee on an entry with no postings yet inserts the postings of that payee's latest transaction, amounts as Tab stops. |
| Complete narrations | completeNarration |
off | Suggest vault narrations in the second quoted field. |
| Pinyin initials matching | pinyinMatching |
off | Also match completions by pinyin initials (cy → 餐饮, Expenses:cy → Expenses:餐饮). |
| Fava executable | favaPath |
empty | Path to fava; empty runs fava from PATH. Only a program named fava is accepted. |
| Fava port | favaPort |
5000 |
TCP port Fava binds on 127.0.0.1 (-p). |
| Run Fava on activate | runFavaOnActivate |
off | Start Fava against the entry ledger (or the active ledger file) when the plugin loads. |
| Incomplete transactions (!) | flagWarnings["!"] |
warning | Marker style for ! transactions (none / warning / error). |
| Cleared transactions (*) | flagWarnings["*"] |
none | Marker style for * / txn transactions. |
entryLedger behaviour
.bean / .beancount file checks that entry's whole include
chain.```beancount / ```bean fences checks
those fences: with an entry ledger they are validated as if included after
it (opens and accounts apply); without one, the fences are checked on their
own.A missing or rejected bean-check shows:
bean-check not found — install beancount (
pip install beancount) or set the bean-check path in the plugin settings.
| Command | Notes |
|---|---|
| Align decimal points | Current transaction block, or the selection when there is one. No default hotkey; bind one under Settings → Hotkeys. |
| Format ledger | Whole .bean / .beancount file, or each beancount / bean fence in any other buffer. A syntax error names the line and changes nothing. No default hotkey. |
| Insert today's date | YYYY-MM-DD at the cursor. No default hotkey; bind one under Settings → Hotkeys. |
| Show outline | Sidebar of * sections, date groups, transactions, open / close / balance. |
| Show problems | Sidebar of the latest bean-check report, grouped by file — including include-chain files that are not open and <load> errors. Clicking a vault row jumps to the line. |
| Show account balances | Sidebar table of per-account unit balances, one line per currency. Uses the entry ledger when set, otherwise the active ledger or the active note's fences, and follows include. |
| Start Fava | Starts Fava (-H 127.0.0.1 -p <favaPort>) on the entry ledger or the active .bean file, then opens the UI. Repeating the command reuses the live process. |
| Stop Fava | Kills the Fava process started by this plugin. |
beancount/bean fenced code blocks get
highlighting through a CodeMirror stream mode ported from
beancount.tmLanguage. Inside those fences, Cmd+/ toggles ; line
comments (Obsidian's own command would wrap %% and break the ledger),
{ [ ( and ' auto-close (" does not — narration uses it), and
;#region / ;#endregion fold. Matching-bracket highlight is not
available: Obsidian paints the fence with a CM5 overlay, so the CM6 tree
has no inner bracket nodes..bean / .beancount files open in the markdown editor
but are painted whole with the same beancount mode, in the monospace font,
with markdown presentation reset (headings, tag pills, emphasis, links, list
hanging indent; the org * shows as text). Live Preview widgets that
replace text (embeds, checkboxes) can still render; Source mode avoids them.
A dotted ruler marks the separator column (showRuler).include "…" (ledger files
and fences) to open it; paths resolve relative to the including file, a
glob opens its first match, a missing target raises a Notice.Assets:Ca…) in the
editor suggests still-open account names found in the vault's ledger text:
whole .bean / .beancount files, and only the beancount / bean fences
of markdown notes (prose such as PG_DATA_DIR:-x never becomes a name).
Names are extracted per file with a regex, cached, and invalidated when files are
created, modified, deleted or renamed. Segments after the capitalized root
may hold non-ASCII letters and CJK middle dots (Expenses:餐饮:午饭,
Expenses:カード・ローン); other punctuation and symbols (,, :, ☕) end a
name, so CJK prose does not index whole clauses.
Suggestions rank prefix matches first, then case-insensitive subsequence
matches; within a rank, names picked more often / more recently float to
the top. Pick counts persist in the plugin data.json (completionUsage);
a corrupt bag is ignored so the plugin still starts.
An account with a close directive
is omitted unless a later open reopens it. The popup shows the latest
open date and constrained currencies when those directives are present.
Balances are not computed here: doing it in JS would reimplement
beancount's booking, and a Python subprocess would add latency on every
index refresh.2026-09-30 * "Am…) suggests every payee found in the vault — that field
of every historical transaction. Ranking matches account completion
(prefix, then subsequence, then frecency). One vault scan feeds both
completion indexes. Picking a payee closes an unclosed payee field. With
payeeAutofill on, picking a payee on an entry
that has no postings yet inserts the postings of that payee's latest
transaction; a two-leg entry keeps one amount and leaves the other leg
implicit, and each amount is a Tab stop with the first one selected.pinyinMatching on, an ASCII query also matches
the pinyin initials of CJK names in account, payee and narration completion
(Expenses:cy → Expenses:餐饮), ranked direct prefix, pinyin prefix,
direct subsequence, pinyin subsequence. Initials data comes from
vscode-beancount (MIT).≠ 0: … on its header. Cost,
price and arithmetic suppress both.txn, open,
balance, …) offers the vscode-beancount templates. txn expands to
YYYY-MM-DD * "" "" with the caret in the payee quotes, so payee completion
still runs. Tab walks $1-style stops; today's date fills $CURRENT_*.sign + integer blocks right-align at it (integers at the
units place) and cost/price annotations stay put. A line inside a multi-line
string is not a posting, even if it looks like one. An optional setting walks
every block of each markdown/beancount file the same way whenever it is
saved. Instant alignment (on by default) intercepts . in a posting amount,
aligns that transaction block onto the separator column (default 50), and
leaves the caret just after the point — one undo restores the insert.
Alignment pads with spaces, so .bean / .beancount editors switch to the
monospace font (--font-monospace); fences are already code blocks..bean /
.beancount file, or each beancount / bean fence in any other buffer
(a markdown note, an untitled file), in one undo step. Postings and directive
metadata use the two-space indent; posting metadata is indented one level
further. Header lines collapse runs of spaces, strings and ; comments keep
their text (a " in a comment does not start a string), and each
transaction's decimal points are aligned. A syntax error raises a Notice
with the document line and changes nothing.YYYY-MM-DD, the beancount date shape — at the cursor, replacing the
selection when there is one. It has no default hotkey; bind one under
Settings → Hotkeys..bean or .beancount file runs
bean-check (debounced) and marks every line it complains about — a zigzag
underline whose tooltip is the message, plus a dot in the gutter. The
message is parsed from stderr, which is the whole report: the exit code is
not consulted. Markdown notes with beancount / bean fences are
checked the same way: fence bodies are copied to a temp .bean file and
error lines are mapped back onto the fence. Settings hold the executable
path (empty uses bean-check from PATH; a missing one prompts to
pip install beancount; only a bean-check binary is accepted) and an
optional entry ledger. A .bean save checks that entry's whole include
chain; a markdown save includes the entry first so the fence is checked
against its opens and accounts. Without an entry ledger each saved file
(or note's fences) is validated on its own. Unloading the plugin cancels
pending timers and drops in-flight bean-check results: they do not mark
the editor or raise a Notice. The process is not killed; only the report
is discarded. Opening a ledger file, or a note with beancount fences,
validates it too, unless its target has not changed since the last report.* section titles nest the same way vscode-beancount's
DocumentSymbolProvider does (including _ fillers for skipped levels).
Dated transactions, open, close and balance directives hang under
consecutive date groups; clicking a row jumps the editor to that line..bean / .beancount file, or the beancount / bean fences of
the active note. include is followed, including ** globs (zero or more
directories, hidden names skipped unless the pattern asks for them). A
posting that omits its amount is filled from the other postings' weights
(cost before price; {per # total} is units × per + sign(units) × total;
{{ }} and @@ use the total) and quantized like beancount 2.3.5
(inferred_tolerance_default, inferred_tolerance_multiplier, and
infer_tolerance_from_cost). pad plus a later balance on that account
or a child inserts the start-of-day difference when it exceeds the balance
tolerance (explicit ~ tol, or inferred from the balance number's
exponent); a difference inside the tolerance still consumes that currency.
Only the first balance of each currency consumes a pad. If a file fails to
parse, or a transaction cannot be interpolated, the view shows the error
instead of numbers. Sums use the literal spelling when the parser kept it,
so 10.00 and amounts past 2^53 stay exact. Each amount renders at the
sum's own scale.Install Beancount from Settings → Community plugins, or copy
apps/obsidian-plugin/main.js, the root manifest.json, and
apps/obsidian-plugin/styles.css (all three are attached to each GitHub
release) into:
<Vault>/.obsidian/plugins/beancount/
Enable Beancount under Settings → Community plugins.
pnpm + Turborepo. Node >=22.12.0.
pnpm install
pnpm build # type-check + production bundle → apps/obsidian-plugin/main.js
pnpm test
pnpm dev # watch mode
Set ESBUILD_OUTFILE to write the bundle into a test vault's
.obsidian/plugins/beancount/main.js; copy the root manifest.json and
apps/obsidian-plugin/styles.css next to it.
From apps/obsidian-plugin, npm version <x.y.z> bumps package.json and
writes the version into the root manifest.json / versions.json. Push a tag
equal to that version (no v prefix); .github/workflows/release.yml builds,
tests, and drafts a GitHub release with main.js, manifest.json, and
styles.css. Publish the draft to ship it.
See CHANGELOG.md for release notes.