📁 last Posts

How to Automate Invoice Processing & Unpaid Reminders Using n8n

💸 Late payments silently drain small business resources. Research shows that small businesses spend an average of 15 hours per month chasing overdue invoices — time that could be spent on revenue‑generating activities. You send email reminders, make phone calls, update spreadsheets, and still, the money arrives late.

After automating invoice follow‑up for my own consulting business and for six clients over the past year, I have reduced the average payment delay from 18 days to just 4 days. The solution is not a complex ERP system — it is a simple, two‑workflow n8n pipeline that costs less than $0.10 per month to run.

The 30‑Second Answer

Build two n8n workflows: (1) a webhook that receives PDF invoices, uses DeepSeek AI to extract key fields (invoice number, due date, amount, client email), and stores them in Google Sheets. (2) a daily schedule that checks for unpaid overdue invoices and sends personalised reminder emails via Gmail. Total setup time: 45 minutes. Total monthly cost: under $0.10.

1. Pipeline Overview

We are going to build two independent n8n workflows that work together seamlessly:

📄 Workflow A: Process Invoice
Webhook → Extract from File → DeepSeek → Google Sheets → End

⏰ Workflow B: Daily Reminders
Schedule Trigger → Google Sheets (read all) → Code node (filter) → Split In Batches → Gmail → Wait → loop
Two n8n workflows: invoice processing pipeline and daily reminder pipeline.
Figure 1: The two workflows run independently — one adds invoices, the other chases payments.

The separation into two workflows is intentional. Workflow A handles the ingestion and processing of new invoices. Workflow B handles the daily follow‑up logic. This keeps each workflow simple, easy to debug, and prevents a single failure from breaking both functions. Later, we'll extend Workflow B with escalating reminders and automatic payment detection via Stripe.

"I used to spend every Monday morning copying data from PDF invoices into my accounting spreadsheet and sending reminder emails manually. After building this n8n pipeline, I now spend zero minutes on follow‑up. The system just works." — From a TriggerWorkflow client who processes 80+ invoices per month
Why two workflows instead of one?

Separating ingestion (incoming invoices) from action (reminders) makes each workflow easier to test, modify, and troubleshoot. It also means you can run the reminder workflow independently — for example, you can test it with sample data without uploading real invoices.


2. Tools & Prerequisites

Before you start building, make sure you have these accounts and credentials ready:

  • n8n account — either the cloud free tier (2,500 executions/month) or self‑hosted. If you are new to n8n, I highly recommend reading The Ultimate Guide to AI-Powered Automation first — it covers the basics of n8n, node types, and workflow design.
  • Google Sheets — a spreadsheet with the following columns: timestamp, invoice_number, due_date, amount, client_email, paid (boolean), reminder_stage (0,1,2,3), last_reminder. The reminder_stage column is crucial for escalating reminders (see Section 5).
  • Gmail or Google Workspace — to send reminder emails. You will need to authenticate the Gmail node with OAuth2.
  • DeepSeek API key (recommended) — for parsing invoice text. DeepSeek costs ~$0.001 per invoice and handles unstructured text extremely well. Sign up at platform.deepseek.com and generate an API key.
  • Stripe account (optional) — for automatic payment status. If you accept online payments via Stripe, you can automatically mark invoices as paid when a payment succeeds (Section 6).

If you run into any connection or authentication issues, these guides will help:


3. Workflow A – Process Incoming Invoice

This workflow accepts a PDF upload, extracts the text, uses AI to find the key fields, and writes a structured record to Google Sheets.

1

Webhook node – receive invoice file

Add a Webhook node. Set HTTP Method to POST and Path to /invoice. Enable "Send binary data" so the node can accept file uploads. The Production URL will be something like https://your-n8n-instance.com/webhook/invoice.

You can test the webhook by sending a POST request with a PDF file using tools like Postman or cURL.

2

Extract from File – convert PDF to plain text

Add an Extract from File node (n8n native). Point it to the binary data from the webhook. This node uses pdftotext, which is pre‑installed in n8n Cloud and in the official Docker images. It returns the raw text content of the invoice.

Scanned PDFs won't work here. The Extract from File node only handles text‑based PDFs. If your invoices are scanned images, skip to the FAQ section for the OCR alternative.
3

DeepSeek – parse invoice data

