AutomationFlowsData & Sheets › Shave of Voice, Checked! — Free Visibility Check

Shave of Voice, Checked! — Free Visibility Check

Shave Of Voice, Checked! — Free Visibility Check. Uses airtable, httpRequest. Webhook trigger; 9 nodes.

Webhook trigger★★★★☆ complexity9 nodesAirtableHTTP Request
Data & Sheets Trigger: Webhook Nodes: 9 Complexity: ★★★★☆ Added:

This workflow follows the Airtable → 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
{
  "name": "Shave Of Voice, Checked! \u2014 Free Visibility Check",
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "id": "webhook",
      "name": "Free Check Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "free-check",
        "responseMode": "responseNode",
        "options": {
          "allowedOrigins": "*"
        }
      }
    },
    {
      "id": "validate",
      "name": "Validate + Anti-Abuse",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ],
      "parameters": {
        "jsCode": "const body = $input.item.json.body || $input.item.json;\nconst brand = (body.brand || '').trim();\nconst email = (body.email || '').trim().toLowerCase();\nconst ip    = $input.item.json.headers?.['x-forwarded-for']?.split(',')[0]?.trim() || 'unknown';\n\n// Basic validation\nif (!brand || brand.length < 2) throw new Error('Brand name too short');\nif (!email || !email.includes('@') || !email.includes('.')) throw new Error('Invalid email');\n\n// Disposable email domain blocklist (extend as needed)\nconst blocked = ['mailinator.com','guerrillamail.com','tempmail.com','throwaway.email','yopmail.com','sharklasers.com','trashmail.com','maildrop.cc'];\nconst domain = email.split('@')[1];\nif (blocked.includes(domain)) throw new Error('Disposable email not allowed');\n\nconst crypto = require('crypto');\nconst ipHash = crypto.createHash('sha256').update(ip).digest('hex').slice(0, 16);\n\nreturn [{ json: { brand, email, ip_hash: ipHash, domain } }];"
      }
    },
    {
      "id": "check-abuse",
      "name": "Check Abuse in Airtable",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2,
      "position": [
        680,
        300
      ],
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "parameters": {
        "operation": "list",
        "base": {
          "__rl": true,
          "value": "={{ $env.AIRTABLE_BASE_ID }}",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "Clients",
          "mode": "name"
        },
        "filterByFormula": "OR(AND({email}='{{ $json.email }}',LOWER({name})=LOWER('{{ $json.brand }}')),{free_check_blocked}=1)",
        "options": {
          "fields": [
            "id",
            "free_check_count",
            "free_check_blocked",
            "free_check_hashes"
          ]
        }
      }
    },
    {
      "id": "abuse-gate",
      "name": "Abuse Gate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        300
      ],
      "parameters": {
        "jsCode": "const existing = $('Check Abuse in Airtable').all();\nconst { brand, email, ip_hash } = $('Validate + Anti-Abuse').item.json;\n\nfor (const rec of existing) {\n  const f = rec.json.fields || rec.json;\n  if (f.free_check_blocked) throw new Error('ABUSE_BLOCKED');\n  // Same brand+email combo already used\n  if (f.email === email) throw new Error('ALREADY_USED');\n  // IP rate limit: check if ip_hash appears in recent hashes\n  try {\n    const hashes = JSON.parse(f.free_check_hashes || '[]');\n    if (hashes.filter(h => h === ip_hash).length >= 3) throw new Error('RATE_LIMITED');\n  } catch(e) { if (e.message === 'RATE_LIMITED') throw e; }\n}\n\nreturn [{ json: { brand, email, ip_hash, existing_id: existing[0]?.json?.id || null } }];"
      }
    },
    {
      "id": "run-prompts",
      "name": "Run 5 Default Prompts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        300
      ],
      "parameters": {
        "jsCode": "const { brand, email, ip_hash } = $json;\n\n// 5 generic buyer-intent prompt templates\nconst templates = [\n  `What are the best tools for companies like ${brand}?`,\n  `Who are the top vendors in the same category as ${brand}?`,\n  `What do people recommend instead of or alongside ${brand}?`,\n  `Is ${brand} a good choice for B2B companies?`,\n  `What are the main alternatives to ${brand}?`\n];\n\nconst results = [];\nfor (const prompt of templates) {\n  const res = await helpers.httpRequest({\n    method: 'POST',\n    url: 'https://openrouter.ai/api/v1/chat/completions',\n    headers: { 'Authorization': 'Bearer ' + $env.OPENROUTER_API_KEY, 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      model: 'gpt-4o-mini',\n      temperature: 0.3,\n      messages: [\n        { role: 'system', content: 'You are a helpful assistant. Answer the user question directly and thoroughly.' },\n        { role: 'user', content: prompt }\n      ]\n    })\n  });\n  const text = res.choices[0].message.content;\n  const mentioned = text.toLowerCase().includes(brand.toLowerCase());\n  results.push({ prompt, response: text, mentioned });\n}\n\nconst score = results.filter(r => r.mentioned).length;\nreturn [{ json: { brand, email, ip_hash, score, total: 5, results } }];"
      }
    },
    {
      "id": "store-result",
      "name": "Store Free Check in Airtable",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2,
      "position": [
        1340,
        300
      ],
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "parameters": {
        "operation": "create",
        "base": {
          "__rl": true,
          "value": "={{ $env.AIRTABLE_BASE_ID }}",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "Clients",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "name": "={{ $json.brand }}",
            "email": "={{ $json.email }}",
            "plan": "free_check",
            "status": "lead",
            "free_check_count": 1,
            "free_check_hashes": "={{ JSON.stringify([$json.ip_hash]) }}",
            "created_at": "={{ new Date().toISOString() }}"
          }
        },
        "options": {}
      }
    },
    {
      "id": "send-result-email",
      "name": "Send Result Email",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1560,
        300
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "parameters": {
        "method": "POST",
        "url": "https://api.resend.com/emails",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  from: 'Shave Of Voice, Checked! <reports@sovcheck.online>',\n  to: [$('Run 5 Default Prompts').item.json.email],\n  subject: `${$('Run 5 Default Prompts').item.json.brand} appeared in ${$('Run 5 Default Prompts').item.json.score}/5 AI prompts`,\n  html: `<!DOCTYPE html><html><body style=\"font-family:Helvetica Neue,Arial,sans-serif;background:#0B0F1A;color:#F0F4FF;margin:0;padding:0\"><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr><td align=\"center\" style=\"padding:40px 16px\"><table width=\"560\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#141927;border-radius:16px;border:1px solid #1E2740;overflow:hidden\"><tr><td style=\"padding:28px 32px;border-bottom:1px solid #1E2740;background:#10172A\"><span style=\"font-size:12px;font-weight:700;color:#8B95B0;letter-spacing:0.08em;text-transform:uppercase\">Shave Of Voice, Checked! &mdash; Free Visibility Check</span></td></tr><tr><td style=\"padding:32px\"><p style=\"margin:0 0 8px;font-size:13px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:#4FFFB0\">Your AI Visibility Score</p><p style=\"margin:0 0 24px;font-size:42px;font-weight:800;color:#F0F4FF;font-family:monospace\">${$('Run 5 Default Prompts').item.json.score}<span style=\"font-size:20px;color:#8B95B0\">/5 prompts</span></p><p style=\"margin:0 0 20px;font-size:15px;color:#8B95B0;line-height:1.6\">We ran 5 buyer-intent prompts about <strong style=\"color:#F0F4FF\">${$('Run 5 Default Prompts').item.json.brand}</strong> through ChatGPT. Your brand appeared in <strong style=\"color:#4FFFB0\">${$('Run 5 Default Prompts').item.json.score} out of 5</strong>.</p><p style=\"margin:0 0 28px;font-size:15px;color:#8B95B0;line-height:1.6\">Want the full picture? The Pro plan runs <strong style=\"color:#F0F4FF\">50 prompts per week</strong> across ChatGPT, Perplexity, and Gemini &mdash; with competitor tracking and week-over-week trends.</p><a href=\"https://tally.so/r/q4Y6xY\" style=\"display:inline-block;background:#4FFFB0;color:#0B0F1A;font-size:14px;font-weight:700;padding:13px 28px;border-radius:10px;text-decoration:none\">Get the Full Report &rarr;</a></td></tr><tr><td style=\"padding:18px 32px;border-top:1px solid #1E2740;background:#10172A;font-size:12px;color:#5C6885\">Shave Of Voice, Checked! &middot; AI Brand Visibility Monitoring</td></tr></table></td></tr></table></body></html>`\n}) }}",
        "options": {}
      }
    },
    {
      "id": "respond-ok",
      "name": "Respond with Score",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1780,
        300
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ ok: true, score: $('Run 5 Default Prompts').item.json.score, total: 5, brand: $('Run 5 Default Prompts').item.json.brand }) }}",
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      }
    },
    {
      "id": "respond-error",
      "name": "Respond with Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        900,
        500
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ ok: false, message: $json.message }) }}",
        "options": {
          "responseCode": 429
        }
      }
    }
  ],
  "connections": {
    "Free Check Webhook": {
      "main": [
        [
          {
            "node": "Validate + Anti-Abuse",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate + Anti-Abuse": {
      "main": [
        [
          {
            "node": "Check Abuse in Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Abuse in Airtable": {
      "main": [
        [
          {
            "node": "Abuse Gate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Abuse Gate": {
      "main": [
        [
          {
            "node": "Run 5 Default Prompts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run 5 Default Prompts": {
      "main": [
        [
          {
            "node": "Store Free Check in Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store Free Check in Airtable": {
      "main": [
        [
          {
            "node": "Send Result Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Result Email": {
      "main": [
        [
          {
            "node": "Respond with Score",
            "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

Shave Of Voice, Checked! — Free Visibility Check. Uses airtable, httpRequest. Webhook trigger; 9 nodes.

Source: https://github.com/alvee1994/promptscore/blob/86adec3e7e7f92f0602e694d78bef062735f9d17/n8n/workflow_free_check.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

This premium n8n workflow harnesses the power of DataForSEO's API combined with Airtable's relational database capabilities to transform your keyword research process, providing deeper insights for co

HTTP Request, Airtable
Data & Sheets

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Airtable, HTTP Request, Google Drive +1
Data & Sheets

This workflow automates the entire lifecycle of a service-based client, combining four distinct business flows into a single view: Intake Leads: Receives a webhook from your form builder, validates th

Airtable, Notion, Google Calendar +3
Data & Sheets

It intelligently syncs confirmed sales orders from your Airtable base to QuickBooks, automatically creating new customers if they don't exist before generating a perfectly matched invoice. It then log

Airtable, QuickBooks, HTTP Request
Data & Sheets

Who is this for? Business who manually prep/route DocuSign envelopes and want zero-touch contract signing from form submission.

Airtable, HTTP Request