Recipes: finance & bookkeeping
Two pipelines that make the money admin a habit instead of a chore — chasing what you’re owed, and turning receipts into rows.
Invoice chaser
Each morning, pull overdue invoices and send yourself a tidy chase list — who owes what, oldest first —
so getting paid becomes a 30-second daily habit. Shown with Xero;
QuickBooks works the same. Auth is an oauth2 credential you connect once.
The shape:
overdue-invoices (xero, daily) → overdue-summary (code) → overdue-gate (filter) → chase-list (email)# jobs/finance/overdue.yaml
id: overdue-invoices
name: Overdue invoices
group: finance
type: xero
credential: xero
operation: getInvoices
where: 'Type=="ACCREC"&&Status=="AUTHORISED"&&AmountDue>0'
schedule:
- { kind: daily, time: '08:00' }A code transform totals what’s owed and builds a per-invoice list:
# jobs/finance/overdue-summary.yaml
id: overdue-summary
name: Summarize overdue
group: finance
type: code
script: |
const inv = (($parent.json && $parent.json.Invoices) || []);
let total = 0;
const lines = [];
for (const i of inv) {
total += Number(i.AmountDue || 0);
const who = (i.Contact && i.Contact.Name) || 'Unknown';
lines.push(who + ' — ' + (i.InvoiceNumber || '') + ': ' + i.AmountDue + ' (due ' + (i.DueDate || '?') + ')');
}
return { count: inv.length, total, list: lines.join('\n') };
schedule:
- { kind: afterParent, parent: overdue-invoices, on: succeeded }# jobs/finance/overdue-gate.yaml
id: overdue-gate
name: Anything overdue?
group: finance
type: filter
input: '{{ $parent.json.count }}'
operator: gt
value: '0'
schedule:
- { kind: afterParent, parent: overdue-summary, on: succeeded }# jobs/finance/chase-list.yaml
id: chase-list
name: Overdue chase list
group: finance
type: email
to: '{{ $vars.ownerEmail }}'
subject: '{{ $parent.json.count }} overdue invoice(s) — {{ $parent.json.total }} outstanding'
body: |
Time to chase:
{{ $parent.json.list }}
schedule:
- { kind: afterParent, parent: overdue-gate, on: succeeded, branch: pass }Chase each customer automatically. To email a reminder the moment a payment fails, use a
stripe-triggeroninvoice.payment_failed→filter→gmail/
Receipt inbox → books
Forward (or BCC) receipts to a dedicated mailbox and each one is parsed into a row in your expenses sheet — vendor, amount, date, category — bookkeeping prep that does itself.
The shape:
receipts (inbox) → receipt-extract (claude-api) → receipt-parse (code) → log-expense (sheets)An inbox trigger fires once per new message (the first poll only sets the watermark, so
it never replays old mail):
# jobs/finance/receipts.yaml
id: receipts
name: Receipts inbox
group: finance
type: inbox
credential: receipts-imap
host: imap.fastmail.com
mailbox: INBOX
pollSeconds: 120
schedule: []A claude-api step extracts the fields as strict JSON:
# jobs/finance/extract.yaml
id: receipt-extract
name: Extract receipt
group: finance
type: claude-api
credential: anthropic
maxTokens: 200
system: >
Extract the receipt's vendor, total amount (number only), ISO date (YYYY-MM-DD), and a spending
category. Reply with ONLY minified JSON: {"vendor":"","amount":"","date":"","category":""}
prompt: '{{ $parent.json.text }}'
schedule:
- { kind: afterParent, parent: receipts, on: succeeded }A code step parses that JSON text into fields for the sheet columns:
# jobs/finance/parse.yaml
id: receipt-parse
name: Parse receipt JSON
group: finance
type: code
script: |
let r;
try { r = JSON.parse($parent.text); }
catch (e) { throw new Error('Could not parse receipt JSON: ' + String($parent.text).slice(0, 200)); }
return {
vendor: r.vendor || '',
amount: r.amount || '',
date: r.date || '',
category: r.category || '',
};
schedule:
- { kind: afterParent, parent: receipt-extract, on: succeeded }# jobs/finance/log-expense.yaml
id: log-expense
name: Log expense
group: finance
type: sheets
credential: google_sa
spreadsheetId: '{{ $vars.expensesSheetId }}'
operation: appendRow
range: Expenses!A:E
values: |
{{ $parent.json.date }}
{{ $parent.json.vendor }}
{{ $parent.json.amount }}
{{ $parent.json.category }}
{{ $now.iso }}
schedule:
- { kind: afterParent, parent: receipt-parse, on: succeeded }Note: the
inboxtrigger reads plain-text bodies well; forward receipts as text (or with the key details in the body) rather than relying on PDF attachments, which v1 doesn’t parse.