Syntax

The Centjes DSL Syntax

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 negative

  • liabilities: never strictly positive

  • equity: never strictly positive

  • expenses: never strictly negative

  • income: never strictly positive

  • other: 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.50 is 10% of an amount (5.00) that, together with the 0.50 sum to 5.50

  • ~e: The amount is an exclusive percentage of the above posting. For example: 0.50 is 10% of 5.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