GSheets CRM
Ceci est unCRM, Multimodal AIworkflow d'automatisation du domainecontenant 24 nœuds.Utilise principalement des nœuds comme If, Set, Code, Gmail, Webhook. Pipeline automatisé prospect-client avec notifications par e-mail Google Sheets et suivi du temps
- •Compte Google et informations d'identification Gmail API
- •Point de terminaison HTTP Webhook (généré automatiquement par n8n)
- •Informations d'identification Google Sheets API
Nœuds utilisés (24)
Catégorie
{
"id": "sbTxG7FVDyFq0sLR",
"meta": {
"instanceId": "{{INSTANCE_ID}}",
"templateCredsSetupCompleted": true
},
"name": "GSheets CRM",
"tags": [],
"nodes": [
{
"id": "c0760f3c-0feb-43bb-9b52-cc9307e0b98f",
"name": "Webhook: Étape du prospect modifiée",
"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
],
"webhookId": "{{WEBHOOK_ID_LEAD_STAGE_CHANGED}}",
"parameters": {
"path": "lead-stage-changed",
"options": {
"responseData": "OK"
},
"httpMethod": "POST"
},
"typeVersion": 1
},
{
"id": "a91971ec-9414-4d0d-98d9-273761071c30",
"name": "Préparer les champs d'email",
"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: Envoyer l'email de changement d'étape",
"type": "n8n-nodes-base.gmail",
"notes": "Sends the templated email on any Stage change.",
"position": [
-224,
-176
],
"webhookId": "{{GMAIL_NODE_WEBHOOK_ID_STAGE_CHANGE}}",
"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": {
"id": "{{GMAIL_CREDENTIAL_ID}}",
"name": "{{GMAIL_CREDENTIAL_NAME}}"
}
},
"typeVersion": 2
},
{
"id": "baa49e6a-3c2e-4c42-ae96-9b2f0e5a0db3",
"name": "SI: Étape == Gagné",
"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": "Formater l'horodatage de début",
"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: Ajouter client (si gagné)",
"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": {
"id": "{{GSHEETS_CREDENTIAL_ID}}",
"name": "{{GSHEETS_CREDENTIAL_NAME}}"
}
},
"typeVersion": 4
},
{
"id": "100c9018-23b7-4724-93ef-373566ae9c42",
"name": "Webhook: Prospect qualifié ?",
"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
],
"webhookId": "{{WEBHOOK_ID_LEAD_QUALIFIED}}",
"parameters": {
"path": "lead-qualified",
"options": {
"responseData": "OK"
},
"httpMethod": "POST"
},
"typeVersion": 1
},
{
"id": "e5fe57ce-c457-4f03-9c96-7ac385b2a42f",
"name": "SI: Est qualifié",
"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: Envoyer l'invitation Cal.com",
"type": "n8n-nodes-base.gmail",
"notes": "Sends the booking link when Qualified? is checked.",
"position": [
-448,
-624
],
"webhookId": "{{GMAIL_NODE_WEBHOOK_ID_CAL_INVITE}}",
"parameters": {
"sendTo": "={{ $json.body.email }}",
"message": "=Hi {{$json.body.name ? $json.body.name.split(' ')[0] : 'there'}},<br><br>Congrats — you’re 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": {
"id": "{{GMAIL_CREDENTIAL_ID}}",
"name": "{{GMAIL_CREDENTIAL_NAME}}"
}
},
"typeVersion": 2
},
{
"id": "99f6f896-ac20-454b-a480-88a71aabe0b0",
"name": "GS: Mettre à jour l'étape du prospect à 'Réunion planifiée'",
"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": {
"id": "{{GSHEETS_CREDENTIAL_ID}}",
"name": "{{GSHEETS_CREDENTIAL_NAME}}"
}
},
"typeVersion": 4
},
{
"id": "b2acf148-b6bc-49d3-8e76-fe890459c1dc",
"name": "Webhook: Statut client modifié",
"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
],
"webhookId": "{{WEBHOOK_ID_CLIENT_STATUS_CHANGED}}",
"parameters": {
"path": "client-status-changed",
"options": {
"responseData": "OK"
},
"httpMethod": "POST"
},
"typeVersion": 1
},
{
"id": "01223e18-22c3-4622-ba66-0425aad4bcd6",
"name": "SI: Livré ?",
"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: Rechercher client par 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": {
"id": "{{GSHEETS_CREDENTIAL_ID}}",
"name": "{{GSHEETS_CREDENTIAL_NAME}}"
}
},
"typeVersion": 4
},
{
"id": "1579d825-5a67-4e52-b7d5-f4dc493cf790",
"name": "Formater l'horodatage de fin",
"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: Mettre à jour fin & durée client",
"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": {
"id": "{{GSHEETS_CREDENTIAL_ID}}",
"name": "{{GSHEETS_CREDENTIAL_NAME}}"
}
},
"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": "Si",
"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: Réunion planifiée",
"type": "n8n-nodes-base.webhook",
"position": [
-896,
-368
],
"webhookId": "{{WEBHOOK_ID_MEETING_BOOKED}}",
"parameters": {
"path": "cal-booked",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "26784981-b9f6-44ca-8329-62072dd4fd0f",
"name": "Note adhésive",
"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": "Note adhésive1",
"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 — Google Apps Script bridge to n8n\n * Spreadsheet: \"ScaleFlow CRM\"\n * Tabs: \"Leads\", \"Clients\"\n *\n * WHAT IT DOES\n * - When a Lead’s Stage (Leads!E) changes → POST to n8n /lead-stage-changed\n * - When Qualified? checkbox (Leads!H) is edited → POST to n8n /lead-qualified\n * - When a Client’s Project Status (Clients!D) changes → POST to n8n /client-status-changed\n *\n * STEPS\n * 1) Paste this into Extensions → 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) + '…' : 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": "Note adhésive2",
"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": "Note adhésive3",
"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": "Note adhésive4",
"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": "Note adhésive5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-944,
192
],
"parameters": {
"color": 4,
"width": 1440,
"height": 304,
"content": "# Project Fulfillment Duration\n"
},
"typeVersion": 1
}
],
"active": true,
"pinData": {},
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"errorWorkflow": "{{ERROR_WORKFLOW_ID}}",
"executionOrder": "v1"
},
"versionId": "fbf5f5f3-e31e-4f4d-a480-f8c382bdfe04",
"connections": {
"12c036dc-4e13-4bfd-95d2-48da0e8337b6": {
"main": [
[
{
"node": "00ef748d-22e8-4436-a0e7-7ef1ed9017fe",
"type": "main",
"index": 0
}
],
[
{
"node": "baa49e6a-3c2e-4c42-ae96-9b2f0e5a0db3",
"type": "main",
"index": 0
}
]
]
},
"1f2a3e16-70c7-402f-ace7-782f9de34fd3": {
"main": [
[
{
"node": "1aec66f5-ce82-4375-9202-a3ba2a5ad118",
"type": "main",
"index": 0
}
]
]
},
"01223e18-22c3-4622-ba66-0425aad4bcd6": {
"main": [
[
{
"node": "438ffb35-490f-49d1-9031-c32447b59992",
"type": "main",
"index": 0
}
]
]
},
"e5fe57ce-c457-4f03-9c96-7ac385b2a42f": {
"main": [
[
{
"node": "04851d87-b2c6-4383-9687-382f3bee1e7b",
"type": "main",
"index": 0
}
]
]
},
"baa49e6a-3c2e-4c42-ae96-9b2f0e5a0db3": {
"main": [
[
{
"node": "855b796a-8588-4f43-8b39-48406c37c67d",
"type": "main",
"index": 0
}
],
[]
]
},
"1579d825-5a67-4e52-b7d5-f4dc493cf790": {
"main": [
[
{
"node": "1f2a3e16-70c7-402f-ace7-782f9de34fd3",
"type": "main",
"index": 0
}
]
]
},
"a91971ec-9414-4d0d-98d9-273761071c30": {
"main": [
[
{
"node": "12c036dc-4e13-4bfd-95d2-48da0e8337b6",
"type": "main",
"index": 0
}
]
]
},
"855b796a-8588-4f43-8b39-48406c37c67d": {
"main": [
[
{
"node": "25f0b4f3-0305-4ce6-83f8-e37fc90d51ca",
"type": "main",
"index": 0
}
]
]
},
"ec815439-bfb1-4edb-8a70-9db1c167a868": {
"main": [
[
{
"node": "99f6f896-ac20-454b-a480-88a71aabe0b0",
"type": "main",
"index": 0
}
]
]
},
"100c9018-23b7-4724-93ef-373566ae9c42": {
"main": [
[
{
"node": "e5fe57ce-c457-4f03-9c96-7ac385b2a42f",
"type": "main",
"index": 0
}
]
]
},
"438ffb35-490f-49d1-9031-c32447b59992": {
"main": [
[
{
"node": "1579d825-5a67-4e52-b7d5-f4dc493cf790",
"type": "main",
"index": 0
}
]
]
},
"c0760f3c-0feb-43bb-9b52-cc9307e0b98f": {
"main": [
[
{
"node": "a91971ec-9414-4d0d-98d9-273761071c30",
"type": "main",
"index": 0
}
]
]
},
"b2acf148-b6bc-49d3-8e76-fe890459c1dc": {
"main": [
[
{
"node": "01223e18-22c3-4622-ba66-0425aad4bcd6",
"type": "main",
"index": 0
}
]
]
}
}
}Comment utiliser ce workflow ?
Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.
Dans quelles scénarios ce workflow est-il adapté ?
Avancé - CRM, IA Multimodale
Est-ce payant ?
Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.
Workflows recommandés
Ziad Adel
@ziadadelAI Automation Growth Partner with 5 years of experience in the tech and hyper growing startups industry
Partager ce workflow