AutomationFlowsAI & RAG › Handle Tamil Real Estate Voice Inquiries with Openai, Sarvam AI and Google…

Handle Tamil Real Estate Voice Inquiries with Openai, Sarvam AI and Google…

Original n8n title: Handle Tamil Real Estate Voice Inquiries with Openai, Sarvam AI and Google Sheets

ByDinakar Selvakumar @jamesdinakar on n8n.io

This workflow builds a Tamil voice AI assistant for real estate inquiries. It handles incoming calls or messages, converts speech to text, generates AI responses, converts them back to speech, and logs lead data into Google Sheets. Voice-based AI assistant using STT and TTS…

Webhook trigger★★★★☆ complexityAI-powered21 nodesOpenAIHTTP RequestGoogle Sheets
AI & RAG Trigger: Webhook Nodes: 21 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #15300 — we link there as the canonical source.

This workflow follows the Google Sheets → HTTP Request 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 →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "279428be-dbfb-44d8-8a3b-fe215e815ab2",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1280,
        80
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Untitled workflow\n\n### How it works\n\n1. Receives incoming data via a webhook.\n2. Parses and determines if the message is a first-time interaction.\n3. Processes the message using speech-to-text if necessary.\n4. Determines if escalation to a human agent is needed or handles the request with AI.\n5. Transforms responses back to text and logs lead information in Google Sheets.\n\n### Setup steps\n\n- [ ] Configure OpenAI API credentials for AI responses.\n- [ ] Set up the Webhook endpoint and ensure it is reachable.\n- [ ] Configure API access for Sarvam STT and TTS services.\n- [ ] Connect and authorize Google Sheets access for logging leads.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "5760d85a-4a13-4b64-a18f-a76d3c4084c9",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -720,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 304,
        "content": "## Receive and parse input\n\nHandles the receipt of incoming data through a webhook and parses it."
      },
      "typeVersion": 1
    },
    {
      "id": "d3be253b-2cf4-41b7-8863-383e776f9f6f",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -272,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 608,
        "content": "## Determine interaction type\n\nChecks if the interaction is a first-time interaction and directs flow based on condition."
      },
      "typeVersion": 1
    },
    {
      "id": "5b28d55f-e5b8-4fc7-9c72-4bd5e976acc4",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 688,
        "content": "## Speech-to-text processing\n\nProcesses speech-to-text conversion and evaluates if escalation is necessary."
      },
      "typeVersion": 1
    },
    {
      "id": "f9818865-a8a1-4bef-b315-aa010ba9766a",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        832,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 384,
        "content": "## Merge and respond\n\nMerges different response paths and sends final response back to the requester."
      },
      "typeVersion": 1
    },
    {
      "id": "4a08a3f8-4062-4c2e-a704-c7400470d7af",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1488,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 304,
        "content": "## Log lead information\n\nPrepares and logs lead information into Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "4784272f-1142-42e8-876f-fbc2e75abed3",
      "name": "OpenAI Model Message",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        592,
        656
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "GPT-4O"
        },
        "options": {
          "maxTokens": 400
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "=\u0ba8\u0bc0 Kavya. \u0ba8\u0bc0 \u0b92\u0bb0\u0bc1 friendly real estate assistant.\n\n== \u0b89\u0ba9\u0bcd \u0baa\u0bc7\u0b9a\u0bcd\u0b9a\u0bc1 style ==\n- \u0b8e\u0baa\u0bcd\u0baa\u0bcb\u0ba4\u0bc1\u0bae\u0bcd simple, friendly Tamil-\u0bb2\u0bcd \u0baa\u0bc7\u0b9a\u0bc1.\n- \u0b92\u0bb5\u0bcd\u0bb5\u0bca\u0bb0\u0bc1 reply-\u0baf\u0bc1\u0bae\u0bcd short \u0b86\u0b95 \u0b87\u0bb0\u0bc1 (max 2 sentences).\n- Customer \u0b9a\u0bca\u0ba9\u0bcd\u0ba9\u0ba4\u0bc1\u0b95\u0bcd\u0b95\u0bc1 first \u0b92\u0bb0\u0bc1 small acknowledgment \u0b9a\u0bca\u0bb2\u0bcd\u0bb2\u0bc1.\n- \u0b85\u0baa\u0bcd\u0baa\u0bc1\u0bb1\u0bae\u0bcd \u0ba4\u0bbe\u0ba9\u0bcd next question \u0b95\u0bc7\u0bb3\u0bcd.\n- Customer \u0baa\u0bc6\u0baf\u0bb0\u0bcd \u0ba4\u0bc6\u0bb0\u0bbf\u0ba8\u0bcd\u0ba4\u0ba4\u0bc1\u0bae\u0bcd \u0b85\u0ba8\u0bcd\u0ba4 \u0baa\u0bc6\u0baf\u0bb0\u0bc8 \u0baa\u0baf\u0ba9\u0bcd\u0baa\u0b9f\u0bc1\u0ba4\u0bcd\u0ba4\u0bbf \u0baa\u0bc7\u0b9a\u0bc1.\n\n== \u0bae\u0bbf\u0b95 \u0bae\u0bc1\u0b95\u0bcd\u0b95\u0bbf\u0baf output rule ==\n- Plain text \u0bae\u0b9f\u0bcd\u0b9f\u0bc1\u0bae\u0bcd return \u0baa\u0ba3\u0bcd\u0ba3\u0bc1.\n- JSON format, code block, quotes-wrapped object \u0bae\u0bbe\u0ba4\u0bbf\u0bb0\u0bbf output \u0b95\u0bca\u0b9f\u0bc1\u0b95\u0bcd\u0b95\u0bbe\u0ba4\u0bc7.\n- \u0b89\u0ba4\u0bbe\u0bb0\u0ba3\u0bae\u0bcd: {\"role\":\"assistant\",\"content\":\"...\"} \u0bae\u0bbe\u0ba4\u0bbf\u0bb0\u0bbf \u0b92\u0bb0\u0bc1\u0baa\u0bcb\u0ba4\u0bc1\u0bae\u0bcd \u0b8e\u0bb4\u0bc1\u0ba4\u0bbe\u0ba4\u0bc7.\n\n== \u0b8e\u0baa\u0bcd\u0baa\u0b9f\u0bbf \u0baa\u0bc7\u0b9a\u0ba3\u0bc1\u0bae\u0bcd ==\n- Numbers Tamil words-\u0bb2\u0bcd \u0b8e\u0bb4\u0bc1\u0ba4\u0ba3\u0bc1\u0bae\u0bcd.\n- \u0b8e\u0ba8\u0bcd\u0ba4 symbols \u0bb5\u0bc7\u0ba3\u0bcd\u0b9f\u0bbe\u0bae\u0bcd.\n- unnecessary English short forms avoid \u0baa\u0ba3\u0bcd\u0ba3\u0bc1.\n- \"plot\" \u0b9a\u0bca\u0bb2\u0bcd\u0bb2\u0bbe\u0ba4\u0bc7; \"\u0bae\u0ba9\u0bc8\" \u0b9a\u0bca\u0bb2\u0bcd.\n- \"site visit\" \u0b9a\u0bca\u0bb2\u0bcd\u0bb2\u0bbe\u0ba4\u0bc7; \"\u0ba8\u0bc7\u0bb0\u0bbf\u0bb2\u0bcd \u0bb5\u0ba8\u0bcd\u0ba4\u0bc1 \u0baa\u0bbe\u0bb0\u0bcd\u0b95\u0bcd\u0b95\" \u0b9a\u0bca\u0bb2\u0bcd.\n- \"ready to move\" \u0b9a\u0bca\u0bb2\u0bcd\u0bb2\u0bbe\u0ba4\u0bc7; \"\u0b87\u0baa\u0bcd\u0baa\u0bcb\u0bb5\u0bc7 \u0b95\u0bc1\u0b9f\u0bbf \u0baa\u0bcb\u0b95\u0bb2\u0bbe\u0bae\u0bcd\" \u0b9a\u0bca\u0bb2\u0bcd.\n- \"under construction\" \u0b9a\u0bca\u0bb2\u0bcd\u0bb2\u0bbe\u0ba4\u0bc7; \"\u0b95\u0b9f\u0bcd\u0b9f\u0bbf \u0b95\u0bca\u0ba3\u0bcd\u0b9f\u0bbf\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bbe\u0b99\u0bcd\u0b95\" \u0b9a\u0bca\u0bb2\u0bcd.\n\n== Properties ==\n\u0baa\u0bbf\u0bb0\u0bc6\u0bb8\u0bcd\u0b9f\u0bbf\u0b9c\u0bcd \u0b95\u0bbe\u0bb0\u0bcd\u0b9f\u0ba9\u0bcd\u0bb8\u0bcd, \u0b93. \u0b8e\u0bae\u0bcd. \u0b86\u0bb0\u0bcd.-\u0bb2\u0bcd \u0b87\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bc1.\n\u0b87\u0bb0\u0ba3\u0bcd\u0b9f\u0bc1 \u0baa\u0bbf \u0b8e\u0b9a\u0bcd \u0b95\u0bc7 \u0b85\u0bb1\u0bc1\u0baa\u0ba4\u0bcd\u0ba4\u0bbf \u0b90\u0ba8\u0bcd\u0ba4\u0bc1 \u0bb2\u0b9f\u0bcd\u0b9a\u0bae\u0bcd.\n\u0bae\u0bc2\u0ba9\u0bcd\u0bb1\u0bc1 \u0baa\u0bbf \u0b8e\u0b9a\u0bcd \u0b95\u0bc7 \u0b8e\u0ba3\u0bcd\u0baa\u0ba4\u0bcd\u0ba4\u0bbf \u0b90\u0ba8\u0bcd\u0ba4\u0bc1 \u0bb2\u0b9f\u0bcd\u0b9a\u0bae\u0bcd.\n\u0b87\u0baa\u0bcd\u0baa\u0bcb\u0bb5\u0bc7 \u0b95\u0bc1\u0b9f\u0bbf \u0baa\u0bcb\u0b95\u0bb2\u0bbe\u0bae\u0bcd.\n\n\u0b95\u0bcb\u0bb2\u0bcd\u0b9f\u0ba9\u0bcd \u0b9a\u0bbf\u0b9f\u0bcd\u0b9f\u0bbf, \u0baa\u0bcb\u0bb0\u0bc2\u0bb0\u0bcd-\u0bb2\u0bcd \u0b87\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bc1.\n\u0bae\u0ba9\u0bc8 \u0bb5\u0bbf\u0bb2\u0bc8 \u0bae\u0bc1\u0baa\u0bcd\u0baa\u0ba4\u0bcd\u0ba4\u0bbf \u0b90\u0ba8\u0bcd\u0ba4\u0bc1 \u0bb2\u0b9f\u0bcd\u0b9a\u0bae\u0bcd-\u0bb2\u0bcd \u0b87\u0bb0\u0bc1\u0ba8\u0bcd\u0ba4\u0bc1 \u0ba4\u0bca\u0b9f\u0b99\u0bcd\u0b95\u0bc1\u0ba4\u0bc1.\n\u0bb0\u0bc7\u0bb0\u0bbe \u0b85\u0ba9\u0bc1\u0bae\u0ba4\u0bbf \u0b95\u0bbf\u0b9f\u0bc8\u0b9a\u0bcd\u0b9a\u0bbf\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bc1.\n\n\u0bb8\u0bcd\u0b95\u0bc8\u0bb2\u0bc8\u0ba9\u0bcd \u0b9f\u0bb5\u0bb0\u0bcd\u0bb8\u0bcd, \u0b85\u0ba3\u0bcd\u0ba3\u0bbe \u0ba8\u0b95\u0bb0\u0bcd-\u0bb2\u0bcd \u0b87\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bc1.\n\u0b87\u0bb0\u0ba3\u0bcd\u0b9f\u0bc1 \u0baa\u0bbf \u0b8e\u0b9a\u0bcd \u0b95\u0bc7 \u0b92\u0bb0\u0bc1 \u0b95\u0bcb\u0b9f\u0bbf\u0baf\u0bc7 \u0b87\u0bb0\u0bc1\u0baa\u0ba4\u0bc1 \u0bb2\u0b9f\u0bcd\u0b9a\u0bae\u0bcd.\n\u0b95\u0b9f\u0bcd\u0b9f\u0bbf \u0b95\u0bca\u0ba3\u0bcd\u0b9f\u0bbf\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bbe\u0b99\u0bcd\u0b95.\n\n== Office info ==\n\u0ba4\u0bbf\u0b99\u0bcd\u0b95\u0bb3\u0bcd \u0bae\u0bc1\u0ba4\u0bb2\u0bcd \u0b9a\u0ba9\u0bbf \u0bb5\u0bb0\u0bc8, \u0b95\u0bbe\u0bb2\u0bc8 \u0b92\u0ba9\u0bcd\u0baa\u0ba4\u0bc1 \u0bae\u0bc1\u0ba4\u0bb2\u0bcd \u0bae\u0bbe\u0bb2\u0bc8 \u0b8f\u0bb4\u0bc1 \u0bae\u0ba3\u0bbf \u0bb5\u0bb0\u0bc8 open.\n\u0ba4\u0bca\u0bb2\u0bc8\u0baa\u0bc7\u0b9a\u0bbf \u0b9a\u0bc0\u0bb0\u0bcb \u0ba8\u0bbe\u0ba9\u0bcd\u0b95\u0bc1 \u0ba8\u0bbe\u0ba9\u0bcd\u0b95\u0bc1 XXXX XXXX.\n\n== Conversation flow ==\nStep 1:\n\u0bae\u0bc1\u0ba4\u0bb2\u0bbf\u0bb2\u0bcd user query-\u0b95\u0bcd\u0b95\u0bc1 acknowledge \u0baa\u0ba3\u0bcd\u0ba3\u0bc1.\n\u0b85\u0ba4\u0bc1\u0b95\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc1\u0bb1\u0bae\u0bcd only \u0b92\u0bb0\u0bc1 question \u0b95\u0bc7\u0bb3\u0bcd:\n\"\u0b8e\u0ba8\u0bcd\u0ba4 area \u0baa\u0bbe\u0bb0\u0bcd\u0b95\u0bcd\u0b95\u0bbf\u0bb1\u0bc0\u0b99\u0bcd\u0b95?\" \u0b85\u0bb2\u0bcd\u0bb2\u0ba4\u0bc1 \"\u0baa\u0b9f\u0bcd\u0b9c\u0bc6\u0b9f\u0bcd \u0b8e\u0ba9\u0bcd\u0ba9?\"\n\nStep 2:\nUser \u0baa\u0ba4\u0bbf\u0bb2\u0bcd \u0b95\u0bca\u0b9f\u0bc1\u0ba4\u0bcd\u0ba4\u0ba4\u0bc1\u0bae\u0bcd acknowledge \u0baa\u0ba3\u0bcd\u0ba3\u0bbf property suggest \u0baa\u0ba3\u0bcd\u0ba3\u0bc1:\n\"\u0b9a\u0bb0\u0bbf, [property name]-\u0bb2\u0bcd [type] \u0b87\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bc1. \u0bb5\u0bbf\u0bb2\u0bc8 [amount]. \u0ba8\u0bc7\u0bb0\u0bbf\u0bb2\u0bcd \u0bb5\u0ba8\u0bcd\u0ba4\u0bc1 \u0baa\u0bbe\u0bb0\u0bcd\u0b95\u0bcd\u0b95\u0bb2\u0bbe\u0bae\u0bbe?\"\n\nStep 3:\nUser interest confirm \u0baa\u0ba3\u0bcd\u0ba3\u0bbf\u0ba9\u0bbe \u0b87\u0ba4\u0bc7 order strict-\u0b86 follow \u0baa\u0ba3\u0bcd\u0ba3\u0bc1:\n- \u0bae\u0bc1\u0ba4\u0bb2\u0bbf\u0bb2\u0bcd \u0baa\u0bc6\u0baf\u0bb0\u0bcd \u0b95\u0bc7\u0bb3\u0bcd\n- \u0baa\u0bc6\u0baf\u0bb0\u0bcd \u0b95\u0bbf\u0b9f\u0bc8\u0ba4\u0bcd\u0ba4\u0ba4\u0bc1\u0bae\u0bcd \u0b85\u0ba8\u0bcd\u0ba4 \u0baa\u0bc6\u0baf\u0bb0\u0bc8 \u0b9a\u0bca\u0bb2\u0bcd\u0bb2\u0bbf acknowledge \u0baa\u0ba3\u0bcd\u0ba3\u0bbf \u0ba8\u0bbe\u0bb3\u0bcd \u0b95\u0bc7\u0bb3\u0bcd\n- \u0ba8\u0bbe\u0bb3\u0bcd \u0b95\u0bbf\u0b9f\u0bc8\u0ba4\u0bcd\u0ba4\u0ba4\u0bc1\u0bae\u0bcd acknowledge \u0baa\u0ba3\u0bcd\u0ba3\u0bbf \u0ba4\u0bca\u0bb2\u0bc8\u0baa\u0bc7\u0b9a\u0bbf \u0b8e\u0ba3\u0bcd \u0b95\u0bc7\u0bb3\u0bcd\n\u0b92\u0bb0\u0bc7 reply-\u0bb2\u0bcd \u0b8e\u0bb2\u0bcd\u0bb2\u0bbe details\u0baf\u0bc1\u0bae\u0bcd \u0b95\u0bc7\u0b9f\u0bcd\u0b95\u0bbe\u0ba4\u0bc7.\n\nStep 4:\nComplex question \u0b87\u0bb0\u0bc1\u0ba8\u0bcd\u0ba4\u0bbe:\n\"\u0baa\u0bc1\u0bb0\u0bbf\u0ba8\u0bcd\u0ba4\u0ba4\u0bc1, \u0ba8\u0bbe\u0ba9\u0bcd \u0b89\u0b99\u0bcd\u0b95\u0bb3\u0bc8 \u0b8e\u0b99\u0bcd\u0b95\u0bb3\u0bcd agent-\u0b95\u0bbf\u0b9f\u0bcd\u0b9f connect \u0b9a\u0bc6\u0baf\u0bcd\u0b95\u0bbf\u0bb1\u0bc7\u0ba9\u0bcd. \u0b9a\u0bbf\u0bb2 \u0ba8\u0bbf\u0bae\u0bbf\u0b9f\u0bae\u0bcd \u0b87\u0bb0\u0bc1\u0b99\u0bcd\u0b95.\"\n\n== name capture example ==\nUser: \u0baa\u0bbe\u0bb0\u0bc1\nKavya: \u0baa\u0bbe\u0bb0\u0bc1, \u0bb0\u0bca\u0bae\u0bcd\u0baa \u0ba8\u0ba9\u0bcd\u0bb1\u0bbf. \u0b8e\u0ba8\u0bcd\u0ba4 \u0ba8\u0bbe\u0bb3\u0bcd \u0bb5\u0bb0 \u0bb5\u0b9a\u0ba4\u0bbf\u0baf\u0bbe\u0b95 \u0b87\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd?\n\nUser: \u0b9e\u0bbe\u0baf\u0bbf\u0bb1\u0bc1\nKavya: \u0b9a\u0bb0\u0bbf \u0baa\u0bbe\u0bb0\u0bc1, \u0b9e\u0bbe\u0baf\u0bbf\u0bb1\u0bc1 note \u0baa\u0ba3\u0bcd\u0ba3\u0bbf\u0b9f\u0bcd\u0b9f\u0bc7\u0ba9\u0bcd. \u0b89\u0b99\u0bcd\u0b95\u0bb3\u0bcd \u0ba4\u0bca\u0bb2\u0bc8\u0baa\u0bc7\u0b9a\u0bbf \u0b8e\u0ba3\u0bcd \u0b9a\u0bca\u0bb2\u0bcd\u0bb2\u0bc1\u0b99\u0bcd\u0b95?"
            },
            {
              "content": "={{ JSON.stringify($json.conversationHistory) }}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "9bdbae03-af0b-4f4e-9a82-85908236a368",
      "name": "When POST to Tamil Agent",
      "type": "n8n-nodes-base.webhook",
      "onError": "continueRegularOutput",
      "position": [
        -672,
        384
      ],
      "parameters": {
        "path": "tamil-voice-agent-v2",
        "options": {
          "rawBody": true
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "cc95b62e-cf7c-403c-b539-d94fb8382a64",
      "name": "Parse Webhook Input",
      "type": "n8n-nodes-base.code",
      "position": [
        -448,
        384
      ],
      "parameters": {
        "jsCode": "// ---------- SAFE INPUT PARSER ----------\n\nconst input = $input.first();\n\n// \u2705 FIX: preserve binary\nconst binary = input.binary || {};\n\n// Normalize browser codec MIME values (e.g. audio/webm;codecs=opus) to base types accepted by STT.\nif (binary.audio?.mimeType && typeof binary.audio.mimeType === 'string') {\n  const normalizedMime = binary.audio.mimeType.split(';')[0].trim().toLowerCase();\n  binary.audio.mimeType = normalizedMime;\n}\n\nconst body = input.json.body || input.json || {};\n\n// ---------- METADATA ----------\nconst callerPhone = body.caller_phone || body.phone || 'unknown';\nconst callSid = body.call_sid || body.session_id || `call_${Date.now()}`;\nconst callType = body.call_type || 'inbound';\n\n// ---------- CONVERSATION ----------\nlet conversationHistory = body.conversation_history || [];\n\nif (typeof conversationHistory === 'string') {\n  try {\n    conversationHistory = JSON.parse(conversationHistory);\n  } catch (e) {\n    conversationHistory = [];\n  }\n}\n\nif (!Array.isArray(conversationHistory)) {\n  conversationHistory = [];\n}\n\n// ---------- INPUT TEXT ----------\nlet inputText = body.text || body.message || null;\n\nif (!inputText && conversationHistory.length > 0) {\n  const lastMsg = conversationHistory[conversationHistory.length - 1];\n  if (lastMsg?.role === 'user') {\n    inputText = lastMsg.content;\n  }\n}\n\n// ---------- AUDIO DETECTION ----------\nconst hasBinaryAudio = !!binary.audio;\n\nlet audioBase64 = body.audio_base64 || null;\nlet audioUrl = body.audio_url || null;\n\nconst hasAudio = !!(audioBase64 || audioUrl || hasBinaryAudio);\nconst isTextOnly = !hasAudio && !!inputText;\n\n// ---------- FIRST TURN ----------\nconst hasUserInput = !!inputText || hasAudio;\nconst hasHistory = conversationHistory.length > 0;\n\nconst isFirstTurn = !hasHistory && !hasUserInput;\n\n// ---------- OUTPUT (CRITICAL FIX) ----------\nreturn {\n  json: {\n    callerPhone,\n    callSid,\n    callType,\n\n    conversationHistory,\n\n    inputText,\n    audioBase64,\n    audioUrl,\n    hasBinaryAudio,\n\n    hasAudio,\n    isTextOnly,\n    isFirstTurn,\n\n    timestamp: new Date().toISOString()\n  },\n\n  // \ud83d\udd25 THIS IS THE FIX\n  binary: binary\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "7620c881-56f9-4325-8304-af91fde9179a",
      "name": "If First Turn",
      "type": "n8n-nodes-base.if",
      "position": [
        -224,
        384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "check-first-turn",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.isFirstTurn }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "cb42a9c0-b105-47de-af39-ccfbc2be4417",
      "name": "Generate Welcome Message",
      "type": "n8n-nodes-base.code",
      "position": [
        -16,
        240
      ],
      "parameters": {
        "jsCode": "// Generate welcome message for first turn (no STT needed)\nconst callType = $('Parse Webhook Input').first().json.callType;\nconst callerPhone = $('Parse Webhook Input').first().json.callerPhone;\n\nlet welcomeText = '';\nif (callType === 'outbound') {\n  welcomeText = '\u0bb9\u0bb2\u0bcb! \u0ba8\u0bbe\u0ba9\u0bcd \u0b89\u0b99\u0bcd\u0b95\u0bb3\u0bcd real estate assistant Kavya. \u0ba8\u0bc0\u0b99\u0bcd\u0b95\u0bb3\u0bcd property enquiry form fill \u0baa\u0ba3\u0bcd\u0ba3\u0bbf\u0bb0\u0bc1\u0ba8\u0bcd\u0ba4\u0bc0\u0b99\u0bcd\u0b95. \u0b8e\u0ba9\u0bcd\u0ba9 \u0bae\u0bbe\u0ba4\u0bbf\u0bb0\u0bbf property \u0baa\u0bbe\u0bb0\u0bcd\u0b95\u0bcd\u0b95\u0bbf\u0bb1\u0bc0\u0b99\u0bcd\u0b95?';\n} else {\n  welcomeText = '\u0bb9\u0bb2\u0bcb! \u0ba8\u0bc0\u0b99\u0bcd\u0b95\u0bb3\u0bcd \u0b8e\u0b99\u0bcd\u0b95\u0bb3\u0bcd real estate team-\u0b95\u0bcd\u0b95\u0bc1 call \u0baa\u0ba3\u0bcd\u0ba3\u0bbf\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bc0\u0b99\u0bcd\u0b95. Property details \u0b9a\u0bca\u0bb2\u0bcd\u0bb2\u0bb2\u0bbe\u0bae\u0bcd, site visit book \u0baa\u0ba3\u0bcd\u0ba3\u0bb2\u0bbe\u0bae\u0bcd. \u0b87\u0baa\u0bcd\u0baa\u0bcb \u0b8e\u0ba9\u0bcd\u0ba9 help \u0bb5\u0bc7\u0ba3\u0bc1\u0bae\u0bcd?';\n}\n\nreturn {\n  transcribedText: null,\n  agentResponse: welcomeText,\n  isFirstTurn: true,\n  callerPhone,\n  callSid: $('Parse Webhook Input').first().json.callSid,\n  conversationHistory: [],\n  skipSTT: true\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "9e695ff3-2905-44e2-b1a7-4f98f2ab4d4e",
      "name": "Post to Sarvam STT",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        -16,
        528
      ],
      "parameters": {
        "url": "https://api.sarvam.ai/speech-to-text",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "file",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "audio"
            },
            {
              "name": "model",
              "value": "saarika:v2.5"
            },
            {
              "name": "language_code",
              "value": "ta-IN"
            },
            {
              "name": "with_timestamps",
              "value": "false"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "api-subscription-key",
              "value": "={{$env.SARVAM_API_KEY}}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "77631eaf-2521-4b0d-9e57-035d08c78579",
      "name": "Prepare STT Results",
      "type": "n8n-nodes-base.code",
      "position": [
        208,
        528
      ],
      "parameters": {
        "jsCode": "// Prepare STT result and conversation context with robust transcript parsing\nconst sttResponse = $input.first().json;\nconst parsedInput = $('Parse Webhook Input').first().json;\n\nconst extractTranscript = (resp) => {\n  if (!resp || typeof resp !== 'object') return '';\n  if (typeof resp.transcript === 'string') return resp.transcript.trim();\n  if (typeof resp.text === 'string') return resp.text.trim();\n  if (typeof resp.output === 'string') return resp.output.trim();\n  if (Array.isArray(resp.results)) {\n    for (const item of resp.results) {\n      if (typeof item?.transcript === 'string' && item.transcript.trim()) {\n        return item.transcript.trim();\n      }\n    }\n  }\n  return '';\n};\n\nconst transcribedText = extractTranscript(sttResponse);\nconst conversationHistory = Array.isArray(parsedInput.conversationHistory)\n  ? [...parsedInput.conversationHistory]\n  : [];\n\nconst sttFailed = transcribedText.length === 0;\n\n// Add user message only when STT produced text\nif (!sttFailed) {\n  conversationHistory.push({\n    role: 'user',\n    content: transcribedText\n  });\n}\n\n// Detect escalation keywords\nconst escalationKeywords = [\n  'manager', 'human', 'person', 'urgent', 'complaint',\n  '\u0bae\u0bc7\u0bb2\u0bbe\u0bb3\u0bb0\u0bcd', '\u0b86\u0bb3\u0bcd', '\u0b85\u0bb5\u0b9a\u0bb0\u0bae\u0bcd', 'problem'\n];\nconst needsEscalation = !sttFailed && escalationKeywords.some((kw) =>\n  transcribedText.toLowerCase().includes(kw.toLowerCase())\n);\n\nreturn [{\n  json: {\n    transcribedText,\n    conversationHistory,\n    callerPhone: parsedInput.callerPhone,\n    callSid: parsedInput.callSid,\n    needsEscalation,\n    sttFailed,\n    sttRaw: sttResponse,\n    isFirstTurn: false\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3413a3ed-e1be-4db6-be7e-0d8ddbe3844b",
      "name": "Check for Escalation",
      "type": "n8n-nodes-base.if",
      "position": [
        432,
        528
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "escalation-check",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.needsEscalation }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "b097ba8c-1191-4b8a-9924-60a51a7cfc51",
      "name": "Handle Escalation",
      "type": "n8n-nodes-base.code",
      "position": [
        640,
        272
      ],
      "parameters": {
        "jsCode": "// Escalation path \u2014 notify human agent\nconst data = $input.first().json;\n\nconst escalationResponse = '\u0baa\u0bc1\u0bb0\u0bbf\u0ba8\u0bcd\u0ba4\u0ba4\u0bc1. \u0ba8\u0bbe\u0ba9\u0bcd \u0b87\u0baa\u0bcd\u0baa\u0bcb\u0ba4\u0bc1 \u0b89\u0b99\u0bcd\u0b95\u0bb3\u0bc8 \u0b8e\u0b99\u0bcd\u0b95\u0bb3\u0bcd human agent-\u0b95\u0bbf\u0b9f\u0bcd\u0b9f transfer \u0b9a\u0bc6\u0baf\u0bcd\u0b95\u0bbf\u0bb1\u0bc7\u0ba9\u0bcd. \u0b9a\u0bbf\u0bb2 \u0ba8\u0bbf\u0bae\u0bbf\u0b9f\u0b99\u0bcd\u0b95\u0bb3\u0bcd hold-\u0bb2\u0bcd \u0b87\u0bb0\u0bc1\u0b99\u0bcd\u0b95\u0bb3\u0bcd.';\n\n// In production: trigger SMS/WhatsApp alert to human agent here\nreturn [{\n  json: {\n    agentResponse: escalationResponse,\n    transcribedText: data.transcribedText,\n    conversationHistory: data.conversationHistory,\n    callerPhone: data.callerPhone,\n    callSid: data.callSid,\n    isEscalated: true,\n    escalationAlert: {\n      phone: data.callerPhone,\n      transcript: data.transcribedText,\n      timestamp: new Date().toISOString()\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "37619e99-36e7-4d83-936a-aa37e5fe7fcc",
      "name": "Merge Response Streams",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        400
      ],
      "parameters": {
        "jsCode": "function safeGet(nodeName) {\n  try {\n    const data = $(nodeName).all();\n    return data.length ? data[0].json : null;\n  } catch (e) {\n    return null;\n  }\n}\n\nconst escalationData = safeGet('\ud83d\udea8 Escalation Handler');\nconst llmData = safeGet('Message a model1');\nconst sttData = safeGet('\ud83d\udcdd Process STT');\nconst welcomeData = safeGet('\ud83d\udc4b Welcome Message');\n\nlet agentResponse = '';\nlet conversationHistory = sttData?.conversationHistory || welcomeData?.conversationHistory || [];\nlet callerPhone = sttData?.callerPhone || welcomeData?.callerPhone;\nlet callSid = sttData?.callSid || welcomeData?.callSid;\nlet isEscalated = false;\nlet isFirstTurn = welcomeData?.isFirstTurn || false;\n\nif (isFirstTurn && welcomeData) {\n  agentResponse = welcomeData.agentResponse;\n} else if (sttData?.sttFailed) {\n  agentResponse = '\u0bae\u0ba9\u0bcd\u0ba9\u0bbf\u0b95\u0bcd\u0b95\u0bb5\u0bc1\u0bae\u0bcd, \u0b89\u0b99\u0bcd\u0b95\u0bb3\u0bcd voice clear-\u0b86 \u0b95\u0bc7\u0b9f\u0bcd\u0b95\u0bb5\u0bbf\u0bb2\u0bcd\u0bb2\u0bc8. \u0b95\u0bca\u0b9e\u0bcd\u0b9a\u0bae\u0bcd \u0bae\u0bc6\u0ba4\u0bc1\u0bb5\u0bbe\u0b95 \u0bae\u0bb1\u0bc1\u0baa\u0b9f\u0bbf\u0baf\u0bc1\u0bae\u0bcd \u0b9a\u0bca\u0bb2\u0bcd\u0bb2\u0bc1\u0b99\u0bcd\u0b95\u0bb3\u0bcd, \u0b85\u0bb2\u0bcd\u0bb2\u0ba4\u0bc1 text-\u0b86 \u0b85\u0ba9\u0bc1\u0baa\u0bcd\u0baa\u0bb2\u0bbe\u0bae\u0bcd.';\n  conversationHistory = [\n    ...conversationHistory,\n    { role: 'assistant', content: agentResponse }\n  ];\n} else if (escalationData?.isEscalated) {\n  agentResponse = escalationData.agentResponse;\n  isEscalated = true;\n} else if (llmData) {\n  const extractAssistantText = (data) => {\n    if (typeof data?.text === 'string' && data.text.trim()) return data.text.trim();\n    if (typeof data?.output === 'string' && data.output.trim()) return data.output.trim();\n\n    const output0 = Array.isArray(data?.output) ? data.output[0] : null;\n    const outContent0 = Array.isArray(output0?.content) ? output0.content[0] : null;\n    if (typeof outContent0?.text === 'string' && outContent0.text.trim()) return outContent0.text.trim();\n\n    const choiceText = data?.choices?.[0]?.message?.content;\n    if (typeof choiceText === 'string' && choiceText.trim()) return choiceText.trim();\n\n    const msgContent0 = Array.isArray(data?.content) ? data.content[0] : null;\n    if (typeof msgContent0?.text === 'string' && msgContent0.text.trim()) return msgContent0.text.trim();\n\n    return '';\n  };\n\n  agentResponse = extractAssistantText(llmData) || 'Sorry, clear-ah \u0b95\u0bc7\u0b9f\u0bcd\u0b95\u0bb2. \u0b87\u0ba9\u0bcd\u0ba9\u0bca\u0bb0\u0bc1 \u0ba4\u0b9f\u0bb5\u0bc8 \u0b9a\u0bca\u0bb2\u0bcd\u0bb2\u0bc1\u0b99\u0bcd\u0b95.';\n\n  // Unwrap accidental JSON-string responses like {\"role\":\"assistant\",\"content\":\"...\"}\n  const maybeJsonText = agentResponse.trim();\n  if (maybeJsonText.startsWith('{') && maybeJsonText.endsWith('}')) {\n    try {\n      const parsed = JSON.parse(maybeJsonText);\n      if (typeof parsed?.content === 'string' && parsed.content.trim()) {\n        agentResponse = parsed.content.trim();\n      }\n    } catch (e) {\n      // Keep original text when parsing fails\n    }\n  }\n\n  // Keep spoken output simple, friendly Tamil/Tanglish (avoid very formal Tamil words).\n  agentResponse = agentResponse\n    .replaceAll('\u0b89\u0ba4\u0bb5\u0b9f\u0bcd\u0b9f\u0bc1\u0bae\u0bcd', 'help \u0baa\u0ba3\u0bcd\u0ba3\u0bb2\u0bbe\u0bae\u0bcd')\n    .replaceAll('\u0b89\u0ba4\u0bb5\u0bb2\u0bbe\u0bae\u0bcd', 'help \u0baa\u0ba3\u0bcd\u0ba3\u0bb2\u0bbe\u0bae\u0bcd')\n    .replaceAll('\u0b85\u0bb5\u0b9a\u0bbf\u0baf\u0bae\u0bcd', '\u0ba4\u0bc7\u0bb5\u0bc8\u0ba9\u0bbe')\n    .replaceAll('\u0ba4\u0bca\u0b9f\u0bb0\u0bb5\u0bc1\u0bae\u0bcd', 'continue \u0baa\u0ba3\u0bcd\u0ba3\u0bb2\u0bbe\u0bae\u0bcd')\n    .replaceAll('\u0bb5\u0ba3\u0b95\u0bcd\u0b95\u0bae\u0bcd', '\u0bb9\u0bb2\u0bcb');\n\n  conversationHistory = [\n    ...conversationHistory,\n    { role: 'assistant', content: agentResponse }\n  ];\n} else {\n  agentResponse = '\u0bae\u0ba9\u0bcd\u0ba9\u0bbf\u0b95\u0bcd\u0b95\u0bb5\u0bc1\u0bae\u0bcd, response generate \u0b86\u0b95\u0bb5\u0bbf\u0bb2\u0bcd\u0bb2\u0bc8. \u0bae\u0bc0\u0ba3\u0bcd\u0b9f\u0bc1\u0bae\u0bcd \u0bae\u0bc1\u0baf\u0bb1\u0bcd\u0b9a\u0bbf \u0b9a\u0bc6\u0baf\u0bcd\u0baf\u0bc1\u0b99\u0bcd\u0b95\u0bb3\u0bcd.';\n}\n\nreturn [{\n  json: {\n    agentResponse,\n    conversationHistory,\n    callerPhone,\n    callSid,\n    isEscalated,\n    transcribedText: sttData?.transcribedText || null,\n    sttFailed: sttData?.sttFailed || false,\n    sttRaw: sttData?.sttRaw || null\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "5fdc4395-d70b-4433-9b71-6e0c393a0471",
      "name": "Post to Sarvam TTS",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1104,
        464
      ],
      "parameters": {
        "url": "https://api.sarvam.ai/text-to-speech",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"text\": {{ JSON.stringify($json.agentResponse) }},\n  \"target_language_code\": \"ta-IN\",\n  \"speaker\": \"ishita\",\n  \"model\": \"bulbul:v3\",\n  \"pace\": 0.95,\n  \"speech_sample_rate\": 22050,\n  \"enable_preprocessing\": true,\n  \"temperature\": 0.60\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "api-subscription-key",
              "value": "={{$env.SARVAM_API_KEY}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "330f377b-daf3-437f-8fc3-9d5a8787b44d",
      "name": "Build Response Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        1344,
        512
      ],
      "parameters": {
        "jsCode": "// Prepare final response payload\nconst ttsData = $('Post to Sarvam TTS').first().json;\nconst mergeData = $('Merge Response Streams').first().json;\n\n// TTS returns base64 audio chunks\nconst audioChunks = ttsData.audios || [];\nconst audioBase64 = audioChunks[0] || '';\n\nreturn {\n  json: {\n    success: true,\n    call_sid: mergeData.callSid,\n    caller_phone: mergeData.callerPhone,\n    transcribed_text: mergeData.transcribedText,\n    stt_failed: mergeData.sttFailed || false,\n    agent_response_text: mergeData.agentResponse,\n    audio_base64: audioBase64,\n    audio_format: 'wav',\n    sample_rate: 8000,\n    is_escalated: mergeData.isEscalated,\n    conversation_history: mergeData.conversationHistory,\n    continue_conversation: !mergeData.isEscalated,\n    timestamp: new Date().toISOString()\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "0672869d-7d19-412c-8b71-7dd675811723",
      "name": "Send Webhook Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1568,
        512
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              },
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "570904b0-0095-4230-b3df-b1c5b1889c83",
      "name": "Prepare Lead Log Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1536,
        832
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\nconst history = Array.isArray(input.conversationHistory) ? input.conversationHistory : [];\nconst latestUserText = typeof input.transcribedText === 'string' ? input.transcribedText.trim() : '';\n\nconst isLikelyName = (text) => {\n  if (!text) return false;\n  const words = text.split(/\\s+/).filter(Boolean);\n  if (words.length === 0 || words.length > 3) return false;\n  if (/\\d/.test(text)) return false;\n  return text.length >= 2 && text.length <= 30;\n};\n\nconst askedNameRecently = history\n  .slice(-4)\n  .some((msg) => msg?.role === 'assistant' && typeof msg?.content === 'string' && msg.content.includes('\u0baa\u0bc6\u0baf\u0bb0\u0bcd'));\n\nconst callerName = askedNameRecently && isLikelyName(latestUserText) ? latestUserText : '';\n\nconst numberMatch = latestUserText.match(/(?:\\+?\\d[\\d\\s\\-]{7,}\\d)/);\nconst extractedCallerNumber = numberMatch ? numberMatch[0].replace(/\\s+/g, ' ').trim() : '';\n\nconst intentKeywords = [\n  { key: 'site_visit', words: ['\u0ba8\u0bc7\u0bb0\u0bbf\u0bb2\u0bcd', 'visit', '\u0baa\u0bbe\u0bb0\u0bcd\u0b95\u0bcd\u0b95'] },\n  { key: 'pricing', words: ['\u0bb5\u0bbf\u0bb2\u0bc8', 'price', '\u0bb2\u0b9f\u0bcd\u0b9a\u0bae\u0bcd', '\u0b95\u0bcb\u0b9f\u0bbf'] },\n  { key: 'location', words: ['area', '\u0b8f\u0bb0\u0bbf\u0baf\u0bbe', 'omr', 'porur', '\u0b85\u0ba3\u0bcd\u0ba3\u0bbe \u0ba8\u0b95\u0bb0\u0bcd', '\u0baa\u0bcb\u0bb0\u0bc2\u0bb0\u0bcd'] },\n  { key: 'callback', words: ['call', 'phone', '\u0ba4\u0bca\u0bb2\u0bc8\u0baa\u0bc7\u0b9a\u0bbf'] }\n];\n\nlet callerIntent = 'general_query';\nconst lowerText = latestUserText.toLowerCase();\nfor (const intent of intentKeywords) {\n  if (intent.words.some((w) => lowerText.includes(w.toLowerCase()))) {\n    callerIntent = intent.key;\n    break;\n  }\n}\n\nreturn [{\n  json: {\n    timestamp: new Date().toISOString(),\n    call_sid: input.callSid || '',\n    caller_id: input.callerPhone || '',\n    caller_phone: input.callerPhone || '',\n    caller_name: callerName,\n    caller_intent: callerIntent,\n    extracted_caller_number: extractedCallerNumber,\n    user_last_message: latestUserText,\n    agent_last_reply: input.agentResponse || '',\n    stt_failed: Boolean(input.sttFailed),\n    is_escalated: Boolean(input.isEscalated),\n    continue_conversation: !Boolean(input.isEscalated),\n    audio_format: '',\n    sample_rate: '',\n    history_length: history.length\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "352a19de-7824-47a8-aa70-79df92b2b918",
      "name": "Append Log to GSheets",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "position": [
        1776,
        832
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "useAppend": true,
          "cellFormat": "USER_ENTERED",
          "handlingExtraData": "insertInNewColumn"
        },
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "{{.GOOGLE_SHEETS_SHEET_ID_1451193517}}"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "{{.GOOGLE_SHEETS_DOCUMENT_ID}}",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/14W9maZj3mvOuuYsbLVe1cDRQog9AeforKUuPvE_aq-4/edit?usp=drivesdk",
          "cachedResultName": "Voice AI"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    }
  ],
  "connections": {
    "If First Turn": {
      "main": [
        [
          {
            "node": "Generate Welcome Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post to Sarvam STT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle Escalation": {
      "main": [
        [
          {
            "node": "Merge Response Streams",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post to Sarvam STT": {
      "main": [
        [
          {
            "node": "Prepare STT Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post to Sarvam TTS": {
      "main": [
        [
          {
            "node": "Build Response Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Webhook Input": {
      "main": [
        [
          {
            "node": "If First Turn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare STT Results": {
      "main": [
        [
          {
            "node": "Check for Escalation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Escalation": {
      "main": [
        [
          {
            "node": "Handle Escalation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "OpenAI Model Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Model Message": {
      "main": [
        [
          {
            "node": "Merge Response Streams",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Lead Log Data": {
      "main": [
        [
          {
            "node": "Append Log to GSheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Response Payload": {
      "main": [
        [
          {
            "node": "Send Webhook Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Response Streams": {
      "main": [
        [
          {
            "node": "Post to Sarvam TTS",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prepare Lead Log Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Welcome Message": {
      "main": [
        [
          {
            "node": "Merge Response Streams",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When POST to Tamil Agent": {
      "main": [
        [
          {
            "node": "Parse Webhook Input",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow builds a Tamil voice AI assistant for real estate inquiries. It handles incoming calls or messages, converts speech to text, generates AI responses, converts them back to speech, and logs lead data into Google Sheets. Voice-based AI assistant using STT and TTS…

Source: https://n8n.io/workflows/15300/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Instantly map all internal URLs, perform AI-powered (ChatGPT) analysis, and deliver results in HTML via webhook, Google Sheets, or email. All from your own n8n instance!

OpenAI, HTTP Request, XML +3
AI & RAG

Watch on Youtube▶️

HTTP Request, Email Send, Google Sheets +3
AI & RAG

This workflow is perfect for marketing agencies, SEO consultants, and growth specialists who need to scale personalized outreach without spending hours on manual research.

Google Sheets, HTTP Request, OpenAI
AI & RAG

This workflow automates the creation of Journal Entries in SAP Business One (SAP B1). Depending on the source of the input data, it dynamically transforms and sends accounting records in the appropria

HTTP Request, Google Sheets, OpenAI
AI & RAG

How it works: Send notes from Obsidian via Webhook to start the audio conversion OpenAI converts your text to natural-sounding audio and generates episode descriptions Audio files are stored in Cloudi

OpenAI, HTTP Request, Google Sheets