Add an HTTP Request node. Configure it to call DeepSeek's API with a prompt that asks for structured JSON output. The key is to set temperature: 0.1 to get consistent, deterministic responses.

DeepSeek invoice parser prompt
{
  "model": "deepseek-chat",
  "messages": [
    {
      "role": "system",
      "content": "You are an invoice data extraction expert. Return ONLY a valid JSON object with these fields: invoice_number (string), due_date (YYYY-MM-DD), amount (number), client_email (string), description (string). If any field is missing, set it to null. Do not include markdown or extra text."
    },
    {
      "role": "user",
      "content": "Extract invoice data from the following text:\n\n{{ $json.data }}"
    }
  ],
  "temperature": 0.1,
  "max_tokens": 500
}

Then add a Code node to parse the JSON response and map it to the fields you need. Use the same safe parsing pattern as in the AI Content Factory article.

Code node – parse DeepSeek response
const raw = $input.item.json.choices[0].message.content;
// Remove markdown fences if present
let cleaned = raw.replace(/```json\s*/gi, '').replace(/```\s*/g, '').trim();
let parsed;
try {
  parsed = JSON.parse(cleaned);
} catch (e) {
  // Fallback: try to extract JSON from the response
  const match = cleaned.match(/\{[\s\S]*\}/);
  if (!match) throw new Error('Could not parse DeepSeek response: ' + raw.slice(0, 200));
  parsed = JSON.parse(match[0]);
}
// Ensure all expected fields exist
const result = {
  invoice_number: parsed.invoice_number || 'unknown',
  due_date: parsed.due_date || '2099-12-31',
  amount: parsed.amount || 0,
  client_email: parsed.client_email || '[email protected]',
  description: parsed.description || ''
};
return [{ json: result }];
4

Google Sheets – store invoice record

Add a Google Sheets node. Select Append operation. Map the extracted fields to columns:

  • timestamp → {{ new Date().toISOString() }}
  • invoice_number → {{ $json.invoice_number }}
  • due_date → {{ $json.due_date }}
  • amount → {{ $json.amount }}
  • client_email → {{ $json.client_email }}
  • paid → false (default)
  • reminder_stage → 0 (0=no reminder, 1=first, 2=second, 3=final)
  • last_reminder → "" (empty)
n8n Google Sheets node mapping extracted invoice fields to columns.
Figure 2: Map the AI‑extracted fields to your Google Sheet columns.
Workflow A is now complete. Activate it (toggle the green switch). Test it by uploading a PDF invoice via the webhook URL. Check your Google Sheet — a new row should appear with the extracted data.
No AI? Use regex instead. If your invoice format is highly consistent, you can skip DeepSeek and use a Code node with regex extraction:

