AutomationFlowsAI & RAG › Monitor Brand Visibility in AI Search with Chatgpt, Perplexity, Gemini,…

Monitor Brand Visibility in AI Search with Chatgpt, Perplexity, Gemini,…

Original n8n title: Monitor Brand Visibility in AI Search with Chatgpt, Perplexity, Gemini, Claude, and Supabase

ByNocode Expert @iamrup on n8n.io

This workflow asks buyer-intent questions to ChatGPT Search (OpenAI), Perplexity, Google Gemini Search, and Anthropic Claude, scores whether your brand is cited or mentioned in each answer, logs every check to Supabase, and outputs an overall and per-engine visibility scorecard.…

Event trigger★★★★☆ complexity19 nodesHTTP RequestSupabase
AI & RAG Trigger: Event Nodes: 19 Complexity: ★★★★☆ Added:

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

This workflow follows the HTTP Request → Supabase 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
{
  "id": "mvxVBIvdjGEVWgGX",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI search visibility monitor (ChatGPT, Perplexity, Gemini, Claude)",
  "tags": [],
  "nodes": [
    {
      "id": "2d482efa-2ae2-4c17-bde2-353995438624",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        14752,
        480
      ],
      "parameters": {
        "width": 668,
        "height": 600,
        "content": "## AI search visibility monitor\n\nAsks the AI answer engines (ChatGPT search, Perplexity, Gemini, Claude) the buyer questions your customers actually type, then checks whether your brand is named or cited. Scores each answer cited(1) / mentioned(0.5) / absent(0), logs every check to Supabase, and prints a panel score you can track over time.\n\n### Why it matters\nBuyers ask AI before Google now. If the engines never name you for 'who should I hire', you are invisible where the decision is made. This turns that into a number you can move and chart.\n\n### Setup\n1. Add to your n8n environment: ANTHROPIC_API_KEY, OPENAI_API_KEY, PERPLEXITY_API_KEY, GEMINI_API_KEY.\n2. Create the Supabase table:\ncreate table ai_visibility_log (\n  id bigint generated always as identity primary key,\n  run_at timestamptz default now(),\n  brand text, engine text, question text,\n  score numeric, cited boolean, mentioned boolean\n);\n3. Add a Supabase credential and pick it on the log node.\n4. Edit brand + questions in Config, then Run. Add a Schedule trigger for a weekly check.\n\n### Dashboard\nChart panel score over time and by engine straight from ai_visibility_log.\n\nBuilt by nocode.expert. https://nocode.expert"
      },
      "typeVersion": 1
    },
    {
      "id": "91808814-a4a2-4907-bb55-90fcea58b1c7",
      "name": "Section: Ask",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        15536,
        496
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 180,
        "content": "## 1. Configure and ask\nSet brand + questions, fan out one item per question, then ask all four engines in parallel."
      },
      "typeVersion": 1
    },
    {
      "id": "5ea3d8df-2a19-413c-b152-058d64a77f1e",
      "name": "Section: Score",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16240,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 180,
        "content": "## 2. Score each answer\nEach engine has a parse node: cited if your domain shows up, mentioned if only your name, else absent."
      },
      "typeVersion": 1
    },
    {
      "id": "112e5b6e-4a4d-4b27-945e-02eb9f4daf1f",
      "name": "Section: Report",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        17008,
        496
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 180,
        "content": "## 3. Log and report\nWrite every check to Supabase for the dashboard, compute the panel score, print the scorecard."
      },
      "typeVersion": 1
    },
    {
      "id": "08bf1d55-a0db-42f3-9f34-5e944a0fa4dd",
      "name": "Run manually",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        15456,
        736
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a4ad670d-678c-4a69-8b17-b748562bf18b",
      "name": "Set config: brand, buyer questions",
      "type": "n8n-nodes-base.code",
      "position": [
        15712,
        736
      ],
      "parameters": {
        "jsCode": "// Two knobs: what counts as YOU (brand domain + name), and the buyer questions\n// to ask each AI engine. Add or edit questions freely.\nreturn [{ json: {\n  BRAND_DOMAIN: 'stripe.com',\n  BRAND_NAME: 'Stripe',\n  QUESTIONS: [\"What is the best payment processor for an online startup? Name specific providers.\", \"How should a SaaS company accept payments and manage subscriptions online?\", \"Which payment API is easiest for developers to integrate?\", \"Who offers the simplest online checkout for a new e-commerce store?\", \"Best Stripe alternatives for a small business?\"],\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "3f64fd63-7f3d-4ca7-a66e-7e82c92b66b6",
      "name": "Build question items",
      "type": "n8n-nodes-base.code",
      "position": [
        15952,
        736
      ],
      "parameters": {
        "jsCode": "// Fan out one item per question so each engine node runs once per question.\nreturn $('Set config: brand, buyer questions').first().json.QUESTIONS.map(q => ({ json: { question: q } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "dfc0d650-43e7-4fdc-a5b8-8d279fc2544e",
      "name": "Ask Claude",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16224,
        544
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { \"model\": \"claude-haiku-4-5\", \"max_tokens\": 1024, \"messages\": [ { \"role\": \"user\", \"content\": $json.question } ] } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.ANTHROPIC_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "21adc6af-a834-418b-b1a5-0f3033bdea3f",
      "name": "Ask ChatGPT (search)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16224,
        704
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { \"model\": \"gpt-4o-search-preview\", \"messages\": [ { \"role\": \"user\", \"content\": $json.question } ] } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.OPENAI_API_KEY }}"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e20348b1-f8b1-43af-b3f8-4cf428bd5488",
      "name": "Ask Perplexity",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16224,
        864
      ],
      "parameters": {
        "url": "https://api.perplexity.ai/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { \"model\": \"sonar\", \"messages\": [ { \"role\": \"user\", \"content\": $json.question } ] } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.PERPLEXITY_API_KEY }}"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "0dafad32-0577-4534-95d1-01fcee0bc69f",
      "name": "Ask Gemini (search)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16224,
        1024
      ],
      "parameters": {
        "url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $env.GEMINI_API_KEY }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { \"contents\": [ { \"parts\": [ { \"text\": $json.question } ] } ], \"tools\": [ { \"google_search\": {} } ] } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "1447d99e-0897-420d-ab36-28eaf823c41c",
      "name": "Score Claude",
      "type": "n8n-nodes-base.code",
      "position": [
        16496,
        544
      ],
      "parameters": {
        "jsCode": "// Extract the answer, then score visibility. cited(1)=brand domain appears\n// anywhere in the response (prose or source URL); mentioned(0.5)=brand name only;\n// absent(0)=neither. Pure transform.\nconst B = $('Set config: brand, buyer questions').first().json;\nconst domain = B.BRAND_DOMAIN.toLowerCase();\nconst name = B.BRAND_NAME.toLowerCase();\nconst questions = $('Build question items').all();\nconst out = [];\n$input.all().forEach((it, i) => {\n  const q = questions[i].json.question;\n  const text = it.json.content && it.json.content[0] && it.json.content[0].text || '';\n  const hay = (text + ' ' + JSON.stringify(it.json)).toLowerCase();\n  const cited = hay.includes(domain);\n  const mentioned = cited || hay.includes(name);\n  const score = cited ? 1 : (mentioned ? 0.5 : 0);\n  out.push({ json: { engine: 'Claude', question: q, score, cited, mentioned, snippet: text.slice(0,180) } });\n});\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "1dbf051e-d288-44b6-9b76-754686bce228",
      "name": "Score ChatGPT",
      "type": "n8n-nodes-base.code",
      "position": [
        16496,
        704
      ],
      "parameters": {
        "jsCode": "// Extract the answer, then score visibility. cited(1)=brand domain appears\n// anywhere in the response (prose or source URL); mentioned(0.5)=brand name only;\n// absent(0)=neither. Pure transform.\nconst B = $('Set config: brand, buyer questions').first().json;\nconst domain = B.BRAND_DOMAIN.toLowerCase();\nconst name = B.BRAND_NAME.toLowerCase();\nconst questions = $('Build question items').all();\nconst out = [];\n$input.all().forEach((it, i) => {\n  const q = questions[i].json.question;\n  const text = it.json.choices && it.json.choices[0] && it.json.choices[0].message && it.json.choices[0].message.content || '';\n  const hay = (text + ' ' + JSON.stringify(it.json)).toLowerCase();\n  const cited = hay.includes(domain);\n  const mentioned = cited || hay.includes(name);\n  const score = cited ? 1 : (mentioned ? 0.5 : 0);\n  out.push({ json: { engine: 'ChatGPT', question: q, score, cited, mentioned, snippet: text.slice(0,180) } });\n});\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "6ac8b2be-a4b6-4e59-946c-b68888e6f81e",
      "name": "Score Perplexity",
      "type": "n8n-nodes-base.code",
      "position": [
        16496,
        864
      ],
      "parameters": {
        "jsCode": "// Extract the answer, then score visibility. cited(1)=brand domain appears\n// anywhere in the response (prose or source URL); mentioned(0.5)=brand name only;\n// absent(0)=neither. Pure transform.\nconst B = $('Set config: brand, buyer questions').first().json;\nconst domain = B.BRAND_DOMAIN.toLowerCase();\nconst name = B.BRAND_NAME.toLowerCase();\nconst questions = $('Build question items').all();\nconst out = [];\n$input.all().forEach((it, i) => {\n  const q = questions[i].json.question;\n  const text = it.json.choices && it.json.choices[0] && it.json.choices[0].message && it.json.choices[0].message.content || '';\n  const hay = (text + ' ' + JSON.stringify(it.json)).toLowerCase();\n  const cited = hay.includes(domain);\n  const mentioned = cited || hay.includes(name);\n  const score = cited ? 1 : (mentioned ? 0.5 : 0);\n  out.push({ json: { engine: 'Perplexity', question: q, score, cited, mentioned, snippet: text.slice(0,180) } });\n});\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "a400fd3e-bcbd-4edf-8535-dc4ff9415e97",
      "name": "Score Gemini",
      "type": "n8n-nodes-base.code",
      "position": [
        16496,
        1024
      ],
      "parameters": {
        "jsCode": "// Extract the answer, then score visibility. cited(1)=brand domain appears\n// anywhere in the response (prose or source URL); mentioned(0.5)=brand name only;\n// absent(0)=neither. Pure transform.\nconst B = $('Set config: brand, buyer questions').first().json;\nconst domain = B.BRAND_DOMAIN.toLowerCase();\nconst name = B.BRAND_NAME.toLowerCase();\nconst questions = $('Build question items').all();\nconst out = [];\n$input.all().forEach((it, i) => {\n  const q = questions[i].json.question;\n  const text = it.json.candidates && it.json.candidates[0] && it.json.candidates[0].content && it.json.candidates[0].content.parts && it.json.candidates[0].content.parts[0] && it.json.candidates[0].content.parts[0].text || '';\n  const hay = (text + ' ' + JSON.stringify(it.json)).toLowerCase();\n  const cited = hay.includes(domain);\n  const mentioned = cited || hay.includes(name);\n  const score = cited ? 1 : (mentioned ? 0.5 : 0);\n  out.push({ json: { engine: 'Gemini', question: q, score, cited, mentioned, snippet: text.slice(0,180) } });\n});\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "bf0e269f-a04a-4846-9794-1d4d01c70646",
      "name": "Prepare rows",
      "type": "n8n-nodes-base.code",
      "position": [
        16768,
        864
      ],
      "parameters": {
        "jsCode": "// Shape each check into a row for Supabase (columns match the table below).\nconst B = $('Set config: brand, buyer questions').first().json;\nreturn $input.all().map(i => ({ json: {\n  brand: B.BRAND_DOMAIN,\n  engine: i.json.engine,\n  question: i.json.question,\n  score: i.json.score,\n  cited: i.json.cited,\n  mentioned: i.json.mentioned,\n} }));"
      },
      "typeVersion": 2
    },
    {
      "id": "e0093310-0794-43ea-aea8-4dc07fa2362a",
      "name": "Log to Supabase",
      "type": "n8n-nodes-base.supabase",
      "position": [
        17248,
        880
      ],
      "parameters": {
        "tableId": "ai_visibility_log",
        "dataToSend": "autoMapInputData"
      },
      "typeVersion": 1
    },
    {
      "id": "0fb23faa-e63f-4891-a7b6-3e39528b4811",
      "name": "Aggregate & score panel",
      "type": "n8n-nodes-base.code",
      "position": [
        17008,
        736
      ],
      "parameters": {
        "jsCode": "// Combine all rows, compute per-engine and overall panel score.\nconst rows = $input.all().map(i => i.json);\nconst byEngine = {};\nfor (const r of rows) {\n  const e = byEngine[r.engine] = byEngine[r.engine] || { checks: 0, score: 0, cited: 0, mentioned: 0 };\n  e.checks++; e.score += r.score; if (r.cited) e.cited++; else if (r.mentioned) e.mentioned++;\n}\nfor (const k of Object.keys(byEngine)) byEngine[k].pct = +(100 * byEngine[k].score / byEngine[k].checks).toFixed(1);\nconst total = rows.reduce((n, r) => n + r.score, 0);\nconst panelPct = rows.length ? +(100 * total / rows.length).toFixed(1) : 0;\nreturn [{ json: { panelPct, totalChecks: rows.length, byEngine, rows } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "18a5f045-2fdd-4fb8-a1da-03cfae563864",
      "name": "Print visibility scorecard",
      "type": "n8n-nodes-base.code",
      "position": [
        17248,
        736
      ],
      "parameters": {
        "jsCode": "// Print the visibility scorecard. Persisted rows go to Supabase for the dashboard.\nconst m = $json;\nconst brand = $('Set config: brand, buyer questions').first().json.BRAND_DOMAIN;\nconst out = ['', '==========================================================',\n'  AI SEARCH VISIBILITY  |  ' + brand, '=========================================================='];\nout.push('Panel score: ' + m.panelPct + '%   (' + m.totalChecks + ' checks: cited=1, mentioned=0.5, absent=0)');\nout.push('');\nfor (const [eng, e] of Object.entries(m.byEngine)) {\n  out.push(eng.padEnd(12) + ' ' + String(e.pct).padStart(5) + '%   cited ' + e.cited + ', mentioned ' + e.mentioned + ' of ' + e.checks);\n}\nout.push(''); out.push('Not cited (targets):');\nm.rows.filter(r => r.score < 1).slice(0, 12).forEach(r => out.push('  [' + r.engine + '] ' + (r.mentioned ? 'name only' : 'absent') + ' : ' + r.question.slice(0, 55)));\nout.push('==========================================================');\nconsole.log(out.join('\\n'));\nreturn [{ json: m }];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "921cd3b6-a75e-47ff-aea1-e9dd621f0d2a",
  "connections": {
    "Ask Claude": {
      "main": [
        [
          {
            "node": "Score Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare rows": {
      "main": [
        [
          {
            "node": "Log to Supabase",
            "type": "main",
            "index": 0
          },
          {
            "node": "Aggregate & score panel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run manually": {
      "main": [
        [
          {
            "node": "Set config: brand, buyer questions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Claude": {
      "main": [
        [
          {
            "node": "Prepare rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Gemini": {
      "main": [
        [
          {
            "node": "Prepare rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score ChatGPT": {
      "main": [
        [
          {
            "node": "Prepare rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask Perplexity": {
      "main": [
        [
          {
            "node": "Score Perplexity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Perplexity": {
      "main": [
        [
          {
            "node": "Prepare rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask Gemini (search)": {
      "main": [
        [
          {
            "node": "Score Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask ChatGPT (search)": {
      "main": [
        [
          {
            "node": "Score ChatGPT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build question items": {
      "main": [
        [
          {
            "node": "Ask Claude",
            "type": "main",
            "index": 0
          },
          {
            "node": "Ask ChatGPT (search)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Ask Perplexity",
            "type": "main",
            "index": 0
          },
          {
            "node": "Ask Gemini (search)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate & score panel": {
      "main": [
        [
          {
            "node": "Print visibility scorecard",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set config: brand, buyer questions": {
      "main": [
        [
          {
            "node": "Build question items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

This workflow asks buyer-intent questions to ChatGPT Search (OpenAI), Perplexity, Google Gemini Search, and Anthropic Claude, scores whether your brand is cited or mentioned in each answer, logs every check to Supabase, and outputs an overall and per-engine visibility scorecard.…

Source: https://n8n.io/workflows/17085/ — 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

This template can be used to generate research ideas from PDF scientific papers based on the content gaps found in text using the InfraNodus knowledge graph GraphRAG knowledge graph representation.

HTTP Request, Form Trigger, Form
AI & RAG

Build Your Own Image Search Using Ai Object Detection, Cdn And Elasticsearchbuild Your Own Image Search Using Ai Object Detection, Cdn And Elasticsearch. Uses manualTrigger, httpRequest, splitOut, edi

HTTP Request, Edit Image, Elasticsearch
AI & RAG

This workflow reads a list of web page URLs from Google Sheets, fetches each page, and uses OpenAI Chat Completions to generate a factual title, short summary, key points, topic, and confidence score,

Google Sheets, HTTP Request
AI & RAG

Summarize YouTube Videos with Google Gemini 2.5. Uses formTrigger, httpRequest. Event-driven trigger; 10 nodes.

Form Trigger, HTTP Request
AI & RAG

This workflow takes two inputs, YouTube video URL (required) and a description of what information to extract from the video. If the description/"what you want" field is left empty, the default prompt

Form Trigger, HTTP Request, Form