{
  "name": "Remove personal information",
  "nodes": [
    {
      "parameters": {
        "content": "## Remove personal information\nFind the personal details in a text, swap each one for a placeholder, change nothing else, and get a short list of what a person must still check before the text goes anywhere.\n\n**How to use**\n1. Open **Chat model** and pick your own credential. To use another AI service, delete that node, add the service's chat model node, and connect it to the Model plug under every step and AI check.\n2. Open **Inputs** and paste your material into each field. Examples are filled in so you can try it first.\n3. Click **Execute workflow**.\n4. Open **Result** to read the answer, anything flagged, and the checks left for you.\n\n**Checks**\n- Check 1 (a plain rule, run automatically with no AI): Every quote in the Detail (quote) column appears word for word in the text to clean. If it fails: try once more, then flag.\n- Check 2 (a plain rule, run automatically with no AI): Every row of the table has one of \"name\", \"email\", \"phone\", \"street address\", \"ID number\", \"birth date\" or \"indirect\" in the Kind column. If it fails: try once more, then flag.\n- Check 3 (a plain rule, run automatically with no AI): Every number in the answer also appears in the text to clean. If it fails: try once more, then flag.\n- Check 4 (a plain rule, run automatically with no AI): The answer includes the words \"Read the cleaned text yourself before you share it.\". If it fails: try once more, then flag.\n- Check 5 (a second AI prompt answers pass or fail): Read the cleaned text in the answer. Does any detail in the table below still appear in it, whole or in part, such as a first name on its own or the last digits of a number? Answer FAIL and quote each place where one is left. If it fails: try once more, then flag.\n- Check 6 (a second AI prompt answers pass or fail): Compare the cleaned text in the answer with the original text below. Apart from the placeholders, is every word, number and punctuation mark the same, and is each placeholder where a listed detail stood? Answer FAIL and quote any other change, such as a word that was dropped, reworded or swapped by mistake. If it fails: try once more, then flag.\n- Check 7 (a second AI prompt answers pass or fail): Count the rows in the table below, in total and by kind. Do the numbers on the Result line of the answer match those counts? Answer FAIL and say which count is wrong. If it fails: try once more, then flag.\n- Check 8 (you check it by hand): Read the cleaned text from top to bottom as someone who knows the people in it. Decide what to do about each item under \"Look at these yourself\", and look for anything else that could point to one person, such as a nickname, a rare job, or a date and a place together. Only a person can say who the reader may already know. If it fails: flag it and carry on.\n\nFrom Logic Lab (logiclabhq.com). Free to use and adapt, with credit.",
        "height": 840,
        "width": 760,
        "color": 5
      },
      "id": "8d125054-8241-4e23-a63d-071231331081",
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -800
      ]
    },
    {
      "parameters": {},
      "id": "2a743ad0-835a-4b63-a0ce-9ca63a4aab09",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        320
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "dccc2576-df2f-4cdf-a164-7e784adfe169",
              "name": "text",
              "value": "Sample: Subject: Feedback on the October budgeting workshop\n\nHello team,\n\nDana Whitfield, who attended on October 12, asked me to pass this on. She said the session ran well but the room was too warm, and she could not hear Marcus Lee at the back. Dana lives at 48 Alder Road, Millbrook and had to leave early to collect her son, so she would like the slides sent to dana.whitfield@example.org. If anything is unclear, call her on 555-0173 after 6 pm. She also mentioned that the only night-shift nurse at Hillside Clinic, a friend of hers, wants to join the next one. Dana's member number is MB-20418.\n\nThanks,\nPriya Raman\nWorkshop coordinator",
              "type": "string"
            },
            {
              "id": "a333f5c0-81fe-4759-acb2-498e0ca469d7",
              "name": "keep",
              "value": "Sample: Keep job titles and the name of the town, Millbrook",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "2dff7d79-5c86-4afc-a542-414bc545603e",
      "name": "Inputs",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        320
      ],
      "notes": "Paste your own material here: text = The text to clean; keep = What may stay (optional).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful privacy reader who spots the details that could identify a person.\n\nYour task is to list every personal detail in the text below.\n\nThe text:\n{{ $(\"Inputs\").first().json[\"text\"] }}\n\nWhat may stay (if this is empty, nothing may stay):\n{{ $(\"Inputs\").first().json[\"keep\"] }}\n\nRules:\n- A personal detail is a name, an email address, a phone number, a street address, an ID or account number, a date of birth, or any wording that points to one person without naming them, such as \"the only night-shift nurse at the clinic\".\n- A job title on its own may stay, but a title together with words like \"the only\" or a workplace can point to one person. Quote those words as one detail.\n- Quote each detail from the text word for word, as short as you can. Do not correct it and do not reword it.\n- List each different form once: a full name and a first name on its own are two rows. List the rows in the order each form first appears.\n- Do not list anything the \"may stay\" line allows. Do not list a detail that is not in the text, and do not skip one because it looks harmless.\n- Do not change the text yet.\n\nReturn one markdown table and nothing else:\n| # | Detail (quote) |{{ $(\"Check 1: Every detail is quoted word for word from the text\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every detail is quoted word for word from the text. What was wrong: \" + $(\"Check 1: Every detail is quoted word for word from the text\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "8d6e0b6b-b4d9-4104-a0ff-dc016ecc2f2a",
      "name": "1. List every personal detail",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        480,
        320
      ],
      "notes": "Pick out each detail that names or points to one person, quoted word for word, before anything is changed. Returns: A numbered table of the personal details, each quoted from the text.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every detail is quoted word for word from the text\n// Rule: Every quote in the Detail (quote) column appears word for word in the text to clean.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"1. List every personal detail\").first().json.text ?? '');\nconst sources = {\n  \"text\": String($(\"Inputs\").first().json[\"text\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"Detail (quote)\",\"input\":\"text\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "d10dee26-6ae5-4b41-ad99-ce3cdb5240c7",
      "name": "Check 1: Every detail is quoted word for word from the text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        320
      ],
      "notes": "Every quote in the Detail (quote) column appears word for word in the text to clean.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8525bda3-8ce3-4a7e-a900-9cad61f3d940",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "875d4320-7611-423b-afd7-b43e9d91b841",
      "name": "Passed check 1?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1000,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "57614ff0-2210-47f9-a485-c16a441b0e4b",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "478e8668-1df5-4c71-ac92-ac76ac42c56f",
      "name": "Try again? (check 1)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1000,
        520
      ],
      "notes": "True = first failure: send back to \"1. List every personal detail\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "2dfbb2c1-55dc-4b14-a182-b1af812ddc4a",
              "name": "flag",
              "value": "=Every detail is quoted word for word from the text: did not pass. {{ $(\"Check 1: Every detail is quoted word for word from the text\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "e116d065-db2f-48a8-a0e5-eec36ed54996",
      "name": "Flag check 1",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1130,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful editor who plans the replacement for each detail before any text is changed.\n\nYour task is to give each detail below a kind and a placeholder.\n\nThe details:\n{{ $(\"1. List every personal detail\").first().json.text }}\n\nWhat may stay (if this is empty, nothing may stay):\n{{ $(\"Inputs\").first().json[\"keep\"] }}\n\nRules:\n- Use exactly one of these kinds: name, email, phone, street address, ID number, birth date, indirect. Use indirect for wording that points to one person without naming them.\n- Write each placeholder in square brackets with a kind word and a letter, such as [Person A], [Email A] or [Phone B]. Use letters and never digits.\n- Give the same placeholder to every form of one person's name, so a full name and a first name on its own get the same one. Give different people different letters.\n- For an indirect detail, keep the words the \"may stay\" line allows and swap only the identifying words for a bracketed placeholder, such as \"a night-shift nurse at [Workplace A]\".\n- Do not add a detail and do not drop one. Do not change the wording of a quote.\n\nReturn one markdown table and nothing else:\n| # | Detail (quote) | Kind | Placeholder |\nThe Kind column must say exactly name, email, phone, street address, ID number, birth date or indirect.{{ $(\"Check 2: Each kind is one of the seven allowed words\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Each kind is one of the seven allowed words. What was wrong: \" + $(\"Check 2: Each kind is one of the seven allowed words\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "6ea340b6-0506-4de5-a0e4-41f089b1661f",
      "name": "2. Choose a placeholder for each detail",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1260,
        320
      ],
      "notes": "Give each detail a kind and a placeholder, with the same placeholder for the same person. Returns: A table that gives each detail a kind and a placeholder.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Each kind is one of the seven allowed words\n// Rule: Every row of the table has one of \"name\", \"email\", \"phone\", \"street address\", \"ID number\", \"birth date\" or \"indirect\" in the Kind column.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"2. Choose a placeholder for each detail\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"column_values_in\", {\"column\":\"Kind\",\"allowed\":[\"name\",\"email\",\"phone\",\"street address\",\"ID number\",\"birth date\",\"indirect\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "2793dd8b-81d8-478a-ad01-3e5547d8c294",
      "name": "Check 2: Each kind is one of the seven allowed words",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        320
      ],
      "notes": "Every row of the table has one of \"name\", \"email\", \"phone\", \"street address\", \"ID number\", \"birth date\" or \"indirect\" in the Kind column.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8425bc10-8de3-4c11-a800-9b1a62f3dad3",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "875f81b7-f60e-4a24-afd0-2ef91d993d86",
      "name": "Passed check 2?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "58615183-2110-4666-a585-c2fd431b0cb8",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "c7870123-a5f2-40f2-a494-c5452c4a4ab4",
      "name": "Try again? (check 2)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        520
      ],
      "notes": "True = first failure: send back to \"2. Choose a placeholder for each detail\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "2cfbb12e-56dc-4ca7-a082-b01c822ddddd",
              "name": "flag",
              "value": "=Each kind is one of the seven allowed words: did not pass. {{ $(\"Check 2: Each kind is one of the seven allowed words\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "de16cbac-de2f-4d61-a1e5-f0566dd54803",
      "name": "Flag check 2",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1910,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a copy editor who swaps listed words and changes nothing else.\n\nYour task is to write out the text below with each detail replaced by its placeholder.\n\nThe text:\n{{ $(\"Inputs\").first().json[\"text\"] }}\n\nThe details and their placeholders:\n{{ $(\"2. Choose a placeholder for each detail\").first().json.text }}\n\nRules:\n- Replace every occurrence of each detail, wherever it appears, with its placeholder. If a detail sits inside a longer word or an email address, replace the whole of it. A word that only looks like a name, such as a verb, is not a detail.\n- Change nothing else. Keep every other word, number, line break and punctuation mark exactly as written. Do not fix spelling, do not shorten and do not add anything.\n- Do not use a placeholder that is not in the table above.\n- Do not add a note, a heading or a comment.\n\nReturn the cleaned text and nothing else.{{ $(\"Check 3: No number appears that the original text does not have\").isExecuted ? \"\\n\\nYour last answer did not pass this check: No number appears that the original text does not have. What was wrong: \" + $(\"Check 3: No number appears that the original text does not have\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 5: No listed detail is left in the cleaned text\").isExecuted ? \"\\n\\nYour last answer did not pass this check: No listed detail is left in the cleaned text. What was wrong: \" + $(\"Check 5: No listed detail is left in the cleaned text\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 6: Only the listed details were swapped\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Only the listed details were swapped. What was wrong: \" + $(\"Check 6: Only the listed details were swapped\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "e6197be4-f3f2-4b5b-a01e-d262bd49b7b1",
      "name": "3. Write the cleaned text",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2040,
        320
      ],
      "notes": "Replace each detail with its placeholder everywhere it appears and change nothing else. Returns: The text with each personal detail replaced by its placeholder.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// No number appears that the original text does not have\n// Rule: Every number in the answer also appears in the text to clean.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"3. Write the cleaned text\").first().json.text ?? '');\nconst sources = {\n  \"text\": String($(\"Inputs\").first().json[\"text\"] ?? ''),\n};\nconst result = runRule(\"numbers_in_source\", {\"input\":\"text\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "d038a70b-8092-4254-ad4c-e53db7f4e1f6",
      "name": "Check 3: No number appears that the original text does not have",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2300,
        320
      ],
      "notes": "Every number in the answer also appears in the text to clean.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8725c0c9-8ae3-4758-a700-998763f3dc66",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "076289ce-f60c-4b8d-afd3-37101d96feef",
      "name": "Passed check 3?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2560,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "59615316-2010-44d3-a285-be44461b1171",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ab8913a6-c1f0-4e6f-a897-ba780847d371",
      "name": "Try again? (check 3)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2560,
        520
      ],
      "notes": "True = first failure: send back to \"3. Write the cleaned text\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "2bfbaf9b-57dc-4e3a-a382-b4d57f2dd924",
              "name": "flag",
              "value": "=No number appears that the original text does not have: did not pass. {{ $(\"Check 3: No number appears that the original text does not have\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "df16cd3f-dd2f-4bce-a2e5-f1e96cd54670",
      "name": "Flag check 3",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2690,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer who tells the reader what an AI tool cannot promise.\n\nYour task is to write the final package: a count, the cleaned text and a short list of things a person must still check.\n\nThe cleaned text:\n{{ $(\"3. Write the cleaned text\").first().json.text }}\n\nThe details and their placeholders:\n{{ $(\"2. Choose a placeholder for each detail\").first().json.text }}\n\nRules:\n- Start with one line that begins \"Result:\" and counts the details that were replaced, in total and by kind.\n- Under \"Cleaned text\", copy the cleaned text exactly as it is above. Do not change a word.\n- Under \"Look at these yourself\", list two to five things a person should still check, each as a short quote from the cleaned text followed by one reason. Look for details that could still point to one person when read together, such as a job, a place and a date, for family or health details, and for anything the table did not cover.\n- Do not say or suggest that all personal information has been removed or that the text is safe to share. Do not invent a detail that is not in the cleaned text.\n- End with exactly this line: Read the cleaned text yourself before you share it. An AI tool can miss a detail.\n\nReturn markdown in this form and nothing else:\nResult: (the counts)\n## Cleaned text\n(the cleaned text)\n## Look at these yourself\n- (a quote from the cleaned text, then the reason)\nRead the cleaned text yourself before you share it. An AI tool can miss a detail.{{ $(\"Check 4: The reminder to read it yourself is in the final text\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The reminder to read it yourself is in the final text. What was wrong: \" + $(\"Check 4: The reminder to read it yourself is in the final text\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 7: The counts add up to the table\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The counts add up to the table. What was wrong: \" + $(\"Check 7: The counts add up to the table\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "d884aa13-5efe-448c-a557-8b2919054962",
      "name": "4. Add the count and the reminder",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2820,
        320
      ],
      "notes": "Put the count, the cleaned text and a short list of what a person must still check into one package. Returns: A count line, the cleaned text, a list of what to check yourself, and the reminder line.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// The reminder to read it yourself is in the final text\n// Rule: The answer includes the words \"Read the cleaned text yourself before you share it.\".\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"4. Add the count and the reminder\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"has_text\", \"Read the cleaned text yourself before you share it.\", work, sources);\nreturn [{ json: result }];"
      },
      "id": "2e1c5133-a04e-47d6-a64e-7df9c64f6ca4",
      "name": "Check 4: The reminder to read it yourself is in the final text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        320
      ],
      "notes": "The answer includes the words \"Read the cleaned text yourself before you share it.\".",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8625bf36-8be3-48eb-a600-97f464f3ddf9",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "075140ad-f61d-44ae-afcb-b1cb1d8a32fc",
      "name": "Passed check 4?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3340,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "5a6154a9-1f10-4340-a385-bfd7451b0fde",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ab95df99-99ee-40e0-a09a-3807083b077e",
      "name": "Try again? (check 4)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3340,
        520
      ],
      "notes": "True = first failure: send back to \"4. Add the count and the reminder\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "2afbae08-58dc-4fcd-a282-b342802ddab7",
              "name": "flag",
              "value": "=The reminder to read it yourself is in the final text: did not pass. {{ $(\"Check 4: The reminder to read it yourself is in the final text\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "dc16c886-e02f-4087-a3e5-f37c73d55175",
      "name": "Flag check 4",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3470,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: No listed detail is left in the cleaned text\nRead the cleaned text in the answer. Does any detail in the table below still appear in it, whole or in part, such as a first name on its own or the last digits of a number? Answer FAIL and quote each place where one is left.\n\nThe details:\n{{ $(\"2. Choose a placeholder for each detail\").first().json.text }}\n\nThe work to check (the answer from Step 3 (Write the cleaned text)):\n{{ $(\"3. Write the cleaned text\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "d260f956-64d8-42a1-a75c-cbfcdc771587",
      "name": "Check 5: No listed detail is left in the cleaned text",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        3600,
        320
      ],
      "notes": "Answers PASS or FAIL. Read the cleaned text in the answer. Does any detail in the table below still appear in it, whole or in part, such as a first name on its own or the last digits of a number? Answer FAIL and quote each place where one is left.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8925c3ef-90e3-40ca-a500-966165f3df8c",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "07537f44-f61b-4617-afcd-f0621d87f465",
      "name": "Passed check 5?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3860,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "5b61563c-2610-4e45-a085-bb1e481b1497",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "cf9856dc-45eb-4e0d-a49c-4a8a2438f4fb",
      "name": "Try again? (check 5)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3860,
        520
      ],
      "notes": "True = first failure: send back to \"3. Write the cleaned text\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "31fbb90d-51dc-44c8-ad82-ab637d2dd5fe",
              "name": "flag",
              "value": "=No listed detail is left in the cleaned text: did not pass. {{ $(\"Check 5: No listed detail is left in the cleaned text\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "dd16ca19-df2f-4ef4-a4e5-f50f72d54fe2",
      "name": "Flag check 5",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3990,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Only the listed details were swapped\nCompare the cleaned text in the answer with the original text below. Apart from the placeholders, is every word, number and punctuation mark the same, and is each placeholder where a listed detail stood? Answer FAIL and quote any other change, such as a word that was dropped, reworded or swapped by mistake.\n\nThe original text:\n{{ $(\"Inputs\").first().json[\"text\"] }}\n\nThe work to check (the answer from Step 3 (Write the cleaned text)):\n{{ $(\"3. Write the cleaned text\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "6db36289-c0c5-407e-a5ac-ea5b16c0c740",
      "name": "Check 6: Only the listed details were swapped",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        4120,
        320
      ],
      "notes": "Answers PASS or FAIL. Compare the cleaned text in the answer with the original text below. Apart from the placeholders, is every word, number and punctuation mark the same, and is each placeholder where a listed detail stood? Answer FAIL and quote any other change, such as a word that was dropped, reworded or swapped by mistake.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8825c25c-91e3-425d-a400-94ce66f3e11f",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0755bddb-7618-4e00-afc6-6b1d9d8f79aa",
      "name": "Passed check 6?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4380,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "5c6157cf-2510-4cb2-a185-bcb1471b1304",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2f909f37-1de9-407e-ac9e-aee9844047e0",
      "name": "Try again? (check 6)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4380,
        520
      ],
      "notes": "True = first failure: send back to \"3. Write the cleaned text\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "30fbb77a-52dc-465b-ac82-a9d07e2dd791",
              "name": "flag",
              "value": "=Only the listed details were swapped: did not pass. {{ $(\"Check 6: Only the listed details were swapped\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "da16c560-e22f-43ad-a5e5-f6a271d54e4f",
      "name": "Flag check 6",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4510,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: The counts add up to the table\nCount the rows in the table below, in total and by kind. Do the numbers on the Result line of the answer match those counts? Answer FAIL and say which count is wrong.\n\nThe details:\n{{ $(\"2. Choose a placeholder for each detail\").first().json.text }}\n\nThe work to check (the answer from Step 4 (Add the count and the reminder)):\n{{ $(\"4. Add the count and the reminder\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "bf72cbe8-8b43-4b0f-a35b-483a2a7cdc91",
      "name": "Check 7: The counts add up to the table",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        4640,
        320
      ],
      "notes": "Answers PASS or FAIL. Count the rows in the table below, in total and by kind. Do the numbers on the Result line of the answer match those counts? Answer FAIL and say which count is wrong.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8b25c715-8ee3-4da4-a300-933b67f3e2b2",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0757fc72-7616-4f69-afc9-73341d8c7193",
      "name": "Passed check 7?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4900,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "5d615962-2410-4b1f-ae85-b7f84a1b17bd",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "5393167a-39e6-4dfb-a0a1-8aec303d850d",
      "name": "Try again? (check 7)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4900,
        520
      ],
      "notes": "True = first failure: send back to \"4. Add the count and the reminder\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "2ffbb5e7-53dc-47ee-af82-ae897b2dd2d8",
              "name": "flag",
              "value": "=The counts add up to the table: did not pass. {{ $(\"Check 7: The counts add up to the table\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "db16c6f3-e12f-421a-a6e5-f83570d54cbc",
      "name": "Flag check 7",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5030,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "content": "**Check 8: check by hand**\nRead the cleaned text from top to bottom as someone who knows the people in it. Decide what to do about each item under \"Look at these yourself\", and look for anything else that could point to one person, such as a nickname, a rare job, or a date and a place together. Only a person can say who the reader may already know.\n\nIf not: flag it and carry on. This check is also listed in Result.",
        "height": 200,
        "width": 240,
        "color": 3
      },
      "id": "c9af5409-5148-46c2-a7c2-fd1332de1ea4",
      "name": "Check 8 (by hand)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        4880,
        70
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "04f61b68-e444-4487-a6ff-e68a387701d1",
              "name": "result",
              "value": "={{ $(\"4. Add the count and the reminder\").first().json.text }}",
              "type": "string"
            },
            {
              "id": "6e540019-40fe-4ec4-a152-9b338593380e",
              "name": "flagged",
              "value": "={{ [$(\"Flag check 1\").isExecuted ? $(\"Flag check 1\").first().json.flag : \"\", $(\"Flag check 2\").isExecuted ? $(\"Flag check 2\").first().json.flag : \"\", $(\"Flag check 3\").isExecuted ? $(\"Flag check 3\").first().json.flag : \"\", $(\"Flag check 4\").isExecuted ? $(\"Flag check 4\").first().json.flag : \"\", $(\"Flag check 5\").isExecuted ? $(\"Flag check 5\").first().json.flag : \"\", $(\"Flag check 6\").isExecuted ? $(\"Flag check 6\").first().json.flag : \"\", $(\"Flag check 7\").isExecuted ? $(\"Flag check 7\").first().json.flag : \"\"].filter(x => x).join(\"\\n\") || \"None\" }}",
              "type": "string"
            },
            {
              "id": "414ffddf-4cd9-4152-a4c9-b3e9711836dc",
              "name": "check_by_hand",
              "value": "- Read the cleaned text from top to bottom as someone who knows the people in it. Decide what to do about each item under \"Look at these yourself\", and look for anything else that could point to one person, such as a nickname, a rare job, or a date and a place together. Only a person can say who the reader may already know.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "1c8e4265-0f40-4300-aeab-b46f2a104142",
      "name": "Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5160,
        320
      ],
      "notes": "The final answer, anything flagged, and the checks left for you.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "7651df2b-ff75-4ad2-a0d5-145595f507ec",
      "name": "Chat model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        480,
        840
      ],
      "notes": "Attach your credential here, or swap in any chat model node.",
      "notesInFlow": true
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inputs": {
      "main": [
        [
          {
            "node": "1. List every personal detail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. List every personal detail": {
      "main": [
        [
          {
            "node": "Check 1: Every detail is quoted word for word from the text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 1: Every detail is quoted word for word from the text": {
      "main": [
        [
          {
            "node": "Passed check 1?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 1?": {
      "main": [
        [
          {
            "node": "2. Choose a placeholder for each detail",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 1)": {
      "main": [
        [
          {
            "node": "1. List every personal detail",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 1": {
      "main": [
        [
          {
            "node": "2. Choose a placeholder for each detail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Choose a placeholder for each detail": {
      "main": [
        [
          {
            "node": "Check 2: Each kind is one of the seven allowed words",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 2: Each kind is one of the seven allowed words": {
      "main": [
        [
          {
            "node": "Passed check 2?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 2?": {
      "main": [
        [
          {
            "node": "3. Write the cleaned text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 2)": {
      "main": [
        [
          {
            "node": "2. Choose a placeholder for each detail",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 2": {
      "main": [
        [
          {
            "node": "3. Write the cleaned text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Write the cleaned text": {
      "main": [
        [
          {
            "node": "Check 3: No number appears that the original text does not have",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 3: No number appears that the original text does not have": {
      "main": [
        [
          {
            "node": "Passed check 3?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 3?": {
      "main": [
        [
          {
            "node": "4. Add the count and the reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 3)": {
      "main": [
        [
          {
            "node": "3. Write the cleaned text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 3": {
      "main": [
        [
          {
            "node": "4. Add the count and the reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Add the count and the reminder": {
      "main": [
        [
          {
            "node": "Check 4: The reminder to read it yourself is in the final text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 4: The reminder to read it yourself is in the final text": {
      "main": [
        [
          {
            "node": "Passed check 4?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 4?": {
      "main": [
        [
          {
            "node": "Check 5: No listed detail is left in the cleaned text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 4)": {
      "main": [
        [
          {
            "node": "4. Add the count and the reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 4": {
      "main": [
        [
          {
            "node": "Check 5: No listed detail is left in the cleaned text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 5: No listed detail is left in the cleaned text": {
      "main": [
        [
          {
            "node": "Passed check 5?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 5?": {
      "main": [
        [
          {
            "node": "Check 6: Only the listed details were swapped",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 5)": {
      "main": [
        [
          {
            "node": "3. Write the cleaned text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 5": {
      "main": [
        [
          {
            "node": "Check 6: Only the listed details were swapped",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 6: Only the listed details were swapped": {
      "main": [
        [
          {
            "node": "Passed check 6?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 6?": {
      "main": [
        [
          {
            "node": "Check 7: The counts add up to the table",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 6)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 6)": {
      "main": [
        [
          {
            "node": "3. Write the cleaned text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 6": {
      "main": [
        [
          {
            "node": "Check 7: The counts add up to the table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 7: The counts add up to the table": {
      "main": [
        [
          {
            "node": "Passed check 7?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 7?": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 7)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 7)": {
      "main": [
        [
          {
            "node": "4. Add the count and the reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 7": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat model": {
      "ai_languageModel": [
        [
          {
            "node": "1. List every personal detail",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "2. Choose a placeholder for each detail",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "3. Write the cleaned text",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "4. Add the count and the reminder",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 5: No listed detail is left in the cleaned text",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 6: Only the listed details were swapped",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 7: The counts add up to the table",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "template": "remove-personal-information"
  }
}
