This workflow corresponds to n8n.io template #9358 — we link there as the canonical source.
This workflow follows the Airtable → 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": "amCkThATp8WSOqFz",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Revenue Attribution & ROI Calculator",
"tags": [],
"nodes": [
{
"id": "15341b2a-14b2-4341-8b18-8cd1fc5c79f1",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-400,
144
],
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"typeVersion": 1.2
},
{
"id": "639b483f-1347-4361-990a-499d7a985398",
"name": "Search Lead Source",
"type": "n8n-nodes-base.airtable",
"position": [
176,
144
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appeCsmVosYvWkVM3",
"cachedResultUrl": "https://airtable.com/appeCsmVosYvWkVM3",
"cachedResultName": "Contacts"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblPecJqyJn6lmSDj",
"cachedResultUrl": "https://airtable.com/appeCsmVosYvWkVM3/tblPecJqyJn6lmSDj",
"cachedResultName": "Lead Sources"
},
"options": {},
"operation": "search"
},
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "d2904611-761a-4c85-8445-b2f60caf5672",
"name": "Code in JavaScript",
"type": "n8n-nodes-base.code",
"position": [
400,
144
],
"parameters": {
"jsCode": "// @ts-nocheck\n\n// Access data from specific nodes by their names\nconst dealsData = $('Deals').all();\nconst leadSourcesData = $('Search Lead Source').all();\n\n// Create a map for lead sources\nconst sourceMap = new Map();\n\nleadSourcesData.forEach((item) => {\n const fields = item.json.fields || item.json;\n const sourceName = fields['Source Name'] || fields['Lead Source'];\n \n sourceMap.set(sourceName, {\n sourceType: fields['Source Type'],\n cost: parseFloat(fields['Cost per Lead']) || 0,\n totalLeads: parseInt(fields['Total Leads Generated']) || 0\n });\n});\n\nconst sourceRevenue = new Map();\n\ndealsData.forEach((item) => {\n const fields = item.json.fields || item.json;\n const source = fields['Deal Source'] || fields['Lead Source'];\n const revenue = parseFloat(fields['Deal Value']) || 0;\n \n if (!sourceRevenue.has(source)) {\n sourceRevenue.set(source, {\n totalRevenue: 0,\n dealCount: 0,\n source: source\n });\n }\n \n const current = sourceRevenue.get(source);\n current.totalRevenue += revenue;\n current.dealCount += 1;\n});\n\n// Calculate ROI for each source\n/** @type {any[]} */\nconst results = [];\n\nsourceRevenue.forEach((data, sourceName) => {\n const sourceInfo = sourceMap.get(sourceName) || { cost: 0, totalLeads: 0 };\n \n const totalCost = sourceInfo.cost * sourceInfo.totalLeads;\n const roi = totalCost > 0 ? ((data.totalRevenue - totalCost) / totalCost) * 100 : 0;\n const avgDealSize = data.dealCount > 0 ? data.totalRevenue / data.dealCount : 0;\n const conversionRate = sourceInfo.totalLeads > 0 ? (data.dealCount / sourceInfo.totalLeads) * 100 : 0;\n \n results.push({\n json: {\n source: sourceName,\n totalRevenue: data.totalRevenue,\n dealCount: data.dealCount,\n avgDealSize: avgDealSize,\n totalCost: totalCost,\n roi: parseFloat(roi.toFixed(2)),\n conversionRate: parseFloat(conversionRate.toFixed(2)),\n updated_at: new Date().toISOString()\n }\n });\n});\n\n// Sort by ROI descending\nresults.sort((a, b) => b.json.roi - a.json.roi);\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "0542491a-d85d-4431-8ac0-fa8278a58088",
"name": "Deals",
"type": "n8n-nodes-base.airtable",
"position": [
-80,
144
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appeCsmVosYvWkVM3",
"cachedResultUrl": "https://airtable.com/appeCsmVosYvWkVM3",
"cachedResultName": "Contacts"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblJlUYIVVJrYrUZl",
"cachedResultUrl": "https://airtable.com/appeCsmVosYvWkVM3/tblJlUYIVVJrYrUZl",
"cachedResultName": "Deals"
},
"options": {},
"operation": "search",
"filterByFormula": "=AND({Stage} = \"Closed Won\", IS_AFTER({Actual Close Date}, DATEADD(NOW(), -30, 'days')))"
},
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "e92d2b3c-16ac-4979-8368-4208b533bab0",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
624,
144
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "fbe7af8c-9d26-4f68-a78b-f35ece1c958e",
"name": "Search records",
"type": "n8n-nodes-base.airtable",
"position": [
832,
160
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appeCsmVosYvWkVM3",
"cachedResultUrl": "https://airtable.com/appeCsmVosYvWkVM3",
"cachedResultName": "Contacts"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblPecJqyJn6lmSDj",
"cachedResultUrl": "https://airtable.com/appeCsmVosYvWkVM3/tblPecJqyJn6lmSDj",
"cachedResultName": "Lead Sources"
},
"options": {},
"operation": "search",
"filterByFormula": "={Source Type} = \"{{ $json.source }}\""
},
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "85233782-5af1-44e7-80e6-4385c5c0f279",
"name": "Update record",
"type": "n8n-nodes-base.airtable",
"position": [
1040,
160
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appeCsmVosYvWkVM3",
"cachedResultUrl": "https://airtable.com/appeCsmVosYvWkVM3",
"cachedResultName": "Contacts"
},
"table": {
"__rl": true,
"mode": "list",
"value": "YOUR_AIRTABLE_VALUE",
"cachedResultUrl": "https://airtable.com/appeCsmVosYvWkVM3/tblPecJqyJn6lmSDj",
"cachedResultName": "Lead Sources"
},
"columns": {
"value": {
"id": "={{ $json.id }}",
"Cost per Lead": "={{ $json['Cost per Lead'] }}",
"Conversion rate": "={{ $json['Conversion rate'] }}",
"Total Leads Generated": "={{ $json['Total Leads Generated'] }}",
"Total Revenue Attributed": "={{ $json['Total Revenue Attributed'] }}"
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "id",
"defaultMatch": true
},
{
"id": "Source Name",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Source Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Source Type",
"type": "options",
"display": true,
"options": [
{
"name": "Paid Advertising",
"value": "Paid Advertising"
},
{
"name": "Organic Search",
"value": "Organic Search"
},
{
"name": "Referral ",
"value": "Referral "
},
{
"name": "Direct Traffic",
"value": "Direct Traffic"
},
{
"name": "Social Media",
"value": "Social Media"
},
{
"name": "Email Marketing",
"value": "Email Marketing"
},
{
"name": "Content Marketing",
"value": "Content Marketing"
},
{
"name": "Events/Trade Shows",
"value": "Events/Trade Shows"
}
],
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Source Type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Cost per Lead",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Cost per Lead",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Conversion rate",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Conversion rate",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Total Leads Generated",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Total Leads Generated",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Total Revenue Attributed",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Total Revenue Attributed",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ROI",
"type": "string",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "ROI",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"id"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update"
},
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "3e7d6497-552d-4639-a911-23ce6412ee90",
"name": "Send a message",
"type": "n8n-nodes-base.slack",
"position": [
1264,
160
],
"parameters": {
"text": "=\ud83d\udcca Weekly Revenue Attribution Report - {{ new Date().toLocaleDateString() }} \n\nSource: {{ $json.fields['Source Type'] }}\nRevenue: ${{ $json.fields['Total Revenue Attributed'] }}\nROI: {{ $json.fields.ROI }}% \nConversion: {{ $json.fields['Conversion rate'] }}%",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09JRB0G41J",
"cachedResultName": "sales-analytics"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "9eb3a37c-bb4b-4c82-967a-d0c365456eea",
"name": "Send a message1",
"type": "n8n-nodes-base.gmail",
"position": [
1472,
160
],
"parameters": {
"sendTo": "user@example.com",
"message": "=Hi Management,<br><br> \n\nIncase you missed it, an update was sent to the sales-analytics Channel.<br><br>\n\nRegards.",
"options": {
"appendAttribution": false
},
"subject": "Sales Analytics Update"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "b9f57a73-014e-4364-ac84-e51895d8f7d7",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1072,
-784
],
"parameters": {
"color": 2,
"width": 752,
"height": 752,
"content": "# \ud83d\udcca REVENUE ATTRIBUTION & ROI CALCULATOR\n\nThis workflow calculates which lead sources generate actual revenue.\n\nRuns: Daily at midnight\nDuration: ~2-3 minutes\nPurpose: Update Lead Sources table with ROI metrics\n\n## Requirements:\n\u2705 Airtable base with Deals & Lead Sources tables\n\u2705 Closed Won deals from last 30 days\n\u2705 Cost data for each marketing source\n\n## What gets calculated:\n- Total revenue per source\n- ROI percentage\n- Conversion rates\n- Average deal size\n\n## \ud83d\ude80 Getting Started Checklist\n\n- [ ] **Gmail account** with API access enabled\n- [ ] **Airtable Account [https://airtable.com/]**\n- [ ] **N8N instance** running\n- [ ] **Slack workspace [https://slack.com/intl/en-gb/]** \n- [ ] **Import this workflow** and configure credentials"
},
"typeVersion": 1
},
{
"id": "68282373-6e14-4b27-8e7c-17caf82c1dc9",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-192,
-352
],
"parameters": {
"width": 512,
"height": 592,
"content": "## \ud83d\udce5 STEP 1 & 2: COLLECT DATA FROM AIRTABLE\n\nNODE 1 - Fetch Closed Won Deals:\nGets deals from last 30 days where Stage = \"Closed Won\"\n\nRequired fields:\n- Stage (Single select)\n- Deal Value (Currency)\n- Deal Source (Single select)\n- Actual Close Date (Date)\n\nNODE 2 - Fetch Lead Sources:\nGets all marketing sources with cost data\n\nRequired fields:\n- Source Name (Single line text)\n- Cost per Lead (Currency)\n- Total Leads Generated (Number)\n\n\u26a0\ufe0f Deal Source values must EXACTLY match Source Name\n (case-sensitive, no extra spaces)"
},
"typeVersion": 1
},
{
"id": "91441ad1-38f5-4b5b-a103-66f5b769d3d4",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
352,
-336
],
"parameters": {
"color": 4,
"width": 432,
"height": 560,
"content": "## \ud83e\uddee STEP 3 & 4: CALCULATE ROI & PROCESS\n\nNODE 3 - Code: Calculate Metrics\nFor each source, calculates:\n- Total Revenue = Sum of Deal Values\n- Total Cost = Cost per Lead \u00d7 Total Leads \n- ROI = ((Revenue - Cost) / Cost) \u00d7 100\n- Conversion Rate = (Deals / Leads) \u00d7 100\n- Avg Deal Size = Revenue / Deal Count\n\nNODE 4 - Loop: Process Each Source\nSplits results array to update sources individually\n\n\ud83d\udd27 CUSTOMIZATION:\n- Edit ROI formula in Code node (line 35-42)\n- Change sorting logic (line 50-55)\n- Adjust date range in Deals node filter\n\n\ud83d\udca1 Add // @ts-nocheck at top of Code if errors appear"
},
"typeVersion": 1
},
{
"id": "f992e5a9-3a68-4b07-86a2-12b07ea3e417",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
816,
-272
],
"parameters": {
"color": 6,
"width": 800,
"height": 464,
"content": "## UPDATE DATA & SEND REPORT\n\nSearches to ensure the lead source exists, if it doesn't exist, it gets skipped to the next. \nUpdate record node updates the existing lead source record with the calculated metrics.\n\nFields updated:\n- Total Revenue Attributed\n- Conversion Rate\n- ROI\n\nNotifications (OPTIONAL):\nSlack sends report of top 3 performing sources to the #sales-analytics channel.\nGmail sends a personal mail to the CEO, incase the slack analytics report was missed.\n\n\n\u26a0\ufe0f Field names must match Airtable EXACTLY\n (case-sensitive)\n\n\ud83d\udca1 Remove Slack and email nodes if not needed."
},
"typeVersion": 1
},
{
"id": "77f9cd46-e398-4151-a549-4d929979a703",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
1664,
-496
],
"parameters": {
"width": 432,
"height": 640,
"content": "## \u26a0\ufe0f TROUBLESHOOTING GUIDE\n\nISSUE: No deals returned from first node\nFIX: \n- Check date filter (last 30 days)\n- Verify \"Closed Won\" spelling in Stage\n- Ensure Actual Close Date is filled\n\nISSUE: Code node errors\nFIX:\n- Add // @ts-nocheck at top of code\n- Check field names match Airtable (case-sensitive)\n- Verify numeric fields contain numbers not text\n\nISSUE: Updates not writing\nFIX:\n- Check Airtable API permissions\n- Verify Record ID mapping in Update node\n- Confirm field names match exactly\n\nISSUE: Wrong calculations\nFIX:\n- Verify Cost per Lead and Total Leads have data\n- Check Deal Source matches Source Name exactly\n- Review Code node logic (line 35-55)\n\n\ud83d\udce7 Need help? Full docs: https://www.notion.so/RevOps-Intelligence-Hub-Complete-Documentation-282ca81d5bda804a9ccdfc974b6f089d?source=copy_link"
},
"typeVersion": 1
},
{
"id": "3265038a-842d-47fc-bb2b-8c604709f009",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-496,
80
],
"parameters": {
"color": 3,
"width": 256,
"height": 224,
"content": "## Start Here"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "08aa22ad-e397-4ad5-9cb3-03743078508f",
"connections": {
"Deals": {
"main": [
[
{
"node": "Search Lead Source",
"type": "main",
"index": 0
}
]
]
},
"Update record": {
"main": [
[
{
"node": "Send a message",
"type": "main",
"index": 0
}
]
]
},
"Search records": {
"main": [
[
{
"node": "Update record",
"type": "main",
"index": 0
}
]
]
},
"Send a message": {
"main": [
[
{
"node": "Send a message1",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[
{
"node": "Search records",
"type": "main",
"index": 0
}
],
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Deals",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Search Lead Source": {
"main": [
[
{
"node": "Code in JavaScript",
"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.
airtableTokenApigmailOAuth2slackOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Use Case: automates marketing ROI tracking by linking closed deals to their lead sources in Airtable, calculating revenue and ROI per channel, and sending daily insights to Slack. Runs nightly to analyze closed deals from last 30 days Matches deals to their original lead sources…
Source: https://n8n.io/workflows/9358/ — 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.
Watch target companies for C-level and VP hiring signals, then send AI-personalized outreach emails when leadership roles are posted.
Monitor customers for competitor tech adoption via PredictLeads and alert CSMs to prevent churn.
This workflow is designed for marketing teams, data analysts, and business owners who need to consistently track key performance indicators (KPIs). It saves hours of manual data collection and reporti
Real estate leads move fast. Manually matching buyer requirements with available properties, sending details, updating CRM records, and notifying sales teams often causes delays and missed opportuniti
Teams that monitor traffic, signups, or conversions in Google Analytics 4 and want automatic Slack/email alerts when a channel suddenly spikes or drops.