AutomationFlowsData & Sheets › Deterministic Identity Resolution with OpenAI & Postgres

Deterministic Identity Resolution with OpenAI & Postgres

Original n8n title: Otto - Épico 1: Resolução Determinística De Identidade

OTTO - Épico 1: Resolução Determinística de Identidade. Uses openAi, postgres, httpRequest. Webhook trigger; 11 nodes.

Webhook trigger★★★★☆ complexityAI-powered11 nodesOpenAIPostgresHTTP Request
Data & Sheets Trigger: Webhook Nodes: 11 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow follows the HTTP Request → OpenAI 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
{
  "name": "OTTO - \u00c9pico 1: Resolu\u00e7\u00e3o Determin\u00edstica de Identidade",
  "nodes": [
    {
      "parameters": {
        "content": "## \ud83c\udfaf \u00c9pico 1: Resolu\u00e7\u00e3o de Identidade\n\n**Regra de Ouro**: O LLM nunca decide quem \u00e9 o lead.\nEle apenas levanta a hip\u00f3tese; o humano aperta o bot\u00e3o.\n\n**Fluxo**:\n1. Vendedor manda \u00e1udio com refer\u00eancia a lead\n2. LLM extrai entidades (profiss\u00e3o, carro, nome)\n3. SQL busca candidatos na base\n4. WhatsApp Interactive pede confirma\u00e7\u00e3o\n5. S\u00f3 continua com ID determin\u00edstico",
        "height": 300,
        "width": 420,
        "color": 4
      },
      "id": "note-docs",
      "name": "\ud83d\udccb Documenta\u00e7\u00e3o",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        40,
        40
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "otto_diario_bordo",
        "authentication": "headerAuth",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-audio",
      "name": "Webhook: \u00c1udio Di\u00e1rio de Bordo",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        260,
        340
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "vendedor_id",
              "name": "vendedor_id",
              "value": "={{ $json.vendedor_id }}",
              "type": "string"
            },
            {
              "id": "vendedor_whatsapp",
              "name": "vendedor_whatsapp",
              "value": "={{ $json.vendedor_whatsapp }}",
              "type": "string"
            },
            {
              "id": "transcricao",
              "name": "transcricao",
              "value": "={{ $json.transcricao }}",
              "type": "string"
            },
            {
              "id": "audio_url",
              "name": "audio_url",
              "value": "={{ $json.audio_url || '' }}",
              "type": "string"
            },
            {
              "id": "canal",
              "name": "canal",
              "value": "={{ $json.canal || 'whatsapp' }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "set-vars",
      "name": "Extrair Vari\u00e1veis",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        500,
        340
      ]
    },
    {
      "parameters": {
        "model": "gpt-4o-mini",
        "messages": {
          "values": [
            {
              "content": "Voc\u00ea \u00e9 um extrator de entidades comerciais para uma concession\u00e1ria de ve\u00edculos premium.\n\nAnalise a transcri\u00e7\u00e3o do vendedor e extraia APENAS as entidades encontradas.\n\nRetorne EXATAMENTE este JSON (sem markdown, sem explica\u00e7\u00e3o):\n{\n  \"entidades\": {\n    \"nome_mencionado\": \"string ou null\",\n    \"profissao\": \"string ou null\",\n    \"veiculo\": \"string ou null\",\n    \"cor_veiculo\": \"string ou null\",\n    \"empresa\": \"string ou null\",\n    \"cidade\": \"string ou null\",\n    \"apelido\": \"string ou null\"\n  },\n  \"confianca_extracao\": 0-100\n}\n\nExemplos:\n- \"Falei com o m\u00e9dico da HRV\" \u2192 nome:null, profissao:\"m\u00e9dico\", veiculo:\"HRV\"\n- \"O Carlos da BMW branca ligou\" \u2192 nome:\"Carlos\", veiculo:\"BMW\", cor:\"branca\"\n- \"Aquele advogado de Uberl\u00e2ndia\" \u2192 profissao:\"advogado\", cidade:\"Uberl\u00e2ndia\"",
              "role": "system"
            },
            {
              "content": "={{ $json.transcricao }}",
              "role": "user"
            }
          ]
        },
        "options": {
          "temperature": 0.1,
          "maxTokens": 500
        }
      },
      "id": "llm-extrai-entidades",
      "name": "LLM: Extrair Entidades",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.4,
      "position": [
        740,
        340
      ],
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Parse entidades extra\u00eddas pelo LLM\nconst item = $input.first().json;\nconst vars = $('Extrair Vari\u00e1veis').first().json;\n\nlet entidades;\ntry {\n  const raw = typeof item.text === 'string' ? item.text : JSON.stringify(item.text);\n  entidades = JSON.parse(raw.replace(/```json?\\n?/g, '').replace(/```/g, '').trim());\n} catch (e) {\n  entidades = { entidades: {}, confianca_extracao: 0 };\n}\n\n// Montar filtros SQL din\u00e2micos\nconst filtros = [];\nconst ent = entidades.entidades || {};\n\nif (ent.nome_mencionado) filtros.push(`nome_cliente ILIKE '%${ent.nome_mencionado}%'`);\nif (ent.veiculo) filtros.push(`(veiculo_interesse ILIKE '%${ent.veiculo}%' OR metadata->>'veiculo' ILIKE '%${ent.veiculo}%')`);\nif (ent.profissao) filtros.push(`(tags ? '${ent.profissao}' OR metadata->>'profissao' ILIKE '%${ent.profissao}%')`);\nif (ent.cor_veiculo) filtros.push(`metadata->>'cor' ILIKE '%${ent.cor_veiculo}%'`);\nif (ent.empresa) filtros.push(`metadata->>'empresa' ILIKE '%${ent.empresa}%'`);\nif (ent.cidade) filtros.push(`metadata->>'cidade' ILIKE '%${ent.cidade}%'`);\nif (ent.apelido) filtros.push(`(nome_cliente ILIKE '%${ent.apelido}%' OR metadata->>'apelido' ILIKE '%${ent.apelido}%')`);\n\nconst whereClause = filtros.length > 0\n  ? `WHERE vendedor_id = '${vars.vendedor_id}' AND (${filtros.join(' OR ')})`\n  : `WHERE vendedor_id = '${vars.vendedor_id}'`;\n\nconst query = `SELECT id, nome_cliente, veiculo_interesse, estagio_atual, temperatura, tags, metadata FROM leads ${whereClause} ORDER BY ultima_interacao DESC LIMIT 5`;\n\nreturn [{ json: { ...vars, entidades: ent, confianca: entidades.confianca_extracao, query_sql: query } }];"
      },
      "id": "build-query",
      "name": "Montar Query de Busca",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        980,
        340
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{ $json.query_sql }}",
        "options": {}
      },
      "id": "supabase-search",
      "name": "Buscar Leads Candidatos",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1220,
        340
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Montar bot\u00f5es interativos para WhatsApp\nconst items = $input.all();\nconst vars = $('Montar Query de Busca').first().json;\nconst leads = items.map(i => i.json).filter(l => l.id);\n\nif (leads.length === 0) {\n  return [{ json: {\n    ...vars,\n    tipo_resposta: 'sem_candidatos',\n    mensagem_whatsapp: '\u26a0\ufe0f N\u00e3o encontrei nenhum lead com essas caracter\u00edsticas. Pode detalhar melhor?',\n    leads_encontrados: 0\n  }}];\n}\n\n// Montar lista de bot\u00f5es (m\u00e1ximo 3 para WhatsApp Interactive)\nconst botoes = leads.slice(0, 3).map((lead, i) => ({\n  type: 'reply',\n  reply: {\n    id: `lead_${lead.id}`,\n    title: `${i + 1}. ${(lead.nome_cliente || 'Sem nome').substring(0, 20)}`\n  }\n}));\n\n// Adicionar op\u00e7\u00e3o \"Outro lead\"\nif (leads.length > 0) {\n  botoes.push({\n    type: 'reply',\n    reply: { id: 'lead_outro', title: 'Outro lead' }\n  });\n}\n\nconst corpo = leads.slice(0, 3).map((lead, i) =>\n  `${i + 1}. *${lead.nome_cliente}* (${lead.veiculo_interesse || 'N/I'}) - ${lead.estagio_atual || 'N/I'}`\n).join('\\n');\n\nreturn [{ json: {\n  ...vars,\n  tipo_resposta: 'confirmacao',\n  leads_encontrados: leads.length,\n  leads_candidatos: leads.slice(0, 3),\n  whatsapp_payload: {\n    messaging_product: 'whatsapp',\n    to: vars.vendedor_whatsapp,\n    type: 'interactive',\n    interactive: {\n      type: 'button',\n      header: { type: 'text', text: '\ud83d\udd0d Identifica\u00e7\u00e3o de Lead' },\n      body: { text: `Encontrei ${leads.length} lead(s):\\n\\n${corpo}\\n\\nQual lead voc\u00ea est\u00e1 atualizando?` },\n      action: { buttons: botoes }\n    }\n  }\n}}];"
      },
      "id": "montar-botoes",
      "name": "Montar WhatsApp Interactive",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1460,
        340
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "has-candidates",
              "leftValue": "={{ $json.tipo_resposta }}",
              "rightValue": "confirmacao",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-tem-candidatos",
      "name": "Tem Candidatos?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1700,
        340
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://graph.facebook.com/v18.0/{{ $env.WHATSAPP_PHONE_ID }}/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.whatsapp_payload) }}",
        "options": {}
      },
      "id": "enviar-whatsapp-botoes",
      "name": "Enviar Bot\u00f5es WhatsApp",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1940,
        240
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://graph.facebook.com/v18.0/{{ $env.WHATSAPP_PHONE_ID }}/messages",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"messaging_product\": \"whatsapp\",\n  \"to\": \"{{ $json.vendedor_whatsapp }}\",\n  \"type\": \"text\",\n  \"text\": { \"body\": \"{{ $json.mensagem_whatsapp }}\" }\n}",
        "options": {}
      },
      "id": "enviar-whatsapp-texto",
      "name": "Enviar Texto (Sem Candidatos)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1940,
        460
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\n  \"success\": true,\n  \"leads_encontrados\": {{ $('Montar WhatsApp Interactive').first().json.leads_encontrados }},\n  \"tipo_resposta\": \"{{ $('Montar WhatsApp Interactive').first().json.tipo_resposta }}\"\n}",
        "options": {}
      },
      "id": "respond-webhook",
      "name": "Responder Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2200,
        340
      ]
    }
  ],
  "connections": {
    "Webhook: \u00c1udio Di\u00e1rio de Bordo": {
      "main": [
        [
          {
            "node": "Extrair Vari\u00e1veis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extrair Vari\u00e1veis": {
      "main": [
        [
          {
            "node": "LLM: Extrair Entidades",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM: Extrair Entidades": {
      "main": [
        [
          {
            "node": "Montar Query de Busca",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Montar Query de Busca": {
      "main": [
        [
          {
            "node": "Buscar Leads Candidatos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar Leads Candidatos": {
      "main": [
        [
          {
            "node": "Montar WhatsApp Interactive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Montar WhatsApp Interactive": {
      "main": [
        [
          {
            "node": "Tem Candidatos?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tem Candidatos?": {
      "main": [
        [
          {
            "node": "Enviar Bot\u00f5es WhatsApp",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Enviar Texto (Sem Candidatos)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enviar Bot\u00f5es WhatsApp": {
      "main": [
        [
          {
            "node": "Responder Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enviar Texto (Sem Candidatos)": {
      "main": [
        [
          {
            "node": "Responder Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [
    {
      "id": "otto-1",
      "name": "OTTO"
    },
    {
      "id": "otto-2",
      "name": "Identidade"
    },
    {
      "id": "otto-3",
      "name": "\u00c9pico 1"
    }
  ],
  "triggerCount": 1,
  "updatedAt": "2026-02-26T12:00:00.000Z",
  "versionId": "1"
}

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

How this works

This workflow resolves user identities deterministically from daily audio logs, enabling precise lead matching and personalised outreach without ambiguity. It suits sales or customer service teams handling voice-based interactions, such as daily check-ins via WhatsApp, by extracting key entities like names or details using OpenAI and querying a Postgres database for matches. The key step involves the AI-driven entity extraction, which powers a targeted search to confirm identities swiftly and accurately.

Use this when processing unstructured audio inputs requires reliable identity verification, like in automated follow-ups for potential clients logging daily updates. Avoid it for high-volume, non-audio triggers or when probabilistic matching suffices, as the deterministic approach demands structured data storage. Common variations include adapting the Postgres query for CRM integrations or extending the WhatsApp response for multi-channel notifications.

About this workflow

OTTO - Épico 1: Resolução Determinística de Identidade. Uses openAi, postgres, httpRequest. Webhook trigger; 11 nodes.

Source: https://github.com/paraisolorrayne/Attra/blob/6dd06914bf6be4654e2cb2b445995786aa402cd3/automations/epico-1-resolucao-identidade.json — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

Postgres. Uses openAi, postgres, postgresTool, httpRequest. Webhook trigger; 19 nodes.

OpenAI, Postgres, Postgres Tool +2
Data & Sheets

Crop Planning. Uses postgres, httpRequest, respondToWebhook, googleGemini. Webhook trigger; 11 nodes.

Postgres, HTTP Request, Google Gemini +1
Data & Sheets

OTTO - Épico 2: Funil de Hipótese (IA Sugere, Sistema Julga). Uses openAi, postgres, httpRequest. Webhook trigger; 10 nodes.

OpenAI, Postgres, HTTP Request
Data & Sheets

Web Scraper with AI Extraction. Uses httpRequest, openAi, postgres. Webhook trigger; 7 nodes.

HTTP Request, OpenAI, Postgres
Data & Sheets

This n8n workflow automates the transformation of spreadsheet data into professional charts and graphs using AI-driven analysis. Triggered via Slack, it processes uploaded files (Excel, CSV, Google Sh

Agent, Postgres, HTTP Request +8