Monthly expenses by category, emailed to your accountant

On the 1st of every month, total last month's expenses by category and email the summary to your accountant.

Easy Saves ~2 hours/month Finance & AdminBy Flowngn team

Works with n8n · Make · Zapier

How it works

Trigger: Schedule — the 1st of every month at 9:00
  1. Read the Expenses sheet (date, amount, category).
  2. Total last month by category, with the percentage of each.
  3. Email the summary to your accountant.

Diagram

1st of the month 9:…Schedule TriggerGRead expensesGoogle Sheets{ }Total by categoryCodeGEmail accountantGmail
Trigger App Code

Code

JavaScript used in the Code step — already included in the template below.

Total by category · Code node
// Expenses sheet columns: date (YYYY-MM-DD) | amount | category | description
// Totals last month's expenses by category for the accountant email.
const now = new Date();
const first = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const month = `${first.getFullYear()}-${String(first.getMonth() + 1).padStart(2, "0")}`;
const label = first.toLocaleString("en-US", { month: "long", year: "numeric" });

const totals = {};
let total = 0;
for (const { json: e } of $input.all()) {
  if (String(e.date).slice(0, 7) !== month) continue;
  const amount = Number(String(e.amount).replace(/[^0-9.-]/g, "")) || 0;
  const cat = e.category || "Other";
  totals[cat] = (totals[cat] || 0) + amount;
  total += amount;
}

const fmt = (n) => n.toLocaleString("en-US", { maximumFractionDigits: 2 });
const lines = Object.entries(totals)
  .sort((a, b) => b[1] - a[1])
  .map(([cat, sum]) => `${cat}: ${fmt(sum)} (${Math.round((sum / total) * 100)}%)`);

return [{
  json: {
    subject: `Expenses for ${label}: ${fmt(total)}`,
    body:
      `Hi,\n\nHere are our expenses for ${label} by category:\n\n${lines.join("\n") || "No expenses recorded."}\n\n` +
      `Total: ${fmt(total)}\n\nThe full list is in the Expenses sheet.\n\nThanks`,
  },
}];

Build it

Pick your tool — same automation, step by step.

Copy and paste straight into an n8n canvas (Ctrl+V), or download and use Import from File. Then connect your own credentials and replace every YOUR_… placeholder.

Workflow JSON
Show code (139 lines)
{
  "name": "Monthly expenses by category, emailed to your accountant (Flowngn)",
  "nodes": [
    {
      "parameters": {
        "content": "## Monthly expenses by category, emailed to your accountant\nOn the 1st of every month, total last month's expenses by category and email the summary to your accountant.\n\n**Setup**\n1. Open each node and connect your own credentials.\n2. Replace every YOUR_… placeholder.\n3. Test once, then activate.\n\nFree template from Flowngn",
        "height": 260,
        "width": 340
      },
      "id": "2f780998-9838-445d-a923-1c16b94b02ed",
      "name": "Setup notes",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -420,
        -60
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 1 * *"
            }
          ]
        }
      },
      "id": "2f780998-9838-445d-a923-1c16b94b02ed",
      "name": "1st of the month 9:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "YOUR_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Expenses",
          "mode": "name"
        },
        "options": {}
      },
      "id": "2f780998-9838-445d-a923-1c16b94b02ed",
      "name": "Read expenses",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        300,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Expenses sheet columns: date (YYYY-MM-DD) | amount | category | description\n// Totals last month's expenses by category for the accountant email.\nconst now = new Date();\nconst first = new Date(now.getFullYear(), now.getMonth() - 1, 1);\nconst month = `${first.getFullYear()}-${String(first.getMonth() + 1).padStart(2, \"0\")}`;\nconst label = first.toLocaleString(\"en-US\", { month: \"long\", year: \"numeric\" });\n\nconst totals = {};\nlet total = 0;\nfor (const { json: e } of $input.all()) {\n  if (String(e.date).slice(0, 7) !== month) continue;\n  const amount = Number(String(e.amount).replace(/[^0-9.-]/g, \"\")) || 0;\n  const cat = e.category || \"Other\";\n  totals[cat] = (totals[cat] || 0) + amount;\n  total += amount;\n}\n\nconst fmt = (n) => n.toLocaleString(\"en-US\", { maximumFractionDigits: 2 });\nconst lines = Object.entries(totals)\n  .sort((a, b) => b[1] - a[1])\n  .map(([cat, sum]) => `${cat}: ${fmt(sum)} (${Math.round((sum / total) * 100)}%)`);\n\nreturn [{\n  json: {\n    subject: `Expenses for ${label}: ${fmt(total)}`,\n    body:\n      `Hi,\\n\\nHere are our expenses for ${label} by category:\\n\\n${lines.join(\"\\n\") || \"No expenses recorded.\"}\\n\\n` +\n      `Total: ${fmt(total)}\\n\\nThe full list is in the Expenses sheet.\\n\\nThanks`,\n  },\n}];"
      },
      "id": "2f780998-9838-445d-a923-1c16b94b02ed",
      "name": "Total by category",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        600,
        0
      ]
    },
    {
      "parameters": {
        "sendTo": "accountant@example.com",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.body }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "2f780998-9838-445d-a923-1c16b94b02ed",
      "name": "Email accountant",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        900,
        0
      ]
    }
  ],
  "connections": {
    "1st of the month 9:00": {
      "main": [
        [
          {
            "node": "Read expenses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read expenses": {
      "main": [
        [
          {
            "node": "Total by category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Total by category": {
      "main": [
        [
          {
            "node": "Email accountant",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}

Open in the playground to edit →

Tips

  • Pairs well with the Telegram expense tracker and the bank SMS flows, which fill the same sheet.

Related flows