Skip to main content

actualq

Read an Actual Budget export from the command line.

No server. No credentials. No dependencies. One file of standard-library Python.

$ actualq accounts
account       on budget  closed   balance  transactions  first       last
------------  ---------  ------  --------  ------------  ----------  ----------
Checking      yes                 4,182.03          593  2023-10-18  2026-08-18
Credit Card   yes                  -514.60          610  2023-10-24  2026-08-18
Savings       yes                12,000.00          184  2025-07-28  2026-07-31

$ actualq txns --since 2026-08 --category Grocery --json
[
  {
    "date": "2026-08-14",
    "account": "Checking",
    "payee": "Costco",
    "category": "Everyday / Grocery",
    "amount": -142.87,
    "amount_cents": -14287,
    ...
  }
]

Why

Every other way to get at Actual data programmatically — the official @actual-app/cli, actualpy, the community CLIs — needs a running actual-server and its password. That is a lot of setup to answer "what did I spend on groceries in July".

You already have the answer on disk. Actual's export (Settings → Export data) is a zip holding an ordinary SQLite database. actualq reads it.

That makes it a good fit for LLM agents in particular: one binary-free command, JSON out, nothing to authenticate, and no way to damage the budget.

Install

It is one file with no dependencies, so the simplest thing is to take it (Python 3.11+):

curl -O https://raw.githubusercontent.com/madhurdeepjain/actualq/main/actualq.py
python3 actualq.py accounts

To get it on your PATH as actualq:

uv tool install git+https://github.com/madhurdeepjain/actualq
# or: pipx install git+https://github.com/madhurdeepjain/actualq

Or run it once without installing anything at all:

uvx --from git+https://github.com/madhurdeepjain/actualq actualq accounts

Use

With no -f, it reads the newest .zip in the current directory, so the usual session is cd to wherever you export and start asking.

actualq accounts                                  # balances, ranges, row counts
actualq txns --since 2026-01 --account Checking   # transactions
actualq categories --since 2026-07 --until 2026-07
actualq payees --limit 20
actualq sql "select ..."                          # anything else
actualq schema                                    # the field guide

txns filters: --since --until --account --category --payee --search --limit --uncategorized --no-transfers --splits.

Dates take a year, a month or a day — --since 2026, --since 2026-08, --since 2026-08-22 — and --until means the last day of whatever you name.

Output is an aligned table by default, --json for an array, --csv for CSV.

What it will not do

It does not write. There is no import, no edit, no sync. The export is copied to a temp directory and opened read-only, so nothing here can reach the budget you actually use, and actualq sql "delete from ..." fails rather than working.

It does not compute budget-vs-actual. The rollover arithmetic behind Actual's budget screen is genuinely involved, and a number that is nearly right is worse than no number in a tool you would use to check yourself. The raw zero_budgets / reflect_budgets tables are there for actualq sql.

The part worth reading: actualq schema

Actual's schema has four traps, and every one of them fails quietly — you get plausible numbers that are wrong. This is most of why this tool exists.

1. Read v_transactions, never transactions. Payees and categories are indirected through mapping tables (payee_mapping.targetId, category_mapping.transferId). transactions.description holds a payee id that may have been merged away, so joining payees directly returns NULL for every payee you ever renamed or merged — and those rows read as having no payee at all. Categories do the same and read as uncategorised.

2. Splits are counted twice if you let them. A split is a parent row carrying the full amount plus children carrying the pieces. Both are real rows. Actual computes balances over is_parent = 0. On a real file:

--splits leaves     5724 rows   matches the account balances   <- the default
--splits parents    5718 rows   matches them too, one row per bank transaction
--splits all        5728 rows   over by the whole value of every split

3. Dates and amounts are integers. date is 20260822, an INTEGER, not a string. amount is cents, negative for money out. actualq gives you both amount (readable) and amount_cents (exact) so nothing has to round twice.

4. Deleted rows are still in the file. Nothing is deleted, it gets tombstone = 1, because Actual syncs by CRDT and has to remember the deletion. Query accounts, categories or payees yourself without WHERE tombstone = 0 and you report on things the user threw away.

actualq schema prints all of this, plus the tables worth knowing about, so an agent can read it before writing SQL.

Tests

python3 -m unittest discover

schema.sql is Actual's own schema, structure only, no data. The tests build a synthetic budget from it containing one of each thing that is easy to get wrong, and pin the behaviour above.

Compatibility

Written against Actual's schema as of 2026. The views it depends on (v_transactions, v_payees) are what the app itself reads, so they are the most stable surface available — but this is not a documented API, and a future migration could change it.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

actualq-0.1.0.tar.gz (15.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

actualq-0.1.0-py3-none-any.whl (11.6 kB view details)

Uploaded Python 3

File details

Details for the file actualq-0.1.0.tar.gz.

File metadata

  • Download URL: actualq-0.1.0.tar.gz
  • Upload date:
  • Size: 15.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for actualq-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4120092e973b24b0ceb72c1e8e93199a0877e09de5e91ce4d95005a65c65d2eb
MD5 57ac0ef909a9d4e7fc2f8a417b7f705a
BLAKE2b-256 95361a1ff57c88bd02362e0c64b22a985aee2587a8d1cdf60c9a53a3202fe84c

See more details on using hashes here.

Provenance

The following attestation bundles were made for actualq-0.1.0.tar.gz:

Publisher: release.yml on madhurdeepjain/actualq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file actualq-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: actualq-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 11.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for actualq-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 782afb0b86cb72cd76e625f77972351f0d7fe3a384c295af48636de875a053fd
MD5 4f989c007dfa27a67e951923d54373dd
BLAKE2b-256 179e1ad5c24d1681f466a3b41a9ef06b96b88e4e4c15eabcaa64f091c42c107d

See more details on using hashes here.

Provenance

The following attestation bundles were made for actualq-0.1.0-py3-none-any.whl:

Publisher: release.yml on madhurdeepjain/actualq

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.1.1

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page