This workflow corresponds to n8n.io template #16911 — we link there as the canonical source.
This workflow follows the Agent → Google Calendar Tool 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": "x1OWcfs9IVTDbN8w",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "For prod",
"tags": [],
"nodes": [
{
"id": "63cb2c19-e245-4272-97b9-a5d8d2e26335",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
3792,
1936
],
"parameters": {
"width": 480,
"height": 848,
"content": "## Automated Property Q/A & appointment scheduler\n\n### How it works\n\n1. Triggers when a new message is received via Twilio.\n2. Looks up the user in Google Sheets and registers them if they do not already exist.\n3. Retrieves relevant context or previous interactions from Google Sheets.\n4. Uses an AI Agent with Google Gemini to process the message and perform actions like checking calendar availability or retrieving property info.\n5. Sends the AI's final response back to the user via Twilio SMS.\n\n### Setup steps\n\n- [ ] Configure Twilio credentials for receiving and sending SMS messages.\n- [ ] Connect Google Sheets with a service account or OAuth to read/write user and context data.\n- [ ] Set up a Google Gemini API key for the AI Agent model.\n- [ ] Connect Google Calendar credentials to allow the AI to check availability and create events.\n- [ ] Configure a Redis connection for persistent chat memory storage.\n\n### Customization\n\nYou can adjust the system prompt within the AI Agent to modify its personality or fine-tune how it leverages the Google Calendar and Sheets tools."
},
"typeVersion": 1
},
{
"id": "a4224415-78a0-445b-bf4d-901488562b9c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
4352,
1984
],
"parameters": {
"color": 7,
"width": 1088,
"height": 352,
"content": "## Process incoming message and identify user\n\nReceives a message via Twilio, checks if the user exists in Google Sheets, and registers them if they are new."
},
"typeVersion": 1
},
{
"id": "8832e274-14af-4b7a-a813-846156b26842",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
5472,
1968
],
"parameters": {
"color": 7,
"width": 864,
"height": 304,
"content": "## Fetch and format context\n\nRetrieves relevant context data from Google Sheets, aggregates it, and formats it for the AI to process."
},
"typeVersion": 1
},
{
"id": "54b534c8-d8f7-4992-bee5-c569eccc85a3",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
6368,
1968
],
"parameters": {
"color": 7,
"width": 1280,
"height": 528,
"content": "## Process message with AI Agent\n\nUses a Google Gemini-powered AI Agent with access to tools for calendar management, property info retrieval, and user updates, alongside Redis memory for chat context."
},
"typeVersion": 1
},
{
"id": "0b4706eb-f289-47ec-95f8-f6b5a68fc9ea",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
7680,
1936
],
"parameters": {
"color": 7,
"height": 320,
"content": "## Send SMS reply\n\nSends the AI-generated response back to the user via Twilio SMS."
},
"typeVersion": 1
},
{
"id": "41538c48-e7e8-4e21-85c9-c31b5a52d7ae",
"name": "When Twilio Message Received",
"type": "n8n-nodes-base.twilioTrigger",
"position": [
4400,
2096
],
"parameters": {
"updates": [
"com.twilio.messaging.inbound-message.received"
]
},
"credentials": {
"twilioApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "051fbbb2-0531-4266-81bc-357d6bda0bd7",
"name": "Fetch Property Details",
"type": "n8n-nodes-base.googleSheets",
"position": [
5520,
2096
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $json.interested_property_id }}",
"lookupColumn": "property_code"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1FIFA3iAiqxkTlM6B97crZtS5EhoTk6Y0LWV0kaUZlho/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1FIFA3iAiqxkTlM6B97crZtS5EhoTk6Y0LWV0kaUZlho",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1FIFA3iAiqxkTlM6B97crZtS5EhoTk6Y0LWV0kaUZlho/edit?usp=drivesdk",
"cachedResultName": "Property details"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "e13b98b1-1828-452f-bc7b-3dafdce8b57f",
"name": "If Results Found",
"type": "n8n-nodes-base.if",
"position": [
6192,
2096
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d751591d-5be3-4799-a3bc-022201cce7c1",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.total_found }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "1cb79fe7-a0d9-4c32-89d6-841021418d4f",
"name": "Fetch Lead Info",
"type": "n8n-nodes-base.googleSheets",
"position": [
4624,
2096
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $json.data.to.replace('+', '') }}",
"lookupColumn": "mobile_number"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE/edit?usp=drivesdk",
"cachedResultName": "leads_info"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "8fc57a1e-aeb8-4f02-b23c-958dc8187fa0",
"name": "Check Lead Exists",
"type": "n8n-nodes-base.if",
"position": [
4848,
2096
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "492d66fb-1158-4945-bf47-01c4a53baf15",
"operator": {
"type": "number",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.row_number }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "1ac011b8-99c9-442e-8126-8a8ad46c9e40",
"name": "Upsert Lead Info",
"type": "n8n-nodes-base.googleSheets",
"position": [
5072,
2176
],
"parameters": {
"columns": {
"value": {
"mobile_number": "={{ $('When Twilio Message Received').item.json.data.to }}",
"interested_property_id": "={{ $('When Twilio Message Received').item.json.data.body.match(/(?<!\\d)\\d{4}(?!\\d)/)?.[0] }}\n"
},
"schema": [
{
"id": "mobile_number",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "mobile_number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "email",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "name",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "interested_property_id",
"type": "string",
"display": true,
"required": false,
"displayName": "interested_property_id",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"mobile_number"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE/edit?usp=drivesdk",
"cachedResultName": "leads_info"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "99025f2e-1dd8-425a-80f4-6595d1183f1c",
"name": "Initialize Output Array",
"type": "n8n-nodes-base.code",
"position": [
5296,
2096
],
"parameters": {
"jsCode": "// Initialize an empty array to hold the new output items\nconst results = [];\n\n// Loop through all incoming items\nfor (const item of $input.all()) {\n // Get the string of property IDs\n const propertyIdsString = item.json.interested_property_id;\n \n // Check if the string exists and isn't empty\n if (propertyIdsString && typeof propertyIdsString === 'string') {\n \n // Split the string by ';' and trim any accidental whitespace\n const propertyIds = propertyIdsString.split(';').map(id => id.trim());\n \n // Create a new item for each individual ID\n for (const id of propertyIds) {\n if (id) {\n // Clone the original JSON object so we don't lose the other fields\n const newItemJson = { ...item.json };\n \n // Overwrite the multiple IDs string with the single current ID\n newItemJson.interested_property_id = id;\n \n // Push the formatted object back into the results array\n results.push({ json: newItemJson });\n }\n }\n } else {\n // If there is no interested_property_id field, just pass the item through unchanged\n results.push(item);\n }\n}\n\n// Return the new array of items\nreturn results;"
},
"typeVersion": 2
},
{
"id": "927aea1e-d066-4ee9-9f8e-554f8cea4c06",
"name": "Parse Lead Data",
"type": "n8n-nodes-base.code",
"position": [
5968,
2096
],
"parameters": {
"jsCode": "// 1. Get the originally requested items from your first Code node\n// Using $('Node Name').all() lets us look back at previous steps in the workflow\nconst requestedItems = $('Initialize Output Array').all();\n\n// Extract just the requested IDs into a clean array of strings\nconst requestedIds = requestedItems.map(item => String(item.json.interested_property_id));\n\n// 2. Get the properties that were successfully found from the Aggregate node\n// $input.first() gets the incoming item from the Aggregate node\nconst foundProperties = $input.first().json.data || [];\n\n// Extract the IDs of the properties that were actually found\nconst foundIds = foundProperties.map(prop => String(prop.property_code));\n\n// 3. Find the missing properties\n// We filter the requested list to keep only the IDs that do NOT exist in the found list\nconst notFoundIds = requestedIds.filter(id => !foundIds.includes(id));\n\n// 4. Output the final structured summary\nreturn {\n json: {\n total_requested: requestedIds.length,\n total_found: foundProperties.length,\n requested_ids: requestedIds,\n found_ids: foundIds,\n not_found_ids: notFoundIds,\n found_property_details: foundProperties // Keeps the actual sheet data for the found ones\n }\n};"
},
"typeVersion": 2
},
{
"id": "01732017-71f2-4ffe-8ece-48cc4089587e",
"name": "Aggregate Property Rows",
"type": "n8n-nodes-base.aggregate",
"position": [
5744,
2096
],
"parameters": {
"options": {},
"aggregate": "aggregateAllItemData"
},
"typeVersion": 1
},
{
"id": "cce9d752-f968-4365-b6dc-c05452273075",
"name": "Update Email Tool",
"type": "n8n-nodes-base.googleSheetsTool",
"position": [
6848,
2320
],
"parameters": {
"columns": {
"value": {
"name": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('name', ``, 'string') }}",
"email": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('email', ``, 'string') }}",
"mobile_number": "={{ $('When Twilio Message Received').item.json.data.to.replace('+', '') }}"
},
"schema": [
{
"id": "mobile_number",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "mobile_number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "email",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "interested_property_id",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "interested_property_id",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"mobile_number"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE/edit?usp=drivesdk",
"cachedResultName": "leads_info"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "88c00523-27ce-42b1-a737-7795f1c3d28b",
"name": "Check Calendar Availability Tool",
"type": "n8n-nodes-base.googleCalendarTool",
"position": [
7024,
2320
],
"parameters": {
"options": {},
"timeMax": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('End_Time', ``, 'string') }}",
"timeMin": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Start_Time', ``, 'string') }}",
"calendar": {
"__rl": true,
"mode": "id",
"value": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Calendar', `The calendar email tied to the specific property the user wants to view, looked up from the Calendar Emails of Properties mapping \u2014 never the user\\\\s own email. You must call this availability tool and review the returned free/busy slots BEFORE ever calling the Create Event tool.`, 'string') }}"
},
"resource": "calendar"
},
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "587cc0b9-abc1-42ca-a9bf-6d4e5aea36cf",
"name": "Create Calendar Event Tool",
"type": "n8n-nodes-base.googleCalendarTool",
"position": [
7200,
2336
],
"parameters": {
"end": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('End', `The exact end time of the verified-free slot, matching what was confirmed via the availability check. Must never be booked without a prior availability check.`, 'string') }}",
"start": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Start', `The exact start time of the slot the user confirmed. Only fill this in if you have ALREADY called \"Get availability in a calendar in Google Calendar\" for this property in this conversation and verified this exact time falls within a free slot. Do not guess or assume availability.`, 'string') }}",
"calendar": {
"__rl": true,
"mode": "list",
"value": "user@example.com",
"cachedResultName": "R01"
},
"additionalFields": {
"summary": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Summary', ``, 'string') }}",
"attendees": [
"={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('attendees0_Attendees', `The user\\\\s saved email address from the User Email context. Do not book an event if no valid email is available \u2014 obtain one via the update user email tool first.`, 'string') }}"
],
"description": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Description', ``, 'string') }}"
}
},
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "4a436b4e-4252-4aed-865b-5d43f218c2dd",
"name": "Fetch Property Info Tool",
"type": "n8n-nodes-base.googleSheetsTool",
"position": [
7360,
2336
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('values0_Value', `Extract the 4-digit property code the user is asking about. Use this tool FIRST to verify the property exists in the database before updating their profile.`, 'string') }}",
"lookupColumn": "property_code"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1FIFA3iAiqxkTlM6B97crZtS5EhoTk6Y0LWV0kaUZlho/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1FIFA3iAiqxkTlM6B97crZtS5EhoTk6Y0LWV0kaUZlho",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1FIFA3iAiqxkTlM6B97crZtS5EhoTk6Y0LWV0kaUZlho/edit?usp=drivesdk",
"cachedResultName": "Property details"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "ec3f632b-04b6-444f-aa7d-d835c17f828d",
"name": "Update Interested Property Tool",
"type": "n8n-nodes-base.googleSheetsTool",
"position": [
7520,
2336
],
"parameters": {
"columns": {
"value": {
"mobile_number": "={{ $('When Twilio Message Received').item.json.data.to.replace('+', '') }}",
"interested_property_id": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('interested_property_id', `The FINAL, COMPLETE string of property IDs separated by semicolons (e.g., \"5431;9999\"). If the user is removing a property, pass the string WITHOUT that ID. If adding, append the new verified ID to the existing list.`, 'string') }}"
},
"schema": [
{
"id": "mobile_number",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "mobile_number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "email",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "name",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "interested_property_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "interested_property_id",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"mobile_number"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1A3WulBnL6RG-GYq_WJYsMJpnkoU5SfepSN8h5_9a-ZE/edit?usp=drivesdk",
"cachedResultName": "leads_info"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "b25dc0cd-6d48-434a-94f3-3e00b721d01f",
"name": "Redis Chat Memory",
"type": "@n8n/n8n-nodes-langchain.memoryRedisChat",
"position": [
6640,
2320
],
"parameters": {
"sessionKey": "={{ $('When Twilio Message Received').item.json.data.to.replace('+', '') }}",
"sessionIdType": "customKey"
},
"credentials": {
"redis": {
"name": "<your credential>"
}
},
"typeVersion": 1.6
},
{
"id": "ea9792b6-03c5-488f-9824-9df74abfa522",
"name": "Gemini Pro Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
6416,
2320
],
"parameters": {
"options": {},
"modelName": "models/gemini-3.1-flash-lite"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "e389c937-17fa-4f63-b6f7-d9f6177e7474",
"name": "Property Inquiry Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
6912,
2096
],
"parameters": {
"text": "=User Message:\n{{ $('When Twilio Message Received').item.json.data.body }}\n\nUser Interested Property:\n{{ $('Fetch Lead Info').item.json.interested_property_id }}\n\nUser Email:\n{{ $('Fetch Lead Info').item.json.email }}\n\nCurrent Time:\n{{ $now }} - use this to intercept the correct time when user said tomorrow, today, while scheduling\n\nWe tried to match the property info via property code that the user have:\nProperty Code: {{ $('Fetch Property Details').item.json.property_code }}\nProperty Name: {{ $('Fetch Property Details').item.json.property_Name }}\nProperty Location: {{ $('Fetch Property Details').item.json.exact_location }}\nSummary of features the property have: {{ $('Fetch Property Details').item.json.summary }}\n\n\nCalendar Emails of Properties:\n5431 - 1683a27f482480c6d99e97e8b8d1dd1a3dd6c7018001d6476c4cf5554eca7e7d@group.calendar.google.com",
"options": {
"systemMessage": "=You are a helpful, professional real estate assistant. Respond to the user's message based on the data below.\n\n--- Context from Database Lookup ---\nCurrent Saved Property IDs: {{ $json.requested_ids.join(';') }}\nTotal Properties Found: {{ $json.total_found }}\nInvalid/Not Found IDs: {{ $json.not_found_ids.length > 0 ? $json.not_found_ids.join(', ') : 'None' }}\n\nFound Property Details:\n{{ $json.found_property_details.length > 0 ? JSON.stringify($json.found_property_details, null, 2) : 'No valid properties found.' }}\n------------------------------------\n\nCore Conversation Rules:\n1. ALL FOUND: Provide the property details naturally and ask how they'd like to proceed.\n2. PARTIALLY FOUND: Provide details for the found ones, and politely inform the user that these IDs were invalid: {{ $json.not_found_ids.join(', ') }}.\n3. NONE FOUND: Inform the user that the requested codes do not exist and ask if they'd like to try different ones.\n\n--- Tool Usage Rules ---\n\n1. \"Get New Property Info\" (lookup tool):\n - ALWAYS call this FIRST whenever the user mentions a new 4-digit property code not already in their saved list.\n - Verify the property exists before discussing, saving, or scheduling anything for it.\n - If invalid, tell the user and ask for a correct code. Do not proceed further.\n\n2. \"update user interested property\" (adding/removing saved properties):\n - REMOVING: pass the FINAL remaining IDs joined by semicolons, excluding the removed one.\n - ADDING: verify first via \"Get New Property Info\", then pass the FULL combined list (existing + new), joined by semicolons.\n - Never call with an unverified code. Always pass the complete final list, not just the delta.\n\n3. \"update user email\" (saving/updating email):\n - Call whenever the user provides or corrects an email address.\n - Extract the exact string given; if it doesn't look like a valid email, ask them to confirm before saving.\n - Confirm to the user once saved.\n\n4. \"Get availability in a calendar in Google Calendar\" (checking open slots):\n - Use whenever the user wants to book, view, or ask about a time for a specific property.\n - `calendar` MUST be the calendar email tied to that property's ID from the \"Calendar Emails of Properties\" mapping \u2014 never the user's own email.\n - Set `timeMin`/`timeMax` to the range the user asked about. If unspecified, ask, or default to the next 7 days.\n - Present the actual free/busy slots back in plain language and let the user pick or confirm one.\n\n5. \"Create an event in Google Calendar\" (booking a viewing):\n \u26a0\ufe0f HARD REQUIREMENT \u2014 NO EXCEPTIONS:\n - You must NEVER call this tool unless, earlier in THIS same turn or conversation, you already called \"Get availability in a calendar in Google Calendar\" for that exact property's calendar AND confirmed the requested start/end time falls within a genuinely free slot returned by that check.\n - If you have not yet checked availability for the requested time, or the user proposes a time before you've checked, your ONLY valid action is to call the availability tool first \u2014 do not book, do not assume, do not \"check later.\"\n - If the requested time is NOT free (it overlaps a busy slot, or wasn't part of the returned free slots), do NOT book it. Tell the user it's unavailable and offer the nearest free alternatives from the availability check instead.\n - Only after the user explicitly confirms a specific, verified-free time slot may you call this tool.\n - Before calling, confirm a valid email is on file for the user (see User Email context). If missing, obtain it via \"update user email\" first.\n - `start`/`end` must exactly match the confirmed, verified-free slot. `attendees` must include the user's email. `description` should note the property ID/name and that it's a viewing appointment.\n - After booking, confirm the final date, time, and property back to the user.\n\nGeneral principles:\n- Never call an update or booking tool with unverified or ambiguous data \u2014 ask a clarifying question instead.\n- Availability check \u2192 user confirmation \u2192 booking. This order is mandatory and cannot be skipped or reordered.\n- Always confirm back to the user in natural language after any successful tool call.\n- If required info (email, property ID, time) is missing, ask before calling the tool that needs it."
},
"promptType": "define"
},
"typeVersion": 3.1
},
{
"id": "4f7567d8-b25f-48e4-a8ca-17a3178d03c8",
"name": "Send Twilio Reply",
"type": "n8n-nodes-base.twilio",
"position": [
7728,
2096
],
"parameters": {
"to": "={{ $('When Twilio Message Received').item.json.data.from }}",
"from": "={{ $('When Twilio Message Received').item.json.data.to }}",
"message": "={{ $json.output }}",
"options": {}
},
"credentials": {
"twilioApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
}
],
"active": true,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "cdeb403a-b85b-4af3-b64c-41673e813c43",
"nodeGroups": [],
"connections": {
"Fetch Lead Info": {
"main": [
[
{
"node": "Check Lead Exists",
"type": "main",
"index": 0
}
]
]
},
"Parse Lead Data": {
"main": [
[
{
"node": "If Results Found",
"type": "main",
"index": 0
}
]
]
},
"Gemini Pro Model": {
"ai_languageModel": [
[
{
"node": "Property Inquiry Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"If Results Found": {
"main": [
[
{
"node": "Property Inquiry Agent",
"type": "main",
"index": 0
}
]
]
},
"Upsert Lead Info": {
"main": [
[
{
"node": "Initialize Output Array",
"type": "main",
"index": 0
}
]
]
},
"Check Lead Exists": {
"main": [
[
{
"node": "Initialize Output Array",
"type": "main",
"index": 0
}
],
[
{
"node": "Upsert Lead Info",
"type": "main",
"index": 0
}
]
]
},
"Redis Chat Memory": {
"ai_memory": [
[
{
"node": "Property Inquiry Agent",
"type": "ai_memory",
"index": 0
}
]
]
},
"Update Email Tool": {
"ai_tool": [
[
{
"node": "Property Inquiry Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Fetch Property Details": {
"main": [
[
{
"node": "Aggregate Property Rows",
"type": "main",
"index": 0
}
]
]
},
"Property Inquiry Agent": {
"main": [
[
{
"node": "Send Twilio Reply",
"type": "main",
"index": 0
}
]
]
},
"Aggregate Property Rows": {
"main": [
[
{
"node": "Parse Lead Data",
"type": "main",
"index": 0
}
]
]
},
"Initialize Output Array": {
"main": [
[
{
"node": "Fetch Property Details",
"type": "main",
"index": 0
}
]
]
},
"Fetch Property Info Tool": {
"ai_tool": [
[
{
"node": "Property Inquiry Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Create Calendar Event Tool": {
"ai_tool": [
[
{
"node": "Property Inquiry Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"When Twilio Message Received": {
"main": [
[
{
"node": "Fetch Lead Info",
"type": "main",
"index": 0
}
]
]
},
"Update Interested Property Tool": {
"ai_tool": [
[
{
"node": "Property Inquiry Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Check Calendar Availability Tool": {
"ai_tool": [
[
{
"node": "Property Inquiry Agent",
"type": "ai_tool",
"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.
googleApigoogleCalendarOAuth2ApigooglePalmApiredistwilioApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Youtube Explanation: https://youtu.be/e_07nGFmVs0
Source: https://n8n.io/workflows/16911/ — 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.
This workflow automates Facebook posting and appointment booking directly from a Telegram bot, making it especially useful for pet grooming businesses that want to keep their social media active while
Youtube Video: https://youtu.be/q2vPN9eub1o?si=lSo4c0kQMNQd_Aad
Template Overview This workflow demonstrates how to build a simple Telegram bot that can schedule events, check service prices, and query company documents using AI integrated with MCP and RAG. It’s d
Youtube Explanation: https://youtu.be/KgmNiV7SwkU
This project is a template for building a complete academic virtual assistant using n8n. It connects to Telegram, answers frequently asked questions by querying MongoDB, keeps the community informed a