The centjes DSL is used to describe transactions of money accross accounts.
The syntax is machine-readable, and auto-formattable.
Modules
Centjes data is housed in files with the .cent extension called modules.
Modules consist of imports and declarations.
The main centjes file is called ledger.cent and is considered the entry-point for centjes' automation.
Imports
Imports look like this:
import banks/ubs
This import will import all the declarations of the module at ./banks/ubs.cent (relative to the current module) into the current module.
Declarations
Comments
Comments in centjes look like this:
-- This is a comment
A comment is a line of its own: there are no comments at the end of a line of syntax. Consecutive comment lines are one comment. The space after the dashes is part of the marker, so --this is not a comment.
Inside a multi-line declaration such as a transaction, a comment must be indented:
2025-01-27
-- I paid in cash because the card reader was broken.
| I bought a coffee.
* assets:cash -5.00 USD
-- This is the good stuff.
* expenses:coffee +5.00 USD
A comment that starts at the first column always starts a new declaration, so indentation is what tells the two apart.
An indented comment belongs to the line below it, which is where the formatter keeps it. That means a comment cannot be the last line of a declaration, because it would belong to no line at all. This is an error, because the comment has no line below it to belong to:
2025-01-27 | I bought a coffee. * assets:cash -5.00 USD * expenses:coffee 5.00 USD -- This comment has no line below it.
Because a description is the whole rest of its line, a -- after a | is description text rather than a comment, and two description lines cannot have a comment between them.
Currency declaration
In order to account for money of a certain currency, you need to declare that currency up front. This helps prevent typos in currencies.
A currency is declared like so:
currency USD 0.01
This declaration says "There is a currency with symbol USD that has a smallest valid amount of 0.01 units.".
Account
In order to declare how money moved between accounts, we first have to declare accounts as well, just lik we have to with currencies.
account assets:bank
This declaration says "There is an account called assets:bank.".
Account types
An account must be one of the following types.
Each type has an associated assertion on the contents of the account.
assets: never strictly negativeliabilities: never strictly positiveequity: never strictly positiveexpenses: never strictly negativeincome: never strictly positiveother: no assertions
Centjes automatically guesses the type of an account using a substring search.
For example, an account name assets:bank will imply that the account type is assets.
To annotate an account with a type manually, you can add it like so:
account foo:bar assets
Account extras
Account attachments
An account can have attachments. You can use this to attach bank statements during the given tax period, for example.
account assets:bank
+ attach statement.pdf
These attachments are checked with centjes check and can be used for automation, such as in centjes-switzterland.
Account assertions
An account can have associated assertions.
Currency assertion
An account currency assertion says that the money in the given account must be of a given currency. It is declared as follows:
account assets:bank
+ assert currency USD
Tags
Accounts can be tagged:
account assets
+ tag legal
However, tags must be declared first, too.
Transaction
Transactions are declared as using a list of postings that have to balance to zero.
A transaction starts with a timestamp and description:
2025-01-27
| This is an example timestamp with no postings.
We add postings to it by describing a change to an account for each account:
2025-01-27
| I bought a coffee.
* assets:cash -5.00 USD
* expenses:coffee +5.00 USD
This example describes that I bought coffee with cash on 2025-01-27.
Virtual postings
Postings starting with * are considered "real" postings. Transactions can also contain virtual postings, which start with !.
2025-01-27
| I bought a coffee.
* assets:cash -5.00 USD
* expenses:coffee +5.00 USD
! expenses:vat +0.50 USD
Virtual postings are ignored when checking if the transaction balances.
Currency exchange
When accounting for transactions involving multiple currencies, postings need a conversion factor to balance. You can add those with an @ sign.
2025-01-27
| Exchange USD for CHF
* assets:bank -5.00 USD @ 1.25 CHF
* assets:bank +6.25 CHF
You can also describe these conversion factors with a rational fraction in case it cannot be expressed as a finite decimal number:
2025-01-27
| Exchange USD for CHF
* assets:bank -7.00 USD @ 1 / 7 CHF
* assets:bank +1.00 CHF
Lots
An asset bought at a given price can be held as a lot, with lot @. A lot is its own commodity: 2 SWDA lot @ 500 EUR never mixes with plain SWDA, nor with SWDA lot @ 480 EUR, so a balance report shows one line per price you paid.
2025-01-27
| Buy two shares
* assets:broker +2 SWDA lot @ 500 EUR
* assets:bank -1000 EUR
Like @, the rate is per unit, and the posting balances at that rate: the two shares above count as 1000 EUR.
You sell by posting a negative amount in the same lot, at the price you bought it for. That leaves the difference between what you paid and what you were paid, which a capital gains posting has to absorb:
2025-04-01
| Sell one at a gain
* assets:broker -1 SWDA lot @ 500 EUR
* assets:bank +600 EUR
* income:capital-gains -100 EUR
Capital losses go to an expenses: account rather than to income:, because a loss is a positive amount and a positive balance in an income account fails the account type assertion.
If you sell at a price you never bought at, the lot's balance goes negative in an assets account and centjes check reports it.
A lot is worth whatever the underlying commodity is worth, so declaring a price for SWDA values every lot of it. Until you declare one, a holding is valued at what it cost.
Assertions can name a lot, or leave it out to assert the total across every lot of the symbol, which is what a broker statement gives you:
2025-02-27
| Buy one more, at a different price
* assets:broker +1 SWDA lot @ 600 EUR
* assets:bank -600 EUR
+ assert assets:broker = +1 SWDA lot @ 600 EUR
+ assert assets:broker = +3 SWDA
Because lot is a keyword inside a posting, a currency cannot be called lot.
Percentage
Postings can have an annotated percentage to express that they represent a given fraction of the previous percentage.
2025-01-27
| I bought a coffee.
* assets:cash -5.50 USD
* expenses:coffee +5.50 USD
! expenses:vat +0.50 USD ~ 10%
These percentages can be calculated in different ways:
~or~i: The amount is an inclusive percentage of the above posting. For example:0.50is10%of an amount (5.00) that, together with the0.50sum to5.50~e: The amount is an exclusive percentage of the above posting. For example:0.50is10%of5.00.
2025-01-27
| I bought a coffee.
* assets:cash -5.00 USD
* expenses:coffee +5.00 USD
! expenses:vat +0.50 USD ~e 10%
Transaction extras
Transactions can be annotated with extra metadata.
Attachments
You can attach files to a transactions.
2025-01-27
| I bought a coffee.
* assets:cash -5.00 USD
* expenses:coffee +5.00 USD
+ attach receipt.pdf
These attachments are checked with centjes check and can be used for automation, such as in centjes-switzterland.
Assertions
You can attach assertions to transactions. These assertions are checked after balancing the transaction.
2025-01-27
| I bought a coffee, now I'm broke.
* assets:cash -5.00 USD
* expenses:coffee +5.00 USD
+ assert assets:cash = +0.00 USD
Assertions do not count virtual postings, so that what an assertion means does not depend on whether you are looking at the report with --virtual. To assert the balance that does include them, write assert virtual:
2025-01-27
| I bought a coffee.
* assets:cash -5.00 USD
* expenses:coffee +5.00 USD
! expenses:vat +0.50 USD
+ assert expenses:coffee = +5.00 USD
+ assert virtual expenses:coffee = +5.00 USD
+ assert virtual expenses:vat = +0.50 USD
An account declared virtual-only has no real balance at all, so a plain assertion on one is refused rather than being trivially true at zero.
Tags
Transactions can be tagged:
2025-01-27
| I bought a coffee, now I'm broke.
* assets:cash -5.00 USD
* expenses:coffee +5.00 USD
+ tag not-tax-deductable
However, tags must be declared first, too.
Tag
Tags can be declared like so:
tag tax-deductable
Price
We can declare the price of a currency expressed in another currency on a given day.
price 2023-12-30 USD 0.85133 CHF