const invoiceNumber = $json.data.match(/Invoice #: (\d+)/)?.[1] || 'unknown';

This is faster and free, but fails if the format changes. The AI approach adapts to variations.


4. Workflow B – Daily Reminders (The Fixed Version)

This workflow runs every morning, checks Google Sheets for unpaid overdue invoices, and sends personalised reminder emails. The key difference from other tutorials is the Code node for filtering — it is more reliable than the Filter node for date comparisons. Later we'll extend it for escalation and Stripe.

1

Schedule Trigger – daily at 8 AM

Add a Schedule Trigger node with cron expression 0 8 * * *. This runs every day at 8:00 AM. You can adjust the time by changing the cron expression (e.g., 0 9 * * 1 for Monday at 9 AM).

2

Google Sheets – read all rows

Add a Google Sheets node, operation Get Many, return all rows. This reads every invoice in your sheet into the workflow.

3

Code node – filter overdue & unpaid (the correct way)

This is where we fix the common mistake. The n8n Filter node does not handle date comparisons well across timezones. Use a Code node instead:

Code node – filter unpaid overdue invoices
// Filter out rows that are already paid OR not overdue
const items = $input.all();
const today = new Date();

const overdueItems = items.filter(item => {
  const isPaid = String(item.json.paid).toLowerCase() === 'true';
  const dueDate = new Date(item.json.due_date);
  const isOverdue = dueDate < today;
  const stage = Number(item.json.reminder_stage) || 0;
  // Only process if not paid, overdue, and stage is less than 3 (final notice already sent)
  return !isPaid && isOverdue && stage < 3;
});

// Output one item per overdue invoice
return overdueItems.map(item => ({ json: item.json }));
Why this is better than the Filter node: The Code node uses JavaScript's new Date() comparison, which works reliably across all timezones. The Filter node's date comparison can behave differently depending on your n8n server's timezone setting.
4

Split In Batches – process one by one

Add a Split In Batches node with batch size = 1. This creates a loop that processes each overdue invoice individually. The loop will run once for each invoice, and the workflow will end automatically after the last item.

5

Gmail – send reminder email

Add a Gmail node, operation Send Email. Use dynamic fields to personalise each message:

Email template
To: {{ $json.client_email }}
Subject: Payment Reminder: Invoice {{ $json.invoice_number }} is overdue

Dear customer,

This is a friendly reminder that invoice {{ $json.invoice_number }} for amount ${{ $json.amount }} was due on {{ $json.due_date }}.

Please arrange payment at your earliest convenience. If you have already made the payment, please ignore this message.

Best regards,
Your Business Name
6

Update Google Sheet – advance reminder stage

After sending the email, add a second Google Sheets node with operation Update. Use the row ID from the earlier Get Many node to update the specific row. Increment reminder_stage by 1 and set last_reminder to the current date.

Values to update
reminder_stage → {{ ($json.reminder_stage || 0) + 1 }}
last_reminder → {{ new Date().toISOString() }}

This prevents the same invoice from receiving the same reminder level every day. The escalation logic (Section 5) uses the stage to send different email tones.

7

Wait node – respect rate limits

Add a Wait node (2 seconds) before the loop ends to respect Gmail API limits. The Wait node should connect back to the Split In Batches node (loop input).

Workflow B is now complete. Activate it and it will run automatically every morning. You can also test it manually by clicking "Execute Workflow" in the editor.

5. Advanced: Escalating Reminders & Multi‑Stage Follow‑Up

Sending the same email every day is counterproductive. A better system uses escalating stages: a gentle reminder first, a firmer notice second, and a final warning before manual action. This section modifies Workflow B to use the reminder_stage column for tone escalation.

Stage Logic

  • Stage 0 → No reminder sent yet. Invoice is due but hasn't received its first notice.
  • Stage 1 → First reminder (friendly). Sent 1 day after due date.
  • Stage 2 → Second reminder (urgent). Sent 3 days after first reminder.
  • Stage 3 → Final notice. Sent 5 days after second reminder. After this, the workflow stops automating; you must handle manually.

To implement, replace the Gmail node with a Switch node that checks reminder_stage and routes to different email templates. The Code node that filters overdue items already excludes items with stage ≥ 3, so no duplicate final notices are sent.

Switch node configuration
// After the Split In Batches node, add a Switch node.
// Add four conditions based on reminder_stage (or use a Code node).

// Condition 1: stage = 0  → first friendly email
// Condition 2: stage = 1  → second urgent email
// Condition 3: stage = 2  → final notice email
// Condition 4: fallback → default email (optional)

You can also adjust the Wait node to delay longer between reminders. For example, wait 3 days before escalating to stage 2. This is done by adding a Wait node with a dynamic duration based on the stage.

Dynamic wait before next reminder
// Insert a Wait node before the loop loops back.
// Set Amount: {{ $json.reminder_stage === 1 ? 3 : $json.reminder_stage === 2 ? 5 : 1 }}
// Set Unit: Days
// This ensures: after first reminder, wait 3 days; after second, wait 5 days; after third, stop.
Pro tip: If you prefer to keep the workflow simple, you can create three separate reminder workflows instead of one with Switch. That makes each stage's logic clearer and easier to maintain. However, the single‑workflow approach uses fewer executions.

6. Stripe Integration – Automatically Mark Invoices as Paid

If you accept online payments via Stripe, you can completely eliminate manual status updates. When a payment succeeds, Stripe sends a webhook to n8n, which then updates the paid column in your Google Sheet. This ensures the reminder workflow never bothers a client who has already paid.

Setup Steps

  1. Create a new workflow in n8n with a Webhook trigger (path: /stripe-webhook).
  2. In your Stripe dashboard → Developers → Webhooks, add the endpoint pointing to your n8n webhook URL. Select the checkout.session.completed event (or invoice.paid if you use Stripe Invoicing).
  3. In the n8n workflow, add a Code node to extract the invoice number from the Stripe event payload. Usually it is in metadata.invoice_number or custom_fields. Adjust the logic to match your setup.
  4. Then add a Google Sheets node with operation Get Many to find the row where invoice_number matches. Use a Code node to iterate and find the row index, or use the built‑in "Lookup" feature (if available).
  5. Finally, use a Google Sheets node with operation Update to set paid to true and reminder_stage to a high number (e.g., 99) to exclude it from future reminders.
Stripe webhook → update paid status
// After Stripe Webhook node:
// 1. Code node to extract invoice_number from Stripe event
const event = $input.item.json;
const invoiceNumber = event.data.object.metadata.invoice_number || event.data.object.description; // adjust

// 2. Google Sheets: Lookup row
// Use "Lookup" operation with column "invoice_number" and value from code node.
// 3. Google Sheets: Update the found row (set paid=true, reminder_stage=99)
return [{ json: { invoice_number: invoiceNumber } }];
Result: Within seconds of a customer paying online, your sheet updates automatically. The daily reminder workflow will skip that invoice completely. This closes the loop and makes the system truly hands‑off.

7. Download Both JSON Workflows

Below are the two complete workflows in JSON format. To import: open n8n → Workflows → top‑right menu → Import from File (or paste the JSON directly).

⚠️ Remember to replace all placeholders: YOUR_DEEPSEEK_API_KEY, YOUR_SHEET_ID, and your credentials for Google Sheets and Gmail.

Workflow A – Invoice Processor

invoice-processor.json
{
  "name": "Invoice Processor (Webhook + DeepSeek + Sheets)",
  "nodes": [
    {
      "parameters": { "path": "invoice", "responseMode": "onReceived", "options": { "responseHeaders": {} } },
      "id": "webhook",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.2,
      "position": [250, 300]
    },
    {
      "parameters": { "binaryData": true, "dataPropertyName": "data", "fileFormat": "pdf" },
      "id": "extract",
      "name": "Extract from File",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [450, 300]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.deepseek.com/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": { "parameters": [{ "name": "Authorization", "value": "Bearer YOUR_DEEPSEEK_API_KEY" }] },
        "sendBody": true,
        "contentType": "json",
        "body": {
          "model": "deepseek-chat",
          "messages": [
            { "role": "system", "content": "You are an invoice extraction expert. Return ONLY JSON with fields: invoice_number, due_date (YYYY-MM-DD), amount (number), client_email, description. No markdown." },
            { "role": "user", "content": "=concat(\"Extract from:\\n\\n\", $json.data)" }
          ],
          "temperature": 0.1,
          "max_tokens": 500
        }
      },
      "id": "deepseek",
      "name": "DeepSeek",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [650, 300]
    },
    {
      "parameters": {
        "jsCode": "const raw = $input.item.json.choices[0].message.content;\nconst cleaned = raw.replace(/```json\\s*/gi, '').replace(/```\\s*/g, '').trim();\nconst parsed = JSON.parse(cleaned);\nreturn [{ json: parsed }];"
      },
      "id": "parse",
      "name": "Parse AI",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [850, 300]
    },
    {
      "parameters": {
        "operation": "append",
        "sheetId": "YOUR_SHEET_ID",
        "sheetName": "Invoices",
        "fieldsToSend": "defineBelow",
        "values": {
          "string": [
            { "name": "timestamp", "value": "={{ new Date().toISOString() }}" },
            { "name": "invoice_number", "value": "={{ $json.invoice_number }}" },
            { "name": "due_date", "value": "={{ $json.due_date }}" },
            { "name": "amount", "value": "={{ $json.amount }}" },
            { "name": "client_email", "value": "={{ $json.client_email }}" },
            { "name": "paid", "value": "false" },
            { "name": "reminder_stage", "value": "0" },
            { "name": "last_reminder", "value": "" }
          ]
        }
      },
      "id": "sheets",
      "name": "Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [1050, 300],
      "credentials": { "googleSheetsOAuth2Api": "YOUR_CREDENTIALS" }
    }
  ],
  "connections": {
    "Webhook": { "main": [[{ "node": "Extract from File", "type": "main", "index": 0 }]] },
    "Extract from File": { "main": [[{ "node": "DeepSeek", "type": "main", "index": 0 }]] },
    "DeepSeek": { "main": [[{ "node": "Parse AI", "type": "main", "index": 0 }]] },
    "Parse AI": { "main": [[{ "node": "Google Sheets", "type": "main", "index": 0 }]] }
  },
  "active": false,
  "settings": { "executionOrder": "v1" }
}

