Bank Alert → Gmail → Budget
Bank alert emails become Actual Budget transactions. Every 5 minutes a Gmail label is polled, each email goes through a bank-specific parser, and transactions are posted to Actual Budget.
Bank & Gmail Prerequisites
Before running any setup commands, the email pipeline needs two things in place on the bank and Gmail side.
1. Enable transaction alerts at your bank
Most banks let you configure email alerts for every transaction — check your bank's notification or alert settings. Enable alerts for all transactions (not just large ones), and make sure they go to your Gmail address. Some banks call this "transaction notifications," "spending alerts," or "purchase alerts."
Not all banks support per-transaction email alerts. If yours doesn't, this flow won't work for that account.
2. Create a Gmail label for those alerts
Once alerts are arriving, set up a Gmail filter to label them automatically:
- Open a transaction alert email in Gmail
- Click the three-dot menu → Filter messages like these
- Use the sender address (e.g.
no.reply.alerts@chase.com) as the filter condition - Choose Apply the label → create a new label (e.g.
Chase/Transaction) - Check Also apply to matching conversations to catch existing emails
Give each bank its own distinct label — the label is how Decree knows which emails to pick up and which parse script to use.
How It Works
1. Cron trigger
volumes/workspace_data/.decree/cron/gmail-transactions-<label>.md fires the gmail_sync machine every 5 minutes. It sets GMAIL_LABEL_FILTER to your bank's alert label and GMAIL_ROUTINE to actual_budget_parse. Fields prefixed fwd_ are forwarded into each child message — this is how parse_script and account_id travel down the chain.
2. gmail_sync
Fetches any new emails from the label using Gmail's History API (incremental — only emails since the last run), and emits each one to actual_budget_parse, headers as params and body as body.
3. actual_budget_parse
Runs a bank-specific parse-<bank>.ts script against the email. The parser reads the subject line and outputs a transaction JSON object. If a transaction is found, it emits a message for actual_budget. If the email isn't a transaction alert, the message is discarded silently.
4. actual_budget
Calls post-transaction.ts using @actual-app/api to post the transaction to your Actual Budget server. Reads credentials from /secrets/actual-budget/credentials.env and the account ID from the message params.
Setup
Run these steps in order. Each builds on the previous one.
Step 1 — Gmail OAuth
If you haven't already authorized Gmail access:
./existential.sh run automation gmail-sync
This grants Decree read-only Gmail access and, as its last step, automatically saves your label
list to /secrets/gmail/labels.json (automation/secrets/gmail/labels.json on the host). See
Gmail for full instructions.
If you've already authorized Gmail but need to refresh just the label cache (e.g. you added a new label):
./existential.sh run automation gmail-labels
Step 2 — Actual Budget credentials
./existential.sh run actual-budget setup
Connect to your Actual Budget server, select a budget, and save credentials. At the end, the script prints all your accounts with their IDs and saves them to /secrets/actual-budget/accounts.json for use in the next step.
See Actual Budget for full instructions.
Step 3 — Create the cron
./existential.sh run automation gmail-transactions-cron
An interactive prompt will:
- List your custom Gmail labels — select the one your bank sends alerts to
- List available parse scripts — select the one matching your bank
- List your open Actual Budget accounts — select the one to post transactions to
- Confirm the schedule (default:
*/5 * * * *) - Write
volumes/workspace_data/.decree/cron/gmail-transactions-<label>.md
No restart needed — Decree picks up new cron files on the next tick.
Verifying
Check the cron is scheduled, then watch the next one fire:
docker exec automation decree status --cron
docker exec automation decree status
docker exec automation decree status <id> # one run's events
To test immediately without waiting for the cron, emit a message by hand:
echo | docker exec -i automation decree emit --machine actual_budget_parse \
--param subject='You made a $42.00 transaction with WHOLE FOODS' \
--param date='Mon, 21 Apr 2026 12:00:00 +0000 (UTC)' \
--param parse_script=/work/.decree/lib/actual-budget/parse-chase.ts \
--param account_id=<your-account-id>
Adding More Banks
Each bank gets its own parse script. actual_budget_parse accepts any parse_script that reads the email headers from its environment (subject,
date, from, to) and outputs JSON:
{ "amount": "-42.00", "payee": "Whole Foods", "date": "2026-04-21", "notes": "..." }
To add a new bank:
- Create
workspace/.decree/lib/actual-budget/parse-<bank>.tsfollowing the same interface asparse-chase.ts - Run
./existential.sh run automation gmail-transactions-cronagain — it will discover the new script automatically