Running an accounting firm means drowning in repetitive data-entry, payment chases, and month-end scrambles. n8n's self-hosted automation engine lets you wire together QuickBooks, Gmail, Google Sheets, and Slack without ever uploading client financials to a third-party SaaS you don't control.
Here are five battle-tested workflows with complete JSON you can paste and run.
1. Invoice Auto-Entry into QuickBooks/Xero
Problem: Someone emails an invoice → staff manually re-types vendor, amount, and date into the accounting system.
Workflow: Webhook (email forward or form) → Code (extract vendor/amount/date via regex) → HTTP Request (QuickBooks Online or Xero API create bill) → Slack (post confirmation to #accounts-payable)
{
"name": "Invoice Auto-Entry",
"nodes": [
{
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"parameters": {"httpMethod": "POST", "path": "invoice-intake", "responseMode": "onReceived"},
"position": [240, 300]
},
{
"name": "Extract Invoice Fields",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const body = $input.first().json.body || '';
const vendor = (body.match(/from:\s*([\w\s,\.]+)/i)||[])[1]?.trim()||'Unknown';
const amount = (body.match(/\$([\d,\.]+)/)||[])[1]?.replace(',','')||'0';
const date = (body.match(/(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/)||[])[1]||new Date().toISOString().slice(0,10);
return [{json:{vendor,amount:parseFloat(amount),date}}];"
},
"position": [460, 300]
},
{
"name": "Create Bill in QuickBooks",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/YOUR_REALM_ID/bill",
"authentication": "oAuth2",
"sendBody": true,
"bodyParameters": {
"parameters": [
{"name": "VendorRef", "value": "={"name": "{{ $json.vendor }}"}"},
{"name": "TotalAmt", "value": "={{ $json.amount }}"},
{"name": "TxnDate", "value": "={{ $json.date }}"}
]
}
},
"position": [680, 300]
},
{
"name": "Slack Confirmation",
"type": "n8n-nodes-base.slack",
"parameters": {
"channel": "#accounts-payable",
"text": "Bill logged: {{ $('Extract Invoice Fields').item.json.vendor }} — ${{ $('Extract Invoice Fields').item.json.amount }} on {{ $('Extract Invoice Fields').item.json.date }}"
},
"position": [900, 300]
}
],
"connections": {
"Webhook": {"main": [[{"node": "Extract Invoice Fields"}]]},
"Extract Invoice Fields": {"main": [[{"node": "Create Bill in QuickBooks"}]]},
"Create Bill in QuickBooks": {"main": [[{"node": "Slack Confirmation"}]]}
}
}
Self-hosted advantage: Client financial data never leaves your server — audit-safe for CPA firms.
2. Expense Report Approval Pipeline
Problem: Staff submit expenses, manager approves in email — no audit trail, easy to lose.
Workflow: Webhook (form submission) → Code (classify by amount threshold) → IF → [< $500] Gmail auto-approve + Sheets log → [≥ $500] Slack manager with Approve/Reject buttons → Gmail outcome notification
{
"name": "Expense Report Approval",
"nodes": [
{
"name": "Expense Form Webhook",
"type": "n8n-nodes-base.webhook",
"parameters": {"httpMethod": "POST", "path": "expense-submit"},
"position": [240, 300]
},
{
"name": "Check Threshold",
"type": "n8n-nodes-base.if",
"parameters": {
"conditions": {
"number": [{"value1": "={{ $json.amount }}", "operation": "smaller", "value2": 500}]
}
},
"position": [460, 300]
},
{
"name": "Auto-Approve Gmail",
"type": "n8n-nodes-base.gmail",
"parameters": {
"to": "={{ $json.employee_email }}",
"subject": "Expense Approved: ${{ $json.amount }}",
"message": "Hi {{ $json.employee_name }},\n\nYour expense of ${{ $json.amount }} ({{ $json.description }}) has been automatically approved.\n\nAccounting"
},
"position": [680, 180]
},
{
"name": "Slack Manager Review",
"type": "n8n-nodes-base.slack",
"parameters": {
"channel": "#expense-approvals",
"text": "REVIEW REQUIRED\nEmployee: {{ $json.employee_name }}\nAmount: ${{ $json.amount }}\nDescription: {{ $json.description }}\nApprove or reject in the accounting system."
},
"position": [680, 420]
}
],
"connections": {
"Expense Form Webhook": {"main": [[{"node": "Check Threshold"}]]},
"Check Threshold": {"main": [[{"node": "Auto-Approve Gmail"}], [{"node": "Slack Manager Review"}]]}
}
}
3. Month-End Close Checklist Runner
Problem: The month-end close list lives in someone's head or an outdated spreadsheet. Tasks slip.
Workflow: Cron (1st of month, 8 AM) → Google Sheets (read checklist + completion flags) → Code (filter incomplete) → Gmail (to controller: here's what's open) → Slack (post count to #accounting)
{
"name": "Month-End Close Checklist",
"nodes": [
{
"name": "1st of Month 8AM",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": {"rule": {"interval": [{"field": "cronExpression", "expression": "0 8 1 * *"}]}},
"position": [240, 300]
},
{
"name": "Read Checklist Sheet",
"type": "n8n-nodes-base.googleSheets",
"parameters": {"operation": "readRows", "sheetId": "YOUR_SHEET_ID", "range": "Checklist!A:C"},
"position": [460, 300]
},
{
"name": "Find Incomplete Tasks",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const rows = $input.all();
const incomplete = rows.filter(r => r.json.Status !== 'Done');
const list = incomplete.map(r => `- ${r.json.Task} (Owner: ${r.json.Owner})`).join('\n');
return [{json:{incomplete_count: incomplete.length, task_list: list}}];"
},
"position": [680, 300]
},
{
"name": "Email Controller",
"type": "n8n-nodes-base.gmail",
"parameters": {
"to": "controller@yourfirm.com",
"subject": "Month-End Close: {{ $json.incomplete_count }} tasks open",
"message": "Open close tasks as of today:\n\n{{ $json.task_list }}\n\nPlease action and mark Done in the checklist sheet."
},
"position": [900, 300]
}
],
"connections": {
"1st of Month 8AM": {"main": [[{"node": "Read Checklist Sheet"}]]},
"Read Checklist Sheet": {"main": [[{"node": "Find Incomplete Tasks"}]]},
"Find Incomplete Tasks": {"main": [[{"node": "Email Controller"}]]}
}
}
4. Late Payment Reminder Escalation
Problem: Chasing overdue invoices by hand wastes a partner's time. Tone needs to match how overdue the bill is.
Workflow: Cron (daily 9 AM) → Google Sheets (open AR ledger) → Code (calculate days overdue, assign tone: gentle/firm/urgent) → Gmail (personalized per client)
{
"name": "Late Payment Reminder Escalation",
"nodes": [
{
"name": "Daily 9AM",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": {"rule": {"interval": [{"field": "cronExpression", "expression": "0 9 * * *"}]}},
"position": [240, 300]
},
{
"name": "Read AR Ledger",
"type": "n8n-nodes-base.googleSheets",
"parameters": {"operation": "readRows", "sheetId": "YOUR_AR_SHEET_ID", "range": "OpenInvoices!A:E"},
"position": [460, 300]
},
{
"name": "Classify Overdue Tier",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const today = new Date();
return $input.all().map(item => {
const due = new Date(item.json.DueDate);
const days = Math.floor((today - due) / 86400000);
let tone = null;
if (days >= 60) tone = 'urgent';
else if (days >= 45) tone = 'firm';
else if (days >= 30) tone = 'gentle';
return tone ? {json: {...item.json, days_overdue: days, tone}} : null;
}).filter(Boolean);"
},
"position": [680, 300]
},
{
"name": "Send Reminder Email",
"type": "n8n-nodes-base.gmail",
"parameters": {
"to": "={{ $json.ClientEmail }}",
"subject": "={{ $json.tone === 'urgent' ? 'URGENT: Invoice ' + $json.InvoiceNum + ' 60+ Days Overdue' : $json.tone === 'firm' ? 'Second Notice: Invoice ' + $json.InvoiceNum + ' Past Due' : 'Friendly Reminder: Invoice ' + $json.InvoiceNum }}",
"message": "Dear {{ $json.ClientName }},\n\nThis is a {{ $json.tone }} reminder that invoice {{ $json.InvoiceNum }} for ${{ $json.Amount }} is {{ $json.days_overdue }} days overdue.\n\nPlease arrange payment at your earliest convenience.\n\n{{ $json.tone === 'urgent' ? 'If payment is not received within 5 business days, we may refer this to collections.' : '' }}\n\nThank you,\nAccounting Team"
},
"position": [900, 300]
}
],
"connections": {
"Daily 9AM": {"main": [[{"node": "Read AR Ledger"}]]},
"Read AR Ledger": {"main": [[{"node": "Classify Overdue Tier"}]]},
"Classify Overdue Tier": {"main": [[{"node": "Send Reminder Email"}]]}
}
}
Result: 30-day gentle, 45-day firm, 60-day urgent — automatically, every morning.
5. Tax Document Collection Tracker
Problem: Every tax season, staff manually chase clients for W-9s, bank statements, and receipts.
Workflow: Cron (daily 8 AM, Jan–Apr) → Google Sheets (client doc checklist) → Code (identify missing documents per client) → Gmail (one email per client listing exactly what's missing) → Slack (weekly summary to #tax-team)
{
"name": "Tax Document Collection Tracker",
"nodes": [
{
"name": "Daily 8AM Jan-Apr",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": {"rule": {"interval": [{"field": "cronExpression", "expression": "0 8 * 1-4 *"}]}},
"position": [240, 300]
},
{
"name": "Read Doc Checklist",
"type": "n8n-nodes-base.googleSheets",
"parameters": {"operation": "readRows", "sheetId": "YOUR_TAX_SHEET_ID", "range": "Clients!A:H"},
"position": [460, 300]
},
{
"name": "Find Missing Docs Per Client",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const docFields = ['W9', 'Bank_Statements', 'Receipts', 'Prior_Return', 'Payroll'];
return $input.all().map(item => {
const missing = docFields.filter(f => item.json[f] !== 'Received');
return missing.length > 0 ? {json: {...item.json, missing_docs: missing.join(', '), missing_count: missing.length}} : null;
}).filter(Boolean);"
},
"position": [680, 300]
},
{
"name": "Email Client for Missing Docs",
"type": "n8n-nodes-base.gmail",
"parameters": {
"to": "={{ $json.ClientEmail }}",
"subject": "Action Required: Missing Documents for Your Tax Return",
"message": "Hi {{ $json.ClientName }},\n\nWe're still waiting on the following documents to complete your {{ new Date().getFullYear() }} tax return:\n\n• {{ $json.missing_docs.split(', ').join('\n• ') }}\n\nPlease upload these to the client portal or reply to this email at your earliest convenience.\n\nThank you,\nTax Team"
},
"position": [900, 300]
}
],
"connections": {
"Daily 8AM Jan-Apr": {"main": [[{"node": "Read Doc Checklist"}]]},
"Read Doc Checklist": {"main": [[{"node": "Find Missing Docs Per Client"}]]},
"Find Missing Docs Per Client": {"main": [[{"node": "Email Client for Missing Docs"}]]}
}
}
Getting the Templates
All five workflows are available as ready-to-import JSON files in the FlowKit Accounting Automation Pack at stripeai.gumroad.com. Each template includes installation notes and the matching Google Sheets structure.
Self-hosting matters for accounting firms. Client financials, bank data, and AR records should never transit a SaaS you don't control. n8n Community Edition is free, runs on a $6/month VPS, and keeps every workflow execution on your server.
Questions about adapting any of these? Drop them in the comments — happy to help.
Top comments (0)