Using Plugins
Plugins add behavior to Zhang without changing Zhang itself. A plugin can generate entries (recurring transactions, split expenses), check your ledger against your own rules, or serve a report page of its own. Plugins are WebAssembly (WASM) modules that Zhang runs in a sandbox while it loads your ledger.
This guide is for using plugins. To write one, see Writing Plugins. The exact syntax of the directive is in Plugin.
Enable plugins
Section titled “Enable plugins”Plugins are off by default. Turn them on in your ledger:
option "features.plugin" "true"features.plugins works too. Without the option, Zhang ignores every plugin directive and never reads the modules.
Add a plugin
Section titled “Add a plugin”Copy the plugin’s .wasm file into your ledger directory, for example into plugins/, and declare it:
option "features.plugin" "true"
plugin "plugins/large_expense.wasm" threshold: "500 CNY"- The first value is the module. Its path is relative to the ledger’s root directory, whichever file the directive is in. For a ledger stored on S3, WebDAV or GitHub, Zhang reads the module from that storage too, so keep it next to your ledger files.
- Values after the module are arguments for the plugin, and the metadata lines hold its settings (here
threshold). Which ones a plugin understands is up to its author. The plugin also receives the ledger’s options. - Some metadata keys are not settings but capabilities you grant the plugin, described below.
- Plugins run in the order they are declared. A module declared twice runs twice.
- Zhang copies each module to
.cache/plugins/in the directory it runs in, and loads it from there. When a module on the local disk changes,zhang servereloads the ledger.
What plugins do
Section titled “What plugins do”A plugin declares what it is, and can be several of these at once:
- A processor receives every entry of the ledger and returns the entries Zhang goes on with. It can add, change or remove entries, or only check them and report problems.
- A mapper does the same one entry at a time.
- A router serves pages and data under
/api/plugins/{name}, such as a custom report. See Router Plugins.
What a processor or mapper changes exists only while the ledger is loaded. Your files stay as they are.
Grant capabilities
Section titled “Grant capabilities”Outside its own memory, a plugin can do nothing that its directive does not grant:
| Metadata | Lets the plugin | Without it |
|---|---|---|
allowed_hosts |
make HTTP requests to these hosts. Repeat the key for several hosts. | no network access |
allowed_paths |
read these files and directories of the ledger. Repeat the key for several. | no file access |
timeout |
run each call for up to this long: whole seconds ("90") or a number with a unit ms, s, m or h ("500ms", "2m"), at most a day |
60 seconds |
seed |
nothing more. Any text: it changes the seed the plugin derives random values from. | the default seed |
plugin "plugins/fx-rates.wasm" allowed_hosts: "api.frankfurter.dev" timeout: "30s"
plugin "plugins/receipts.wasm" allowed_paths: "documents" allowed_paths: "statements/2024.csv"About allowed_paths:
- Each value is a file or a directory, relative to the ledger root and written with
/."."is the whole root. - Access is read-only, and limited to the ledger root, on the local disk and on remote storage alike.
- Names starting with a dot are hidden: below a granted directory, a plugin can read
.gitor.envonly if a value names them. - A value that is absolute or contains
..grants nothing. - Only processors and mappers can read files. On a local ledger,
zhang servereloads when a file a plugin read changes.
A value Zhang cannot use, such as timeout: "soon", is reported as a ParseInvalidMeta error on the plugin directive, and the plugin gets the default.
Decide which plugins to trust
Section titled “Decide which plugins to trust”Every plugin receives your whole ledger: every entry and every option. It can change what Zhang shows you, by changing entries before Zhang checks and displays them.
What the sandbox prevents:
- network access, except to the hosts in
allowed_hosts; - reading files, except those
allowed_pathsgrants inside the ledger root; - writing files, including your ledger files;
- running longer than its
timeout.
A router plugin’s pages are served from Zhang’s own address. A script on such a page can call Zhang’s API, including the endpoints that change your ledger files, with your browser’s session.
Only declare plugins you trust, ideally ones whose source you can read, and grant each one only what it needs.
See which plugins are loaded
Section titled “See which plugins are loaded”- The Settings page lists the loaded plugins, in the order they run, with their name, version and types. A router plugin has a link to its pages.
GET /api/pluginsreturns the same list as JSON, with theallowed_hostseach plugin was granted and the route of each router plugin. To reviewallowed_pathsandtimeout, read theplugindirectives of your ledger.
When a plugin goes wrong
Section titled “When a plugin goes wrong”- A problem a plugin reports appears in the ledger’s error list as a
PluginError, with the plugin’s name and message, on the entry the plugin points at or on itsplugindirective. The ledger still loads. - A plugin that fails, by returning an error, crashing or running past its timeout, stops the whole ledger from loading, and so does a module that cannot be loaded, such as a path where no module is. When Zhang starts, it exits with the error. While it is running, it keeps serving the last version that loaded and only logs the error: the web UI shows no error. A timeout message says which call ran too long; raise the plugin’s
timeoutif it needs more time.
Plugins, pads and balance assertions
Section titled “Plugins, pads and balance assertions”Zhang runs your plugins first, then checks that every account is open, then fills the pads, then checks the balance assertions. So:
- Transactions a plugin adds count in the amounts pads fill up to and in the balances assertions check. A posting a plugin adds to an account that is not open is reported like one you wrote.
- A plugin sees your
balanceandbalance … with paddirectives, but not the padding transactions, which are added after it runs. It does not seepaddirectives: abalanceapadserves is shown to it as abalance … with pad. See Writing plugins. - A plugin sees transactions as you wrote them: a missing amount is not filled in yet, and sales are not matched to lots.
- An
optionorplugindirective a plugin adds has no effect.
See Balances and Padding for how pads and assertions work.
Beancount plugins
Section titled “Beancount plugins”Beancount’s plugins are Python code, which Zhang does not run. In a beancount ledger without features.plugin, Zhang ignores lines such as plugin "beancount.plugins.auto_accounts". With features.plugin on, it tries to load them as WASM modules, and the ledger fails to load. See Coming from Beancount.
