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 →
{
"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.
googlePalmApigoogleSheetsOAuth2ApislackOAuth2Api
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Resume Screening & Behavioral Interviews with Gemini, Elevenlabs, & Notion ATS copy. Uses outputParserStructured, chainLlm, googleDrive, stickyNote. Webhook trigger; 67 nodes.
Candidate Engagement | Resume Screening | AI Voice Interviews | Applicant Insights
leads. Uses supabase, gmail, formTrigger, httpRequest. Webhook trigger; 62 nodes.
SEO Articles to Github Publish Workflow. Uses outputParserAutofixing, outputParserStructured, github, lmChatGoogleGemini. Webhook trigger; 44 nodes.
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