{
  "name": "Reply to a complaint",
  "nodes": [
    {
      "parameters": {
        "content": "## Reply to a complaint\nTurn a customer's complaint and the facts on your file into a calm reply that acknowledges the concern, states only the facts on the record, says what will happen and when, makes no promise or admission beyond them, and waits for a person to approve it.\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): Each quote in the What the customer says (quote) column appears in the complaint, with shortened fragments in the same order. If it fails: try once more, then flag.\n- Check 2 (a plain rule, run automatically with no AI): Each quote in the What the record shows (quote) column appears in the what the record shows, with shortened fragments in the same order. If it fails: try once more, then flag.\n- Check 3 (a plain rule, run automatically with no AI): Each quote in the What will happen and when (quote) column appears in the what will happen and when, with shortened fragments in the same order. If it fails: try once more, then flag.\n- Check 4 (a plain rule, run automatically with no AI): The answer contains the text fragment \"Check this reply with your manager or your compliance team before you send it.\" (including inside a longer word). If it fails: try once more, then flag.\n- Check 5 (a plain rule, run automatically with no AI): Every row of the table has one of \"happened\", \"wants\" or \"feels\" in the Kind column. If it fails: try once more, then flag.\n- Check 6 (a plain rule, run automatically with no AI): Every row of the table has one of \"confirmed\", \"partly confirmed\" or \"not on record\" in the Result column. If it fails: try once more, then flag.\n- Check 7 (a plain rule, run automatically with no AI): Every number in the answer also appears somewhere in at least one of \"the complaint\", \"what the record shows\", \"what will happen and when\" and \"who signs the reply\"; this does not check what each number means. If it fails: try once more, then flag.\n- Check 8 (a second AI prompt answers pass or fail): Read the reply. List each statement of fact in it about what happened, such as a date, an action or an event. Is each one shown by a row of the table below marked confirmed or partly confirmed? Does the reply state as fact anything marked not on record, or anything the table does not have? Answer FAIL and quote each statement that has no support. If it fails: try once more, then flag.\n- Check 9 (a second AI prompt answers pass or fail): Read the reply. A plain fact from the record, stated in neutral words, is fine. Does the reply go further and admit that the company was at fault, made an error, broke a rule or a promise, or is liable? Does it give a reason or a cause that the record does not state? Does it blame a named person, team or other company? Does it say sorry for a fault rather than for the customer's worry or trouble? Answer FAIL and quote each such phrase. If it fails: try once more, then flag.\n- Check 10 (a second AI prompt answers pass or fail): Read the reply. Does it promise or suggest any outcome, such as a payment, a refund, an approval, a decision in the customer's favor, or that something is or is not covered? Does it promise a step, a date or a person that the table below does not have? Answer FAIL and quote each such phrase. If it fails: try once more, then flag.\n- Check 11 (a second AI prompt answers pass or fail): Look at the customer's points below. For each point whose Kind is wants, does the reply give an answer, either the agreed step with its timing or a plain statement that it has been passed on? Does the reply ignore any request? Answer FAIL and name each request that has no answer. If it fails: try once more, then flag.\n- Check 12 (you check it by hand): Read the reply next to the complaint and the file. Check each fact against the file yourself, and make sure every step and date is one your team has really agreed to. Ask your manager, your compliance team or your lawyer first if the complaint is formal, mentions a regulator, a deadline or legal action, or involves an injury or a dispute about who is at fault. Only a person can decide what your organization says and does, and the reply must be one you are willing to stand behind. If it fails: flag it and carry on.\n\nFrom Logic Lab (logiclabhq.com). Free to use and adapt, with credit.",
        "height": 1260,
        "width": 760,
        "color": 5
      },
      "id": "ede7db3a-3a90-470f-afdd-39ac34834539",
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -1220
      ]
    },
    {
      "parameters": {},
      "id": "cec3bc3e-e8bd-4a2f-a5cf-f7780fc596f1",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        320
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a60b1594-644e-4ecd-a6b3-b98a614b2353",
              "name": "complaint",
              "value": "Sample: Subject: Complaint about my bookcase order. I ordered a bookcase on September 28 and your website said it would arrive within 5 business days. It arrived on October 12 with a cracked side panel. I phoned your support line twice and nobody called me back. I would like a new panel or my money back, and I want someone to explain why nobody returned my calls. Alex Moreno",
              "type": "string"
            },
            {
              "id": "99b60fb7-52b2-4034-afb3-a279ce8d5ef6",
              "name": "file_notes",
              "value": "Sample: Order placed September 28. Order page promised delivery within 5 business days. Carrier scan shows delivered October 12. Support calls from this customer logged on October 13 and October 15. No callback is logged for either call. A photo of the cracked panel was received October 15.",
              "type": "string"
            },
            {
              "id": "713629de-916c-46bd-a6b5-0c3458a9685b",
              "name": "next_steps",
              "value": "Sample: A replacement side panel will ship within 3 business days of this reply (fulfillment team). The returns team will review the photo and tell the customer by October 23 whether a refund is possible. A support manager will look into the missed callbacks and write to the customer by October 23.",
              "type": "string"
            },
            {
              "id": "463f310a-dede-4e45-a12a-d2545cfcd6df",
              "name": "sender",
              "value": "Sample: Priya Shah, Customer Care, 555-0160",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "29457a13-5df6-4f08-ac27-f9b10b10bed6",
      "name": "Inputs",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        320
      ],
      "notes": "Paste your own material here: complaint = The complaint; file_notes = What the record shows; next_steps = What will happen and when; sender = Who signs the reply (optional).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are an experienced customer service reviewer who reads a complaint closely before anyone replies.\n\nYour task is to list each separate point the customer makes in the complaint below.\n\nThe complaint:\n{{ $(\"Inputs\").first().json[\"complaint\"] }}\n\nRules:\n- A point is one thing the customer says happened, one thing they want, or one way they feel. Split a sentence that holds two points.\n- Quote each point from the complaint word for word, as short as you can. Do not correct it and do not reword it.\n- In the Kind column, write exactly one of: happened, wants, feels.\n- One row for each point, in the order the complaint gives them. Do not add a point the customer did not make and do not guess what they mean.\n- Do not leave out a point because it is hard to answer or because it is rude.\n\nReturn one markdown table and nothing else:\n| # | What the customer says (quote) | Kind |{{ $(\"Check 1: Every point is quoted word for word from the complaint\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every point is quoted word for word from the complaint. What was wrong: \" + $(\"Check 1: Every point is quoted word for word from the complaint\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 5: Every point is marked happened, wants or feels\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every point is marked happened, wants or feels. What was wrong: \" + $(\"Check 5: Every point is marked happened, wants or feels\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "f8c51620-f9cf-4efb-a784-a70a80dc823d",
      "name": "1. List what the customer says",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        480,
        320
      ],
      "notes": "Pick out each point in the complaint, quoted word for word, and mark whether it says what happened, what the customer wants or how they feel. Returns: A numbered table of the customer's points, each quoted from the complaint and marked happened, wants or feels.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every point is quoted word for word from the complaint\n// Rule: Each quote in the What the customer says (quote) column appears in the complaint, with shortened fragments in the same order.\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                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\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 ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].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 what the customer says\").first().json.text ?? '');\nconst sources = {\n  \"complaint\": String($(\"Inputs\").first().json[\"complaint\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"What the customer says (quote)\",\"input\":\"complaint\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "06a64fb5-02f0-4b64-ab68-edcbd9bc2e8a",
      "name": "Check 1: Every point is quoted word for word from the complaint",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        320
      ],
      "notes": "Each quote in the What the customer says (quote) column appears in the complaint, with shortened fragments in the same order.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "5d2710a5-4db5-47c2-a3cf-556ba1fd56a8",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "86fa5c26-5ddd-4047-a9e3-17f87b9ce8d9",
      "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": "c37fb482-23ac-40dd-acd5-ae98f7f473b3",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "debea1de-758b-413d-aa80-c5a0e8017be7",
      "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 what the customer says\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "75d08517-ce46-4578-a6b1-c8194af69f82",
              "name": "flag",
              "value": "=Every point is quoted word for word from the complaint: did not pass. {{ $(\"Check 1: Every point is quoted word for word from the complaint\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "a723fc1b-49d1-4fdc-a61a-7ced346e6e5e",
      "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 case reviewer who checks what a customer says against the file.\n\nYour task is to find, for each point below whose Kind is happened, what the record shows.\n\nThe customer's points:\n{{ $(\"1. List what the customer says\").first().json.text }}\n\nThe record:\n{{ $(\"Inputs\").first().json[\"file_notes\"] }}\n\nRules:\n- One or more rows for each point whose Kind is happened, one passage in each row. Leave out the points marked wants or feels.\n- In the Point # column, give the number from the points table.\n- Quote what the record shows word for word, as short as you can while keeping the facts. The same passage may serve two points. If nothing in the record speaks to the point, write exactly None found in that cell.\n- In the Result column, write exactly one of: confirmed, partly confirmed, not on record. Use confirmed only when the record says the same as the point. Use partly confirmed when the record says part of it, or says something different. Use not on record when nothing speaks to it.\n- Use only the record above. Do not use what is usual in a case like this, do not guess a cause, and do not join two passages into one quote.\n\nReturn one markdown table and nothing else:\n| # | Point # | What the record shows (quote) | Result |{{ $(\"Check 2: Every passage is quoted word for word from the record\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every passage is quoted word for word from the record. What was wrong: \" + $(\"Check 2: Every passage is quoted word for word from the record\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 6: Every result is one of the three allowed words\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every result is one of the three allowed words. What was wrong: \" + $(\"Check 6: Every result is one of the three allowed words\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "1ffbde9d-64d7-4a7c-a593-8acb056b1262",
      "name": "2. Match each point to the record",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1260,
        320
      ],
      "notes": "For each point about what happened, quote what the file shows and say whether it agrees, agrees in part, or is not on file. Returns: A table that puts a quoted passage from the record beside each point about what happened, with a result for each.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every passage is quoted word for word from the record\n// Rule: Each quote in the What the record shows (quote) column appears in the what the record shows, with shortened fragments in the same order.\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                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\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 ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].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. Match each point to the record\").first().json.text ?? '');\nconst sources = {\n  \"file_notes\": String($(\"Inputs\").first().json[\"file_notes\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"What the record shows (quote)\",\"input\":\"file_notes\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "465e9c48-0cfc-4d5b-acb3-c14e04651759",
      "name": "Check 2: Every passage is quoted word for word from the record",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        320
      ],
      "notes": "Each quote in the What the record shows (quote) column appears in the what the record shows, with shortened fragments in the same order.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "5c270f12-4eb5-4955-a2cf-53d8a2fd583b",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "86f3a061-ddda-4830-a9e5-568ffba46e1e",
      "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": "c47fb615-22ac-4f4a-add5-b02bf6f47220",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "06c11f6d-0d88-4eee-aa79-72bb4808cecc",
      "name": "Try again? (check 2)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        520
      ],
      "notes": "True = first failure: send back to \"2. Match each point to the record\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "74d08384-cf46-470b-a5b1-c6864bf6a115",
              "name": "flag",
              "value": "=Every passage is quoted word for word from the record: did not pass. {{ $(\"Check 2: Every passage is quoted word for word from the record\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "a823fdae-4cd1-4495-a31a-7834336e6ccb",
      "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 customer service lead who turns agreed actions into answers for a customer.\n\nYour task is to find, for each point below whose Kind is wants, the step your team has agreed to take.\n\nThe customer's points:\n{{ $(\"1. List what the customer says\").first().json.text }}\n\nWhat will happen and when:\n{{ $(\"Inputs\").first().json[\"next_steps\"] }}\n\nRules:\n- One row for each point whose Kind is wants. Leave out the other points. One point may have more than one row.\n- In the Point # column, give the number from the points table.\n- Quote what will happen and when from the list above word for word, as short as you can. Keep any day, date or number of days the list gives. If the list does not speak to the point, write exactly None found in that cell.\n- In the Owner column, write the person or team the list names for that step, or Not named.\n- Use only the list above. Do not invent a step, a date or a person, and do not turn \"we will look into it\" into a result.\n\nReturn one markdown table and nothing else:\n| # | Point # | What will happen and when (quote) | Owner |{{ $(\"Check 3: Every step is quoted word for word from your list\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every step is quoted word for word from your list. What was wrong: \" + $(\"Check 3: Every step is quoted word for word from your list\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "321a6aa7-804b-423e-a2b2-3b21fa6def50",
      "name": "3. Match each request to what will happen",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2040,
        320
      ],
      "notes": "For each thing the customer wants, quote the step your team has agreed to take and when, or say that none was agreed. Returns: A table that puts the agreed step and its timing beside each request, or None found.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every step is quoted word for word from your list\n// Rule: Each quote in the What will happen and when (quote) column appears in the what will happen and when, with shortened fragments in the same order.\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                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\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 ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].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. Match each request to what will happen\").first().json.text ?? '');\nconst sources = {\n  \"next_steps\": String($(\"Inputs\").first().json[\"next_steps\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"What will happen and when (quote)\",\"input\":\"next_steps\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "28e5a4c8-0402-4b07-a81c-68b223fcd239",
      "name": "Check 3: Every step is quoted word for word from your list",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2300,
        320
      ],
      "notes": "Each quote in the What will happen and when (quote) column appears in the what will happen and when, with shortened fragments in the same order.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "5b270d7f-4bb5-449c-a5cf-5891a3fd59ce",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "86f5def8-ddd8-4999-a9e7-9526fba22f87",
      "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": "c17fb15c-21ac-4db7-aed5-b1bef9f476d9",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "dac318c0-e985-47ab-ae7b-853e6406bc49",
      "name": "Try again? (check 3)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2560,
        520
      ],
      "notes": "True = first failure: send back to \"3. Match each request to what will happen\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "77d0883d-d046-489e-a4b1-c4f348f69c5c",
              "name": "flag",
              "value": "=Every step is quoted word for word from your list: did not pass. {{ $(\"Check 3: Every step is quoted word for word from your list\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "a923ff41-4bd1-4302-a41a-79c7326e6b38",
      "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 an experienced customer service writer who replies to complaints in plain, kind words.\n\nYour task is to write the reply to the customer from the points, the record and the agreed steps.\n\nThe complaint:\n{{ $(\"Inputs\").first().json[\"complaint\"] }}\n\nThe customer's points:\n{{ $(\"1. List what the customer says\").first().json.text }}\n\nWhat the record shows:\n{{ $(\"2. Match each point to the record\").first().json.text }}\n\nWhat will happen and when:\n{{ $(\"3. Match each request to what will happen\").first().json.text }}\n\nWho signs the reply (if this is empty, end by asking the customer to reply to this message):\n{{ $(\"Inputs\").first().json[\"sender\"] }}\n\nRules:\n- Open by thanking the customer and saying you have read the complaint. Name their main worry in their own words. Say you are sorry for the worry, the delay or the trouble they describe. Do not say you are sorry for a fault.\n- Then write a short paragraph that starts \"Here is what our record shows.\" Use only the rows marked confirmed or partly confirmed. For a partly confirmed row, say what the record shows and no more. For a point marked not on record, say the record does not show it. State a fact the record gives in neutral words, even if it is not flattering. Do not add a cause, a reason or a view on who was at fault.\n- Then write a short list that starts \"Here is what will happen next.\" Use only the rows of the steps table. Keep each date as the table gives it. If a step has no date, say you will confirm the date and do not make one up. If a request has no step, say that you are looking into it and will write again, only if the steps table says so; otherwise say that you have passed it on.\n- Close with the sign-off.\n- Do not admit liability or fault. Do not promise an outcome such as a payment, a refund, an approval or a decision. Do not say what a policy or contract covers. Do not give legal, medical or financial advice. Do not blame a person, a team or another company by name.\n- Use plain words in sentences of about 15 words. Keep the reply under 250 words, not counting the last line.\n- After the reply, add one blank line and then exactly this line: Check this reply with your manager or your compliance team before you send it.\n\nReturn the reply as plain text, ending with that line, and nothing else.{{ $(\"Check 4: The reminder to check the reply with your manager or compliance team is in the final text\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The reminder to check the reply with your manager or compliance team is in the final text. What was wrong: \" + $(\"Check 4: The reminder to check the reply with your manager or compliance team is in the final text\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 7: No number appears that your complaint, record, steps and sign-off do not have\").isExecuted ? \"\\n\\nYour last answer did not pass this check: No number appears that your complaint, record, steps and sign-off do not have. What was wrong: \" + $(\"Check 7: No number appears that your complaint, record, steps and sign-off do not have\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 8: Every fact in the reply is on the record\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every fact in the reply is on the record. What was wrong: \" + $(\"Check 8: Every fact in the reply is on the record\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 9: The reply admits no fault and gives no cause or blame\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The reply admits no fault and gives no cause or blame. What was wrong: \" + $(\"Check 9: The reply admits no fault and gives no cause or blame\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 10: The reply promises only the agreed steps\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The reply promises only the agreed steps. What was wrong: \" + $(\"Check 10: The reply promises only the agreed steps\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 11: Every request has an answer in the reply\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every request has an answer in the reply. What was wrong: \" + $(\"Check 11: Every request has an answer in the reply\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "99f2b908-8afc-4947-ab27-b0a6930969a5",
      "name": "4. Write the reply",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2820,
        320
      ],
      "notes": "Write a short, calm reply that states the confirmed facts, says what will happen and when, and promises nothing else. Returns: A short reply to the customer that ends with the reminder line.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// The reminder to check the reply with your manager or compliance team is in the final text\n// Rule: The answer contains the text fragment \"Check this reply with your manager or your compliance team before you send it.\" (including inside a longer word).\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                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\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 ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].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. Write the reply\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"has_text\", \"Check this reply with your manager or your compliance team before you send it.\", work, sources);\nreturn [{ json: result }];"
      },
      "id": "9d4a3013-c198-49b0-aabf-eed95eaad1ee",
      "name": "Check 4: The reminder to check the reply with your manager or compliance team is in the final text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        320
      ],
      "notes": "The answer contains the text fragment \"Check this reply with your manager or your compliance team before you send it.\" (including inside a longer word).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "5a270bec-4cb5-462f-a4cf-56fea4fd5b61",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "06ee59b3-ddd6-4b02-a9d6-4c05fb956394",
      "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": "c27fb2ef-20ac-4c24-afd5-b351f8f47546",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "02c5964f-f197-4d64-ae88-513163f9f056",
      "name": "Try again? (check 4)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3340,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the reply\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "76d086aa-d146-4a31-a3b1-c36049f69def",
              "name": "flag",
              "value": "=The reminder to check the reply with your manager or compliance team is in the final text: did not pass. {{ $(\"Check 4: The reminder to check the reply with your manager or compliance team is in the final text\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "aa2400d4-46d1-4b23-a11a-750e396e763d",
      "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": {
        "jsCode": "// Every point is marked happened, wants or feels\n// Rule: Every row of the table has one of \"happened\", \"wants\" or \"feels\" 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                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\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 ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].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 what the customer says\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"column_values_in\", {\"column\":\"Kind\",\"allowed\":[\"happened\",\"wants\",\"feels\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "e87c1300-bf3d-4dcd-a136-0a46cb630abb",
      "name": "Check 5: Every point is marked happened, wants or feels",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3600,
        320
      ],
      "notes": "Every row of the table has one of \"happened\", \"wants\" or \"feels\" in the Kind column.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "59270a59-49b5-4176-a7cf-5bb7a5fd5cf4",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "06f0984a-5dd3-42eb-a9d9-541cfb9324fd",
      "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": "bf7fae36-1fac-4a91-a0d5-b4e4fbf479ff",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "66c87252-0d95-4ae1-a28a-c8743ff77913",
      "name": "Try again? (check 5)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3860,
        520
      ],
      "notes": "True = first failure: send back to \"1. List what the customer says\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "71d07ecb-d246-4bc4-aab1-ce6546f69936",
              "name": "flag",
              "value": "=Every point is marked happened, wants or feels: did not pass. {{ $(\"Check 5: Every point is marked happened, wants or feels\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "ab240267-45d1-4990-a21a-76a1386e74aa",
      "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": {
        "jsCode": "// Every result is one of the three allowed words\n// Rule: Every row of the table has one of \"confirmed\", \"partly confirmed\" or \"not on record\" in the Result 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                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\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 ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].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. Match each point to the record\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"column_values_in\", {\"column\":\"Result\",\"allowed\":[\"confirmed\",\"partly confirmed\",\"not on record\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "3261fd2d-56e0-41f8-a4d2-f7e3c7752d96",
      "name": "Check 6: Every result is one of the three allowed words",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4120,
        320
      ],
      "notes": "Every row of the table has one of \"confirmed\", \"partly confirmed\" or \"not on record\" in the Result column.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "582708c6-4ab5-4309-a6cf-5a24a6fd5e87",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "86e91305-5dd0-4454-a9db-92b37b9aaa42",
      "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": "c07fafc9-1eac-48fe-a1d5-b677faf4786c",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "5ecaa451-9592-4f62-a283-432fbffefe58",
      "name": "Try again? (check 6)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4380,
        520
      ],
      "notes": "True = first failure: send back to \"2. Match each point to the record\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "70d07d38-d346-4d57-a9b1-ccd247f69ac9",
              "name": "flag",
              "value": "=Every result is one of the three allowed words: did not pass. {{ $(\"Check 6: Every result is one of the three allowed words\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "ac2403fa-48d1-4e49-af1a-71e8376e7317",
      "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": {
        "jsCode": "// No number appears that your complaint, record, steps and sign-off do not have\n// Rule: Every number in the answer also appears somewhere in at least one of \"the complaint\", \"what the record shows\", \"what will happen and when\" and \"who signs the reply\"; this does not check what each number means.\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                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\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                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\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 ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].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. Write the reply\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"numbers_in_source\", {\"inputs\":[\"complaint\",\"file_notes\",\"next_steps\",\"sender\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "776c22a4-8509-47ab-ab08-9342a403c5b9",
      "name": "Check 7: No number appears that your complaint, record, steps and sign-off do not have",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4640,
        320
      ],
      "notes": "Every number in the answer also appears somewhere in at least one of \"the complaint\", \"what the record shows\", \"what will happen and when\" and \"who signs the reply\"; this does not check what each number means.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "57270733-47b5-4e50-a9cf-5edda7fd601a",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "06ec1b1c-5dce-45bd-a9dd-d14afb97a22b",
      "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": "bd7fab10-1dac-476b-a2d5-b80afdf47d25",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "82cd1b94-718f-481f-a685-55b2cbfcd2a5",
      "name": "Try again? (check 7)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4900,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the reply\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "73d081f1-d446-4eea-a8b1-cb3f44f69610",
              "name": "flag",
              "value": "=No number appears that your complaint, record, steps and sign-off do not have: did not pass. {{ $(\"Check 7: No number appears that your complaint, record, steps and sign-off do not have\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "ad24058d-47d1-4cb6-a01a-737b366e7184",
      "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": {
        "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: Every fact in the reply is on the record\nRead the reply. List each statement of fact in it about what happened, such as a date, an action or an event. Is each one shown by a row of the table below marked confirmed or partly confirmed? Does the reply state as fact anything marked not on record, or anything the table does not have? Answer FAIL and quote each statement that has no support.\n\nWhat the record shows:\n{{ $(\"2. Match each point to the record\").first().json.text }}\n\nThe work to check (the answer from Step 4 (Write the reply)):\n{{ $(\"4. Write the reply\").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": "32f71e5c-83a0-409d-a019-a00aca704f83",
      "name": "Check 8: Every fact in the reply is on the record",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        5160,
        320
      ],
      "notes": "Answers PASS or FAIL. Read the reply. List each statement of fact in it about what happened, such as a date, an action or an event. Is each one shown by a row of the table below marked confirmed or partly confirmed? Does the reply state as fact anything marked not on record, or anything the table does not have? Answer FAIL and quote each statement that has no support.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "562705a0-48b5-4fe3-a8cf-5d4aa8fd61ad",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "86e495d7-5df3-4696-a9cc-8829fbb378a8",
      "name": "Passed check 8?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5420,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "be7faca3-1cac-45d8-a3d5-b99dfcf47b92",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "dacfe4b3-89a1-4708-a692-21a5cbf006b2",
      "name": "Try again? (check 8)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5420,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the reply\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "72d0805e-d546-407d-a7b1-c9ac45f697a3",
              "name": "flag",
              "value": "=Every fact in the reply is on the record: did not pass. {{ $(\"Check 8: Every fact in the reply is on the record\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "9e23edf0-52d1-4e07-ad1a-6ec22d6e6359",
      "name": "Flag check 8",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5550,
        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 reply admits no fault and gives no cause or blame\nRead the reply. A plain fact from the record, stated in neutral words, is fine. Does the reply go further and admit that the company was at fault, made an error, broke a rule or a promise, or is liable? Does it give a reason or a cause that the record does not state? Does it blame a named person, team or other company? Does it say sorry for a fault rather than for the customer's worry or trouble? Answer FAIL and quote each such phrase.\n\nThe work to check (the answer from Step 4 (Write the reply)):\n{{ $(\"4. Write the reply\").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": "f604859e-93bd-4055-a948-877c4ce7d65b",
      "name": "Check 9: The reply admits no fault and gives no cause or blame",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        5680,
        320
      ],
      "notes": "Answers PASS or FAIL. Read the reply. A plain fact from the record, stated in neutral words, is fine. Does the reply go further and admit that the company was at fault, made an error, broke a rule or a promise, or is liable? Does it give a reason or a cause that the record does not state? Does it blame a named person, team or other company? Does it say sorry for a fault rather than for the customer's worry or trouble? Answer FAIL and quote each such phrase.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "65271d3d-55b5-445a-abcf-48d399fd4a10",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "86e6d46e-5df1-47ff-a9ce-c6c07bb07091",
      "name": "Passed check 9?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5940,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "cb7fc11a-1bac-4445-a4d5-a200fff4804b",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "fed25bf6-959f-4b55-aa94-4d58e7edf42f",
      "name": "Try again? (check 9)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5940,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the reply\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "6dd0787f-c646-48e0-aeb1-d4b152f6ac1a",
              "name": "flag",
              "value": "=The reply admits no fault and gives no cause or blame: did not pass. {{ $(\"Check 9: The reply admits no fault and gives no cause or blame\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "9f23ef83-51d1-4c74-ae1a-70552c6e61c6",
      "name": "Flag check 9",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        6070,
        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 reply promises only the agreed steps\nRead the reply. Does it promise or suggest any outcome, such as a payment, a refund, an approval, a decision in the customer's favor, or that something is or is not covered? Does it promise a step, a date or a person that the table below does not have? Answer FAIL and quote each such phrase.\n\nWhat will happen and when:\n{{ $(\"3. Match each request to what will happen\").first().json.text }}\n\nThe work to check (the answer from Step 4 (Write the reply)):\n{{ $(\"4. Write the reply\").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": "e8103dc6-8aca-412d-a20d-e20890ccf36f",
      "name": "Check 10: The reply promises only the agreed steps",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        6200,
        320
      ],
      "notes": "Answers PASS or FAIL. Read the reply. Does it promise or suggest any outcome, such as a payment, a refund, an approval, a decision in the customer's favor, or that something is or is not covered? Does it promise a step, a date or a person that the table below does not have? Answer FAIL and quote each such phrase.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "64271baa-56b5-45ed-aacf-47409afd4ba3",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "153a0858-3bae-466b-a77c-2faefe1931e1",
      "name": "Passed check 10?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6460,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "cc7fc2ad-1aac-42b2-a5d5-a393fef47eb8",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "f12f4260-7a62-4c71-a5f4-ce9684183fbf",
      "name": "Try again? (check 10)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6460,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the reply\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "6cd076ec-c746-4a73-adb1-d31e53f6adad",
              "name": "flag",
              "value": "=The reply promises only the agreed steps: did not pass. {{ $(\"Check 10: The reply promises only the agreed steps\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "48a5f7b1-2117-4084-a8b2-8fe7f7d7d72a",
      "name": "Flag check 10",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        6590,
        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: Every request has an answer in the reply\nLook at the customer's points below. For each point whose Kind is wants, does the reply give an answer, either the agreed step with its timing or a plain statement that it has been passed on? Does the reply ignore any request? Answer FAIL and name each request that has no answer.\n\nThe customer's points:\n{{ $(\"1. List what the customer says\").first().json.text }}\n\nThe work to check (the answer from Step 4 (Write the reply)):\n{{ $(\"4. Write the reply\").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": "90c199f1-c317-4e62-af69-d0df0fb7d7e0",
      "name": "Check 11: Every request has an answer in the reply",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        6720,
        320
      ],
      "notes": "Answers PASS or FAIL. Look at the customer's points below. For each point whose Kind is wants, does the reply give an answer, either the agreed step with its timing or a plain statement that it has been passed on? Does the reply ignore any request? Answer FAIL and name each request that has no answer.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "337cd286-4d12-49ff-a461-16389fd19951",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "1537c9c1-bbb1-4e82-a779-f117fe1b7078",
      "name": "Passed check 11?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6980,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c981fc8b-1daa-48d4-a2d8-c02191f1948a",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "9d2c7f8d-9e64-43b4-a1f2-5753a81ab702",
      "name": "Try again? (check 11)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6980,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the reply\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "983f545c-86ed-41e1-acdb-d082ba3fad3f",
              "name": "flag",
              "value": "=Every request has an answer in the reply: did not pass. {{ $(\"Check 11: Every request has an answer in the reply\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "47a5f61e-2217-4217-a7b2-8e54f8d7d8bd",
      "name": "Flag check 11",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        7110,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "content": "**Check 12: check by hand**\nRead the reply next to the complaint and the file. Check each fact against the file yourself, and make sure every step and date is one your team has really agreed to. Ask your manager, your compliance team or your lawyer first if the complaint is formal, mentions a regulator, a deadline or legal action, or involves an injury or a dispute about who is at fault. Only a person can decide what your organization says and does, and the reply must be one you are willing to stand behind.\n\nIf not: flag it and carry on. This check is also listed in Result.",
        "height": 200,
        "width": 240,
        "color": 3
      },
      "id": "50612210-9b0c-411f-afd2-2bf6e4042895",
      "name": "Check 12 (by hand)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        6960,
        70
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "87afe2b2-a73b-4373-af7b-0be06eff8769",
              "name": "result",
              "value": "={{ $(\"4. Write the reply\").first().json.text }}",
              "type": "string"
            },
            {
              "id": "3b6220eb-7d03-4eb8-a76e-12a1e15aa0d6",
              "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 : \"\", $(\"Flag check 8\").isExecuted ? $(\"Flag check 8\").first().json.flag : \"\", $(\"Flag check 9\").isExecuted ? $(\"Flag check 9\").first().json.flag : \"\", $(\"Flag check 10\").isExecuted ? $(\"Flag check 10\").first().json.flag : \"\", $(\"Flag check 11\").isExecuted ? $(\"Flag check 11\").first().json.flag : \"\"].filter(x => x).join(\"\\n\") || \"None\" }}",
              "type": "string"
            },
            {
              "id": "02ee7f81-017d-41b6-a5a5-3827fe219644",
              "name": "check_by_hand",
              "value": "- Read the reply next to the complaint and the file. Check each fact against the file yourself, and make sure every step and date is one your team has really agreed to. Ask your manager, your compliance team or your lawyer first if the complaint is formal, mentions a regulator, a deadline or legal action, or involves an injury or a dispute about who is at fault. Only a person can decide what your organization says and does, and the reply must be one you are willing to stand behind.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "454889a7-b7bd-4fd4-a3d6-480d0a9f3c6a",
      "name": "Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        7240,
        320
      ],
      "notes": "The final answer, anything flagged, and the checks left for you.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "609000fd-a9c0-4236-acab-87032539a9a4",
      "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 what the customer says",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. List what the customer says": {
      "main": [
        [
          {
            "node": "Check 1: Every point is quoted word for word from the complaint",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 1: Every point is quoted word for word from the complaint": {
      "main": [
        [
          {
            "node": "Passed check 1?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 1?": {
      "main": [
        [
          {
            "node": "2. Match each point to the record",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 1)": {
      "main": [
        [
          {
            "node": "1. List what the customer says",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 1": {
      "main": [
        [
          {
            "node": "2. Match each point to the record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Match each point to the record": {
      "main": [
        [
          {
            "node": "Check 2: Every passage is quoted word for word from the record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 2: Every passage is quoted word for word from the record": {
      "main": [
        [
          {
            "node": "Passed check 2?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 2?": {
      "main": [
        [
          {
            "node": "3. Match each request to what will happen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 2)": {
      "main": [
        [
          {
            "node": "2. Match each point to the record",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 2": {
      "main": [
        [
          {
            "node": "3. Match each request to what will happen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Match each request to what will happen": {
      "main": [
        [
          {
            "node": "Check 3: Every step is quoted word for word from your list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 3: Every step is quoted word for word from your list": {
      "main": [
        [
          {
            "node": "Passed check 3?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 3?": {
      "main": [
        [
          {
            "node": "4. Write the reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 3)": {
      "main": [
        [
          {
            "node": "3. Match each request to what will happen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 3": {
      "main": [
        [
          {
            "node": "4. Write the reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Write the reply": {
      "main": [
        [
          {
            "node": "Check 4: The reminder to check the reply with your manager or compliance team is in the final text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 4: The reminder to check the reply with your manager or compliance team is in the final text": {
      "main": [
        [
          {
            "node": "Passed check 4?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 4?": {
      "main": [
        [
          {
            "node": "Check 5: Every point is marked happened, wants or feels",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 4)": {
      "main": [
        [
          {
            "node": "4. Write the reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 4": {
      "main": [
        [
          {
            "node": "Check 5: Every point is marked happened, wants or feels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 5: Every point is marked happened, wants or feels": {
      "main": [
        [
          {
            "node": "Passed check 5?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 5?": {
      "main": [
        [
          {
            "node": "Check 6: Every result is one of the three allowed words",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 5)": {
      "main": [
        [
          {
            "node": "1. List what the customer says",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 5": {
      "main": [
        [
          {
            "node": "Check 6: Every result is one of the three allowed words",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 6: Every result is one of the three allowed words": {
      "main": [
        [
          {
            "node": "Passed check 6?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 6?": {
      "main": [
        [
          {
            "node": "Check 7: No number appears that your complaint, record, steps and sign-off do not have",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 6)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 6)": {
      "main": [
        [
          {
            "node": "2. Match each point to the record",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 6": {
      "main": [
        [
          {
            "node": "Check 7: No number appears that your complaint, record, steps and sign-off do not have",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 7: No number appears that your complaint, record, steps and sign-off do not have": {
      "main": [
        [
          {
            "node": "Passed check 7?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 7?": {
      "main": [
        [
          {
            "node": "Check 8: Every fact in the reply is on the record",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 7)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 7)": {
      "main": [
        [
          {
            "node": "4. Write the reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 7": {
      "main": [
        [
          {
            "node": "Check 8: Every fact in the reply is on the record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 8: Every fact in the reply is on the record": {
      "main": [
        [
          {
            "node": "Passed check 8?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 8?": {
      "main": [
        [
          {
            "node": "Check 9: The reply admits no fault and gives no cause or blame",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 8)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 8)": {
      "main": [
        [
          {
            "node": "4. Write the reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 8": {
      "main": [
        [
          {
            "node": "Check 9: The reply admits no fault and gives no cause or blame",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 9: The reply admits no fault and gives no cause or blame": {
      "main": [
        [
          {
            "node": "Passed check 9?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 9?": {
      "main": [
        [
          {
            "node": "Check 10: The reply promises only the agreed steps",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 9)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 9)": {
      "main": [
        [
          {
            "node": "4. Write the reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 9",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 9": {
      "main": [
        [
          {
            "node": "Check 10: The reply promises only the agreed steps",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 10: The reply promises only the agreed steps": {
      "main": [
        [
          {
            "node": "Passed check 10?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 10?": {
      "main": [
        [
          {
            "node": "Check 11: Every request has an answer in the reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 10)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 10)": {
      "main": [
        [
          {
            "node": "4. Write the reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 10",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 10": {
      "main": [
        [
          {
            "node": "Check 11: Every request has an answer in the reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 11: Every request has an answer in the reply": {
      "main": [
        [
          {
            "node": "Passed check 11?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 11?": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 11)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 11)": {
      "main": [
        [
          {
            "node": "4. Write the reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 11",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 11": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat model": {
      "ai_languageModel": [
        [
          {
            "node": "1. List what the customer says",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "2. Match each point to the record",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "3. Match each request to what will happen",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "4. Write the reply",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 8: Every fact in the reply is on the record",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 9: The reply admits no fault and gives no cause or blame",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 10: The reply promises only the agreed steps",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 11: Every request has an answer in the reply",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "template": "reply-to-a-complaint"
  }
}