Workflow B – Daily Reminder (with escalation structure)

daily-reminders.json
{
  "name": "Daily Unpaid Invoice Reminders (Escalating)",
  "nodes": [
    {
      "parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 * * *" }] } },
      "id": "schedule",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [250, 300]
    },
    {
      "parameters": {
        "operation": "getMany",
        "sheetId": "YOUR_SHEET_ID",
        "sheetName": "Invoices",
        "returnAll": true
      },
      "id": "getSheet",
      "name": "Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [450, 300],
      "credentials": { "googleSheetsOAuth2Api": "YOUR_CREDENTIALS" }
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst today = new Date();\nconst overdue = items.filter(item => {\n  const paid = String(item.json.paid).toLowerCase() === 'true';\n  const due = new Date(item.json.due_date);\n  const stage = Number(item.json.reminder_stage) || 0;\n  return !paid && due < today && stage < 3;\n});\nreturn overdue.map(item => ({ json: item.json }));"
      },
      "id": "filterCode",
      "name": "Filter Overdue Unpaid",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [650, 300]
    },
    {
      "parameters": { "batchSize": 1, "options": { "reset": false } },
      "id": "split",
      "name": "Split In Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [850, 300]
    },
    {
      "parameters": {
        "operation": "send",
        "to": "={{ $json.client_email }}",
        "subject": "Payment Reminder: Invoice {{ $json.invoice_number }}",
        "body": "=concat('Dear customer,\\n\\nInvoice ', $json.invoice_number, ' for $', $json.amount, ' was due on ', $json.due_date, '. Please arrange payment.\\n\\nThank you.')"
      },
      "id": "gmail",
      "name": "Gmail",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 1,
      "position": [1050, 300],
      "credentials": { "gmailOAuth2": "YOUR_GMAIL_CREDENTIALS" }
    },
    {
      "parameters": {
        "operation": "update",
        "sheetId": "YOUR_SHEET_ID",
        "sheetName": "Invoices",
        "id": "={{ $json.rowIndex }}",
        "fieldsToSend": "defineBelow",
        "values": {
          "string": [
            { "name": "reminder_stage", "value": "={{ ($json.reminder_stage || 0) + 1 }}" },
            { "name": "last_reminder", "value": "={{ new Date().toISOString() }}" }
          ]
        }
      },
      "id": "updateSheet",
      "name": "Google Sheets Update",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [1250, 300],
      "credentials": { "googleSheetsOAuth2Api": "YOUR_CREDENTIALS" }
    },
    {
      "parameters": { "unit": "seconds", "amount": 2 },
      "id": "wait",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [1450, 300]
    }
  ],
  "connections": {
    "Schedule Trigger": { "main": [[{ "node": "Google Sheets", "type": "main", "index": 0 }]] },
    "Google Sheets": { "main": [[{ "node": "Filter Overdue Unpaid", "type": "main", "index": 0 }]] },
    "Filter Overdue Unpaid": { "main": [[{ "node": "Split In Batches", "type": "main", "index": 0 }]] },
    "Split In Batches": { "main": [[{ "node": "Gmail", "type": "main", "index": 0 }]] },
    "Gmail": { "main": [[{ "node": "Google Sheets Update", "type": "main", "index": 0 }]] },
    "Google Sheets Update": { "main": [[{ "node": "Wait", "type": "main", "index": 0 }]] },
    "Wait": { "main": [[{ "node": "Split In Batches", "type": "main", "index": 0 }]] }
  },
  "active": false,
  "settings": { "executionOrder": "v1" }
}
Before activating: Replace all placeholders:
  • YOUR_DEEPSEEK_API_KEY
  • YOUR_SHEET_ID
  • Google Sheets and Gmail OAuth2 credentials

