{
  "name": "Pull key facts into a table",
  "nodes": [
    {
      "parameters": {
        "content": "## Pull key facts into a table\nPull the facts you name out of a text into a table, one row for each item, copying every value as written, quoting where each came from, and marking every fact the text does not give as Not stated.\n\n**How to use**\n1. Open **Chat model** and pick your own credential. To use another AI service, delete that node, add the service's chat model node, and connect it to the Model plug under every step and AI check.\n2. Open **Inputs** and paste your material into each field. Examples are filled in so you can try it first.\n3. Click **Execute workflow**.\n4. Open **Result** to read the answer, anything flagged, and the checks left for you.\n\n**Checks**\n- Check 1 (a plain rule, run automatically with no AI): Every quote in the Passage (quote) column appears word for word in the text to read. If it fails: try once more, then flag.\n- Check 2 (a plain rule, run automatically with no AI): Every quote in the Quote column appears word for word in the text to read. If it fails: try once more, then flag.\n- Check 3 (a plain rule, run automatically with no AI): Every number in the answer also appears in the text to read. If it fails: try once more, then flag.\n- Check 4 (a second AI prompt answers pass or fail): Compare each Value in the answer with the text below. Is every value written exactly as the text writes it, with no change to a digit, unit or word, is the latest version used where the text corrects itself, and does every row marked Not stated really have no answer in the text? Answer FAIL and quote each row that is wrong. If it fails: try once more, then flag.\n- Check 5 (a plain rule, run automatically with no AI): The answer includes the words \"Not stated:\". If it fails: try once more, then flag.\n- Check 6 (a second AI prompt answers pass or fail): Check the table in the answer against the lists below. Does it have exactly one row for each item in the item list and no other rows, one column for each fact the reader named in that order, and in every cell the same value as the facts table? Answer FAIL and quote any row, column or cell that is wrong. If it fails: try once more, then flag.\n- Check 7 (you check it by hand): Open the text and check three cells against it, starting with any date or amount. For each cell marked Not stated, decide whether to ask the person who wrote the text or leave it open. Only you know which facts the table is for. If it fails: flag it and carry on.\n\nFrom Logic Lab (logiclabhq.com). Free to use and adapt, with credit.",
        "height": 740,
        "width": 760,
        "color": 5
      },
      "id": "b53f49fa-9ff7-4ef5-aa16-ff8c99201a57",
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -700
      ]
    },
    {
      "parameters": {},
      "id": "a7e09afe-f5f0-427d-a493-10d8e012ffa7",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        320
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "135ca480-f767-47ed-ae6f-cf6ebcd2adb3",
              "name": "text",
              "value": "Sample: Spring workshops at the Harbor View Community Center. Seed Starting is on Saturday, March 14, from 10 am to noon, in the garden room. It costs 8 dollars. Sign up with Tamsin at tamsin@example.org. Bike Repair Night follows on Thursday, March 19, from 6 to 8 pm, in the back workshop. It is free, and spaces are limited to 12 people. Knitting Circle meets on Saturday, March 28 from 2 pm. It costs 5 dollars, which includes yarn. Call Lena on 555-0148 to book.",
              "type": "string"
            },
            {
              "id": "d2932f10-cd42-452d-abfa-a17e301acbd3",
              "name": "wanted",
              "value": "Sample: date, time, place, cost, contact",
              "type": "string"
            },
            {
              "id": "6b953c20-3b25-424d-a1d0-81168ffe5cbb",
              "name": "item",
              "value": "Sample: each workshop",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "ebf30ad3-6e26-48d2-a949-50910d626a90",
      "name": "Inputs",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        320
      ],
      "notes": "Paste your own material here: text = The text to read; wanted = The facts to pull out; item = What each row is.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reader who finds each thing a text is about.\nThe text is a document a client sent a consultant, such as an email, a contract summary or a plan, and the table goes into the consultant's working file or report.\n\nYour task is to list every item the text mentions, where one item is: {{ $(\"Inputs\").first().json[\"item\"] }}\n\nThe text:\n{{ $(\"Inputs\").first().json[\"text\"] }}\n\nRules:\n- One row for each item, in the order the text gives them. Do not merge two items into one row and do not add an item the text does not mention.\n- In \"Item\", use the name the text gives it. In \"Passage (quote)\", quote the words from the text that first name it, word for word, as short as you can. Do not reword.\n- Use only the text. Do not pull out any facts yet.\n\nReturn one markdown table and nothing else:\n| Item | Passage (quote) |{{ $(\"Check 1: Every passage is quoted word for word from the text\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every passage is quoted word for word from the text. What was wrong: \" + $(\"Check 1: Every passage is quoted word for word from the text\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "56c0afd7-1d92-41ee-aeb5-02d5454a8f54",
      "name": "1. List every item",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        480,
        320
      ],
      "notes": "Find each item the text is about and quote the words that introduce it, before any fact is pulled out. Returns: A table with one row for each item and the passage that names it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every passage is quoted word for word from the text\n// Rule: Every quote in the Passage (quote) column appears word for word in the text to read.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"1. List every item\").first().json.text ?? '');\nconst sources = {\n  \"text\": String($(\"Inputs\").first().json[\"text\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"Passage (quote)\",\"input\":\"text\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "33095321-1b07-4634-a327-69e3c09597ee",
      "name": "Check 1: Every passage is quoted word for word from the text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        320
      ],
      "notes": "Every quote in the Passage (quote) column appears word for word in the text to read.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "9e96a365-32b8-430c-a917-800b67505b5a",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "6fc1e0e6-2a60-47f5-afe3-dbd857d8b44f",
      "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": "6bb26542-20ee-4637-a061-34b88cf14f75",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0ccdfe1e-ca2e-41e3-a923-6e007d5b5ae5",
      "name": "Try again? (check 1)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1000,
        520
      ],
      "notes": "True = first failure: send back to \"1. List every item\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e3b943d7-7765-40a6-a872-553984a86d60",
              "name": "flag",
              "value": "=Every passage is quoted word for word from the text: did not pass. {{ $(\"Check 1: Every passage is quoted word for word from the text\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "5af7495b-4e18-4922-a436-984d25fb4454",
      "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 clerk who copies facts out of a text exactly as written.\nThe text is a document a client sent a consultant, such as an email, a contract summary or a plan, and the table goes into the consultant's working file or report.\n\nYour task is to find, for each item below, each of the facts the reader named.\n\nThe items:\n{{ $(\"1. List every item\").first().json.text }}\n\nThe facts to pull out, in this order: {{ $(\"Inputs\").first().json[\"wanted\"] }}\n\nThe text:\n{{ $(\"Inputs\").first().json[\"text\"] }}\n\nRules:\n- Write one row for each item and each fact, with the items in the order above and each item's facts in the order the reader named them.\n- In \"Value\", copy the answer as the text writes it: the same digits, units and wording. Do not convert, round, reformat or add up anything.\n- In \"Quote\", give the words from the text that state the value, word for word, as short as you can.\n- If the text changes a fact later, such as a correction, use the latest version and quote it.\n- If the text does not give the fact for that item, write exactly Not stated in \"Value\" and exactly None found in \"Quote\". Do not fill a gap from another item, do not guess, and do not use anything from outside the text.\n- Do not put one item's fact in another item's row.\n\nReturn one markdown table and nothing else:\n| Item | Fact | Value | Quote |{{ $(\"Check 2: Every quote is word for word from the text\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every quote is word for word from the text. What was wrong: \" + $(\"Check 2: Every quote is word for word from the text\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 4: Every value is exactly as the text writes it\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every value is exactly as the text writes it. What was wrong: \" + $(\"Check 4: Every value is exactly as the text writes it\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "2670939b-56ce-4688-a8a5-df79d1fdc80e",
      "name": "2. Pull each fact for each item",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1260,
        320
      ],
      "notes": "For every item and every fact the reader named, copy the value as written and quote the words it came from. Returns: A table with one row for each item and fact, a value copied as written and the quote it came from, or Not stated.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every quote is word for word from the text\n// Rule: Every quote in the Quote column appears word for word in the text to read.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"2. Pull each fact for each item\").first().json.text ?? '');\nconst sources = {\n  \"text\": String($(\"Inputs\").first().json[\"text\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"Quote\",\"input\":\"text\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "160a59fc-f3cf-49ab-a5d6-374ab1d28cd1",
      "name": "Check 2: Every quote is word for word from the text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        320
      ],
      "notes": "Every quote in the Quote column appears word for word in the text to read.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "9d96a1d2-33b8-449f-a817-7e7868505ced",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "6fbb2521-aa67-4d3a-afe6-e3ef57d675b8",
      "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": "6cb266d5-1fee-44a4-a161-364b8bf14de2",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "34d07bad-4a35-4728-a91c-1b1b15587896",
      "name": "Try again? (check 2)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        520
      ],
      "notes": "True = first failure: send back to \"2. Pull each fact for each item\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e2b94244-7865-4239-a772-53a685a86ef3",
              "name": "flag",
              "value": "=Every quote is word for word from the text: did not pass. {{ $(\"Check 2: Every quote is word for word from the text\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "5bf74aee-4d18-478f-a136-939428fb490d",
      "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 tidy analyst who lays facts out in a table without changing them.\nThe text is a document a client sent a consultant, such as an email, a contract summary or a plan, and the table goes into the consultant's working file or report.\n\nYour task is to lay the facts below out as one table for the reader.\n\nThe facts:\n{{ $(\"2. Pull each fact for each item\").first().json.text }}\n\nThe columns the reader wants, in this order: {{ $(\"Inputs\").first().json[\"wanted\"] }}\n\nRules:\n- The first column is \"Item\". After it, add one column for each fact the reader named, in that order, headed by the fact's name with a capital letter.\n- One row for each item, in the order of the facts above. Do not add a row, a column or a total, and do not number the rows.\n- Copy each value from the facts above exactly. Do not change a digit, unit or word, and do not add a unit that is not there.\n- Where the facts above say Not stated, write Not stated in that cell.\n- After the table, write one line that starts \"Not stated:\" and lists each item and fact that is Not stated, or write \"Not stated: none.\" if there are none.\n- Do not add a number, name or fact that is not in the facts above.\n\nReturn markdown in this form and nothing else:\n| Item | (one column for each fact) |\nNot stated: (the list){{ $(\"Check 3: Every number in the table is in the text\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every number in the table is in the text. What was wrong: \" + $(\"Check 3: Every number in the table is in the text\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 5: The table ends with a Not stated line\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The table ends with a Not stated line. What was wrong: \" + $(\"Check 5: The table ends with a Not stated line\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 6: The table matches the items and the facts\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The table matches the items and the facts. What was wrong: \" + $(\"Check 6: The table matches the items and the facts\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "679c2c52-c718-445f-adc4-73f0f32107a5",
      "name": "3. Lay out the table",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2040,
        320
      ],
      "notes": "Turn the rows into one table with a row for each item and a column for each fact, without changing a value. Returns: One table with a row for each item and a column for each fact, and a line listing what the text does not say.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every number in the table is in the text\n// Rule: Every number in the answer also appears in the text to read.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"3. Lay out the table\").first().json.text ?? '');\nconst sources = {\n  \"text\": String($(\"Inputs\").first().json[\"text\"] ?? ''),\n};\nconst result = runRule(\"numbers_in_source\", {\"input\":\"text\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "fa6101ea-5b13-4e7d-af00-09647862e7a7",
      "name": "Check 3: Every number in the table is in the text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2300,
        320
      ],
      "notes": "Every number in the answer also appears in the text to read.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "9c96a03f-34b8-4632-ab17-833165505834",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "6fbd63b8-aa65-4ea3-afe9-228657d43721",
      "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": "69b2621c-22ee-495d-a261-37de8af14c4f",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "08d27500-d632-41f5-ad1e-f71ef1560153",
      "name": "Try again? (check 3)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2560,
        520
      ],
      "notes": "True = first failure: send back to \"3. Lay out the table\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e5b946fd-7565-4d80-a672-521386a87086",
              "name": "flag",
              "value": "=Every number in the table is in the text: did not pass. {{ $(\"Check 3: Every number in the table is in the text\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "5cf74c81-4c18-45fc-a236-952727fb477a",
      "name": "Flag check 3",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2690,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Every value is exactly as the text writes it\nCompare each Value in the answer with the text below. Is every value written exactly as the text writes it, with no change to a digit, unit or word, is the latest version used where the text corrects itself, and does every row marked Not stated really have no answer in the text? Answer FAIL and quote each row that is wrong.\n\nThe text:\n{{ $(\"Inputs\").first().json[\"text\"] }}\n\nThe work to check (the answer from Step 2 (Pull each fact for each item)):\n{{ $(\"2. Pull each fact for each item\").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": "60963eed-b142-40a2-aa7c-0a7be7895f18",
      "name": "Check 4: Every value is exactly as the text writes it",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2820,
        320
      ],
      "notes": "Answers PASS or FAIL. Compare each Value in the answer with the text below. Is every value written exactly as the text writes it, with no change to a digit, unit or word, is the latest version used where the text corrects itself, and does every row marked Not stated really have no answer in the text? Answer FAIL and quote each row that is wrong.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "9b969eac-35b8-47c5-aa17-819e665059c7",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "efb5de73-2a6d-43e8-afd7-d965d7d12f0a",
      "name": "Passed check 4?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3080,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6ab263af-21ee-47ca-a361-397189f14abc",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "30d4f28f-563a-473a-ad2a-f991f967570c",
      "name": "Try again? (check 4)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3080,
        520
      ],
      "notes": "True = first failure: send back to \"2. Pull each fact for each item\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e4b9456a-7665-4f13-a572-508087a87219",
              "name": "flag",
              "value": "=Every value is exactly as the text writes it: did not pass. {{ $(\"Check 4: Every value is exactly as the text writes it\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "5df74e14-4b18-4469-af36-906e22fb3f9b",
      "name": "Flag check 4",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3210,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// The table ends with a Not stated line\n// Rule: The answer includes the words \"Not stated:\".\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"3. Lay out the table\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"has_text\", \"Not stated:\", work, sources);\nreturn [{ json: result }];"
      },
      "id": "179ed621-d9ea-4be8-acbf-b50f99bc51fe",
      "name": "Check 5: The table ends with a Not stated line",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3340,
        320
      ],
      "notes": "The answer includes the words \"Not stated:\".",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "9a969d19-2eb8-4cc0-ad17-86576350550e",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "efb81d0a-aa6a-4bd1-afda-17fcd7cef073",
      "name": "Passed check 5?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3600,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "67b25ef6-1cee-4feb-a461-3b0488f14929",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "94d7ce92-3237-4ff7-a12d-70d415654489",
      "name": "Try again? (check 5)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3600,
        520
      ],
      "notes": "True = first failure: send back to \"3. Lay out the table\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "dfb93d8b-7b65-46f2-ac72-5b8588a873ac",
              "name": "flag",
              "value": "=The table ends with a Not stated line: did not pass. {{ $(\"Check 5: The table ends with a Not stated line\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "5ef74fa7-4a18-42d6-a036-920121fb3e08",
      "name": "Flag check 5",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3730,
        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 table matches the items and the facts\nCheck the table in the answer against the lists below. Does it have exactly one row for each item in the item list and no other rows, one column for each fact the reader named in that order, and in every cell the same value as the facts table? Answer FAIL and quote any row, column or cell that is wrong.\n\nThe items:\n{{ $(\"1. List every item\").first().json.text }}\n\nThe facts the reader named: {{ $(\"Inputs\").first().json[\"wanted\"] }}\n\nThe facts table:\n{{ $(\"2. Pull each fact for each item\").first().json.text }}\n\nThe work to check (the answer from Step 3 (Lay out the table)):\n{{ $(\"3. Lay out the table\").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": "f99fd5ce-0c6d-4e33-a832-5ad84babcc85",
      "name": "Check 6: The table matches the items and the facts",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        3860,
        320
      ],
      "notes": "Answers PASS or FAIL. Check the table in the answer against the lists below. Does it have exactly one row for each item in the item list and no other rows, one column for each fact the reader named in that order, and in every cell the same value as the facts table? Answer FAIL and quote any row, column or cell that is wrong.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "99969b86-2fb8-4e53-ac17-84c4645056a1",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "6fb097c5-2a71-4116-afdc-5693d7ccb1dc",
      "name": "Passed check 6?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4120,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "68b26089-1bee-4e58-a561-3c9787f14796",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "8cda0091-d23f-479c-a125-eb8ffd62e02a",
      "name": "Try again? (check 6)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4120,
        520
      ],
      "notes": "True = first failure: send back to \"3. Lay out the table\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "deb93bf8-7c65-4885-ab72-59f289a8753f",
              "name": "flag",
              "value": "=The table matches the items and the facts: did not pass. {{ $(\"Check 6: The table matches the items and the facts\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "5ff7513a-4918-4143-ad36-8d4824fb42c1",
      "name": "Flag check 6",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4250,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "content": "**Check 7: check by hand**\nOpen the text and check three cells against it, starting with any date or amount. For each cell marked Not stated, decide whether to ask the person who wrote the text or leave it open. Only you know which facts the table is for.\n\nIf not: flag it and carry on. This check is also listed in Result.",
        "height": 200,
        "width": 240,
        "color": 3
      },
      "id": "6170c6b6-574c-4d31-ab1c-00309cf0ef53",
      "name": "Check 7 (by hand)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        4100,
        70
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "76af41f2-8486-47b5-a14b-a9803dbcdf9b",
              "name": "result",
              "value": "={{ $(\"3. Lay out the table\").first().json.text }}",
              "type": "string"
            },
            {
              "id": "3c8cde2b-3b35-4aaa-a5a1-99c118bc3290",
              "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 : \"\"].filter(x => x).join(\"\\n\") || \"None\" }}",
              "type": "string"
            },
            {
              "id": "e853b1c1-d248-4398-a3da-5247798af86e",
              "name": "check_by_hand",
              "value": "- Open the text and check three cells against it, starting with any date or amount. For each cell marked Not stated, decide whether to ask the person who wrote the text or leave it open. Only you know which facts the table is for.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "2142cf67-45be-43ee-a5ee-eced146e7cac",
      "name": "Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4380,
        320
      ],
      "notes": "The final answer, anything flagged, and the checks left for you.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "e275e03d-7898-4c28-a14b-a1638e1cfa9e",
      "name": "Chat model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        480,
        840
      ],
      "notes": "Attach your credential here, or swap in any chat model node.",
      "notesInFlow": true
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inputs": {
      "main": [
        [
          {
            "node": "1. List every item",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. List every item": {
      "main": [
        [
          {
            "node": "Check 1: Every passage is quoted word for word from the text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 1: Every passage is quoted word for word from the text": {
      "main": [
        [
          {
            "node": "Passed check 1?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 1?": {
      "main": [
        [
          {
            "node": "2. Pull each fact for each item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 1)": {
      "main": [
        [
          {
            "node": "1. List every item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 1": {
      "main": [
        [
          {
            "node": "2. Pull each fact for each item",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Pull each fact for each item": {
      "main": [
        [
          {
            "node": "Check 2: Every quote is word for word from the text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 2: Every quote is word for word from the text": {
      "main": [
        [
          {
            "node": "Passed check 2?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 2?": {
      "main": [
        [
          {
            "node": "3. Lay out the table",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 2)": {
      "main": [
        [
          {
            "node": "2. Pull each fact for each item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 2": {
      "main": [
        [
          {
            "node": "3. Lay out the table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Lay out the table": {
      "main": [
        [
          {
            "node": "Check 3: Every number in the table is in the text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 3: Every number in the table is in the text": {
      "main": [
        [
          {
            "node": "Passed check 3?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 3?": {
      "main": [
        [
          {
            "node": "Check 4: Every value is exactly as the text writes it",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 3)": {
      "main": [
        [
          {
            "node": "3. Lay out the table",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 3": {
      "main": [
        [
          {
            "node": "Check 4: Every value is exactly as the text writes it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 4: Every value is exactly as the text writes it": {
      "main": [
        [
          {
            "node": "Passed check 4?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 4?": {
      "main": [
        [
          {
            "node": "Check 5: The table ends with a Not stated line",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 4)": {
      "main": [
        [
          {
            "node": "2. Pull each fact for each item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 4": {
      "main": [
        [
          {
            "node": "Check 5: The table ends with a Not stated line",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 5: The table ends with a Not stated line": {
      "main": [
        [
          {
            "node": "Passed check 5?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 5?": {
      "main": [
        [
          {
            "node": "Check 6: The table matches the items and the facts",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 5)": {
      "main": [
        [
          {
            "node": "3. Lay out the table",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 5": {
      "main": [
        [
          {
            "node": "Check 6: The table matches the items and the facts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 6: The table matches the items and the facts": {
      "main": [
        [
          {
            "node": "Passed check 6?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 6?": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 6)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 6)": {
      "main": [
        [
          {
            "node": "3. Lay out the table",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 6": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat model": {
      "ai_languageModel": [
        [
          {
            "node": "1. List every item",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "2. Pull each fact for each item",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "3. Lay out the table",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 4: Every value is exactly as the text writes it",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 6: The table matches the items and the facts",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "template": "pull-key-facts-into-a-table-for-consultants"
  }
}
