Invoice Approval Workflow (easybits & Slack)
👋 Hey everyone,
You might remember my friend "Mike" – the one with the small company whose finance person Sarah was manually checking every invoice for duplicates. That duplicate detector workflow I shared saved them so much hassle that Mike called me again last week.
"Our approval process is a mess," he said. "An invoice comes in, I forward it to Sarah, she checks it, emails me back, I reply to approve, she updates a spreadsheet... it's 4 emails and a spreadsheet for every single invoice."
I told him I'd build something – partly to help him out, partly because I wanted to learn how to properly handle Slack interactivity with n8n for future projects. Turns out, catching button clicks from Slack isn't as straightforward as I expected.
The Problem
Mike's approval workflow looked like this:
- Invoice arrives via email
- Sarah extracts the details manually
- She emails Mike (or the CFO for bigger amounts)
- Back-and-forth emails until someone says "approved" or "rejected"
- Sarah updates the master spreadsheet
- Everyone forgets which invoices are still pending
Sound familiar?
The Solution
I built a two-part workflow that turns this into a single Slack button click:
Workflow 1: Invoice → Slack Approval Request
- Upload an invoice (form, email, photo – whatever)
- AI extracts supplier, amount, date, invoice number via easybits' Extractor
- Based on the amount, it assigns an approval tier (🟢 Standard, 🟡 Medium, 🔴 High)
- A rich Slack message gets posted with all the details and three buttons: ✅ Approve, ❌ Reject, 🚩 Flag
Workflow 2: Approval Handler (Slack Listener)
- Listens for button clicks from Slack
- Parses who clicked what
- Routes to the right Google Sheet tab (Approved / Rejected / Flagged)
- Sends a confirmation DM to the approver
Why Two Workflows?
This was my biggest learning: n8n doesn't like multiple triggers in one workflow. I originally tried to have the form trigger AND the Slack webhook in the same workflow. It kept breaking – webhooks wouldn't register properly, triggers would conflict.
The fix? Split them. One workflow sends the Slack message, another listens for the response. They're connected by the Slack message itself – the button click carries all the invoice data back.
If you're building anything with Slack interactivity in n8n, save yourself the headache and plan for two workflows from the start.
The AHA Moment
Mike called me after the first day: "Sarah just approved 12 invoices in 10 minutes. She used to spend an hour on this. Why didn't we do this sooner?"
The answer is always the same – it feels like it should be complicated, but once it's built, you wonder how you ever did it manually.
The Workflows
I've attached both workflow JSONs below. You'll need:
- An easybits Extractor pipeline (free tier works fine for testing)
- A Slack app with
chat:write permission and Interactivity enabled - A Google Sheet with three tabs: Approved, Rejected, Flagged
The sticky notes in each workflow explain the setup step by step.
Workflow 1: Invoice → Slack Approval (powered by easybits)
{ "name": "Invoice → Slack Approval (powered by easybits)", "nodes": [ { "parameters": { "formTitle": "Invoice Upload", "formDescription": "Upload Document", "formFields": { "values": [ { "fieldLabel": "image", "fieldType": "file" } ] }, "options": {} }, "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.5, "position": [ -16, 0 ], "id": "3f67804e-0440-4b23-a073-7305e2e28ae1", "name": "Invoice Upload Form" }, { "parameters": {}, "type": "@easybits/n8n-nodes-extractor.easybitsExtractor", "typeVersion": 2, "position": [ 256, 0 ], "id": "89354feb-0386-401b-b2ae-cebc56255758", "name": "easybits Extractor: Extract Invoice Data" }, { "parameters": { "assignments": { "assignments": [ { "id": "765af92d-48a8-4dcd-921f-90e3f7a24d86", "name": "supplier_name", "value": "={{ $json.data.vendor_name }}", "type": "string" }, { "id": "3c7ea123-d091-4606-8b3d-d079801e1706", "name": "invoice_number", "value": "={{ $json.data.invoice_number }}", "type": "string" }, { "id": "951876e7-a7b6-4d5d-ad6a-d55127810f94", "name": "invoice_date", "value": "={{ $json.data.invoice_date }}", "type": "string" }, { "id": "434b58b3-678f-4fc4-9d3a-8d333f9506de", "name": "total_amount", "value": "={{ $json.data.total_amount }}", "type": "string" }, { "id": "b8e43c31-2d1b-49e7-86dc-83f228488e02", "name": "currency", "value": "EUR", "type": "string" }, { "id": "afe9e84d-d1d8-4197-88de-11583a3a9b5b", "name": "approval_tier", "value": "={{ $json.data.total_amount >= 5000 ? 'high_value' : ($json.data.total_amount >= 1000 ? 'medium_value' : 'low_value') }}", "type": "string" }, { "id": "b7f6bfb5-e8c5-4351-a5ec-4cb95c938d54", "name": "approver_channel", "value": "={{ $json.data.total_amount >= 5000 ? '#finance-leads' : '#invoice-review' }}", "type": "string" }, { "id": "61366345-a58b-4609-ad72-3026014ccff5", "name": "customer_name", "value": "={{ $json.data.customer_name }}", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 528, 0 ], "id": "11e57ed1-cd67-4ab5-9fd9-f9e79ab23966", "name": "Map Invoice Fields" }, { "parameters": { "method": "POST", "url": "https://slack.com/api/chat.postMessage", "authentication": "predefinedCredentialType", "nodeCredentialType": "slackApi", "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"channel\": \"YOUR_CHANNEL_ID\",\n \"text\": \"New invoice: {{ $json.supplier_name }} - {{ $json.currency }} {{ $json.total_amount }}\",\n \"blocks\": [\n {\"type\":\"header\",\"text\":{\"type\":\"plain_text\",\"text\":\"📄 New Invoice for Approval\"}},\n {\"type\":\"section\",\"fields\":[{\"type\":\"mrkdwn\",\"text\":\"*Supplier:*\\n{{ $json.supplier_name }}\"},{\"type\":\"mrkdwn\",\"text\":\"*Invoice #:*\\n{{ $json.invoice_number }}\"},{\"type\":\"mrkdwn\",\"text\":\"*Amount:*\\n{{ $json.currency }} {{ $json.total_amount }}\"},{\"type\":\"mrkdwn\",\"text\":\"*Date:*\\n{{ $json.invoice_date }}\"},{\"type\":\"mrkdwn\",\"text\":\"*Tier:*\\n{{ $json.approval_tier === 'high_value' ? '🔴 High (€5k+)' : $json.approval_tier === 'medium_value' ? '🟡 Medium (€1k-5k)' : '🟢 Standard (<€1k)' }}\"}]},\n {\"type\":\"actions\",\"elements\":[{\"type\":\"button\",\"text\":{\"type\":\"plain_text\",\"text\":\"✅ Approve\"},\"style\":\"primary\",\"value\":\"approved\",\"action_id\":\"invoice_approve\"},{\"type\":\"button\",\"text\":{\"type\":\"plain_text\",\"text\":\"❌ Reject\"},\"style\":\"danger\",\"value\":\"rejected\",\"action_id\":\"invoice_reject\"},{\"type\":\"button\",\"text\":{\"type\":\"plain_text\",\"text\":\"🚩 Flag\"},\"value\":\"flagged\",\"action_id\":\"invoice_flag\"}]}\n ]\n}", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ 800, 0 ], "id": "4aa9ba77-eaeb-44ed-8a8e-87408690989b", "name": "Send to Slack for Approval" }, { "parameters": { "content": "### 🚀 Form for Invoice Upload\nForm accepts invoice uploads (PDF, image)", "height": 288, "width": 256, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ -96, -112 ], "typeVersion": 1, "id": "a3b4ea9f-ea6f-4f81-a229-6d8ca4d90777", "name": "Sticky Note" }, { "parameters": { "content": "### 🤖 Data Extraction\neasybits Extractor pulls: supplier, invoice #, date, amount", "height": 288, "width": 256, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ 176, -112 ], "typeVersion": 1, "id": "ccb4539d-f6b2-4dbf-8161-e4ce7e6f9b6e", "name": "Sticky Note1" }, { "parameters": { "content": "### 📊 Field Mapping\nMaps extracted data + determines approval tier based on amount", "height": 288, "width": 256, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ 448, -112 ], "typeVersion": 1, "id": "e5262e79-eea0-425f-839c-a110bccf519a", "name": "Sticky Note2" }, { "parameters": { "content": "### 💬 Slack Notification\nSends approval request with interactive buttons", "height": 288, "width": 256, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ 720, -112 ], "typeVersion": 1, "id": "9f404edc-a313-4f4f-a184-2a1cc9e8f550", "name": "Sticky Note3" }, { "parameters": { "content": "# 📄 Invoice Approval Workflow\n(powered by easybits + Slack)\n\n## What This Workflow Does\nUpload an invoice (PDF, PNG, or JPEG) via a hosted web form. The file is sent to **easybits Extractor**, which extracts key invoice data (supplier, amount, date, etc.). Based on the amount, an approval tier is assigned. The invoice details are then posted to **Slack** with interactive Approve / Reject / Flag buttons.\n\n## How It Works\n1. **Form Upload** – A user uploads an invoice through the n8n web form\n2. **Extraction via easybits** – The data URI is POSTed to the easybits Extractor, which returns structured invoice data\n3. **Field Mapping** – Extracted fields are mapped + approval tier is calculated based on amount\n4. **Slack Notification** – A message is posted to Slack with invoice details and interactive buttons\n\n## Approval Tiers\n- 🟢 **Standard:** < €1,000\n- 🟡 **Medium:** €1,000 – €5,000\n- 🔴 **High:** > €5,000\n\n---\n\n## Setup Guide\n\n### 1. Create Your easybits Extractor Pipeline\n1. Go to **extractor.easybits.tech** and create a new pipeline\n2. Add the following fields to the mapping:\n - `vendor_name` – The supplier/company name on the invoice\n - `invoice_number` – The invoice reference number\n - `invoice_date` – The date on the invoice\n - `total_amount` – The total amount due (number only)\n - `customer_name` – The recipient/customer name\n3. Copy your **Pipeline ID** and **API Key**\n\n### 2. Connect the Nodes in n8n\n1. Add the **easybits Extractor** node from the n8n community nodes\n2. Enter your **Pipeline ID** and **API Key** as credentials\n3. Create a **Slack API** credential using your Slack Bot Token and assign it to the Slack node\n4. Update the Slack channel ID in the **Send to Slack for Approval** node to your target channel\n\n### 3. Set Up the Slack App\n1. Go to **api.slack.com/apps** and create a new app\n2. Add Bot Token Scopes: `chat:write`, `chat:write.public`\n3. Install the app to your workspace\n4. Copy the **Bot User OAuth Token** (starts with `xoxb-`)\n5. Enable **Interactivity** and set the Request URL to your approval handler webhook\n\n### 4. Activate & Test\n1. Click **Active** in the top-right corner of n8n\n2. Open the form URL and upload a test invoice\n3. Check Slack – you should see the approval message with buttons", "height": 1296, "width": 672 }, "type": "n8n-nodes-base.stickyNote", "position": [ -784, -608 ], "typeVersion": 1, "id": "681da2ff-b940-428f-b5d8-d434ebd0190b", "name": "Sticky Note4" } ], "pinData": {}, "connections": { "Invoice Upload Form": { "main": [ [ { "node": "easybits Extractor: Extract Invoice Data", "type": "main", "index": 0 } ] ] }, "easybits Extractor: Extract Invoice Data": { "main": [ [ { "node": "Map Invoice Fields", "type": "main", "index": 0 } ] ] }, "Map Invoice Fields": { "main": [ [ { "node": "Send to Slack for Approval", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1" }, "tags": [ { "name": "easybits" }, { "name": "Finance" }, { "name": "Invoice Automation" }, { "name": "AI" } ] }
Workflow 2: Invoice Approval Handler (Slack Listener)
{ "name": "Invoice Approval Handler (Slack Listener)", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "invoice-approval-handler", "options": {} }, "id": "f024271f-524f-4ab5-b7fb-43ed93e2bea3", "name": "Receive Slack Button Click", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ -64, 416 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "a493fcd6-0bf3-4887-bd69-4026c14c3d22", "name": "payload", "value": "={{ JSON.parse($json.body.payload) }}", "type": "object" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 208, 416 ], "id": "3c8f8381-a479-4e37-a35a-32f185762422", "name": "Parse Slack Payload" }, { "parameters": { "assignments": { "assignments": [ { "id": "ecc7c2a7-85a2-4bb6-8213-06b409ab9281", "name": "decision", "value": "={{ $json.payload.actions[0].value }}", "type": "string" }, { "id": "d6f8aad0-508f-4a38-99ed-00eb76bcd7f8", "name": "decided_by", "value": "={{ $json.payload.user.name }}", "type": "string" }, { "id": "277dbaef-143b-4603-85d4-b69f1284b3d9", "name": "decided_by_id", "value": "={{ $json.payload.user.id }}", "type": "string" }, { "id": "44951b7f-73c9-48df-b8ce-189bbcaa2112", "name": "channel_id", "value": "={{ $json.payload.channel.id }}", "type": "string" }, { "id": "224e0814-6d1f-4ff8-90d5-a08bb05da837", "name": "message_ts", "value": "={{ $json.payload.message.ts }}", "type": "string" }, { "id": "1d96a304-f9ca-43e1-b1c2-590b8ce92c21", "name": "supplier_name", "value": "={{ $json.payload.message.blocks[1].fields[0].text.split('\\n')[1] }}", "type": "string" }, { "id": "99f3f0a9-b595-4057-ac33-2421cc5b6ca5", "name": "invoice_number", "value": "={{ $json.payload.message.blocks[1].fields[1].text.split('\\n')[1] }}", "type": "string" }, { "id": "fb44a6ec-6441-4c8e-9143-85fe7577211c", "name": "amount", "value": "={{ $json.payload.message.blocks[1].fields[2].text.split('\\n')[1] }}", "type": "string" }, { "id": "3b17ccc8-108a-4586-b6de-2b1685b21539", "name": "invoice_date", "value": "={{ $json.payload.message.blocks[1].fields[3].text.split('\\n')[1] }}", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 480, 416 ], "id": "e22d06ad-d82d-433d-8360-3089e8ad35e4", "name": "Extract Decision & Invoice Data" }, { "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "leftValue": "={{ $json.decision }}", "rightValue": "approved", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Approved" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "leftValue": "={{ $json.decision }}", "rightValue": "rejected", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Rejected" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "leftValue": "={{ $json.decision }}", "rightValue": "flagged", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Flagged" } ] }, "options": {} }, "id": "899c3038-7397-441e-990e-8ef38977fa97", "name": "Route: Approved / Rejected / Flagged", "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [ 752, 400 ] }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "YOUR_GOOGLE_SHEET_ID", "mode": "id" }, "sheetName": { "__rl": true, "value": "Approved", "mode": "name" }, "columns": { "mappingMode": "defineBelow", "value": { "Supplier Name": "={{ $json.supplier_name }}", "Invoice Number": "={{ $json.invoice_number }}", "Amount": "={{ $json.amount }}", "Date": "={{ $json.invoice_date }}" }, "matchingColumns": [], "schema": [ { "id": "Supplier Name", "displayName": "Supplier Name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "Invoice Number", "displayName": "Invoice Number", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "Amount", "displayName": "Amount", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "Date", "displayName": "Date", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true } ] }, "options": {} }, "id": "dfe0a576-d0f1-4b6e-affb-ec93ab80597f", "name": "Log to Sheets: Approved", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.5, "position": [ 1024, 240 ] }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "YOUR_GOOGLE_SHEET_ID", "mode": "id" }, "sheetName": { "__rl": true, "value": "Rejected", "mode": "name" }, "columns": { "mappingMode": "defineBelow", "value": { "Supplier Name": "={{ $json.supplier_name }}", "Invoice Number": "={{ $json.invoice_number }}", "Amount": "={{ $json.amount }}", "Date": "={{ $json.invoice_date }}" }, "matchingColumns": [], "schema": [ { "id": "Supplier Name", "displayName": "Supplier Name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "Invoice Number", "displayName": "Invoice Number", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "Amount", "displayName": "Amount", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "Date", "displayName": "Date", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true } ] }, "options": {} }, "id": "9fb05679-ed5b-44b2-a72a-e2f0f88ea622", "name": "Log to Sheets: Rejected", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.5, "position": [ 1024, 416 ] }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "YOUR_GOOGLE_SHEET_ID", "mode": "id" }, "sheetName": { "__rl": true, "value": "Flagged", "mode": "name" }, "columns": { "mappingMode": "defineBelow", "value": { "Supplier Name": "={{ $json.supplier_name }}", "Invoice Number": "={{ $json.invoice_number }}", "Amount": "={{ $json.amount }}", "Date": "={{ $json.invoice_date }}" }, "matchingColumns": [], "schema": [ { "id": "Supplier Name", "displayName": "Supplier Name", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "Invoice Number", "displayName": "Invoice Number", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "Amount", "displayName": "Amount", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true }, { "id": "Date", "displayName": "Date", "required": false, "defaultMatch": false, "display": true, "type": "string", "canBeUsedToMatch": true } ] }, "options": {} }, "id": "a2d9b4fe-16e0-4514-aa9c-daf65ee080ad", "name": "Log to Sheets: Flagged", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.5, "position": [ 1024, 592 ] }, { "parameters": { "select": "user", "user": { "__rl": true, "value": "YOUR_USER_ID", "mode": "id" }, "text": "=✅ *Invoice Approved & Ready to Pay*\n\n*Supplier:* {{ $json['Supplier Name'] }}\n*Invoice #:* {{ $json['Invoice Number'] }}\n*Amount:* {{ $json.Amount }}\n*Date:* {{ $json.Date }}\n*Approved by:* {{ $('Route: Approved / Rejected / Flagged').item.json.decided_by }}", "otherOptions": {} }, "id": "f444c5fb-0d76-413d-b170-4dd085d668c8", "name": "Slack DM: Approved ✅", "type": "n8n-nodes-base.slack", "typeVersion": 2.3, "position": [ 1296, 240 ] }, { "parameters": { "select": "user", "user": { "__rl": true, "value": "YOUR_USER_ID", "mode": "id" }, "text": "=❌ *Invoice Rejected*\n\n*Supplier:* {{ $json['Supplier Name'] }}\n*Invoice #:* {{ $json['Invoice Number'] }}\n*Amount:* {{ $json.Amount }}\n*Date:* {{ $json.Date }}\n*Rejected by:* {{ $('Route: Approved / Rejected / Flagged').item.json.decided_by }}", "otherOptions": {} }, "id": "58e53f7a-c8f1-4ddd-b8ad-f16691d55cab", "name": "Slack DM: Rejected ❌", "type": "n8n-nodes-base.slack", "typeVersion": 2.3, "position": [ 1296, 416 ] }, { "parameters": { "select": "user", "user": { "__rl": true, "value": "YOUR_USER_ID", "mode": "id" }, "text": "=🚩 *Invoice Flagged for Review*\n\n*Supplier:* {{ $json['Supplier Name'] }}\n*Invoice #:* {{ $json['Invoice Number'] }}\n*Amount:* {{ $json.Amount }}\n*Date:* {{ $json.Date }}\n*Flagged by:* {{ $('Route: Approved / Rejected / Flagged').item.json.decided_by }}\n\n⚠️ Please review this invoice manually.", "otherOptions": {} }, "id": "5473fdea-f831-4da8-8c0d-94fcae527931", "name": "Slack DM: Flagged 🚩", "type": "n8n-nodes-base.slack", "typeVersion": 2.3, "position": [ 1296, 592 ] }, { "parameters": { "content": "### 🔔 Slack Webhook\nReceives button click events from Slack Interactivity", "height": 272, "width": 256, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ -144, 320 ], "typeVersion": 1, "id": "45e5c100-b3c5-430d-b5cb-33c8ba1b7f8a", "name": "Sticky Note" }, { "parameters": { "content": "### 📦 Parse Payload\nConverts Slack's URL-encoded payload into JSON object", "height": 272, "width": 256, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ 128, 320 ], "typeVersion": 1, "id": "6ebf1842-3c96-425a-9cf5-454a491bb489", "name": "Sticky Note1" }, { "parameters": { "content": "### 📋 Extract Decision Data\nPulls decision, user, and invoice details from Slack message", "height": 272, "width": 256, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ 400, 320 ], "typeVersion": 1, "id": "f536f100-b96e-45dd-aa23-8b2a36d7063f", "name": "Sticky Note2" }, { "parameters": { "content": "### 🔀 Route by Decision\nBranches flow: Approved → Rejected → Flagged", "height": 336, "width": 256, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ 672, 288 ], "typeVersion": 1, "id": "0c8770a8-1c00-455d-acb0-b6b6f3f48c5b", "name": "Sticky Note3" }, { "parameters": { "content": "### 📊 Log to Sheets\nAppends invoice data to the appropriate Google Sheets tab", "height": 608, "width": 256, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ 944, 144 ], "typeVersion": 1, "id": "3e58c7fc-8301-490f-acaa-85a013c82ef1", "name": "Sticky Note4" }, { "parameters": { "content": "### 💬 Send Confirmation\nNotifies the approver via Slack DM", "height": 608, "width": 256, "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ 1216, 144 ], "typeVersion": 1, "id": "783c33e9-68a3-4082-9184-a2a4db6f04e4", "name": "Sticky Note5" }, { "parameters": { "content": "# 📥 Invoice Approval Handler\n(Slack Button Listener)\n\n## What This Workflow Does\nListens for button clicks from the **Invoice → Slack Approval** workflow. When someone clicks Approve, Reject, or Flag on an invoice in Slack, this workflow captures that decision, logs it to **Google Sheets**, and sends a confirmation notification.\n\n## How It Works\n1. **Webhook Trigger** – Receives POST request from Slack when a button is clicked\n2. **Parse Payload** – Extracts the JSON payload from Slack's request body\n3. **Extract Decision Data** – Pulls decision, user info, and invoice details from the message\n4. **Route by Decision** – Branches to Approved, Rejected, or Flagged path\n5. **Log to Sheets** – Appends invoice data to the appropriate sheet tab\n6. **Notify via Slack** – Sends confirmation DM to the approver\n\n## Decision Routes\n- ✅ **Approved** → Logs to \"Approved\" sheet → DM confirmation\n- ❌ **Rejected** → Logs to \"Rejected\" sheet → DM confirmation\n- 🚩 **Flagged** → Logs to \"Flagged\" sheet → DM for manual review\n\n---\n\n## Setup Guide\n\n### 1. Create Google Sheet\n1. Create a new Google Sheet with 3 tabs: `Approved`, `Rejected`, `Flagged`\n2. Add these column headers to each tab:\n - Supplier Name\n - Invoice Number\n - Amount\n - Date\n3. Copy the **Sheet ID** from the URL\n\n### 2. Connect the Nodes in n8n\n1. Add your **Google Sheets OAuth2** credential to all three logging nodes\n2. Update the **Document ID** in each Google Sheets node to your Sheet ID\n3. Add your **Slack API** credential to all three notification nodes\n4. Update the **User ID** in the notification nodes (or change to channel)\n\n### 3. Configure Slack Interactivity\n1. Go to **api.slack.com/apps** → your app → **Interactivity & Shortcuts**\n2. Set the Request URL to this workflow's webhook URL\n3. Save Changes\n\n### 4. Activate & Test\n1. Click **Active** in the top-right corner of n8n\n2. Trigger the Invoice Approval workflow to send a Slack message\n3. Click a button in Slack\n4. Check Google Sheets and Slack for results", "height": 1264, "width": 672 }, "type": "n8n-nodes-base.stickyNote", "position": [ -832, -160 ], "typeVersion": 1, "id": "32f0f5bd-897b-49dc-b85f-71dcfe5505a6", "name": "Sticky Note6" } ], "pinData": {}, "connections": { "Receive Slack Button Click": { "main": [ [ { "node": "Parse Slack Payload", "type": "main", "index": 0 } ] ] }, "Parse Slack Payload": { "main": [ [ { "node": "Extract Decision & Invoice Data", "type": "main", "index": 0 } ] ] }, "Extract Decision & Invoice Data": { "main": [ [ { "node": "Route: Approved / Rejected / Flagged", "type": "main", "index": 0 } ] ] }, "Route: Approved / Rejected / Flagged": { "main": [ [ { "node": "Log to Sheets: Approved", "type": "main", "index": 0 } ], [ { "node": "Log to Sheets: Rejected", "type": "main", "index": 0 } ], [ { "node": "Log to Sheets: Flagged", "type": "main", "index": 0 } ] ] }, "Log to Sheets: Approved": { "main": [ [ { "node": "Slack DM: Approved ✅", "type": "main", "index": 0 } ] ] }, "Log to Sheets: Rejected": { "main": [ [ { "node": "Slack DM: Rejected ❌", "type": "main", "index": 0 } ] ] }, "Log to Sheets: Flagged": { "main": [ [ { "node": "Slack DM: Flagged 🚩", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1" }, "tags": [ { "name": "Finance" }, { "name": "Invoice Automation" }, { "name": "Slack" } ] }
Happy to answer questions about the Slack setup – that part definitely took some trial and error. Would love to hear if anyone else has built approval flows and how you handled the multi-trigger problem.
Best,
Felix