AutomationFlowsAI & RAG › Generate Pii-safe Helpdocs From Crisp Support Chats with Gpt-4.1-mini

Generate Pii-safe Helpdocs From Crisp Support Chats with Gpt-4.1-mini

ByCooper @cooper on n8n.io

Automatically create help articles from resolved Crisp chats. This n8n workflow listens for chat events, formats Q&A pairs, and uses an LLM to generate a PII‑safe helpdoc saved to a Data Table.

Webhook trigger★★★★☆ complexityAI-powered14 nodesOpenAI ChatData TableChain Llm
AI & RAG Trigger: Webhook Nodes: 14 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Chainllm → OpenAI Chat 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": "LfzcEYPmQ3RRkrSB",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "crisp",
  "tags": [],
  "nodes": [
    {
      "id": "8e5d273c-6e7b-4478-83b1-9ee5c2647749",
      "name": "crisp",
      "type": "n8n-nodes-base.webhook",
      "position": [
        640,
        736
      ],
      "parameters": {
        "path": "b16f5841-783c-47de-a4ae-b11a81be200c",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "c7617483-2442-4349-ac9b-1279baf0bed9",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1616,
        864
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "f651678d-6155-40e4-abe5-c9a97f9563fa",
      "name": "get",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        1088,
        640
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "session_id",
              "keyValue": "={{ $json.body.data.session_id }}"
            }
          ]
        },
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "UTnFJYqs2RBUfKbl",
          "cachedResultUrl": "/projects/x7mWBmZLdHejTdq6/datatables/UTnFJYqs2RBUfKbl",
          "cachedResultName": "crisp"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "89620a25-f766-4f8b-933e-b8a070fca336",
      "name": "closed",
      "type": "n8n-nodes-base.if",
      "position": [
        864,
        736
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.body.data.content.namespace }}",
              "rightValue": "state:resolved"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f6ee5bc3-953a-4f2a-a4be-b62e06bdf907",
      "name": "insert",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        1088,
        832
      ],
      "parameters": {
        "columns": {
          "value": {
            "text": "={{ $json.body.data.content }}",
            "type": "={{ $json.body.event }}",
            "session_id": "={{ $json.body.data.session_id }}"
          },
          "schema": [
            {
              "id": "session_id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "session_id",
              "defaultMatch": false
            },
            {
              "id": "text",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "text",
              "defaultMatch": false
            },
            {
              "id": "type",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "type",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "UTnFJYqs2RBUfKbl",
          "cachedResultUrl": "/projects/x7mWBmZLdHejTdq6/datatables/UTnFJYqs2RBUfKbl",
          "cachedResultName": "crisp"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "b8cc6245-5337-4846-ba98-4bd685595f46",
      "name": "format",
      "type": "n8n-nodes-base.code",
      "position": [
        1312,
        640
      ],
      "parameters": {
        "jsCode": "// Get all messages from the data table\nconst messages = $input.all();\n\n// Sort messages by their id to maintain chronological order\nconst sortedMessages = messages.sort((a, b) => a.json.id - b.json.id);\n\n// Separate user messages (questions) and operator messages (answers)\nconst userMessages = [];\nconst operatorMessages = [];\n\nfor (const message of sortedMessages) {\n  const messageData = message.json;\n  \n  // Check type field for 'message:send' (user questions)\n  if (messageData.type === 'message:send') {\n    userMessages.push({\n      content: messageData.text || '',\n      session_id: messageData.session_id,\n      id: messageData.id\n    });\n  } \n  // Check type field for 'message:received' (operator answers)\n  else if (messageData.type === 'message:received') {\n    operatorMessages.push({\n      content: messageData.text || '',\n      session_id: messageData.session_id,\n      id: messageData.id\n    });\n  }\n}\n\n// Format into Q&A pairs matching user messages with operator responses in chronological order\nconst qaPairs = [];\nconst maxPairs = Math.max(userMessages.length, operatorMessages.length);\n\nfor (let i = 0; i < maxPairs; i++) {\n  const question = userMessages[i]?.content || '';\n  const answer = operatorMessages[i]?.content || '';\n  \n  if (question || answer) {\n    qaPairs.push({\n      question: question,\n      answer: answer\n    });\n  }\n}\n\n// Create a formatted text version for AI processing\nlet formattedConversation = 'Conversation Q&A:\\n\\n';\n\nfor (let i = 0; i < qaPairs.length; i++) {\n  formattedConversation += `Q${i + 1}: ${qaPairs[i].question}\\n`;\n  formattedConversation += `A${i + 1}: ${qaPairs[i].answer}\\n\\n`;\n}\n\n// Return the formatted data\nreturn [\n  {\n    json: {\n      qaPairs: qaPairs,\n      formattedConversation: formattedConversation,\n      totalQuestions: userMessages.length,\n      totalAnswers: operatorMessages.length,\n      conversationSummary: `${qaPairs.length} Q&A pairs extracted from conversation`\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "1f63d5e0-4f3a-462d-b6aa-f9a6bdeb0571",
      "name": "Gen Help",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        1536,
        640
      ],
      "parameters": {
        "text": "=Based on the following customer support conversation Q&A pairs, create a comprehensive help documentation article. Extract the key problem, solution, and any important details. Format it as a clear, reusable help doc entry.\n\nIMPORTANT: Remove all personal identifying information (PII) from the help documentation including:\n- Email addresses\n- Names of people or organizations\n- Organization codes or IDs\n- Passwords or credentials\n- Phone numbers\n- Account numbers\n- Any other sensitive personal data\n\nReplace PII with generic placeholders like [USER_EMAIL], [ORGANIZATION_NAME], [ACCOUNT_ID], etc.\n\nConversation: {{ $json.formattedConversation }}",
        "batching": {},
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "70bd923d-664b-44ac-abda-d5f0844487d2",
      "name": "store-doc",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        1888,
        640
      ],
      "parameters": {
        "columns": {
          "value": {
            "doc": "={{ $json.text }}",
            "publish": false
          },
          "schema": [
            {
              "id": "doc",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "doc",
              "defaultMatch": false
            },
            {
              "id": "publish",
              "type": "boolean",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "publish",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "cMiRhzsbTqFjiXXW",
          "cachedResultUrl": "/projects/x7mWBmZLdHejTdq6/datatables/cMiRhzsbTqFjiXXW",
          "cachedResultName": "crisphelp"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1463b4c3-69c6-4ed8-8c40-57c9fbf76c7c",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        512,
        368
      ],
      "parameters": {
        "width": 1072,
        "height": 176,
        "content": "## Crisp chat \u2192 Helpdoc generator\n\n### How it works\n1. Receive Crisp chat events via a webhook and trigger when a session is marked resolved.\n2. Save the chat messages to a raw chats datatable to capture the full session.\n3. When the chat is finished, format messages into chronological Q&A pairs and a short summary.\n4. Call the language model to convert the Q&A into a clear help article and automatically remove or replace all PII with placeholders.\n5. Store the sanitized help article in a helpdocs datatable for publishing, emailing, or further review.\n\n### Setup\n- [ ] Add the workflow webhook URL to Crisp (Workspace settings \u2192 Advanced \u2192 Webhooks).\n- [ ] Connect your OpenAI account / API key in workflow credentials.\n- [ ] Create or select a datatable for raw chats and set its ID in the workflow.\n- [ ] Create or select a datatable for generated helpdocs and set its ID in the workflow.\n- [ ] Verify the trigger condition matches your \"resolved\" chat state.\n- [ ] Run a closed-chat test to confirm a helpdoc is generated and saved."
      },
      "typeVersion": 1
    },
    {
      "id": "eb65cade-fed9-443b-89bd-805b4e485e3d",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        640
      ],
      "parameters": {
        "content": "### Check if resolved"
      },
      "typeVersion": 1
    },
    {
      "id": "9c653b23-4870-43e1-8dbb-84ea7249451d",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1072,
        800
      ],
      "parameters": {
        "content": "### Add chat to table"
      },
      "typeVersion": 1
    },
    {
      "id": "129ad5e4-3ff0-4a8f-acc3-bf4731a788c8",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1072,
        560
      ],
      "parameters": {
        "width": 384,
        "height": 208,
        "content": "### Chat finished, format the chats into a Q+A "
      },
      "typeVersion": 1
    },
    {
      "id": "dac54ef9-abe8-4f03-a080-17ff67fe4130",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1520,
        560
      ],
      "parameters": {
        "width": 528,
        "height": 208,
        "content": "### Create the Helpdoc and store it.\n#### You could add an email node or Google docs node after here too."
      },
      "typeVersion": 1
    },
    {
      "id": "768593a2-0ffd-4ac4-b69d-46e1bbab3e17",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        640
      ],
      "parameters": {
        "height": 224,
        "content": "### Use this in Crisp/Settings/Workspace settings/Advanced"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "2938d0ae-fc32-42b3-a9ff-4025b98d5061",
  "connections": {
    "get": {
      "main": [
        [
          {
            "node": "format",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "crisp": {
      "main": [
        [
          {
            "node": "closed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "closed": {
      "main": [
        [
          {
            "node": "get",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "insert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "format": {
      "main": [
        [
          {
            "node": "Gen Help",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gen Help": {
      "main": [
        [
          {
            "node": "store-doc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Gen Help",
            "type": "ai_languageModel",
            "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

Automatically create help articles from resolved Crisp chats. This n8n workflow listens for chat events, formats Q&A pairs, and uses an LLM to generate a PII‑safe helpdoc saved to a Data Table.

Source: https://n8n.io/workflows/10773/ — 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 workflow transforms natural language queries into research reports through a five-stage AI pipeline. When triggered via webhook (typically from Google Sheets using the companion [](https://gist.g

Redis, Agent, Output Parser Structured +7
AI & RAG

This workflow is an AI-powered contact form triage and auto-response system built for businesses that want to handle website enquiries in a faster, cleaner, and more professional way.

Gmail, OpenAI Chat, Chain Llm +3
AI & RAG

Transform any Google Sheets cell into an intelligent web scraper! Type and get AI-filtered result from every website in ~20 seconds.

Agent, Mcp Client Tool, HTTP Request +4
AI & RAG

CLINICAINTEGRAL_secretary. Uses postgres, mcpClientTool, googleDriveTool, toolWorkflow. Webhook trigger; 89 nodes.

Postgres, Mcp Client Tool, Google Drive Tool +14
AI & RAG

This n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La

Google Sheets, HTTP Request, Slack +10