AutomationFlowsAI & RAG › Moderate Toxic Text, Redact Pii, and Audit with a Gemini Http API

Moderate Toxic Text, Redact Pii, and Audit with a Gemini Http API

ByOka Hironobu @okp29 on n8n.io

This workflow exposes a POST webhook that sends user text (and optional context) to Google Gemini for moderation, returning an allow/flag/block decision, toxicity level, matched categories, detected PII types, a short reason, and a cleaned version with slurs masked and PII…

Webhook trigger★★★★☆ complexityAI-powered17 nodesChain LlmGoogle Gemini ChatOutput Parser StructuredGoogle SheetsSlack
AI & RAG Trigger: Webhook Nodes: 17 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Chainllm → Google Sheets 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": "w5BvmIQZYnDSV6Kl",
  "meta": {
    "builderVariant": "mcp",
    "aiBuilderAssisted": true
  },
  "name": "Moderate toxic text and redact PII with a batch Gemini HTTP API",
  "tags": [],
  "nodes": [
    {
      "id": "69f2f2be-9ab8-42b9-a3d6-a000da7f8103",
      "name": "Moderation API Endpoint",
      "type": "n8n-nodes-base.webhook",
      "position": [
        48,
        48
      ],
      "parameters": {
        "path": "moderate",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "2e9d996e-94ae-4ebc-8b52-d0fd138e5b23",
      "name": "Normalize Request",
      "type": "n8n-nodes-base.code",
      "position": [
        272,
        48
      ],
      "parameters": {
        "jsCode": "const body = ($json.body) || {};\nconst headers = ($json.headers) || {};\nlet texts = [];\nif (Array.isArray(body.texts)) texts = body.texts;\nelse if (body.text != null) texts = [body.text];\nif (texts.length === 0) texts = [''];\nconst requester = body.requester || headers['x-requester'] || 'anonymous';\nconst context = body.context || '';\nreturn texts.map(function (t, i) { return { json: { id: i + 1, text: String(t == null ? '' : t), context: context, requester: requester } }; });"
      },
      "typeVersion": 2
    },
    {
      "id": "fc85c785-997b-41b0-b188-650f9dc5d89c",
      "name": "Moderate Text with Gemini",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "onError": "continueRegularOutput",
      "position": [
        496,
        48
      ],
      "parameters": {
        "text": "=You are a content moderation engine. Classify the text and respond with JSON. Set action to \"allow\" for safe text, \"flag\" for borderline text a human should review, and \"block\" for clearly toxic, hateful, sexual or harassing content. Rate toxicity none/low/medium/high, list categories, detect personal data (emails, phone numbers, addresses, card numbers) and list the types, give a short reason, and return a cleaned version with slurs masked and PII redacted.\n\nContext: {{ $json.context }}\nText:\n{{ $json.text }}",
        "batching": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.9
    },
    {
      "id": "40c44c3c-7fb2-4aa7-9ce8-2dc02d0f5ad6",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        496,
        224
      ],
      "parameters": {
        "options": {
          "temperature": 0
        },
        "modelName": "models/gemini-3.1-flash-lite"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "0de58844-669d-4081-b655-d8dcf8fe6d29",
      "name": "Moderation Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        656,
        224
      ],
      "parameters": {
        "jsonSchemaExample": "{\"action\":\"block\",\"toxicity\":\"high\",\"categories\":[\"insult\"],\"contains_pii\":true,\"pii_found\":[\"email\"],\"reason\":\"Insult and an email address\",\"cleaned_text\":\"You are an [removed], email [email removed]\"}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "7e94cb6e-3c73-47bf-a5d0-30491ba5b346",
      "name": "Shape Result",
      "type": "n8n-nodes-base.code",
      "position": [
        800,
        48
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const meta = $('Normalize Request').item.json;\nconst d = $json.output || {};\nconst join = function (x) { return Array.isArray(x) ? x.join(', ') : (x || ''); };\nconst has = (typeof d.action === 'string');\nreturn {\n  id: meta.id,\n  requester: meta.requester,\n  text: meta.text,\n  action: has ? String(d.action).toLowerCase() : 'flag',\n  toxicity: d.toxicity || 'unknown',\n  categories: join(d.categories),\n  containsPii: d.contains_pii ? true : false,\n  piiFound: join(d.pii_found),\n  reason: has ? (d.reason || '') : 'Moderation engine did not return a result; defaulted to human review.',\n  cleanedText: d.cleaned_text || meta.text\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c9c974e0-06c4-4d1b-a537-ea07869b3db9",
      "name": "Log to Audit Trail",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        960,
        48
      ],
      "parameters": {
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "Moderation Log"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "Select your spreadsheet"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "6f281d97-a790-4761-ba00-94d052499863",
      "name": "Bundle Results",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1168,
        48
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "d26bef31-53f0-416f-be44-009104d73c9f",
      "name": "Build API Response",
      "type": "n8n-nodes-base.code",
      "position": [
        1360,
        48
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const rows = $json.data || [];\nconst results = rows.map(function (r) { return { id: r.id, action: r.action, toxicity: r.toxicity, categories: r.categories, contains_pii: r.containsPii, pii_found: r.piiFound, reason: r.reason, cleaned_text: r.cleanedText }; });\nconst count = function (a) { return results.filter(function (r) { return r.action === a; }).length; };\nconst blocked = count('block');\nconst flagged = count('flag');\nreturn { summary: { total: results.length, blocked: blocked, flagged: flagged, allowed: results.length - blocked - flagged }, results: results };"
      },
      "typeVersion": 2
    },
    {
      "id": "b20f6798-c4dd-4d7f-9e21-88e7fe412784",
      "name": "Return Result",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1584,
        48
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "4e532cad-a9fd-4c18-9af3-ee4063069496",
      "name": "Any Blocked",
      "type": "n8n-nodes-base.if",
      "position": [
        1776,
        192
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.summary.blocked }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "ec77acb8-bc3e-4230-9df8-a3af8ce8ca50",
      "name": "Alert on Blocked Content",
      "type": "n8n-nodes-base.slack",
      "position": [
        2016,
        176
      ],
      "parameters": {
        "text": "=*:no_entry: Moderation API blocked content*\n{{ $json.summary.blocked }} of {{ $json.summary.total }} submitted messages were blocked. Check the audit log for details.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "#moderation"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "7039219c-f01e-483f-b074-cb65596f86fd",
      "name": "Overview Sticky",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -608,
        -224
      ],
      "parameters": {
        "width": 584,
        "height": 684,
        "content": "## Moderate toxic text and redact PII with a batch Gemini HTTP API\n\n### How it works\nThis is a production-style moderation API, not a single classify-and-return call. POST a JSON body with either one text or an array of texts (plus an optional requester ID) to the webhook. The request is normalised into one item per message, and a Basic LLM Chain with Google Gemini classifies each: an allow / flag / block decision, toxicity, categories, any personal data found, a reason, and a cleaned version with slurs masked and PII redacted. Every message is written to a Google Sheets audit trail, all results are aggregated into a single JSON response with a summary count, and the Respond to Webhook node returns it to the caller. If any message is blocked, a parallel branch posts a Slack alert. Moderation failures fail safe to human review, so the API never errors out.\n\n### Setup\n1. Connect Google Gemini (PaLM) API, Google Sheets and Slack.\n2. Point the audit-log node at your spreadsheet (tab: Moderation Log) and pick a Slack channel.\n3. Activate the workflow and POST {\"texts\": [\"...\"]} to the production URL.\n\n### Customization tips\nEnable webhook Header Auth to require an API key, or return HTTP 422 when anything is blocked."
      },
      "typeVersion": 1
    },
    {
      "id": "f2e24ac6-fcbd-48ab-a29c-450511b98309",
      "name": "S1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        -224
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 680,
        "content": "## 1. Accept a batch\nThe webhook accepts one text or an array. A Code node normalises it into one item per message with a requester ID."
      },
      "typeVersion": 1
    },
    {
      "id": "61706798-538b-45ee-a306-2656899ffe5f",
      "name": "S2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        448,
        -224
      ],
      "parameters": {
        "color": 7,
        "width": 620,
        "height": 664,
        "content": "## 2. Moderate & audit (Basic LLM Chain)\nGemini classifies each message and every result is written to a Google Sheets audit trail. Model failures fail safe to human review."
      },
      "typeVersion": 1
    },
    {
      "id": "d5894240-24be-4116-b847-75e61ff4970e",
      "name": "S3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1088,
        -224
      ],
      "parameters": {
        "color": 7,
        "width": 620,
        "height": 664,
        "content": "## 3. Aggregate & respond\nAll per-message results are bundled into one JSON response with a summary count and returned to the caller."
      },
      "typeVersion": 1
    },
    {
      "id": "03f03894-4090-4efa-9784-5ca6dab97770",
      "name": "S4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1728,
        -224
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 668,
        "content": "## 4. Alert on blocks\nIf any message was blocked, a parallel branch posts a Slack alert - without delaying the API response."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": true,
    "executionOrder": "v1"
  },
  "versionId": "7e835dec-71f9-46c1-8987-bc4db6d848b2",
  "nodeGroups": [],
  "connections": {
    "Any Blocked": {
      "main": [
        [
          {
            "node": "Alert on Blocked Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Shape Result": {
      "main": [
        [
          {
            "node": "Log to Audit Trail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bundle Results": {
      "main": [
        [
          {
            "node": "Build API Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Moderation Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Moderate Text with Gemini",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Request": {
      "main": [
        [
          {
            "node": "Moderate Text with Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build API Response": {
      "main": [
        [
          {
            "node": "Return Result",
            "type": "main",
            "index": 0
          },
          {
            "node": "Any Blocked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log to Audit Trail": {
      "main": [
        [
          {
            "node": "Bundle Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Moderation API Endpoint": {
      "main": [
        [
          {
            "node": "Normalize Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Moderate Text with Gemini",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Moderate Text with Gemini": {
      "main": [
        [
          {
            "node": "Shape Result",
            "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 exposes a POST webhook that sends user text (and optional context) to Google Gemini for moderation, returning an allow/flag/block decision, toxicity level, matched categories, detected PII types, a short reason, and a cleaned version with slurs masked and PII…

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

Resume Screening & Behavioral Interviews with Gemini, Elevenlabs, & Notion ATS copy. Uses outputParserStructured, chainLlm, googleDrive, stickyNote. Webhook trigger; 67 nodes.

Output Parser Structured, Chain Llm, Google Drive +9
AI & RAG

Candidate Engagement | Resume Screening | AI Voice Interviews | Applicant Insights

Output Parser Structured, Chain Llm, Google Drive +9
AI & RAG

leads. Uses supabase, gmail, formTrigger, httpRequest. Webhook trigger; 62 nodes.

Supabase, Gmail, Form Trigger +13
AI & RAG

SEO Articles to Github Publish Workflow. Uses outputParserAutofixing, outputParserStructured, github, lmChatGoogleGemini. Webhook trigger; 44 nodes.

Output Parser Autofixing, Output Parser Structured, GitHub +3
AI & RAG

This workflow receives a blog request via webhook, researches the topic with Tavily, generates a long-form HTML article using Google Gemini, creates two images via the kie.ai API, stores assets in Goo

Agent, @Tavily/N8N Nodes Tavily, Google Gemini Chat +6