This workflow corresponds to n8n.io template #9905 — we link there as the canonical source.
This workflow follows the Gmail → Gmail Trigger 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": "QHTTTSIjepK3ipPO",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Invoice Processing System for submission",
"tags": [
{
"id": "CMJL1dF3mMlRu0Jb",
"name": "Invoice Processing",
"createdAt": "2025-10-11T20:13:04.997Z",
"updatedAt": "2025-10-11T20:13:04.997Z"
}
],
"nodes": [
{
"id": "189e9199-b377-4acc-a0e7-d5769e69a762",
"name": "Gmail Trigger",
"type": "n8n-nodes-base.gmailTrigger",
"position": [
-96,
16
],
"parameters": {
"simple": false,
"filters": {
"q": "has:attachment filename:pdf"
},
"options": {
"downloadAttachments": true
},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
}
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "7c5278ea-c32f-4f55-9ba4-4448c06c21f0",
"name": "Code in JavaScript",
"type": "n8n-nodes-base.code",
"position": [
80,
16
],
"parameters": {
"jsCode": "// Get email data\nconst email = $input.item.json;\nconst binary = $input.item.binary;\n\n// Check if we have binary attachments\nif (!binary || Object.keys(binary).length === 0) {\n return { \n json: { \n error: 'No invoice attachment found',\n emailId: email.id,\n emailSubject: email.subject\n } \n };\n}\n\n// Find first PDF or image attachment\nlet invoiceFile = null;\nlet invoiceKey = null;\n\nfor (const key in binary) {\n const file = binary[key];\n if (file.mimeType?.includes('pdf') || \n file.mimeType?.includes('image') ||\n file.mimeType?.includes('png') ||\n file.mimeType?.includes('jpg')) {\n invoiceFile = file;\n invoiceKey = key;\n break;\n }\n}\n\nif (!invoiceFile) {\n return { \n json: { \n error: 'No PDF or image attachment found',\n emailId: email.id,\n emailSubject: email.subject\n } \n };\n}\n\n// The binary data in n8n is already base64 encoded\n// Just use it directly without re-encoding\nlet base64Data = invoiceFile.data;\n\n// If it's a Buffer, convert to base64 string\nif (Buffer.isBuffer(base64Data)) {\n base64Data = base64Data.toString('base64');\n}\n\n// Build the data URL for OCR.space\nconst dataUrl = `data:${invoiceFile.mimeType};base64,${base64Data}`;\n\nreturn {\n json: {\n emailId: email.id,\n emailSubject: email.subject,\n emailFrom: email.from,\n emailDate: email.date,\n attachmentData: base64Data,\n attachmentName: invoiceFile.fileName,\n mimeType: invoiceFile.mimeType,\n dataUrl: dataUrl\n }\n};"
},
"typeVersion": 2
},
{
"id": "4ddb1479-1b7f-4f8d-ad41-1a92654c98c8",
"name": "Append or update row in sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
1296,
-48
],
"parameters": {
"columns": {
"value": {
"Amount": "={{ $json.amount }}",
"Status": "Pending",
"Vendor": "={{ $json.vendor }}",
"Currency": "={{ $json.currency }}",
"Due Date": "={{ $json.dueDate }}",
"Email ID": "={{ $node[\"Code in JavaScript\"].json.emailId }}",
"Timestamp": "={{ $now.format('yyyy-MM-dd HH:mm:ss') }}",
"Description": "={{ $json.description }}",
"Invoice Date": "={{ $json.invoiceDate }}",
"Invoice Number": "={{ $json.invoiceNumber }}"
},
"schema": [
{
"id": "Invoice Number",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Invoice Number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Vendor",
"type": "string",
"display": true,
"required": false,
"displayName": "Vendor",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Amount",
"type": "string",
"display": true,
"required": false,
"displayName": "Amount",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Currency",
"type": "string",
"display": true,
"required": false,
"displayName": "Currency",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Invoice Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Invoice Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Due Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Due Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Description",
"type": "string",
"display": true,
"required": false,
"displayName": "Description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Email ID",
"type": "string",
"display": true,
"required": false,
"displayName": "Email ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Timestamp",
"type": "string",
"display": true,
"required": false,
"displayName": "Timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Processed By",
"type": "string",
"display": true,
"required": false,
"displayName": "Processed By",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Invoice Number"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1TWE856dG0g0bSRr3M4SKPcpnJ32veWlq3m_DWJoSaTQ/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "=\"YOUR_SPREADSHEET_ID\""
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "abea5730-c60f-47f4-83b3-50cb42c6c9e0",
"name": "Slack Approval Request",
"type": "n8n-nodes-base.slack",
"position": [
1904,
-48
],
"parameters": {
"select": "channel",
"message": "=\ud83e\uddfe *New Invoice Requires Approval*\n\n*Vendor:* {{ $json.vendor }}\n*Amount:* ${{ $json.amount }} {{ $json.currency }}\n*Invoice #:* {{ $json.invoiceNumber }}\n*Due Date:* {{ $json.dueDate }}\n*Description:* {{ $json.description }}",
"options": {},
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09LDKZJGBB",
"cachedResultName": "invoice-approvals"
},
"operation": "sendAndWait",
"authentication": "oAuth2",
"approvalOptions": {
"values": {
"approvalType": "double"
}
}
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "955f6439-8d79-4f9f-b35c-7bf3e39f6aad",
"name": "Slack Rejection message",
"type": "n8n-nodes-base.slack",
"position": [
1296,
144
],
"parameters": {
"text": "=Channel: #invoice-approvals\n\nText:\n\u274c *Invoice Auto-Rejected*\n\nAn invoice was rejected by AI analysis:\n\n- From: {{ $node[\"Code in JavaScript\"].json.emailFrom }}\n- Subject: {{ $node[\"Code in JavaScript\"].json.emailSubject }}\n- Reason: Did not pass qualification checks\n- Confidence: {{ $json.confidence }}%\n- Red Flags: {{ $json.redFlags.join(', ') }}\n\nThe email has been labeled \"Rejected\" in Gmail.",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09LDKZJGBB",
"cachedResultName": "invoice-approvals"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "424382f8-ff99-457e-9714-569c83e29157",
"name": "Check Qualification",
"type": "n8n-nodes-base.if",
"position": [
1040,
0
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1237bc0c-6f1a-4a3b-98ff-45369c8ebc06",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.qualified }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "be9d4c45-2bb3-4ca4-944e-65045fa75a60",
"name": "Parse Invoice Data",
"type": "n8n-nodes-base.code",
"position": [
720,
0
],
"parameters": {
"jsCode": "// Get OCR result\nconst ocrResult = $json;\n\n// Extract the text\nlet extractedText = '';\n\nif (ocrResult.ParsedResults && ocrResult.ParsedResults[0]) {\n extractedText = ocrResult.ParsedResults[0].ParsedText;\n} else {\n return {\n json: {\n error: 'OCR failed to extract text',\n qualified: false,\n redFlags: ['Could not read invoice text']\n }\n };\n}\n\n// Helper function to parse dates correctly without timezone issues\nfunction parseDate(dateString) {\n // Try to parse dates like \"October 1, 2025\"\n const date = new Date(dateString + ' 12:00:00 UTC'); // Add noon UTC to avoid timezone shifts\n \n if (!isNaN(date)) {\n const year = date.getUTCFullYear();\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n const day = String(date.getUTCDate()).padStart(2, '0');\n return `${year}-${month}-${day}`;\n }\n \n return '';\n}\n\n// Parse invoice fields using regex\nconst invoiceData = {\n qualified: true,\n vendor: '',\n invoiceNumber: '',\n amount: 0,\n currency: 'USD',\n invoiceDate: '',\n dueDate: '',\n description: '',\n confidence: 100,\n redFlags: []\n};\n\n// Extract Vendor (line after \"From:\")\nconst vendorMatch = extractedText.match(/From:\\s*([^\\n]+)/i);\nif (vendorMatch) {\n invoiceData.vendor = vendorMatch[1].trim();\n}\n\n// Extract Invoice Number\nconst invoiceNumMatch = extractedText.match(/Invoice Number:\\s*([^\\n]+)/i);\nif (invoiceNumMatch) {\n invoiceData.invoiceNumber = invoiceNumMatch[1].trim();\n}\n\n// Extract Amount (from \"Total:\" line) - CRITICAL!\nconst totalMatch = extractedText.match(/Total:\\s*\\$\\s*([0-9,]+\\.?[0-9]*)/i);\nif (totalMatch) {\n invoiceData.amount = parseFloat(totalMatch[1].replace(/,/g, ''));\n}\n\n// If Total not found, try Amount line\nif (invoiceData.amount === 0) {\n const amountMatch = extractedText.match(/Amount:\\s*\\$\\s*([0-9,]+\\.?[0-9]*)/i);\n if (amountMatch) {\n invoiceData.amount = parseFloat(amountMatch[1].replace(/,/g, ''));\n }\n}\n\n// Extract Currency\nconst currencyMatch = extractedText.match(/(USD|EUR|GBP|CAD)/i);\nif (currencyMatch) {\n invoiceData.currency = currencyMatch[1].toUpperCase();\n}\n\n// Extract Invoice Date - FIXED!\nconst dateMatch = extractedText.match(/Date:\\s*([^\\n]+)/i);\nif (dateMatch) {\n const dateStr = dateMatch[1].trim();\n invoiceData.invoiceDate = parseDate(dateStr);\n}\n\n// Extract Due Date - FIXED!\nconst dueMatch = extractedText.match(/Due Date:\\s*([^\\n]+)/i);\nif (dueMatch) {\n const dateStr = dueMatch[1].trim();\n invoiceData.dueDate = parseDate(dateStr);\n}\n\n// Extract Description\nconst descMatch = extractedText.match(/Description:\\s*([^\\n]+)/i);\nif (descMatch) {\n invoiceData.description = descMatch[1].trim();\n}\n\n// Validation checks\nif (!invoiceData.vendor) {\n invoiceData.qualified = false;\n invoiceData.redFlags.push('Missing vendor name');\n}\n\nif (!invoiceData.invoiceNumber) {\n invoiceData.qualified = false;\n invoiceData.redFlags.push('Missing invoice number');\n}\n\nif (invoiceData.amount === 0) {\n invoiceData.qualified = false;\n invoiceData.redFlags.push('Missing or invalid amount');\n}\n\nif (invoiceData.amount > 10000) {\n invoiceData.qualified = false;\n invoiceData.redFlags.push('Amount exceeds $10,000');\n}\n\nreturn {\n json: invoiceData\n};"
},
"typeVersion": 2
},
{
"id": "36afc95e-af94-4c98-a458-744e9bfd1139",
"name": "Format for Slack",
"type": "n8n-nodes-base.code",
"position": [
1504,
-48
],
"parameters": {
"jsCode": "const sheetData = $node[\"Append or update row in sheet\"].json;\n\nreturn {\n json: {\n vendor: sheetData.Vendor,\n amount: sheetData.Amount,\n currency: sheetData.Currency,\n invoiceNumber: sheetData[\"Invoice Number\"],\n invoiceDate: sheetData[\"Invoice Date\"], // \u2190 ADD THIS LINE!\n dueDate: sheetData[\"Due Date\"],\n description: sheetData.Description\n }\n};"
},
"typeVersion": 2
},
{
"id": "b524a093-e827-4049-b0c9-809dc839465e",
"name": "Rejection message",
"type": "n8n-nodes-base.slack",
"position": [
2896,
16
],
"parameters": {
"text": "\u274c Invoice rejected",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09LDKZJGBB",
"cachedResultName": "invoice-approvals"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "afcf2f08-41ec-4ce5-bb32-98733317379f",
"name": "Success message",
"type": "n8n-nodes-base.slack",
"position": [
3248,
-176
],
"parameters": {
"text": "\u2705 Invoice posted to Xero",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09LDKZJGBB",
"cachedResultName": "invoice-approvals"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "2a70f5c0-db5f-44eb-8eb8-24c978892049",
"name": "Update Rejected Status",
"type": "n8n-nodes-base.googleSheets",
"position": [
2704,
16
],
"parameters": {
"columns": {
"value": {
"Status": "Rejected",
"Invoice Number": "={{ $('Clean Invoice Payload').item.json.invoiceNumber }}"
},
"schema": [
{
"id": "Invoice Number",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Invoice Number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Vendor",
"type": "string",
"display": true,
"required": false,
"displayName": "Vendor",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Amount",
"type": "string",
"display": true,
"required": false,
"displayName": "Amount",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Currency",
"type": "string",
"display": true,
"required": false,
"displayName": "Currency",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Invoice Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Invoice Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Due Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Due Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Description",
"type": "string",
"display": true,
"required": false,
"displayName": "Description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Email ID",
"type": "string",
"display": true,
"required": false,
"displayName": "Email ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Timestamp",
"type": "string",
"display": true,
"required": false,
"displayName": "Timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Processed By",
"type": "string",
"display": true,
"required": false,
"displayName": "Processed By",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "number",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Invoice Number"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1TWE856dG0g0bSRr3M4SKPcpnJ32veWlq3m_DWJoSaTQ/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "=\"YOUR_SPREADSHEET_ID\""
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "f775d6e5-a97b-4aea-be73-f6a3030cbfc3",
"name": "Update Approved Status",
"type": "n8n-nodes-base.googleSheets",
"position": [
3024,
-176
],
"parameters": {
"columns": {
"value": {
"Status": "Approved",
"Invoice Number": "={{ $('Clean Invoice Payload').item.json.invoiceNumber }}"
},
"schema": [
{
"id": "Invoice Number",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Invoice Number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Vendor",
"type": "string",
"display": true,
"required": false,
"displayName": "Vendor",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Amount",
"type": "string",
"display": true,
"required": false,
"displayName": "Amount",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Currency",
"type": "string",
"display": true,
"required": false,
"displayName": "Currency",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Invoice Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Invoice Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Due Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Due Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Description",
"type": "string",
"display": true,
"required": false,
"displayName": "Description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Email ID",
"type": "string",
"display": true,
"required": false,
"displayName": "Email ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Timestamp",
"type": "string",
"display": true,
"required": false,
"displayName": "Timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Processed By",
"type": "string",
"display": true,
"required": false,
"displayName": "Processed By",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "number",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Invoice Number"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "id",
"value": "=Sheet1"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "=\"YOUR_SPREADSHEET_ID\""
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "b062f234-61a1-4913-8135-be9d9be322a8",
"name": "Add Processed label",
"type": "n8n-nodes-base.gmail",
"position": [
2832,
-176
],
"parameters": {
"labelIds": [
"Label_3710233975993850496"
],
"messageId": "={{$node[\"Code in JavaScript\"].json.emailId}}",
"operation": "addLabels"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "854afcd4-6559-4753-b75d-2a65766c492d",
"name": "Add Rejected label",
"type": "n8n-nodes-base.gmail",
"position": [
2432,
16
],
"parameters": {
"labelIds": [
"Label_8899302689673155790"
],
"messageId": "={{$node[\"Code in JavaScript\"].json.emailId}}",
"operation": "addLabels"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "025efb16-32dd-4a7e-8349-baef18b32040",
"name": "Get many contacts",
"type": "n8n-nodes-base.xero",
"position": [
2416,
-176
],
"parameters": {
"limit": 1,
"options": {},
"resource": "contact",
"operation": "getAll",
"organizationId": "=9a46b4a9-feac-446e-a04d-4e7421565898"
},
"credentials": {
"xeroOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "b43a1017-8ecb-4c39-b92f-8865349ccec2",
"name": "Clean Invoice Payload",
"type": "n8n-nodes-base.code",
"position": [
1696,
-48
],
"parameters": {
"jsCode": "const invoiceData = { ...$node[\"Format for Slack\"].json };\n\n// Strip off any vendor or itemCode field that might be passed\ndelete invoiceData.vendor;\ndelete invoiceData.itemCode;\ndelete invoiceData[\"Item Code Name or ID\"]; // Just in case\n\nreturn [{ json: invoiceData }];\n"
},
"typeVersion": 2
},
{
"id": "a77a3860-e69a-428f-b74e-d926859c83e3",
"name": "Create invoice",
"type": "n8n-nodes-base.httpRequest",
"position": [
2624,
-176
],
"parameters": {
"url": "https://api.xero.com/api.xro/2.0/Invoices",
"method": "POST",
"options": {},
"jsonBody": "={\n \"Type\": \"ACCPAY\",\n \"Contact\": {\n \"ContactID\": \"{{$node[\"Get many contacts\"].json.ContactID}}\"\n },\n \"Date\": \"{{$node[\"Format for Slack\"].json.invoiceDate}}\",\n \"DueDate\": \"{{$node[\"Format for Slack\"].json.dueDate}}\",\n \"InvoiceNumber\": \"{{$node[\"Format for Slack\"].json.invoiceNumber}}\",\n \"Reference\": \"{{$node[\"Format for Slack\"].json.invoiceNumber}}\",\n \"Status\": \"DRAFT\",\n \"LineAmountTypes\": \"Exclusive\",\n \"LineItems\": [\n {\n \"Description\": \"{{$node[\"Format for Slack\"].json.description}}\",\n \"Quantity\": 1,\n \"UnitAmount\": {{$node[\"Format for Slack\"].json.amount}},\n \"AccountCode\": \"463\",\n \"TaxType\": \"INPUT2\"\n }\n ]\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
},
{
"name": "xero-tenant-id",
"value": "=\"{{$credentials.xero.apiKey}}\""
}
]
},
"nodeCredentialType": "xeroOAuth2Api"
},
"credentials": {
"xeroOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "270ea877-b081-4704-8137-3cbcf62606fb",
"name": "Has Valid Attachment?",
"type": "n8n-nodes-base.if",
"position": [
288,
16
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "5b78e677-d068-411a-8c43-e41a9725f370",
"operator": {
"type": "number",
"operation": "notExists",
"singleValue": true
},
"leftValue": "={{$json.error}}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "4128986d-09d7-44b5-a9f5-5f8a532c1712",
"name": "Was Approved?",
"type": "n8n-nodes-base.if",
"position": [
2128,
-48
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b58f3729-809c-4b21-9210-6c581dbeed50",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.data.approved }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "8400ed04-51ba-4cc6-92cc-abb03a09e258",
"name": "Extract Text",
"type": "n8n-nodes-base.httpRequest",
"position": [
544,
0
],
"parameters": {
"url": "https://api.ocr.space/parse/image",
"method": "POST",
"options": {},
"jsonBody": "={\n \"name\": \"apikey\",\n \"value\": \"{{$credentials.ocrspaceApi.apiKey}}\"\n}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.2
},
{
"id": "d132fdec-f017-49f6-a4bc-8a938df37d98",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-912,
-464
],
"parameters": {
"width": 608,
"height": 1696,
"content": "### Automate Invoice Processing with Gmail, OCR.space, Slack & Xero\n\nThis n8n template demonstrates how to automatically extract, validate, approve, and sync invoices received via email using AI and automation \u2014 reducing manual processing time and minimizing errors.\n\n### Use Cases\n\n- Finance teams looking to eliminate invoice approval bottlenecks \n- Businesses that receive PDF/image invoices via email \n- Companies currently logging invoices manually into spreadsheets or accounting tools \n\n### How It Works\n\n1. **Trigger**: Watches for new emails in Gmail with PDF/image attachments. \n2. **OCR**: Sends the attachment to OCR.space API (https://ocr.space/OCRAPI) to extract invoice text. \n3. **Parsing**: Extracts key fields: \n - Vendor \n - Invoice number \n - Amount \n - Currency \n - Invoice date \n - Due date \n - Description \n4. **Validation Logic**: \n - Checks if amount is valid \n - Ensures vendor and invoice number are present \n - Flags high-value invoices (e.g., over $10,000) \n5. **Routing**: \n - If invalid: \n - Sends a Slack message highlighting issues\n - Labels email as **Rejected** \n - If valid: \n - Logs the invoice into Google Sheets \n - Sends a Slack message to the finance team for approval \n - After approval, creates a draft invoice in Xero \n - Labels the email as **Processed** in Gmail \n\n### Customisation Options\n\n- Swap Gmail trigger with a file uploader or webhook \n- Adjust the Slack notification channels or approval thresholds \n- Replace Xero with QuickBooks, Zoho, or another tool \n- Adapt log storage from Google Sheets to Airtable or Notion \n- Add a reminder flow if no approval is received within 24h \n\n### Prerequisites/Credential Setup\n\nTo use this workflow securely, you'll need the following credentials set up in n8n:\n\n- **Gmail OAuth2** \u2013 to watch for incoming invoice emails and apply labels\n- **OCR.space API** \u2013 create an HTTP Credential in n8n named `ocrspaceApi`, with your OCR.space key\n- **Slack OAuth2 or Webhook (with connected bot)** \u2013 to send approval/rejection messages to your team\n- **Google Sheets OAuth2 credentials** \u2013 to log processed invoices for audit and tracking\n- **Xero OAuth2 credentials (via n8n\u2019s native integration)** \u2013 connect your Xero account to push approved invoices in Draft status\n \n### Secure Configuration\n- All credential fields use **n8n Credential Types** \n- No sensitive data (API keys, spreadsheet IDs) is hardcoded \n\n### Why This Helps\n\n- Reduces time spent manually reviewing and copying invoices\n- Maintains structured logs with clean data\n- Automates Slack approvals and reduces email back-and-forth\n- Improves audit trails and AP visibility across tools\n\n---\nWith this template, finance teams can streamline invoice approvals, reduce processing time, and maintain a clean audit trail, all fully automated."
},
"typeVersion": 1
},
{
"id": "b16d6e58-0125-4735-b939-040945fcc89b",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1008,
-336
],
"parameters": {
"width": 1248,
"height": 640,
"content": "## 2. Approve or Reject Invoice in Slack\n[Read more about Slack Approval workflows in n8n](https://docs.n8n.io/integrations/slack/)\n\nAfter parsing, the workflow evaluates whether the invoice meets preset criteria (e.g., approved vendors or amount limits). It logs the result in a Google Sheet and sends a Slack message for manual approval. Based on the response, it either proceeds to Xero or marks it as rejected.\n\nSetup Tip: Replace YOUR_SLACK_CLIENT_ID with the Client ID from your Slack apps page - https://api.slack.com/apps.\n"
},
"typeVersion": 1
},
{
"id": "af22b982-18c5-4adc-9e88-d7ecf53f357c",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-144,
-336
],
"parameters": {
"width": 1120,
"height": 640,
"content": "## 1. Extract & Parse Invoice from Gmail\n[Read more about the Gmail and HTTP Request nodes](https://docs.n8n.io/integrations/builtin/email-read-imap/)\n \nThis section listens for new emails via Gmail, checks if an attachment exists and is valid, then extracts and parses invoice data using OCR (via OCR.Space API). The data is cleaned and prepared for downstream steps.\n\nSetup Tip: Replace YOUR_GOOGLE_CLIENT_ID with the Client ID from your Google Console - https://console.cloud.google.com/ for your Google Sheets, Google Docs, Gmail."
},
"typeVersion": 1
},
{
"id": "422c5b66-d77a-4224-b689-e4b57152373e",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2288,
-336
],
"parameters": {
"width": 1168,
"height": 640,
"content": "## 3. Sync Outcome to Xero & Notify\n[Read more about Xero and Gmail automation](https://docs.n8n.io/integrations/xero/)\n\nIf the invoice is approved, the workflow fetches the contact from Xero, creates the invoice, updates Gmail labels, logs final status in Google Sheets, and sends a success message in Slack. If rejected, it applies a different Gmail label, updates status, and notifies the team.\n"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "a1d9da45-06e7-460c-9c56-bc430ee568b6",
"connections": {
"Extract Text": {
"main": [
[
{
"node": "Parse Invoice Data",
"type": "main",
"index": 0
}
]
]
},
"Gmail Trigger": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"Was Approved?": {
"main": [
[
{
"node": "Get many contacts",
"type": "main",
"index": 0
}
],
[
{
"node": "Add Rejected label",
"type": "main",
"index": 0
}
]
]
},
"Create invoice": {
"main": [
[
{
"node": "Add Processed label",
"type": "main",
"index": 0
}
]
]
},
"Format for Slack": {
"main": [
[
{
"node": "Clean Invoice Payload",
"type": "main",
"index": 0
}
]
]
},
"Get many contacts": {
"main": [
[
{
"node": "Create invoice",
"type": "main",
"index": 0
}
]
]
},
"Add Rejected label": {
"main": [
[
{
"node": "Update Rejected Status",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "Has Valid Attachment?",
"type": "main",
"index": 0
}
]
]
},
"Parse Invoice Data": {
"main": [
[
{
"node": "Check Qualification",
"type": "main",
"index": 0
}
]
]
},
"Add Processed label": {
"main": [
[
{
"node": "Update Approved Status",
"type": "main",
"index": 0
}
]
]
},
"Check Qualification": {
"main": [
[
{
"node": "Append or update row in sheet",
"type": "main",
"index": 0
}
],
[
{
"node": "Slack Rejection message",
"type": "main",
"index": 0
}
]
]
},
"Clean Invoice Payload": {
"main": [
[
{
"node": "Slack Approval Request",
"type": "main",
"index": 0
}
]
]
},
"Has Valid Attachment?": {
"main": [
[
{
"node": "Extract Text",
"type": "main",
"index": 0
}
]
]
},
"Slack Approval Request": {
"main": [
[
{
"node": "Was Approved?",
"type": "main",
"index": 0
}
]
]
},
"Update Approved Status": {
"main": [
[
{
"node": "Success message",
"type": "main",
"index": 0
}
]
]
},
"Update Rejected Status": {
"main": [
[
{
"node": "Rejection message",
"type": "main",
"index": 0
}
]
]
},
"Append or update row in sheet": {
"main": [
[
{
"node": "Format for Slack",
"type": "main",
"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.
gmailOAuth2googleSheetsOAuth2ApislackOAuth2ApixeroOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
How It Works Trigger: Watches for new emails in Gmail with PDF/image attachments. OCR: Sends the attachment to OCR.space API (https://ocr.space/OCRAPI) to extract invoice text. Parsing: Extracts key fields: Vendor Invoice number Amount Currency Invoice date Due date Description…
Source: https://n8n.io/workflows/9905/ — 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.
This workflow is a sophisticated, end-to-end solution that automates the entire billing lifecycle, from invoice creation to intelligent payment reminders and status tracking. It's designed to give you
Automatically processes vendor invoices received by email, creates QuickBooks bills with full details, and attaches the original PDF. Small/medium businesses using QuickBooks Online Bookkeepers proces
This workflow automatically generates and delivers professional invoice PDFs whenever a Stripe checkout session completes. It fetches the line items from Stripe, formats them into a clean invoice with
Invoice PDF to Sheets. Uses googleDriveTrigger, googleDrive, httpRequest, googleSheets. Event-driven trigger; 10 nodes.
This is the ultimate sales-to-cash automation. When a deal in Airtable is marked "Approved for Invoicing," this workflow intelligently syncs customer data across QuickBooks and Stripe (creating them i