DEV Community

Alex Kane
Alex Kane

Posted on

n8n for Accounting Firms: 5 Automations That Eliminate Manual Bookkeeping Work (Free Workflow JSON)

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"}]]}
  }
}
Enter fullscreen mode Exit fullscreen mode

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"}]]}
  }
}
Enter fullscreen mode Exit fullscreen mode

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"}]]}
  }
}
Enter fullscreen mode Exit fullscreen mode

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"}]]}
  }
}
Enter fullscreen mode Exit fullscreen mode

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"}]]}
  }
}
Enter fullscreen mode Exit fullscreen mode

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)