This workflow corresponds to n8n.io template #15723 — we link there as the canonical source.
This workflow follows the Chainllm → HTTP Request 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": "QXtn97U966JAVBIR",
"name": "Invoice Processor Microsoft 365",
"tags": [],
"nodes": [
{
"id": "7da0ed77-4b9d-43a2-a902-cc432747697c",
"name": "Sticky Note Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"width": 620,
"height": 908,
"content": "### Invoice Processor \u2013 Microsoft 365\n\nAutomatically fetches unread emails from Outlook, uses GPT-4.1-mini to extract structured invoice data from email bodies and PDF attachments, saves PDFs to a dated OneDrive folder, appends each record to an Excel tracker table, marks emails as read, and sends a daily HTML digest summary.\n\n### How it works\n1. **Schedule \u2014 Daily Trigger** fires once per day (default: 7 AM).\n2. **Outlook \u2014 Get Unread Emails** fetches unread messages with attachments from a specified folder.\n3. **Code \u2014 Prepare Invoice Text** strips HTML email bodies to clean plain text.\n4. **AI \u2014 Extract Invoice Fields** sends each email body to GPT-4.1-mini to extract 14 structured invoice fields as JSON.\n5. **Code \u2014 Parse Invoice JSON** parses the AI response and merges it with email metadata.\n6. **Set \u2014 Format Excel Row** maps all fields to Excel column names.\n7. **IF \u2014 Has Attachment?** routes emails with PDF attachments through the OneDrive upload branch.\n8. **OneDrive Upload Branch**: fetches attachment bytes via Graph API \u2192 checks/creates a dated folder (`Invoices/YYYY-MM-DD`) \u2192 uploads the PDF as `VENDOR-YYYY-MM.pdf` \u2192 captures the `webUrl`.\n9. **Merge \u2014 Rejoin Attachment Branches** reunites both paths.\n10. **Parallel outputs**: Excel append (terminates) + Mark as Read \u2192 Filter \u2192 Digest email.\n\n### Setup\n1. Add credentials for **Microsoft Outlook OAuth2** (used for fetching and sending emails).\n2. Add credentials for **MS Graph OAuth2** (used for OneDrive and Excel via Graph API).\n3. In the **Outlook \u2014 Get Unread Emails** node, update the `foldersToInclude` filter to your inbox folder ID.\n4. In the **Excel \u2014 Append Invoice Row** node, update the workbook, worksheet, and table IDs to your `tblInvoices` tracker.\n5. In **Code \u2014 Build Digest Email**, update `oneDriveFolderUrl` to your OneDrive Invoices folder link.\n6. Set your recipient email in **Outlook \u2014 Send Daily Digest**.\n\n### Customization\n- Change the daily trigger time in **Schedule \u2014 Daily Trigger**.\n- Modify the PDF folder path logic in **Code \u2014 Build Upload Path**.\n- Add more fields to the AI prompt in **AI \u2014 Extract Invoice Fields** for additional extraction.\n\nSupport: support@legalgpts.com\nhttps://automatedintelligentsolutions.com"
},
"typeVersion": 1
},
{
"id": "92e5b27e-2a57-4ee0-af9d-4269c8c0f6fd",
"name": "Sticky Note Trigger",
"type": "n8n-nodes-base.stickyNote",
"position": [
640,
0
],
"parameters": {
"color": 7,
"width": 396,
"height": 304,
"content": "## Trigger + Fetch\nSchedule fires daily and fetches unread Outlook emails."
},
"typeVersion": 1
},
{
"id": "e298d324-bd2c-4dd3-8382-3c88e684e86c",
"name": "Sticky Note Prepare",
"type": "n8n-nodes-base.stickyNote",
"position": [
1040,
0
],
"parameters": {
"color": 7,
"width": 280,
"height": 304,
"content": "## Prepare HTML \u2192 Text\nStrips HTML email body to clean plain text for the AI node."
},
"typeVersion": 1
},
{
"id": "bf96b71a-2801-4ff7-9fcb-af98feb6ca2d",
"name": "Sticky Note AI",
"type": "n8n-nodes-base.stickyNote",
"position": [
1328,
0
],
"parameters": {
"color": 7,
"width": 664,
"height": 304,
"content": "## AI Extract \u2192 Parse \u2192 Format\nGPT-4.1-mini extracts 14 invoice fields; Code node parses JSON; Set node maps to Excel columns."
},
"typeVersion": 1
},
{
"id": "20666dd2-a1b5-4522-a949-e3acd27c36c4",
"name": "Sticky Note OneDrive",
"type": "n8n-nodes-base.stickyNote",
"position": [
2176,
0
],
"parameters": {
"color": 7,
"width": 1332,
"height": 288,
"content": "## OneDrive Attachment Upload\nFetches PDF from Graph API \u2192 checks/creates dated folder \u2192 uploads renamed file \u2192 captures webUrl."
},
"typeVersion": 1
},
{
"id": "a8dd1033-cae8-4efd-89d2-73ce9fcb825f",
"name": "Sticky Note Write",
"type": "n8n-nodes-base.stickyNote",
"position": [
3520,
0
],
"parameters": {
"color": 7,
"width": 1388,
"height": 288,
"content": "## Output: Excel Write + Mark Read \u2192 Filter \u2192 Digest\nParallel outputs: appends row to Excel and sends an HTML daily digest via Outlook."
},
"typeVersion": 1
},
{
"id": "dd62c8dc-5f4e-46a8-9d5d-5ff4525b2557",
"name": "Schedule \u2014 Daily Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
688,
128
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 7 * * *"
}
]
}
},
"typeVersion": 1.3
},
{
"id": "4af21336-ef98-4275-8aaa-fb5b2ff6385f",
"name": "Outlook \u2014 Get Unread Emails",
"type": "n8n-nodes-base.microsoftOutlook",
"position": [
896,
128
],
"parameters": {
"limit": 5,
"fields": [
"sentDateTime",
"sender",
"subject",
"from",
"body",
"internetMessageId",
"receivedDateTime"
],
"output": "fields",
"options": {
"downloadAttachments": true
},
"filtersUI": {
"values": {
"filters": {
"readStatus": "unread",
"foldersToInclude": [
"YOUR_OUTLOOK_FOLDER_ID"
]
}
}
},
"operation": "getAll"
},
"typeVersion": 2
},
{
"id": "19724371-eb60-491b-8dd1-a8a85c430109",
"name": "Code \u2014 Prepare Invoice Text",
"type": "n8n-nodes-base.code",
"position": [
1120,
128
],
"parameters": {
"jsCode": "function decodeEntities(s) {\n return s\n .replace(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, '\"')\n .replace(/'/gi, \"'\")\n .replace(/'/gi, \"'\")\n .replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCharCode(parseInt(h, 16)))\n .replace(/#(\\d+);/g, (_, d) => String.fromCharCode(parseInt(d, 10)));\n}\n\nfunction htmlToText(html) {\n if (!html) return '';\n let s = String(html);\n s = s.replace(/<script[\\s\\S]*?<\\/script>/gi, '');\n s = s.replace(/<style[\\s\\S]*?<\\/style>/gi, '');\n s = s.replace(/<!--[\\s\\S]*?-->/g, '');\n s = s.replace(/<head[\\s\\S]*?<\\/head>/gi, '');\n s = s.replace(/<\\/td\\s*>/gi, '\\t');\n s = s.replace(/<\\/th\\s*>/gi, '\\t');\n s = s.replace(/<\\/tr\\s*>/gi, '\\n');\n s = s.replace(/<br\\s*\\/?>/gi, '\\n');\n s = s.replace(/<\\/p\\s*>/gi, '\\n');\n s = s.replace(/<\\/div\\s*>/gi, '\\n');\n s = s.replace(/<\\/h[1-6]\\s*>/gi, '\\n');\n s = s.replace(/<\\/li\\s*>/gi, '\\n');\n s = s.replace(/<[^>]+>/g, ' ');\n s = decodeEntities(s);\n s = s.split('\\n')\n .map(line => line.replace(/[ \\t\\f\\v]+/g, ' ').trim())\n .filter(line => line.length > 0)\n .join('\\n');\n return s.substring(0, 12000);\n}\n\nconst results = [];\nfor (const item of items) {\n const json = item.json || {};\n const bodyContent = (json.body && json.body.content) ? json.body.content : '';\n const isHtml = (json.body && json.body.contentType === 'html');\n const invoiceText = isHtml ? htmlToText(bodyContent) : String(bodyContent || '').substring(0, 12000);\n\n let emailFrom = '';\n if (json.from && json.from.emailAddress) {\n emailFrom = (json.from.emailAddress.name || '') + ' <' + (json.from.emailAddress.address || '') + '>';\n } else if (typeof json.from === 'string') {\n emailFrom = json.from;\n }\n\n results.push({\n json: {\n invoiceContent: invoiceText,\n emailSubject: json.subject || '',\n emailFrom: emailFrom,\n emailDate: json.receivedDateTime || '',\n emailUid: json.id || '',\n hasAttachment: Object.keys(item.binary || {}).length > 0\n },\n binary: item.binary\n });\n}\n\nreturn results;\n"
},
"typeVersion": 2
},
{
"id": "f4b86a07-5432-4281-9b6e-275e62696924",
"name": "AI \u2014 Extract Invoice Fields",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
1376,
128
],
"parameters": {
"text": "=You are an expert invoice data extractor. Extract the following fields from the invoice text below and return ONLY a valid JSON object with no markdown, no code fences, no backticks, no explanation. Start your response with { and end with }.\n\nIMPORTANT: The text may be plain text, structured email text, or HTML email converted to text. Look carefully for invoice-like patterns: invoice numbers, totals, line items, dates, vendor names. Forwarded emails may contain the invoice within the forwarded content. If the email does not appear to contain an invoice, return null for invoiceNumber to indicate this is not an invoice. If you are unsure of a vendor inspect the from address and resolve the domain to an entity.\n\nFields to extract:\n- invoiceNumber: unique alphanumeric identifier (string or null)\n- poNumber: purchase order number (string or null)\n- vendorName: legal name of the vendor (string or null)\n- contactInfo: billing contact email or phone (string or null)\n- issueDate: invoice issue date in YYYY-MM-DD format (string or null)\n- dueDate: payment due date in YYYY-MM-DD format (string or null)\n- subtotal: total before tax as number (number or null)\n- taxAmount: total tax amount as number (number or null)\n- totalAmountDue: final total as number (number or null)\n- currency: three-letter currency code e.g. USD, EUR (string or null)\n- paymentTerms: e.g. Net 30, Due on Receipt (string or null)\n- description: brief description of goods/services (string or null)\n- quantityHours: quantity or hours, comma-separated if multiple (string or null)\n- unitPrice: unit price or rate, comma-separated if multiple (string or null)\n\nInvoice text:\n{{ $json.invoiceContent }}",
"batching": {
"batchSize": 5,
"delayBetweenBatches": 0
},
"promptType": "define"
},
"typeVersion": 1.9
},
{
"id": "e583593d-fc5a-4f71-a397-b81d673171e1",
"name": "OpenAI \u2014 GPT-4.1-mini Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
1344,
320
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini",
"cachedResultName": "gpt-4.1-mini"
},
"options": {
"temperature": 0
},
"builtInTools": {}
},
"typeVersion": 1.3
},
{
"id": "fd6bfa6f-5e58-4c72-876d-7b42bcb30f8f",
"name": "Code \u2014 Parse Invoice JSON",
"type": "n8n-nodes-base.code",
"position": [
1648,
128
],
"parameters": {
"jsCode": "function parseInvoiceJson(raw) {\n let cleaned = String(raw || '').trim()\n .replace(/^```json\\s*/i, '')\n .replace(/^```\\s*/i, '')\n .replace(/\\s*```$/i, '')\n .trim();\n let parsed = {};\n try {\n const tmp = JSON.parse(cleaned);\n if (tmp && typeof tmp === 'object' && !Array.isArray(tmp)) parsed = tmp;\n } catch(e) {\n const m = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (m) {\n try {\n const tmp2 = JSON.parse(m[0]);\n if (tmp2 && typeof tmp2 === 'object' && !Array.isArray(tmp2)) parsed = tmp2;\n } catch(e2) {}\n }\n }\n return parsed;\n}\n\nconst fields = ['invoiceNumber','poNumber','vendorName','contactInfo','issueDate','dueDate',\n 'subtotal','taxAmount','totalAmountDue','currency','paymentTerms','description',\n 'quantityHours','unitPrice'];\n\nconst out = [];\nfor (let i = 0; i < items.length; i++) {\n const parsed = parseInvoiceJson(items[i].json.text);\n const row = {};\n for (const f of fields) {\n const v = parsed[f];\n row[f] = (v !== null && v !== undefined) ? String(v) : '';\n }\n let meta = {};\n try {\n meta = $('Code \u2014 Prepare Invoice Text').itemMatching(i).json || {};\n } catch(e) { meta = {}; }\n row.emailSubject = meta.emailSubject || '';\n row.emailFrom = meta.emailFrom || '';\n row.emailDate = meta.emailDate || '';\n row.emailUid = meta.emailUid || '';\n row.hasAttachment = meta.hasAttachment || false;\n out.push({ json: row });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "ca8cc552-1de3-4f01-bcac-225f83d2f4ad",
"name": "Set \u2014 Format Excel Row",
"type": "n8n-nodes-base.set",
"position": [
1840,
128
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "row-invnum",
"name": "Invoice Number",
"type": "string",
"value": "={{ $json.invoiceNumber || '' }}"
},
{
"id": "row-po",
"name": "PO Number",
"type": "string",
"value": "={{ $json.poNumber || '' }}"
},
{
"id": "row-vendor",
"name": "Vendor / Client Name",
"type": "string",
"value": "={{ $json.vendorName || '' }}"
},
{
"id": "row-contact",
"name": "Contact Info",
"type": "string",
"value": "={{ $json.contactInfo || '' }}"
},
{
"id": "row-issue",
"name": "Issue Date",
"type": "string",
"value": "={{ $json.issueDate || '' }}"
},
{
"id": "row-due",
"name": "Due Date",
"type": "string",
"value": "={{ $json.dueDate || '' }}"
},
{
"id": "row-subtotal",
"name": "Subtotal",
"type": "string",
"value": "={{ $json.subtotal || '' }}"
},
{
"id": "row-tax",
"name": "Tax Amount",
"type": "string",
"value": "={{ $json.taxAmount || '' }}"
},
{
"id": "row-total",
"name": "Total Amount Due",
"type": "string",
"value": "={{ $json.totalAmountDue || '' }}"
},
{
"id": "row-currency",
"name": "Currency",
"type": "string",
"value": "={{ $json.currency || '' }}"
},
{
"id": "row-terms",
"name": "Payment Terms",
"type": "string",
"value": "={{ $json.paymentTerms || '' }}"
},
{
"id": "row-desc",
"name": "Description",
"type": "string",
"value": "={{ $json.description || '' }}"
},
{
"id": "row-qty",
"name": "Quantity / Hours",
"type": "string",
"value": "={{ $json.quantityHours || '' }}"
},
{
"id": "row-unit",
"name": "Unit Price / Rate",
"type": "string",
"value": "={{ $json.unitPrice || '' }}"
},
{
"id": "row-source",
"name": "Source Email From",
"type": "string",
"value": "={{ $json.emailFrom || '' }}"
},
{
"id": "row-emailsubject",
"name": "Source Email Subject",
"type": "string",
"value": "={{ $json.emailSubject || '' }}"
},
{
"id": "row-emaildate",
"name": "Email Received Date",
"type": "string",
"value": "={{ $json.emailDate || '' }}"
},
{
"id": "row-processed",
"name": "Processed At",
"type": "string",
"value": "={{ $now.toISO() }}"
},
{
"id": "row-uid",
"name": "Email UID",
"type": "string",
"value": "={{ $json.emailUid || '' }}"
},
{
"id": "row-hasattach",
"name": "hasAttachment",
"type": "boolean",
"value": "={{ $json.hasAttachment || false }}"
},
{
"id": "row-vendorraw",
"name": "vendorNameRaw",
"type": "string",
"value": "={{ $json.vendorName || '' }}"
},
{
"id": "row-issueraw",
"name": "issueDateRaw",
"type": "string",
"value": "={{ $json.issueDate || '' }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "b67f504d-57fe-447f-8b87-2458851d615e",
"name": "IF \u2014 Has Attachment?",
"type": "n8n-nodes-base.if",
"position": [
2048,
144
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "cond-has-attach",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.hasAttachment }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "ef6c684c-0bbb-4dd6-9b91-3256985f99b1",
"name": "HTTP \u2014 Get Attachments",
"type": "n8n-nodes-base.httpRequest",
"position": [
2272,
128
],
"parameters": {
"url": "=https://graph.microsoft.com/v1.0/me/messages/{{ $json['Email UID'] }}/attachments",
"options": {
"response": {
"response": {}
}
},
"sendHeaders": true,
"authentication": "genericCredentialType",
"genericAuthType": "oAuth2Api",
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "3651a8fe-5faa-4fcb-8abb-77d07a9a8c5c",
"name": "Code \u2014 Build Upload Path",
"type": "n8n-nodes-base.code",
"position": [
2432,
128
],
"parameters": {
"jsCode": "const results = [];\nconst trueItems = $('IF \u2014 Has Attachment?').all(0, 0); \n\nconst today = new Date();\nconst pad = (n) => String(n).padStart(2, '0');\nconst folderName = today.getFullYear() + '-' + pad(today.getMonth() + 1) + '-' + pad(today.getDate());\nconst baseFolder = 'Invoices';\nconst folderPath = baseFolder + '/' + folderName;\n\nfor (let i = 0; i < items.length; i++) {\n const attachData = items[i].json;\n const row = trueItems[i] ? trueItems[i].json : {};\n \n let vendorSlug = String(row.vendorNameRaw || 'UNKNOWN')\n .toUpperCase()\n .replace(/[^A-Z0-9]/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .substring(0, 30);\n if (!vendorSlug) vendorSlug = 'UNKNOWN';\n\n let monthStr = '';\n const issueRaw = String(row.issueDateRaw || '');\n const dateMatch = issueRaw.match(/(\\d{4})-(\\d{2})/);\n if (dateMatch) {\n monthStr = dateMatch[1] + '-' + dateMatch[2];\n } else {\n monthStr = today.getFullYear() + '-' + pad(today.getMonth() + 1);\n }\n\n const fileName = vendorSlug + '-' + monthStr + '.pdf';\n\n const attachments = attachData.value || [];\n let pdfBase64 = null;\n let pdfFound = false;\n for (const att of attachments) {\n const name = String(att.name || '').toLowerCase();\n const ctype = String(att.contentType || '').toLowerCase();\n if (name.endsWith('.pdf') || ctype.includes('pdf')) {\n pdfBase64 = att.contentBytes || null;\n pdfFound = true;\n break;\n }\n }\n\n if (!pdfFound && attachments.length > 0) {\n pdfBase64 = attachments[0].contentBytes || null;\n pdfFound = !!pdfBase64;\n }\n\n results.push({\n json: {\n folderName: folderName,\n folderPath: folderPath,\n fileName: fileName,\n pdfBase64: pdfBase64,\n pdfFound: pdfFound,\n uploadPath: folderPath + '/' + fileName,\n ...row\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "a2946710-5136-491a-af14-4908ffde9934",
"name": "HTTP \u2014 Check Folder Exists",
"type": "n8n-nodes-base.httpRequest",
"position": [
2624,
128
],
"parameters": {
"url": "=https://graph.microsoft.com/v1.0/me/drive/root:/{{ $json.folderPath }}",
"options": {
"response": {
"response": {
"neverError": true,
"fullResponse": true
}
}
},
"authentication": "genericCredentialType",
"genericAuthType": "oAuth2Api"
},
"typeVersion": 4.2
},
{
"id": "fb2ea952-4ab7-47ea-bb5d-c762f391413c",
"name": "IF \u2014 Folder Exists?",
"type": "n8n-nodes-base.if",
"position": [
2800,
128
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "cond-folder-exists",
"operator": {
"type": "number",
"operation": "equals"
},
"leftValue": "={{ $json.statusCode }}",
"rightValue": 200
}
]
}
},
"typeVersion": 2.2
},
{
"id": "0d7b63c3-8025-4011-9948-97aed628e6ce",
"name": "HTTP \u2014 Create Folder",
"type": "n8n-nodes-base.httpRequest",
"position": [
2992,
208
],
"parameters": {
"url": "=https://graph.microsoft.com/v1.0/me/drive/root:/Invoices:/children",
"method": "POST",
"options": {
"response": {
"response": {}
}
},
"jsonBody": "={\n \"name\": \"{{ $('Code \u2014 Build Upload Path').first().json.folderName }}\",\n \"folder\": {},\n \"@microsoft.graph.conflictBehavior\": \"replace\"\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "oAuth2Api",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "08f47013-8547-46a4-83ff-40b680526088",
"name": "Merge \u2014 After Folder Check",
"type": "n8n-nodes-base.merge",
"position": [
3168,
128
],
"parameters": {},
"typeVersion": 3
},
{
"id": "0735256b-a611-42e0-8120-7ae1c0ce2e1a",
"name": "Code \u2014 Decode PDF Binary",
"type": "n8n-nodes-base.code",
"position": [
3360,
128
],
"parameters": {
"jsCode": "const results = [];\nconst uploadItems = $('Code \u2014 Build Upload Path').all();\n\nfor (let i = 0; i < items.length; i++) {\n const uploadData = uploadItems[i] ? uploadItems[i].json : items[i].json;\n const pdfBase64 = uploadData.pdfBase64 || '';\n\n if (!pdfBase64) {\n const emptyBinary = await this.helpers.prepareBinaryData(\n Buffer.from(''), 'blank.pdf', 'application/pdf'\n );\n results.push({\n json: { ...uploadData, pdfBinaryReady: false },\n binary: { pdfFile: emptyBinary }\n });\n continue;\n }\n\n const binaryData = await this.helpers.prepareBinaryData(\n Buffer.from(pdfBase64, 'base64'),\n uploadData.fileName,\n 'application/pdf'\n );\n\n results.push({\n json: { ...uploadData, pdfBinaryReady: true },\n binary: { pdfFile: binaryData }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "bf87db7b-1bd1-4743-88fd-f9df013e01e4",
"name": "HTTP \u2014 Upload PDF",
"type": "n8n-nodes-base.httpRequest",
"position": [
3584,
128
],
"parameters": {
"url": "=https://graph.microsoft.com/v1.0/me/drive/root:/{{ $json.uploadPath }}:/content",
"method": "PUT",
"options": {
"response": {
"response": {}
}
},
"sendBody": true,
"contentType": "binaryData",
"sendHeaders": true,
"authentication": "genericCredentialType",
"genericAuthType": "oAuth2Api",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/pdf"
}
]
},
"inputDataFieldName": "pdfFile"
},
"typeVersion": 4.2
},
{
"id": "eca3ca02-6391-4453-8aa2-28842bee0483",
"name": "Code \u2014 Capture PDF Link",
"type": "n8n-nodes-base.code",
"position": [
3760,
128
],
"parameters": {
"jsCode": "const results = [];\nconst rowItems = $('Code \u2014 Build Upload Path').all();\n\nfor (let i = 0; i < items.length; i++) {\n const uploadResponse = items[i].json;\n const rowData = rowItems[i] ? rowItems[i].json : {};\n \n const pdfWebUrl = uploadResponse.webUrl || uploadResponse['@microsoft.graph.downloadUrl'] || '';\n \n const { folderName, folderPath, fileName, pdfBase64, pdfFound, uploadPath, pdfBinaryReady, vendorNameRaw, issueDateRaw, hasAttachment, ...cleanRow } = rowData;\n \n results.push({\n json: {\n ...cleanRow,\n pdfLink: pdfWebUrl\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "2a130129-d5b3-4ada-825d-62cbb76f2aa0",
"name": "Code \u2014 No Attachment Pass-Through",
"type": "n8n-nodes-base.code",
"position": [
2400,
384
],
"parameters": {
"jsCode": "// No attachment \u2014 pass through all items with empty pdfLink\nreturn items.map(item => {\n const { hasAttachment, vendorNameRaw, issueDateRaw, ...cleanRow } = item.json;\n return { json: { ...cleanRow, pdfLink: '' } };\n});\n"
},
"typeVersion": 2
},
{
"id": "b3375685-e4fe-4110-8833-6e294531e0fd",
"name": "Merge \u2014 Rejoin Attachment Branches",
"type": "n8n-nodes-base.merge",
"position": [
3952,
128
],
"parameters": {},
"typeVersion": 3
},
{
"id": "41a9febe-e02b-4516-8cef-d3967e6a3fd4",
"name": "Outlook \u2014 Mark as Read",
"type": "n8n-nodes-base.microsoftOutlook",
"onError": "continueRegularOutput",
"position": [
4160,
128
],
"parameters": {
"messageId": {
"__rl": true,
"mode": "id",
"value": "={{ $json['Email UID'] }}"
},
"operation": "update",
"updateFields": {
"isRead": true
}
},
"typeVersion": 2
},
{
"id": "6bbd7946-b4d5-472c-bdea-3da67f7f6980",
"name": "Excel \u2014 Append Invoice Row",
"type": "n8n-nodes-base.microsoftExcel",
"position": [
4160,
336
],
"parameters": {
"table": {
"__rl": true,
"mode": "list",
"value": "YOUR_TABLE_ID",
"cachedResultName": "tblInvoices"
},
"options": {},
"fieldsUi": {
"values": [
{
"column": "Invoice Number",
"fieldValue": "={{ $json['Invoice Number'] }}"
},
{
"column": "PO Number",
"fieldValue": "={{ $json['PO Number'] }}"
},
{
"column": "Vendor / Client Name",
"fieldValue": "={{ $json['Vendor / Client Name'] }}"
},
{
"column": "Contact Info",
"fieldValue": "={{ $json['Contact Info'] }}"
},
{
"column": "Issue Date",
"fieldValue": "={{ $json['Issue Date'] }}"
},
{
"column": "Due Date",
"fieldValue": "={{ $json['Due Date'] }}"
},
{
"column": "Subtotal",
"fieldValue": "={{ $json.Subtotal }}"
},
{
"column": "Tax Amount",
"fieldValue": "={{ $json['Tax Amount'] }}"
},
{
"column": "Total Amount Due",
"fieldValue": "={{ $json['Total Amount Due'] }}"
},
{
"column": "Currency",
"fieldValue": "={{ $json.Currency }}"
},
{
"column": "Payment Terms",
"fieldValue": "={{ $json['Payment Terms'] }}"
},
{
"column": "Description",
"fieldValue": "={{ $json.Description }}"
},
{
"column": "Quantity / Hours",
"fieldValue": "={{ $json['Quantity / Hours'] }}"
},
{
"column": "Unit Price / Rate",
"fieldValue": "={{ $json['Unit Price / Rate'] }}"
},
{
"column": "Source Email From",
"fieldValue": "={{ $json['Source Email From'] }}"
},
{
"column": "Source Email Subject",
"fieldValue": "={{ $json['Source Email Subject'] }}"
},
{
"column": "Email Received Date",
"fieldValue": "={{ $json['Email Received Date'] }}"
},
{
"column": "Processed At",
"fieldValue": "={{ $json['Processed At'] }}"
},
{
"column": "EUID",
"fieldValue": "={{ $json['Email UID'] }}"
},
{
"column": "PDF Link",
"fieldValue": "={{ $json.pdfLink || '' }}"
}
]
},
"resource": "table",
"workbook": {
"__rl": true,
"mode": "list",
"value": "YOUR_WORKBOOK_ID",
"cachedResultName": "YOUR_WORKBOOK_NAME"
},
"worksheet": {
"__rl": true,
"mode": "list",
"value": "YOUR_WORKSHEET_ID",
"cachedResultName": "YOUR_WORKSHEET_NAME"
}
},
"typeVersion": 2.2
},
{
"id": "5ac608c8-f054-4dbf-b2e5-0e2574b84353",
"name": "Filter \u2014 Has Invoice Data",
"type": "n8n-nodes-base.filter",
"position": [
4368,
128
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-has-invoice",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $('Code \u2014 Parse Invoice JSON').item.json.invoiceNumber }}"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "ad662b94-827b-45a2-845a-6d43446deaef",
"name": "Code \u2014 Build Digest Email",
"type": "n8n-nodes-base.code",
"position": [
4560,
128
],
"parameters": {
"jsCode": "const today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });\nconst count = items.length;\n\n// === CONFIGURATION ===\n// Paste the direct link to your main OneDrive \"Invoices\" folder between the quotes below:\nconst oneDriveFolderUrl = 'YOUR_ONEDRIVE_FOLDER_URL'; \n// =====================\n\nif (count === 0) {\n return [{ json: {\n subject: 'Invoice Digest - ' + today + ' - No Invoices Found',\n body: '<p>No invoice emails were detected in this run (' + today + ').</p>'\n }}];\n}\n\nconst esc = (s) => String(s == null ? '' : s)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n\nconst formattedRows = $('Set \u2014 Format Excel Row').all();\n\nlet rows = '';\nfor (const it of items) {\n const matchedRow = formattedRows.find(row => row.json['Email UID'] === it.json.id);\n const r = matchedRow ? matchedRow.json : it.json;\n\n const inv = r['Invoice Number'] || 'N/A';\n const vendor = r['Vendor / Client Name'] || 'N/A';\n const issue = r['Issue Date'] || 'N/A';\n const due = r['Due Date'] || 'N/A';\n const from = r['Source Email From'] || 'N/A';\n \n let total = 'N/A';\n if (r['Total Amount Due']) {\n const amount = parseFloat(String(r['Total Amount Due']).replace(/[^0-9.-]+/g, ''));\n const currencyCode = (r['Currency'] || 'USD').trim().toUpperCase();\n if (!isNaN(amount)) {\n try {\n total = new Intl.NumberFormat('en-US', { style: 'currency', currency: currencyCode }).format(amount);\n } catch (e) {\n total = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);\n }\n } else {\n total = r['Total Amount Due']; \n }\n }\n\n rows +=\n '<tr>' +\n '<td style=\"padding:6px 8px;border:1px solid #ddd;\">' + esc(inv) + '</td>' +\n '<td style=\"padding:6px 8px;border:1px solid #ddd;\">' + esc(vendor) + '</td>' +\n '<td style=\"padding:6px 8px;border:1px solid #ddd;\">' + esc(issue) + '</td>' +\n '<td style=\"padding:6px 8px;border:1px solid #ddd;\">' + esc(due) + '</td>' +\n '<td style=\"padding:6px 8px;border:1px solid #ddd;text-align:right;\">' + esc(total) + '</td>' +\n '<td style=\"padding:6px 8px;border:1px solid #ddd;font-size:12px;\">' + esc(from) + '</td>' +\n '</tr>';\n}\n\nconst body =\n '<!DOCTYPE html><html><body style=\"font-family:Arial,sans-serif;color:#222;\">\\n' +\n '<h2 style=\"color:#002657;\">Invoice Digest - ' + today + '</h2>\\n' +\n '<p><strong>' + count + ' invoice(s)</strong> were detected, extracted, and logged to OneDrive today.</p>\\n' +\n '<table style=\"border-collapse:collapse;width:100%;font-size:13px;\">\\n' +\n '<thead><tr style=\"background:#002657;color:#fff;\">\\n' +\n '<th style=\"padding:8px;border:1px solid #ddd;\">Invoice #</th>\\n' +\n '<th style=\"padding:8px;border:1px solid #ddd;\">Vendor / Client</th>\\n' +\n '<th style=\"padding:8px;border:1px solid #ddd;\">Issue Date</th>\\n' +\n '<th style=\"padding:8px;border:1px solid #ddd;\">Due Date</th>\\n' +\n '<th style=\"padding:8px;border:1px solid #ddd;\">Total Due</th>\\n' +\n '<th style=\"padding:8px;border:1px solid #ddd;\">Source Email</th>\\n' +\n '</tr></thead>\\n' +\n '<tbody>' + rows + '</tbody>\\n' +\n '</table>\\n' +\n '<div style=\"text-align: center; margin-top: 35px; margin-bottom: 25px;\">\\n' +\n ' <a href=\"' + oneDriveFolderUrl + '\" style=\"background-color: #002657; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold; display: inline-block; font-size: 14px;\">Open OneDrive Folder</a>\\n' +\n '</div>\\n' +\n '<p style=\"margin-top:20px;font-size:12px;color:#888;\">Processed emails have been marked as read.</p>\\n' +\n '</body></html>';\n\nreturn [{ json: {\n subject: 'Invoice Digest - ' + today + ' - ' + count + ' Invoice(s) Logged',\n body: body\n}}];"
},
"typeVersion": 2
},
{
"id": "27caa89e-e970-4128-88b0-90b3c3064c29",
"name": "Outlook \u2014 Send Daily Digest",
"type": "n8n-nodes-base.microsoftOutlook",
"position": [
4752,
128
],
"parameters": {
"subject": "={{ $json.subject }}",
"bodyContent": "={{ $json.body }}",
"toRecipients": "user@example.com",
"additionalFields": {
"bodyContentType": "html"
}
},
"typeVersion": 2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "b738b69e-8fd1-4e69-87d3-5527b6e3f4f8",
"connections": {
"HTTP \u2014 Upload PDF": {
"main": [
[
{
"node": "Code \u2014 Capture PDF Link",
"type": "main",
"index": 0
}
]
]
},
"IF \u2014 Folder Exists?": {
"main": [
[
{
"node": "Merge \u2014 After Folder Check",
"type": "main",
"index": 0
}
],
[
{
"node": "HTTP \u2014 Create Folder",
"type": "main",
"index": 0
}
]
]
},
"HTTP \u2014 Create Folder": {
"main": [
[
{
"node": "Merge \u2014 After Folder Check",
"type": "main",
"index": 1
}
]
]
},
"IF \u2014 Has Attachment?": {
"main": [
[
{
"node": "HTTP \u2014 Get Attachments",
"type": "main",
"index": 0
}
],
[
{
"node": "Code \u2014 No Attachment Pass-Through",
"type": "main",
"index": 0
}
]
]
},
"HTTP \u2014 Get Attachments": {
"main": [
[
{
"node": "Code \u2014 Build Upload Path",
"type": "main",
"index": 0
}
]
]
},
"Outlook \u2014 Mark as Read": {
"main": [
[
{
"node": "Filter \u2014 Has Invoice Data",
"type": "main",
"index": 0
}
]
]
},
"Set \u2014 Format Excel Row": {
"main": [
[
{
"node": "IF \u2014 Has Attachment?",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 Capture PDF Link": {
"main": [
[
{
"node": "Merge \u2014 Rejoin Attachment Branches",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 Build Upload Path": {
"main": [
[
{
"node": "HTTP \u2014 Check Folder Exists",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 Decode PDF Binary": {
"main": [
[
{
"node": "HTTP \u2014 Upload PDF",
"type": "main",
"index": 0
}
]
]
},
"Schedule \u2014 Daily Trigger": {
"main": [
[
{
"node": "Outlook \u2014 Get Unread Emails",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 Build Digest Email": {
"main": [
[
{
"node": "Outlook \u2014 Send Daily Digest",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 Parse Invoice JSON": {
"main": [
[
{
"node": "Set \u2014 Format Excel Row",
"type": "main",
"index": 0
}
]
]
},
"Filter \u2014 Has Invoice Data": {
"main": [
[
{
"node": "Code \u2014 Build Digest Email",
"type": "main",
"index": 0
}
]
]
},
"HTTP \u2014 Check Folder Exists": {
"main": [
[
{
"node": "IF \u2014 Folder Exists?",
"type": "main",
"index": 0
}
]
]
},
"Merge \u2014 After Folder Check": {
"main": [
[
{
"node": "Code \u2014 Decode PDF Binary",
"type": "main",
"index": 0
}
]
]
},
"AI \u2014 Extract Invoice Fields": {
"main": [
[
{
"node": "Code \u2014 Parse Invoice JSON",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 Prepare Invoice Text": {
"main": [
[
{
"node": "AI \u2014 Extract Invoice Fields",
"type": "main",
"index": 0
}
]
]
},
"OpenAI \u2014 GPT-4.1-mini Model": {
"ai_languageModel": [
[
{
"node": "AI \u2014 Extract Invoice Fields",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Outlook \u2014 Get Unread Emails": {
"main": [
[
{
"node": "Code \u2014 Prepare Invoice Text",
"type": "main",
"index": 0
}
]
]
},
"Code \u2014 No Attachment Pass-Through": {
"main": [
[
{
"node": "Merge \u2014 Rejoin Attachment Branches",
"type": "main",
"index": 1
}
]
]
},
"Merge \u2014 Rejoin Attachment Branches": {
"main": [
[
{
"node": "Outlook \u2014 Mark as Read",
"type": "main",
"index": 0
},
{
"node": "Excel \u2014 Append Invoice Row",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
> Automatically extract, log, and archive invoices from your Outlook inbox using GPT-4.1-mini, OneDrive, and Excel — no manual data entry required.
Source: https://n8n.io/workflows/15723/ — 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.
It's scheduled to run once a day and sends the newsletter directly to subscriber via a nicely formatted email. If you've had trouble keeping up with the latest and greatest templates beign published d
3446. Uses microsoftExcel, lmChatOpenAi, chainLlm, httpRequest. Scheduled trigger; 33 nodes.
Complete PostgreSQL-backed system: Keyword scoring → AI research → Multi-part content generation → fal.ai Nano Banana image generation → WordPress publishing
Automate Microsoft Teams Meeting Analysis with GPT-4.1, Outlook & Mem.ai Watch the YouTube video to get started Follow along with the blog post
This workflow automates the creation, rendering, approval, and posting of TikTok-style POV (Point of View) videos to Instagram, with cross-posting to Facebook and YouTube. It eliminates manual video p