This workflow corresponds to n8n.io template #7514 — we link there as the canonical source.
This workflow follows the Gmail → Google Sheets 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": "sbTxG7FVDyFq0sLR",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "GSheets CRM",
"tags": [],
"nodes": [
{
"id": "c0760f3c-0feb-43bb-9b52-cc9307e0b98f",
"name": "Webhook: Lead Stage Changed",
"type": "n8n-nodes-base.webhook",
"notes": "Receives JSON when a lead's Stage (Leads!E) changes.\nExpected body keys:\n- name (Leads!A)\n- email (Leads!C)\n- source_id (for mapping 1=Instagram,2=Facebook) OR source_text\n- stage (new stage text)\n- previous_stage (optional)",
"position": [
-896,
-80
],
"parameters": {
"path": "lead-stage-changed",
"options": {
"responseData": "OK"
},
"httpMethod": "POST"
},
"typeVersion": 1
},
{
"id": "a91971ec-9414-4d0d-98d9-273761071c30",
"name": "Prepare Email Fields",
"type": "n8n-nodes-base.set",
"notes": "Derives firstName and sourcePlatform for the email body.",
"position": [
-672,
-80
],
"parameters": {
"fields": {
"values": [
{
"name": "firstName",
"stringValue": "=={{ $json.body.name ? $json.body.name.trim().split(' ')[0] : 'there' }}"
},
{
"name": "sourcePlatform ",
"stringValue": "={{ $json.body.source_text || 'Unknown' }}"
},
{
"name": "stage",
"stringValue": "={{ $json.body.stage }}"
},
{
"name": "previousStage",
"stringValue": "={{ $json.body.previous_stage || '' }}"
}
]
},
"options": {
"includeBinary": false
}
},
"typeVersion": 3
},
{
"id": "00ef748d-22e8-4436-a0e7-7ef1ed9017fe",
"name": "Gmail: Send Stage-Change Email",
"type": "n8n-nodes-base.gmail",
"notes": "Sends the templated email on any Stage change.",
"position": [
-224,
-176
],
"parameters": {
"sendTo": "={{ $('Prepare Email Fields').item.json.body.email }}",
"message": "=Hi {{ $('Prepare Email Fields').item.json.body.name }},<br><br>Thanks for the call earlier & happy I was able to meet you. Just letting you know that I'll work on a proposal immediately and have something over to you in the next 2-3 hours.<br><br>Stay tuned & let me know if you have any questions.<br><br>Thanks,<br>{{SENDER_NAME}}",
"options": {},
"subject": "Next Steps"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "baa49e6a-3c2e-4c42-ae96-9b2f0e5a0db3",
"name": "IF: Stage == Won",
"type": "n8n-nodes-base.if",
"notes": "Branch for when the lead has been marked Won.",
"position": [
-224,
16
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "5feb2d51-b3ba-4b5e-9064-4735e10d5a38",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.stage }}",
"rightValue": "Won"
}
]
}
},
"typeVersion": 2
},
{
"id": "855b796a-8588-4f43-8b39-48406c37c67d",
"name": "Format Start Timestamp",
"type": "n8n-nodes-base.dateTime",
"notes": "Produces Start Date & Time as: dd/mm/yyyy at HH:MM",
"position": [
0,
16
],
"parameters": {
"value": "={{ $now }}",
"custom": true,
"options": {},
"toFormat": "DD/MM/YYYY 'at' HH:mm"
},
"typeVersion": 1
},
{
"id": "25f0b4f3-0305-4ce6-83f8-e37fc90d51ca",
"name": "GS: Clients Append (On Won)",
"type": "n8n-nodes-base.googleSheets",
"notes": "Appends the new client with Project Status=In Progress and Start Date & Time.",
"position": [
224,
16
],
"parameters": {
"columns": {
"value": {
"Name": "={{ $json.body.name }}",
"Client Email": "={{ $json.body.email }}",
"Start Date & Time": "={{ $json.data }}"
},
"schema": [
{
"id": "Name",
"type": "string",
"display": true,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Client Type",
"type": "string",
"display": true,
"required": false,
"displayName": "Client Type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Client Email",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Client Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Project Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Project Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tenure",
"type": "string",
"display": true,
"required": false,
"displayName": "Tenure",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Start Date & Time",
"type": "string",
"display": true,
"required": false,
"displayName": "Start Date & Time",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "End Date & Time",
"type": "string",
"display": true,
"required": false,
"displayName": "End Date & Time",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Time to Deliver",
"type": "string",
"display": true,
"required": false,
"displayName": "Time to Deliver",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {
"useAppend": true,
"cellFormat": "USER_ENTERED"
},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "{{CLIENTS_GID}}",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/{{GOOGLE_SHEETS_DOC_ID}}/edit#gid={{CLIENTS_GID}}",
"cachedResultName": "Clients"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "{{GOOGLE_SHEETS_DOC_ID}}",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/{{GOOGLE_SHEETS_DOC_ID}}/edit",
"cachedResultName": "{{SHEET_NAME}}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "100c9018-23b7-4724-93ef-373566ae9c42",
"name": "Webhook: Lead Qualified?",
"type": "n8n-nodes-base.webhook",
"notes": "Receives JSON when Leads!H (Qualified?) is checked.\nExpected body keys:\n- name (A)\n- email (C)\n- qualified (boolean or 'TRUE'/1)",
"position": [
-896,
-624
],
"parameters": {
"path": "lead-qualified",
"options": {
"responseData": "OK"
},
"httpMethod": "POST"
},
"typeVersion": 1
},
{
"id": "e5fe57ce-c457-4f03-9c96-7ac385b2a42f",
"name": "IF: Is Qualified",
"type": "n8n-nodes-base.if",
"position": [
-672,
-624
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "285b4bc9-5a4b-4f6b-bd55-eb54d6a6fcc1",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.body.qualified }}",
"rightValue": "=\"true\""
}
]
}
},
"typeVersion": 2
},
{
"id": "04851d87-b2c6-4383-9687-382f3bee1e7b",
"name": "Gmail: Send Cal.com Invite",
"type": "n8n-nodes-base.gmail",
"notes": "Sends the booking link when Qualified? is checked.",
"position": [
-448,
-624
],
"parameters": {
"sendTo": "={{ $json.body.email }}",
"message": "=Hi {{$json.body.name ? $json.body.name.split(' ')[0] : 'there'}},<br><br>Congrats \u2014 you\u2019re qualified and we are super excited to know more from you! Please book your discovery call here: <a href=\"{{CAL_COM_BOOKING_URL}}\" target=\"_blank\">Schedule on Cal.com</a>.<br><br>Thanks,<br>{{SENDER_NAME}}",
"options": {},
"subject": "Book Your Discovery Call"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "99f6f896-ac20-454b-a480-88a71aabe0b0",
"name": "GS: Leads Update Stage to Meeting Booked",
"type": "n8n-nodes-base.googleSheets",
"notes": "Uses Email (Column C) as key to update Stage (E) to 'Meeting Booked'.",
"position": [
-672,
-368
],
"parameters": {
"columns": {
"value": {
"Stage": "Meeting Booked",
"Client Email": "={{ $json.body.payload.attendees[0].email }}"
},
"schema": [
{
"id": "Name",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Date Updated",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Date Updated",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Client Email",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Client Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lead Source",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Lead Source",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Stage",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Stage",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Asignee",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Asignee",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Discovery Call URL",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Discovery Call URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Qualitfied?",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Qualitfied?",
"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": [
"Client Email"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {
"cellFormat": "USER_ENTERED"
},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid={{LEADS_GID}}",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/{{GOOGLE_SHEETS_DOC_ID}}/edit#gid={{LEADS_GID}}",
"cachedResultName": "Leads"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "{{GOOGLE_SHEETS_DOC_ID}}",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/{{GOOGLE_SHEETS_DOC_ID}}/edit",
"cachedResultName": "{{SHEET_NAME}}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "b2acf148-b6bc-49d3-8e76-fe890459c1dc",
"name": "Webhook: Client Status Changed",
"type": "n8n-nodes-base.webhook",
"notes": "Receives JSON when Clients!D (Project Status) changes.\nExpected body keys:\n- email (Clients!C)\n- project_status (Clients!D new value)",
"position": [
-880,
304
],
"parameters": {
"path": "client-status-changed",
"options": {
"responseData": "OK"
},
"httpMethod": "POST"
},
"typeVersion": 1
},
{
"id": "01223e18-22c3-4622-ba66-0425aad4bcd6",
"name": "IF: Delivered?",
"type": "n8n-nodes-base.if",
"position": [
-656,
304
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "8be5b372-0114-4595-ab9a-d7903d63a716",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.body.project_status }}",
"rightValue": "Delivered"
}
]
}
},
"typeVersion": 2
},
{
"id": "438ffb35-490f-49d1-9031-c32447b59992",
"name": "GS: Clients Lookup by Email",
"type": "n8n-nodes-base.googleSheets",
"notes": "Fetches the row to read Start Date & Time (F) for duration calc.",
"position": [
-432,
304
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $json.body.email }}",
"lookupColumn": "Client Email"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "{{CLIENTS_GID}}",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/{{GOOGLE_SHEETS_DOC_ID}}/edit#gid={{CLIENTS_GID}}",
"cachedResultName": "Clients"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "{{GOOGLE_SHEETS_DOC_ID}}",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/{{GOOGLE_SHEETS_DOC_ID}}/edit",
"cachedResultName": "{{SHEET_NAME}}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "1579d825-5a67-4e52-b7d5-f4dc493cf790",
"name": "Format End Timestamp",
"type": "n8n-nodes-base.dateTime",
"notes": "End Date & Time formatted as dd/mm/yyyy at HH:MM",
"position": [
-208,
304
],
"parameters": {
"value": "={{ $now }}",
"custom": true,
"options": {},
"toFormat": "DD/MM/YYYY 'at' HH:mm"
},
"typeVersion": 1
},
{
"id": "1aec66f5-ce82-4375-9202-a3ba2a5ad118",
"name": "GS: Clients Update End & Duration",
"type": "n8n-nodes-base.googleSheets",
"notes": "Sets End Date & Time (G) and Time To Deliver (H).",
"position": [
240,
304
],
"parameters": {
"columns": {
"value": {
"Client Email": "={{ $json[\"Client Email\"] }}",
"End Date & Time": "={{ $json.endStr }}",
"Time to Deliver": "={{ $json.duration }}"
},
"schema": [
{
"id": "Name",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Client Type",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Client Type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Client Email",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Client Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Project Status",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Project Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tenure",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Tenure",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Start Date & Time",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Start Date & Time",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "End Date & Time",
"type": "string",
"display": true,
"required": false,
"displayName": "End Date & Time",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Time to Deliver",
"type": "string",
"display": true,
"required": false,
"displayName": "Time to Deliver",
"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": [
"Client Email"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {
"cellFormat": "USER_ENTERED"
},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "{{CLIENTS_GID}}",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/{{GOOGLE_SHEETS_DOC_ID}}/edit#gid={{CLIENTS_GID}}",
"cachedResultName": "Clients"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "{{GOOGLE_SHEETS_DOC_ID}}",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/{{GOOGLE_SHEETS_DOC_ID}}/edit",
"cachedResultName": "{{SHEET_NAME}}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "1f2a3e16-70c7-402f-ace7-782f9de34fd3",
"name": "Code",
"type": "n8n-nodes-base.code",
"position": [
16,
304
],
"parameters": {
"jsCode": "// Compute delivery duration from \"Start Date & Time\" (F) and formatted \"End\" from previous node\n// Expects this node to run AFTER \"Format End Timestamp\" and AFTER the Clients row lookup\n\nconst items = $input.all();\n\n// Helper: parse \"dd/mm/yyyy at HH:MM\"\nfunction parseDT(s) {\n if (!s) return null;\n const m = String(s).match(/(\\d{2})\\/(\\d{2})\\/(\\d{4}).*?(\\d{2}):(\\d{2})/);\n if (!m) return null;\n const [, d, M, y, h, min] = m;\n return new Date(Number(y), Number(M) - 1, Number(d), Number(h), Number(min), 0);\n}\n\n// Get the formatted end timestamp from the Date & Time node\nconst endStrFromNode = $node[\"Format End Timestamp\"]?.json?.data || \"\";\n\nfor (const item of items) {\n const row = item.json || {};\n\n // Try several keys for Start column F (depending on how Google Sheets node labeled it)\n const startStr =\n row[\"Start Date & Time\"] ??\n row[\"F\"] ??\n row[\"Start\"] ??\n \"\";\n\n // Prefer the value from Date & Time node; fall back to any end value already present\n const endStr = endStrFromNode || row[\"End Date & Time\"] || \"\";\n\n const start = parseDT(startStr);\n const end = parseDT(endStr);\n\n let duration = \"N/A\";\n if (start && end) {\n const ms = end - start;\n const days = Math.floor(ms / 86_400_000);\n const hrs = Math.floor((ms % 86_400_000) / 3_600_000);\n const mins = Math.floor((ms % 3_600_000) / 60_000);\n duration = `${days}d ${hrs}h ${mins}m`;\n }\n\n // Attach results to the item\n item.json.endStr = endStr;\n item.json.duration = duration;\n}\n\nreturn items;\n"
},
"typeVersion": 2
},
{
"id": "12c036dc-4e13-4bfd-95d2-48da0e8337b6",
"name": "If",
"type": "n8n-nodes-base.if",
"position": [
-448,
-80
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "37fbb2ef-5861-4fc8-ab06-71fe4364be63",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.stage }}",
"rightValue": "Awaiting Proposal"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "ec815439-bfb1-4edb-8a70-9db1c167a868",
"name": "Webhook: Meeting Booked",
"type": "n8n-nodes-base.webhook",
"position": [
-896,
-368
],
"parameters": {
"path": "cal-booked",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "26784981-b9f6-44ca-8329-62072dd4fd0f",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1232,
-848
],
"parameters": {
"width": 1968,
"height": 1408,
"content": "# Google Sheets Automated CRM"
},
"typeVersion": 1
},
{
"id": "815c2626-dd90-4976-92d7-ce93bad40e1e",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2080,
-848
],
"parameters": {
"color": 6,
"width": 816,
"height": 4128,
"content": "# The App Script Needed (Use this for your Google Worksheet App Script)\n\n/**\n * ScaleFlow CRM \u2014 Google Apps Script bridge to n8n\n * Spreadsheet: \"ScaleFlow CRM\"\n * Tabs: \"Leads\", \"Clients\"\n *\n * WHAT IT DOES\n * - When a Lead\u2019s Stage (Leads!E) changes \u2192 POST to n8n /lead-stage-changed\n * - When Qualified? checkbox (Leads!H) is edited \u2192 POST to n8n /lead-qualified\n * - When a Client\u2019s Project Status (Clients!D) changes \u2192 POST to n8n /client-status-changed\n *\n * STEPS\n * 1) Paste this into Extensions \u2192 Apps Script.\n * 2) Replace the WEBHOOK URLs below with your n8n endpoints.\n * 3) Save. First edit will prompt for authorization. (Optional) Run createInstallableTrigger() once.\n */\n\nconst WEBHOOKS = {\n STAGE_CHANGED: 'https://{{YOUR_N8N_DOMAIN}}/webhook/lead-stage-changed',\n LEAD_QUALIFIED: 'https://{{YOUR_N8N_DOMAIN}}/webhook/lead-qualified',\n CLIENT_STATUS_CHANGED: 'https://{{YOUR_N8N_DOMAIN}}/webhook/client-status-changed',\n};\n\nconst TABS = {\n LEADS: 'Leads',\n CLIENTS: 'Clients',\n};\n\n// Column indexes (1-based)\nconst COL = {\n NAME: 1, // Leads!A\n CLIENT_TYPE: 2, // Clients!B\n EMAIL: 3, // Leads/Clients!C\n LEAD_SOURCE: 4, // Leads!D\n LEAD_STAGE: 5, // Leads!E\n LEAD_ASSIGNEE: 6, // Leads!F\n PROJECT_STATUS: 4,// Clients!D\n QUALIFIED: 8, // Leads!H (checkbox)\n};\n\n/**\n * MAIN: fires on any cell edit\n */\nfunction onEdit(e) {\n try {\n if (!e || !e.range) return;\n // Only process single-cell edits\n if (e.range.getNumRows() !== 1 || e.range.getNumColumns() !== 1) return;\n\n const sheet = e.range.getSheet();\n const tabName = sheet.getName();\n const row = e.range.getRow();\n const col = e.range.getColumn();\n\n // Skip header row\n if (row === 1) return;\n\n if (tabName === TABS.LEADS) {\n handleLeadsEdit_(sheet, row, col, e);\n } else if (tabName === TABS.CLIENTS) {\n handleClientsEdit_(sheet, row, col, e);\n }\n } catch (err) {\n console.error('onEdit error:', err);\n }\n}\n\n/**\n * Handle edits on Leads tab\n */\nfunction handleLeadsEdit_(sheet, row, col, e) {\n // Read row A..H (adjust width if you have more columns)\n const values = sheet.getRange(row, 1, 1, 8).getValues()[0];\n const name = asString_(values[COL.NAME - 1]);\n const email = asString_(values[COL.EMAIL - 1]);\n const sourceText = asString_(values[COL.LEAD_SOURCE - 1]);\n const stageText = asString_(values[COL.LEAD_STAGE - 1]);\n const qualifiedVal = values[COL.QUALIFIED - 1];\n\n // 1) Stage changed (Leads!E)\n if (col === COL.LEAD_STAGE) {\n const payload = {\n name,\n email,\n source_text: sourceText || '',\n stage: stageText || '',\n previous_stage: e.oldValue || '',\n row: row,\n tab: TABS.LEADS,\n };\n postJson_(WEBHOOKS.STAGE_CHANGED, payload);\n }\n\n // 2) Qualified? checkbox edited (Leads!H)\n if (col === COL.QUALIFIED) {\n const payload = {\n name,\n email,\n qualified: isChecked_(qualifiedVal),\n row: row,\n tab: TABS.LEADS,\n };\n postJson_(WEBHOOKS.LEAD_QUALIFIED, payload);\n }\n}\n\n/**\n * Handle edits on Clients tab\n */\nfunction handleClientsEdit_(sheet, row, col, e) {\n // Read row A..H (adjust width if you have more columns)\n const values = sheet.getRange(row, 1, 1, 8).getValues()[0];\n const email = asString_(values[COL.EMAIL - 1]);\n const projectStatus = asString_(values[COL.PROJECT_STATUS - 1]);\n\n // 3) Project Status changed (Clients!D)\n if (col === COL.PROJECT_STATUS) {\n const payload = {\n email,\n project_status: projectStatus || '',\n previous_status: e.oldValue || '',\n row: row,\n tab: TABS.CLIENTS,\n };\n postJson_(WEBHOOKS.CLIENT_STATUS_CHANGED, payload);\n }\n}\n\n/**\n * POST JSON helper\n */\nfunction postJson_(url, obj) {\n if (!url || url.indexOf('{{YOUR_N8N_DOMAIN}}') !== -1) {\n console.warn('Webhook URL not set. Skipping POST:', url, obj);\n return;\n }\n try {\n const res = UrlFetchApp.fetch(url, {\n method: 'post',\n contentType: 'application/json',\n payload: JSON.stringify(obj),\n muteHttpExceptions: true,\n });\n // Optional: log for debugging\n console.log('POST', url, res.getResponseCode(), safeTrunc_(res.getContentText(), 500));\n } catch (err) {\n console.error('postJson_ error:', url, err && err.message);\n }\n}\n\n/**\n * UTILITIES\n */\nfunction isChecked_(val) {\n // Accepts true/false, \"TRUE\"/\"FALSE\", 1/0\n if (val === true) return true;\n if (val === false) return false;\n const s = String(val).trim().toUpperCase();\n return s === 'TRUE' || s === '1' || s === 'CHECKED' || s === 'YES';\n}\n\nfunction asString_(v) {\n return v == null ? '' : String(v);\n}\n\nfunction safeTrunc_(s, n) {\n if (!s) return '';\n return s.length > n ? s.slice(0, n) + '\u2026' : s;\n}\n\n/**\n * OPTIONAL: create an installable onEdit trigger (runs with full auth scope)\n * Run this once if you want to avoid simple-trigger limitations.\n */\nfunction createInstallableTrigger() {\n const ss = SpreadsheetApp.getActive();\n // Clear existing triggers for safety\n ScriptApp.getProjectTriggers().forEach(t => {\n if (t.getHandlerFunction && t.getHandlerFunction() === 'onEdit') {\n ScriptApp.deleteTrigger(t);\n }\n });\n ScriptApp.newTrigger('onEdit').forSpreadsheet(ss).onEdit().create();\n}\n\n/**\n * OPTIONAL: quick test (does not fire onEdit)\n * Replace payloads and run from editor to verify webhooks.\n */\nfunction testPing_() {\n postJson_(WEBHOOKS.STAGE_CHANGED, {\n name: 'Test User',\n email: 'test@example.com',\n source_text: 'Instagram',\n stage: 'Proposal',\n previous_stage: 'Screening',\n row: 2,\n tab: TABS.LEADS,\n });\n\n postJson_(WEBHOOKS.LEAD_QUALIFIED, {\n name: 'Test User',\n email: 'test@example.com',\n qualified: true,\n row: 2,\n tab: TABS.LEADS,\n });\n\n postJson_(WEBHOOKS.CLIENT_STATUS_CHANGED, {\n email: 'client@example.com',\n project_status: 'Delivered',\n previous_status: 'In Progress',\n row: 3,\n tab: TABS.CLIENTS,\n });\n}\n"
},
"typeVersion": 1
},
{
"id": "7b60fbcd-c39a-4313-967a-e7b8310286c1",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-944,
-688
],
"parameters": {
"color": 4,
"width": 848,
"height": 240,
"content": "# Lead Qualification"
},
"typeVersion": 1
},
{
"id": "bff994fa-80d2-4ccd-8964-72fc8a694cc3",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-944,
-448
],
"parameters": {
"color": 4,
"width": 848,
"height": 256,
"content": "# Changing Stage to Meeting Booked\n"
},
"typeVersion": 1
},
{
"id": "ea32e27d-3fc6-4bed-a61d-57ec7aa24fc0",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-944,
-192
],
"parameters": {
"color": 4,
"width": 1440,
"height": 384,
"content": "# Proposal Follow-Up / Client Won Flow\n"
},
"typeVersion": 1
},
{
"id": "5eb61a5b-b07c-42a7-9ff4-d178985e04b2",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-944,
192
],
"parameters": {
"color": 4,
"width": 1440,
"height": 304,
"content": "# Project Fulfillment Duration\n"
},
"typeVersion": 1
}
],
"active": true,
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"errorWorkflow": "{{ERROR_WORKFLOW_ID}}",
"executionOrder": "v1"
},
"versionId": "fbf5f5f3-e31e-4f4d-a480-f8c382bdfe04",
"connections": {
"If": {
"main": [
[
{
"node": "Gmail: Send Stage-Change Email",
"type": "main",
"index": 0
}
],
[
{
"node": "IF: Stage == Won",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "GS: Clients Update End & Duration",
"type": "main",
"index": 0
}
]
]
},
"IF: Delivered?": {
"main": [
[
{
"node": "GS: Clients Lookup by Email",
"type": "main",
"index": 0
}
]
]
},
"IF: Is Qualified": {
"main": [
[
{
"node": "Gmail: Send Cal.com Invite",
"type": "main",
"index": 0
}
]
]
},
"IF: Stage == Won": {
"main": [
[
{
"node": "Format Start Timestamp",
"type": "main",
"index": 0
}
],
[]
]
},
"Format End Timestamp": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Prepare Email Fields": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Format Start Timestamp": {
"main": [
[
{
"node": "GS: Clients Append (On Won)",
"type": "main",
"index": 0
}
]
]
},
"Webhook: Meeting Booked": {
"main": [
[
{
"node": "GS: Leads Update Stage to Meeting Booked",
"type": "main",
"index": 0
}
]
]
},
"Webhook: Lead Qualified?": {
"main": [
[
{
"node": "IF: Is Qualified",
"type": "main",
"index": 0
}
]
]
},
"GS: Clients Lookup by Email": {
"main": [
[
{
"node": "Format End Timestamp",
"type": "main",
"index": 0
}
]
]
},
"Webhook: Lead Stage Changed": {
"main": [
[
{
"node": "Prepare Email Fields",
"type": "main",
"index": 0
}
]
]
},
"Webhook: Client Status Changed": {
"main": [
[
{
"node": "IF: Delivered?",
"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.
gmailOAuth2googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Turn a simple Google Sheet into a lightweight CRM powered by n8n.
Source: https://n8n.io/workflows/7514/ — 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.
Who is this for? Solo founders, sales teams, and event organizers who need email outreach without expensive tools but want full control from Telegram.
This workflow acts as an instant SDR that replies to new inbound leads across multiple channels in real time. It first captures and normalizes all incoming lead data into a unified structure. The work
A comprehensive n8n workflow template for streamlining influencer application processing with real-time social media data validation, intelligent scoring algorithms, and automated onboarding workflows
Note: This template is designed for both self-hosted and cloud-based n8n instances. The workflow image above shows the complete automation flow.
AI Lead Qualification & Follow-Up. Uses httpRequest, slack, googleSheets, gmail. Webhook trigger; 18 nodes.