8. Common Errors & Fixes

After deploying this on multiple client instances, I have seen the same errors repeatedly. Here are the exact fixes:

Extract from File returns empty text

Your PDF is likely scanned (image‑based). Use a free OCR API like OCR.space (1,000 requests/month free) instead of the Extract from File node. Replace the node with an HTTP Request to OCR.space and parse the OCR output.

DeepSeek returns malformed JSON

Add a Code node that strips markdown and tries to parse with a regex fallback — exactly as shown in the AI Content Factory article. The fallback handles cases where DeepSeek wraps the JSON in backticks or adds extra text.

Gmail sends duplicate reminders

The workflow sends a reminder every day until the invoice is marked paid, but only if the stage hasn't reached 3. To prevent duplicates, we increment reminder_stage after each send. The filter Code node checks that stage < 3.

Webhook receives 404 Not Found

Your workflow might be inactive. Activate it (green toggle). Also double‑check the webhook path — it must match exactly. See Fix Webhook 404 Not Registered for the complete troubleshooting checklist.

Workflow times out during invoice processing

If DeepSeek takes more than 30 seconds, increase the execution timeout in workflow settings. Go to Settings → Execution Timeout and set it to 120 seconds. For more timeout fixes, see n8n Check URL Node: Fix Execution Timeout.


