Business Splits
The business split engine automatically allocates a percentage of personal expenses to your business for ATO deduction purposes. This is configured in config/tax.yaml and computed by etl/splitter.py.
How it works
There are two allocation mechanisms:
1. Implicit 100% — Business categories
Any transaction categorised with a name starting with Business: is automatically allocated at 100% to your business:
Business: Hosting & Infrastructure— hosting, domains, DNSBusiness: Software & Subscriptions— SaaS tools, dev subscriptionsBusiness: Equipment— cameras, computers, hardwareBusiness: Advertising & MarketingBusiness: Other
No config needed for these — the split engine detects the Business: prefix.
2. Partial allocation — Split rules
Personal expenses that are partially business-use require explicit split rules in config/tax.yaml:
businesses: - name: "My Business" abn: "00000000000" split_rules: - category: Utilities tag: internet business_pct: 50 # 50% of internet bill - category: Utilities tag: mobile business_pct: 20 # 20% of mobile billA split rule matches when both conditions are true:
- The transaction’s category matches
category - The transaction has a tag matching
tag
For example, a Vodafone bill categorised as “Utilities” with tag “mobile” would have 20% allocated as a business expense.
The split pipeline
At ingest time
If tax_config is passed to the normalizer, splits are computed during ingestion:
- Transaction is categorised and tagged
apply_splits()checks if category starts withBusiness:(100% allocation) or matches a split rule- A row is written to
transaction_splitswith:transaction_id,business_name,business_pct,business_amount
Backfill
More commonly, splits are computed after ingestion using the CLI:
ledger split --backfill --fy 2025This:
- Clears all existing splits for the given FY
- Iterates every non-transfer transaction in the FY date range
- Loads each transaction’s category and tags
- Computes splits using the rules from
config/tax.yaml - Writes results to
transaction_splits
Manual override
Individual splits can be overridden via the API:
curl -X PATCH http://localhost:5050/api/transactions/42/split \ -H "Content-Type: application/json" \ -d '{"business_pct": 75, "business_name": "My Business"}'Set business_pct to 0 to remove a split.
Database schema
CREATE TABLE transaction_splits ( id INTEGER PRIMARY KEY, transaction_id INTEGER NOT NULL REFERENCES transactions(id), business_name TEXT NOT NULL, business_pct REAL NOT NULL, business_amount REAL NOT NULL, UNIQUE(transaction_id, business_name));A transaction can have splits for multiple businesses (the unique constraint is per business name).
Viewing splits
Dashboard
The Spreadsheet > Outgoing tab shows all expenses with business split columns: percentage, amount, and business name.
API
GET /api/spreadsheet/outgoing?fy=2025 returns each expense transaction with a splits array and convenience fields biz_pct, biz_amount, biz_name.
ATO return
GET /api/ato/return?fy=2025 aggregates splits per business and includes them in the business schedule section with total income, expenses, depreciation, and net profit/loss.
CLI
ledger tax --fy 2025Prints business expenses from splits, depreciation items, and manual entries.
Tags are essential
Split rules depend on tags to distinguish sub-types within a category. Make sure your config/categories.yaml has tag rules for the relevant patterns:
tag_rules: - tag: internet pattern: "TPG|SUPERLOOP" - tag: mobile pattern: "OPTUS|VODAFONE"Without the correct tags, split rules will not match and the expense will not be allocated.
Example workflow
-
Add a split rule to
config/tax.yaml:split_rules:- category: Utilitiestag: internetbusiness_pct: 50 -
Ensure a tag rule exists in
config/categories.yaml:tag_rules:- tag: internetpattern: "TPG|SUPERLOOP" -
Ingest statements:
ledger ingest -
Backfill splits:
ledger split --backfill --fy 2025 -
View results:
ledger tax --fy 2025or check the dashboard Tax tab