This workflow corresponds to n8n.io template #16469 — we link there as the canonical source.
This workflow follows the Agent → Gmail recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "1aFrKsqyV9BvmMI5",
"name": "Invoice & Payment Reminder System",
"tags": [],
"nodes": [
{
"id": "9989919b-14fe-4359-b301-caba32585305",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-512,
-96
],
"parameters": {
"color": 2,
"width": 592,
"height": 1040,
"content": "# Invoice & Payment Reminder System\n\nTracks invoices in Notion as the source of truth, escalates reminder tone progressively as they go overdue, and unlike most invoice reminder templates alerts the freelancer themselves when payment risk is high, so they can decide whether to keep working for that client.\n\n---\n\n\n## How it works\n\n1. **Daily schedule** fetches all invoice pages from a Notion database.\n2. **Normalize Notion Records** flattens Notion's property structure into a usable shape.\n3. **Calculate Overdue Status** computes days overdue and determines which escalation tier (1/2/3) is due, skipping invoices already at their current tier or marked paid/cancelled.\n4. **Claude** drafts a tone-appropriate reminder: tier 1 friendly (3+ days), tier 2 firm (10+ days), tier 3 final notice (20+ days).\n5. **Send Reminder Email** goes to the client.\n6. **Update Notion Record** marks the tier as sent so the same reminder isn't generated twice.\n7. **If At Risk (tier 3)** also sends a Telegram alert to the freelancer themselves not the client flagging this invoice as a payment risk worth a personal decision.\n\n---\n\n\n## Setup steps\n\n- [ ] Create a Notion database with properties: Client Name (text), Client Email (text), Invoice Number (text), Amount (number), Currency (select), Due Date (date), Status (select: unpaid/paid/cancelled), Payment Link (url), Reminder Tier Sent (number), Stripe Invoice ID (text)\n- [ ] Connect Notion credentials and select your database in both Notion nodes\n- [ ] Add Anthropic credentials to the Anthropic Chat Model node\n- [ ] Connect Gmail/SMTP credentials for Send Reminder Email\n- [ ] Connect your own Telegram account (bot + chat ID) for the at-risk alert\n- [ ] Optional: connect Stripe to auto-update Status to 'paid' when a matching payment comes in (not included here pairs well with a separate Stripe webhook workflow that updates the same Notion database)\n\n---\n\n\n## Why this protects the freelancer, not just the cash flow\n\nMost invoice reminder templates only nudge the client. This one also tells the freelancer at the point things look genuinely at risk that it might be time to stop working for free. That's the decision most freelancers don't make until it's too late, because nobody is tracking it for them."
},
"typeVersion": 1
},
{
"id": "6cca6a0a-5243-4deb-bdda-3d7f2f134422",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
176,
-96
],
"parameters": {
"color": 7,
"width": 448,
"height": 624,
"content": "## Fetch and normalize\n\nDaily schedule pulls all invoice pages from Notion, flattens the property structure into a usable shape."
},
"typeVersion": 1
},
{
"id": "ce18125e-9995-4331-bdb1-08946782ee7d",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
672,
-96
],
"parameters": {
"color": 7,
"width": 464,
"height": 624,
"content": "## Determine escalation tier\n\nComputes days overdue, skips paid/cancelled, determines which tier (1/2/3) is due based on what's already been sent."
},
"typeVersion": 1
},
{
"id": "fc2dd0c6-7b46-49be-ae8b-4a0a716a70e7",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1184,
-96
],
"parameters": {
"color": 7,
"width": 592,
"height": 624,
"content": "## Draft and send\n\nClaude writes a tone-matched reminder. Sent directly to the client via email. Notion record updated so it doesn't repeat."
},
"typeVersion": 1
},
{
"id": "9f968d8a-d781-4523-b92c-1a8afa33b819",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1984,
-96
],
"parameters": {
"color": 7,
"width": 560,
"height": 736,
"content": "## At-risk alert\n\nTier 3 (final notice) also pings the freelancer directly via Telegram \u2014 a decision point, not just a client nudge."
},
"typeVersion": 1
},
{
"id": "7656f2a2-d93c-48cb-9533-8240a1386c5f",
"name": "Every Morning at 8 AM",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
224,
144
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8 * * *"
}
]
}
},
"typeVersion": 1.3
},
{
"id": "dccd67bc-6612-4184-b348-436e872dd81a",
"name": "Fetch Invoice Records",
"type": "n8n-nodes-base.notion",
"position": [
464,
144
],
"parameters": {
"filters": {
"conditions": [
{
"key": "Status|select",
"condition": "equals",
"selectValue": "unpaid"
}
]
},
"options": {},
"resource": "databasePage",
"operation": "getAll",
"returnAll": true,
"databaseId": {
"__rl": true,
"mode": "id",
"value": "REPLACE_WITH_YOUR_NOTION_DATABASE_ID"
},
"filterType": "manual"
},
"typeVersion": 2.2
},
{
"id": "c04c57d3-609e-4f26-9ad8-f82281fb1471",
"name": "Normalize Notion Records",
"type": "n8n-nodes-base.code",
"position": [
752,
144
],
"parameters": {
"jsCode": "// Reads invoice pages from Notion (already fetched by the upstream Notion node)\n// and normalizes the property shapes into a flat object we can work with downstream.\n\nconst pages = $input.all().map(i => i.json);\n\nreturn pages.map(page => {\n const props = page.properties || {};\n\n const getText = (prop) => {\n if (!prop) return null;\n if (prop.title) return prop.title.map(t => t.plain_text).join('');\n if (prop.rich_text) return prop.rich_text.map(t => t.plain_text).join('');\n return null;\n };\n const getNumber = (prop) => prop?.number ?? null;\n const getSelect = (prop) => prop?.select?.name ?? null;\n const getDate = (prop) => prop?.date?.start ?? null;\n const getUrl = (prop) => prop?.url ?? null;\n\n return {\n json: {\n page_id: page.id,\n client_name: getText(props['Client Name']),\n client_email: getText(props['Client Email']),\n invoice_number: getText(props['Invoice Number']),\n amount: getNumber(props['Amount']),\n currency: getSelect(props['Currency']) || 'USD',\n due_date: getDate(props['Due Date']),\n status: getSelect(props['Status']),\n payment_link: getUrl(props['Payment Link']),\n reminder_tier_sent: getNumber(props['Reminder Tier Sent']) || 0,\n stripe_invoice_id: getText(props['Stripe Invoice ID'])\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "41835f8f-78b8-485a-bf3f-758600632a9b",
"name": "Calculate Overdue Status",
"type": "n8n-nodes-base.code",
"position": [
960,
144
],
"parameters": {
"jsCode": "// Computes days overdue and which escalation tier (if any) is due,\n// based on the invoice's due date and how many reminders have already been sent.\n\nconst records = $input.all().map(i => i.json);\nconst now = new Date();\n\n// Tiers: [days_overdue_threshold, tier_number]\nconst TIERS = [\n { threshold: 3, tier: 1, label: 'tier_1' },\n { threshold: 10, tier: 2, label: 'tier_2' },\n { threshold: 20, tier: 3, label: 'tier_3' }\n];\n\nconst results = [];\n\nfor (const r of records) {\n if (r.status === 'paid' || r.status === 'cancelled') continue;\n if (!r.due_date) continue;\n\n const dueDate = new Date(r.due_date);\n const daysOverdue = Math.floor((now - dueDate) / 86400000);\n\n if (daysOverdue < TIERS[0].threshold) continue;\n\n const alreadySent = r.reminder_tier_sent || 0;\n\n // Find the highest tier that's due but not yet sent\n let dueTier = null;\n for (const t of TIERS) {\n if (daysOverdue >= t.threshold && t.tier > alreadySent) {\n dueTier = t;\n }\n }\n\n if (!dueTier) continue;\n\n results.push({\n json: {\n page_id: r.page_id,\n client_name: r.client_name,\n client_email: r.client_email,\n invoice_number: r.invoice_number,\n amount: r.amount,\n currency: r.currency,\n due_date: r.due_date,\n days_overdue: daysOverdue,\n tier: dueTier.label,\n tier_number: dueTier.tier,\n payment_link: r.payment_link\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "b339efa5-1e92-4dfb-8d78-738c1dbe3707",
"name": "Draft Reminder",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
1312,
144
],
"parameters": {
"text": "=Write a payment reminder for this overdue invoice.\n\nClient name: {{ $json.client_name }}\nInvoice number: {{ $json.invoice_number }}\nAmount: {{ $json.amount }} {{ $json.currency }}\nOriginal due date: {{ $json.due_date }}\nDays overdue: {{ $json.days_overdue }}\nEscalation tier: {{ $json.tier }}\nPayment link (if available): {{ $json.payment_link || 'Not provided' }}\n\nWrite ONLY the message body for an email, no subject line beyond what you return separately, no signature block.\n\nReturn ONLY valid JSON: {\"subject\": \"email subject line\", \"message\": \"the reminder body\"}",
"options": {
"systemMessage": "You are a professional billing assistant who writes payment reminder messages for freelancers and small agencies. Your tone escalates appropriately based on how overdue an invoice is, while always remaining professional, never aggressive, never apologetic.\n\n## Tone by escalation tier\n- tier_1 (friendly, ~3 days overdue): Light, assume oversight. \"Just a heads up\" energy. No urgency language.\n- tier_2 (firm, ~10 days overdue): Direct. State the amount and due date plainly. Ask for a payment date if there's an issue.\n- tier_3 (final, ~20 days overdue): Clear and serious without being threatening. State this is a final reminder before the next steps (which the freelancer decides, not you). No legal threats, no aggressive language, just clarity that this is a final notice.\n\n## Rules\n- Always state the invoice number, amount, and original due date plainly, never vaguely\n- Never guilt-trip or use passive-aggressive phrasing\n- Keep messages short: 3-5 sentences\n- Always end with a clear, easy next step for the recipient\n- This is a B2B relationship, preserve it even when reminding about money"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 3
},
{
"id": "8b3b435d-b7d6-4205-a9b7-2394f5b3a3e1",
"name": "Anthropic Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
1232,
384
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-sonnet-4-5-20250929",
"cachedResultName": "Claude Sonnet 4.5"
},
"options": {
"temperature": 0.4
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "7a9191c4-2983-4c54-ac55-1b7cf3831ca8",
"name": "Structured Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
1472,
384
],
"parameters": {
"autoFix": true,
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"subject\": {\n \"type\": \"string\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"subject\",\n \"message\"\n ],\n \"additionalProperties\": false\n}"
},
"typeVersion": 1.2
},
{
"id": "6a657dd1-8614-46de-8c40-64e070eb53e3",
"name": "Build Output",
"type": "n8n-nodes-base.code",
"position": [
1632,
144
],
"parameters": {
"jsCode": "// Combine AI draft with record metadata, decide if this warrants\n// a Telegram alert to the freelancer themselves (tier 3 = at risk)\n\nconst ai = $input.first().json.output || $input.first().json;\nconst record = $('Calculate Overdue Status').first().json;\n\nconst isAtRisk = record.tier_number === 3;\n\nreturn [{\n json: {\n page_id: record.page_id,\n client_name: record.client_name,\n client_email: record.client_email,\n invoice_number: record.invoice_number,\n amount: record.amount,\n currency: record.currency,\n days_overdue: record.days_overdue,\n tier_number: record.tier_number,\n email_subject: ai.subject,\n email_body: ai.message,\n isAtRisk,\n telegramMessage: isAtRisk\n ? `*Payment at risk, Invoice ${record.invoice_number}*\\n\\n*Client:* ${record.client_name}\\n*Amount:* ${record.amount} ${record.currency}\\n*Days overdue:* ${record.days_overdue}\\n\\nThis is the final reminder tier. Consider whether to pause work for this client until payment is received.`\n : null\n }\n}];"
},
"typeVersion": 2
},
{
"id": "30a35f1f-33e0-4e77-90d9-d20b60662b72",
"name": "Send Reminder Email",
"type": "n8n-nodes-base.gmail",
"position": [
2080,
144
],
"parameters": {
"sendTo": "={{ $json.client_email }}",
"message": "={{ $json.email_body }}",
"options": {
"appendAttribution": false
},
"subject": "={{ $json.email_subject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "90d20e43-3bb3-4ee1-88d4-50d0ad9f40d9",
"name": "Update Notion Record",
"type": "n8n-nodes-base.notion",
"position": [
2080,
304
],
"parameters": {
"pageId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.page_id }}"
},
"options": {},
"resource": "databasePage",
"operation": "update",
"propertiesUi": {
"propertyValues": [
{
"key": "Reminder Tier Sent|number",
"numberValue": "={{ $json.tier_number }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "57b9dc9f-27a3-45a2-918a-1e0167af0e0b",
"name": "If At Risk",
"type": "n8n-nodes-base.if",
"position": [
2080,
480
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "risk-check",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.isAtRisk }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "c32963a6-bc48-4d34-804a-7963a44e8c4d",
"name": "Alert Freelancer (At Risk)",
"type": "n8n-nodes-base.telegram",
"position": [
2352,
464
],
"parameters": {
"text": "={{ $json.telegramMessage }}",
"chatId": "REPLACE_WITH_YOUR_TELEGRAM_CHAT_ID",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "e25f0a7e-a34e-4755-8bbe-7970035df822",
"connections": {
"If At Risk": {
"main": [
[
{
"node": "Alert Freelancer (At Risk)",
"type": "main",
"index": 0
}
]
]
},
"Build Output": {
"main": [
[
{
"node": "Send Reminder Email",
"type": "main",
"index": 0
},
{
"node": "Update Notion Record",
"type": "main",
"index": 0
},
{
"node": "If At Risk",
"type": "main",
"index": 0
}
]
]
},
"Draft Reminder": {
"main": [
[
{
"node": "Build Output",
"type": "main",
"index": 0
}
]
]
},
"Anthropic Chat Model": {
"ai_languageModel": [
[
{
"node": "Draft Reminder",
"type": "ai_languageModel",
"index": 0
},
{
"node": "Structured Output Parser",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Every Morning at 8 AM": {
"main": [
[
{
"node": "Fetch Invoice Records",
"type": "main",
"index": 0
}
]
]
},
"Fetch Invoice Records": {
"main": [
[
{
"node": "Normalize Notion Records",
"type": "main",
"index": 0
}
]
]
},
"Calculate Overdue Status": {
"main": [
[
{
"node": "Draft Reminder",
"type": "main",
"index": 0
}
]
]
},
"Normalize Notion Records": {
"main": [
[
{
"node": "Calculate Overdue Status",
"type": "main",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "Draft Reminder",
"type": "ai_outputParser",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
anthropicApigmailOAuth2telegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs every morning to pull unpaid invoices from a Notion database, calculates how overdue each one is, uses Anthropic Claude to draft an escalation-appropriate reminder, emails the client via Gmail, updates the invoice record in Notion, and alerts you in Telegram…
Source: https://n8n.io/workflows/16469/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Aggregates communication data from Slack, Microsoft Teams, Gmail, GitHub, and Confluence into a single, unified AI-powered analysis workflow designed for quality review and automated documentation upd
This workflow runs on a schedule, searches Google via SerpApi for a chosen topic, scrapes the top results’ webpages with Apify, uses Anthropic Claude to extract email addresses from the page text, and
This workflow automates short-interval market signal evaluation for intraday trading using live technical indicators and deterministic decision logic. It is designed for traders, analysts, and automat
LinkedIn_Job_Hunt_and_Cover_Letter. Uses outputParserStructured, outputParserAutofixing, googleDrive, agent. Scheduled trigger; 85 nodes.
Ingest meeting webhooks, process transcript, classify the meeting, generate structured notes with AI Agent, file the transcript to Google Drive, write rich pages to Notion, and create assigned tasks.