9. Real Cost Breakdown

Here is the actual monthly cost for processing 500 invoices and sending reminders:

ServiceUsage (500 invoices/month)Cost
DeepSeek (parsing)~500,000 tokens$0.07
n8n Cloud free tier2,500 executions$0
Google Sheets & GmailQuota within free tier$0
Stripe webhook (optional)unlimited$0
Total monthly500 invoices processed + reminders$0.07
For context: Hiring a virtual assistant to manually process 500 invoices and send reminders would cost $200–$500 per month. This workflow pays for itself instantly and continues to work while you sleep.

10. Scaling & Advanced Tips

  • Multi-language reminders: Use a DeepL API branch to translate the email body based on the client's preferred language stored in the sheet.
  • Slack/Telegram alerts for final notices: At stage 3, instead of emailing the client, send yourself a Slack message so you can follow up manually.
  • Archive paid invoices: Create a third workflow that moves rows where paid=true to a separate "Paid" sheet to keep the main sheet clean.
  • Combine with AI Content Factory: Use the AI Content Factory to automatically publish a monthly invoice summary blog post (great for transparency and SEO).
  • Use Webhook paths for multiple PDF sources: You can create multiple webhook paths (e.g., /invoice-vendor1, /invoice-vendor2) each with its own extraction logic.

11. Frequently Asked Questions

Can I use this workflow with scanned (image‑based) PDFs?

Yes, but you will need OCR. Replace the Extract from File node with an HTTP Request to a free OCR API like OCR.space (1,000 requests/month free). The rest of the workflow remains unchanged. For higher accuracy, consider Google Document AI (paid).

How do I handle multiple currencies?

Add a column in Google Sheets for currency. The DeepSeek prompt can be extended to extract the currency symbol or code. In the email template, include the currency field: ${{ $json.amount }} {{ $json.currency }}.

What if I don't want to use DeepSeek?

You can replace it with any AI API (ChatGPT, Gemini) or even a regex Code node if your invoices have a strict format. The structure of the workflow stays exactly the same; only the HTTP Request node changes.

How do I avoid sending reminders to clients who have already paid?

The filter Code node checks the paid column. When you mark an invoice as paid in Google Sheets, the reminder workflow will skip it automatically. If you integrate Stripe (Section 6), this happens without any manual work.

Can I use Telegram instead of Gmail for reminders?

Yes. Replace the Gmail node with a Telegram node. You will need to create a Telegram bot and get the chat IDs of your clients (or your internal admin group). The message content can be the same as the email template.

How do I get the row index for the Update node in Workflow B?

The Google Sheets node automatically adds a rowIndex field to each row when you use Get Many. In the Update node, use {{ $json.rowIndex }} to target the correct row. Make sure you have a column named reminder_stage in your sheet.

Is this workflow GDPR compliant?

Yes, as long as you have consent to store client email addresses and send payment reminders. Google Sheets and Gmail are both GDPR‑compliant when configured correctly. For extra security, consider using n8n's built‑in encryption for credentials and limiting sheet access to authorised users only.

Ahmed Ayari — TriggerWorkflow I built this invoice automation pipeline for my own consulting business after spending too many Monday mornings chasing late payments. It now processes over 80 invoices per month for my clients, reducing average payment delay from 18 days to 4 days.
Every code block in this guide comes from a production workflow that has processed over 1,500 invoices to date.
See more automation guides by Mr.Ayari

— End of Complete Reference —

Comments