{
  "name": "Research from supplied material",
  "nodes": [
    {
      "parameters": {
        "content": "## Research from supplied material\nAnswer a focused research question from documents you supply, trace each finding to a passage, show conflicts and unknowns, and write a concise answer within the evidence.\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 row of the table has something written in \"Document\", \"Relevant topic\" and \"Limit\" (a blank or a dash does not count). If it fails: try once more, then flag.\n- Check 2 (a plain rule, run automatically with no AI): Each quote in the Passage (quote) column appears in the material to use, 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): The answer contains the text fragment \"Unknown\" (including inside a longer word). 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 \"What remains unknown\" (including inside a longer word). If it fails: try once more, then flag.\n- Check 5 (a second AI prompt answers pass or fail): Compare the answer with {{steps.extract}} and {{steps.assess}}. Does each material finding have a matching passage or shown calculation, are conflicts still visible, and is any unsupported assertion presented as fact? Answer FAIL and quote an unsupported assertion. If it fails: try once more, then flag.\n- Check 6 (you check it by hand): Reopen the named documents and read the passages behind the main answer. Confirm that the packet is complete enough for your use, and ask for missing material where an unknown could change the decision. If it fails: flag it and carry on.\n\nFrom Logic Lab (logiclabhq.com). Free to use and adapt, with credit.",
        "height": 700,
        "width": 760,
        "color": 5
      },
      "id": "bf274e55-34cf-4b72-a4da-861b0e3f7650",
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -660
      ]
    },
    {
      "parameters": {},
      "id": "af0c9ddd-c915-4f86-a47a-4a0bb121200c",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        320
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "325af0ce-2893-4ceb-a8eb-96fcfbade6a9",
              "name": "question",
              "value": "Sample: Did the Saturday workshop pilot improve attendance and cost per attendee?",
              "type": "string"
            },
            {
              "id": "744b9553-734f-4936-a3b7-c27900c161ec",
              "name": "material",
              "value": "Sample: Pilot log, June: 4 Saturday sessions, 80 attendees, $800 spent. Spring log, April: 4 weekday sessions, 60 attendees, $720 spent. Staff note, June: attendance counts include repeat visitors.",
              "type": "string"
            },
            {
              "id": "6d2a7024-ceee-4071-a359-e186ad1b4953",
              "name": "boundary",
              "value": "Sample: Brief the program manager in plain language.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "723b8cb2-03a4-44ab-ac29-70345dbae6e5",
      "name": "Inputs",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        320
      ],
      "notes": "Paste your own material here: question = Research question; material = Material to use; boundary = What the answer should cover (optional).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a research assistant who inventories a document packet.\n\nYour task is to map the material that can answer this question: {{ $(\"Inputs\").first().json[\"question\"] }}\n\nMaterial:\n{{ $(\"Inputs\").first().json[\"material\"] }}\n\nRules:\n- Give one row per titled document in the order supplied. Keep its title as written.\n- State the relevant topic in plain words and one limit, such as a missing measure or unclear definition.\n- Do not answer the question yet. Do not invent a source or a publication date.\n\nReturn one markdown table: | Document | Relevant topic | Limit |{{ $(\"Check 1: Each document row has a topic and a limit\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Each document row has a topic and a limit. What was wrong: \" + $(\"Check 1: Each document row has a topic and a limit\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "d63177e3-18a7-4280-a9e4-cb51c65c2fbe",
      "name": "1. Map the supplied documents",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        480,
        320
      ],
      "notes": "Record what each document can answer and where the packet is thin. Returns: A document map with relevant topics and limits.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Each document row has a topic and a limit\n// Rule: Every row of the table has something written in \"Document\", \"Relevant topic\" and \"Limit\" (a blank or a dash does not count).\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            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. Map the supplied documents\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"table_cells_filled\", [\"Document\",\"Relevant topic\",\"Limit\"], work, sources);\nreturn [{ json: result }];"
      },
      "id": "6753c15b-ae35-4ab6-a7e2-5d6975986194",
      "name": "Check 1: Each document row has a topic and a limit",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        320
      ],
      "notes": "Every row of the table has something written in \"Document\", \"Relevant topic\" and \"Limit\" (a blank or a dash does not count).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2c22c8ac-12c3-49cd-a125-ee06b1967747",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b0943b55-8d1c-4a1e-a9fd-34b315b7edcc",
      "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": "f5ae1f57-9a5c-448a-ab18-21e1d4bf66b4",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "4db33dc3-599a-4956-aed3-3c6976e8eb24",
      "name": "Try again? (check 1)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1000,
        520
      ],
      "notes": "True = first failure: send back to \"1. Map the supplied documents\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "720ceac6-5d4a-40cf-a7ff-df0c834245e5",
              "name": "flag",
              "value": "=Each document row has a topic and a limit: did not pass. {{ $(\"Check 1: Each document row has a topic and a limit\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "1cf2a982-3cfd-4923-af47-9b100dfe6b59",
      "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 reader of supplied research material.\n\nYour task is to extract each fact in the packet that bears on the question.\n\nQuestion: {{ $(\"Inputs\").first().json[\"question\"] }}\nDocument map:\n{{ $(\"1. Map the supplied documents\").first().json.text }}\nMaterial:\n{{ $(\"Inputs\").first().json[\"material\"] }}\n\nRules:\n- Use one row per distinct relevant fact. Name the document and quote the shortest passage that states it, word for word.\n- Include facts that disagree with another document. Keep dates, units and qualifiers as written.\n- Do not import facts from memory or silently calculate new figures. Do not treat an absent fact as zero.\n\nReturn one markdown table: | Finding | Document | Passage (quote) |{{ $(\"Check 2: Every evidence passage is in the supplied material\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every evidence passage is in the supplied material. What was wrong: \" + $(\"Check 2: Every evidence passage is in the supplied material\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "4677cc99-24f9-4e64-ac54-38878c3b31da",
      "name": "2. Extract evidence for the question",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1260,
        320
      ],
      "notes": "Pull relevant facts and quote the source words behind each one. Returns: A source-linked evidence table for the question.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every evidence passage is in the supplied material\n// Rule: Each quote in the Passage (quote) column appears in the material to use, 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            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. Extract evidence for the question\").first().json.text ?? '');\nconst sources = {\n  \"material\": String($(\"Inputs\").first().json[\"material\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"Passage (quote)\",\"input\":\"material\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "57b3cec6-43f1-4d29-ab28-b50038b907cb",
      "name": "Check 2: Every evidence passage is in the supplied material",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        320
      ],
      "notes": "Each quote in the Passage (quote) column appears in the material to use, with shortened fragments in the same order.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2d22ca3f-11c3-483a-a225-ef99b09675b4",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "309bc09a-0d15-44d9-a9fa-f61c95baf5e3",
      "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": "f4ae1dc4-9b5c-461d-aa18-204ed5bf6847",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "cdbac308-c19d-4ba5-a6d0-d80af6e165df",
      "name": "Try again? (check 2)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        520
      ],
      "notes": "True = first failure: send back to \"2. Extract evidence for the question\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "730cec59-5c4a-4f3c-a8ff-e09f82424452",
              "name": "flag",
              "value": "=Every evidence passage is in the supplied material: did not pass. {{ $(\"Check 2: Every evidence passage is in the supplied material\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "1bf2a7ef-3dfd-4ab6-a247-9fc90afe66a0",
      "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 an evidence reviewer who distinguishes a finding from a gap.\n\nYour task is to assess what the evidence can answer and what remains open.\n\nQuestion: {{ $(\"Inputs\").first().json[\"question\"] }}\nEvidence:\n{{ $(\"2. Extract evidence for the question\").first().json.text }}\nDocument limits:\n{{ $(\"1. Map the supplied documents\").first().json.text }}\n\nRules:\n- Put each material point under Supported, Conflicting or Unknown. Explain a conflict by naming the documents and values that differ.\n- Arithmetic is allowed only when every operand is in the evidence. Show the calculation and its units.\n- If a question needs a comparison that the packet cannot support, mark it Unknown rather than guess.\n- Do not resolve a conflict by choosing the newer document without an explicit correction in the material.\n\nReturn markdown with headings Supported, Conflicting and Unknown, with short bullets and document names.{{ $(\"Check 3: The assessment names unknowns\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The assessment names unknowns. What was wrong: \" + $(\"Check 3: The assessment names unknowns\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "3c966154-da3d-42f3-ac2d-d5e20aa62a31",
      "name": "3. Sort findings and unknowns",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2040,
        320
      ],
      "notes": "Separate supported findings, conflicts and missing information before drafting. Returns: Findings and unresolved questions with their evidence limits.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// The assessment names unknowns\n// Rule: The answer contains the text fragment \"Unknown\" (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            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. Sort findings and unknowns\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"has_text\", \"Unknown\", work, sources);\nreturn [{ json: result }];"
      },
      "id": "dceb8330-e576-415d-a487-5436e495e3b3",
      "name": "Check 3: The assessment names unknowns",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2300,
        320
      ],
      "notes": "The answer contains the text fragment \"Unknown\" (including inside a longer word).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2e22cbd2-10c3-46a7-af25-eae0b3967a6d",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b098b883-0d17-4370-a9f7-ee0595bd347a",
      "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": "f7ae227d-985c-4164-a918-1ebbd6bf69da",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "d9b89755-b59f-4758-a2ce-c5871ae3dd22",
      "name": "Try again? (check 3)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2560,
        520
      ],
      "notes": "True = first failure: send back to \"3. Sort findings and unknowns\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "700ce7a0-5f4a-43f5-a9ff-e232814242bf",
              "name": "flag",
              "value": "=The assessment names unknowns: did not pass. {{ $(\"Check 3: The assessment names unknowns\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "1af2a65c-3efd-4c49-a147-9e360bfe6833",
      "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 concise research writer.\n\nYour task is to answer the question for the stated reader and boundary.\n\nQuestion: {{ $(\"Inputs\").first().json[\"question\"] }}\nBoundary: {{ $(\"Inputs\").first().json[\"boundary\"] }}\nAssessed findings:\n{{ $(\"3. Sort findings and unknowns\").first().json.text }}\nEvidence:\n{{ $(\"2. Extract evidence for the question\").first().json.text }}\n\nRules:\n- Lead with the direct answer, or say the packet cannot answer it fully.\n- Cite the document title beside each material finding. Keep conflicting figures and unknowns visible.\n- When you calculate a figure, show the source numbers and arithmetic. Preserve qualifiers from the material.\n- Do not imply that you searched outside this packet. Do not add a source, a quote or a number.\n\nReturn markdown with headings Answer, Evidence, What remains unknown and Check before using.{{ $(\"Check 4: The answer includes an unknowns section\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The answer includes an unknowns section. What was wrong: \" + $(\"Check 4: The answer includes an unknowns section\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 5: Each material finding traces to the evidence table\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Each material finding traces to the evidence table. What was wrong: \" + $(\"Check 5: Each material finding traces to the evidence table\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "02f44b60-5bff-4f67-a130-0b0ac3e7dbe1",
      "name": "4. Write the sourced answer",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2820,
        320
      ],
      "notes": "Answer in plain words and keep evidence limits visible. Returns: A concise sourced answer with limits and a human review cue.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// The answer includes an unknowns section\n// Rule: The answer contains the text fragment \"What remains unknown\" (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            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"4. Write the sourced answer\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"has_text\", \"What remains unknown\", work, sources);\nreturn [{ json: result }];"
      },
      "id": "6356889c-db18-4835-adc5-06dadb175c8b",
      "name": "Check 4: The answer includes an unknowns section",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        320
      ],
      "notes": "The answer contains the text fragment \"What remains unknown\" (including inside a longer word).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2f22cd65-0fc3-4514-a025-ec73b29678da",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "30a03dc8-8d0f-4e2b-aa09-372695bf7311",
      "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": "f6ae20ea-995c-42f7-a818-1d28d7bf6b6d",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "59c01c9a-dda2-44e7-aacc-47f8fadceefd",
      "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 sourced answer\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "710ce933-5e4a-4262-aaff-e3c58042412c",
              "name": "flag",
              "value": "=The answer includes an unknowns section: did not pass. {{ $(\"Check 4: The answer includes an unknowns section\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "19f2a4c9-3ffd-4ddc-a447-a2ef10fe7012",
      "name": "Flag check 4",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3470,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Each material finding traces to the evidence table\nCompare the answer with {{ $(\"2. Extract evidence for the question\").first().json.text }} and {{ $(\"3. Sort findings and unknowns\").first().json.text }}. Does each material finding have a matching passage or shown calculation, are conflicts still visible, and is any unsupported assertion presented as fact? Answer FAIL and quote an unsupported assertion.\n\nThe work to check (the answer from Step 4 (Write the sourced answer)):\n{{ $(\"4. Write the sourced answer\").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": "25f77270-8dc7-4def-a412-4d065de9792d",
      "name": "Check 5: Each material finding traces to the evidence table",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        3600,
        320
      ],
      "notes": "Answers PASS or FAIL. Compare the answer with {{steps.extract}} and {{steps.assess}}. Does each material finding have a matching passage or shown calculation, are conflicts still visible, and is any unsupported assertion presented as fact? Answer FAIL and quote an unsupported assertion.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2822c260-0ec3-4381-a525-f452ad9670fb",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "309dff31-0d12-4642-aa06-f88f15c27b28",
      "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": "f1ae190b-965c-4e3e-af18-282dd0bf6068",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "35bda557-41a5-40ea-a6c9-52c50edf4d10",
      "name": "Try again? (check 5)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3860,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the sourced answer\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "760cf112-594a-4a83-a3ff-d8c07f423f99",
              "name": "flag",
              "value": "=Each material finding traces to the evidence table: did not pass. {{ $(\"Check 5: Each material finding traces to the evidence table\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "18f2a336-40fd-4f6f-a347-a15c11fe71a5",
      "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": {
        "content": "**Check 6: check by hand**\nReopen the named documents and read the passages behind the main answer. Confirm that the packet is complete enough for your use, and ask for missing material where an unknown could change the decision.\n\nIf not: flag it and carry on. This check is also listed in Result.",
        "height": 200,
        "width": 240,
        "color": 3
      },
      "id": "ddfd98b6-0053-46e9-a048-681cac409d0f",
      "name": "Check 6 (by hand)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3840,
        70
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "aa9c7b55-cfe0-44aa-ac4b-559f420e98a4",
              "name": "result",
              "value": "={{ $(\"4. Write the sourced answer\").first().json.text }}",
              "type": "string"
            },
            {
              "id": "2e81d9ca-2b37-4fd3-a51a-c4ecc13783cd",
              "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 : \"\"].filter(x => x).join(\"\\n\") || \"None\" }}",
              "type": "string"
            },
            {
              "id": "c31bfcb8-77fc-4089-a63e-733a4b24fac3",
              "name": "check_by_hand",
              "value": "- Reopen the named documents and read the passages behind the main answer. Confirm that the packet is complete enough for your use, and ask for missing material where an unknown could change the decision.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "659a65ce-c0d0-49cf-ae97-48883e9c82c9",
      "name": "Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4120,
        320
      ],
      "notes": "The final answer, anything flagged, and the checks left for you.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "60012d08-20dc-4db5-a4ea-efca3da3a7af",
      "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. Map the supplied documents",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Map the supplied documents": {
      "main": [
        [
          {
            "node": "Check 1: Each document row has a topic and a limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 1: Each document row has a topic and a limit": {
      "main": [
        [
          {
            "node": "Passed check 1?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 1?": {
      "main": [
        [
          {
            "node": "2. Extract evidence for the question",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 1)": {
      "main": [
        [
          {
            "node": "1. Map the supplied documents",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 1": {
      "main": [
        [
          {
            "node": "2. Extract evidence for the question",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Extract evidence for the question": {
      "main": [
        [
          {
            "node": "Check 2: Every evidence passage is in the supplied material",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 2: Every evidence passage is in the supplied material": {
      "main": [
        [
          {
            "node": "Passed check 2?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 2?": {
      "main": [
        [
          {
            "node": "3. Sort findings and unknowns",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 2)": {
      "main": [
        [
          {
            "node": "2. Extract evidence for the question",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 2": {
      "main": [
        [
          {
            "node": "3. Sort findings and unknowns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Sort findings and unknowns": {
      "main": [
        [
          {
            "node": "Check 3: The assessment names unknowns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 3: The assessment names unknowns": {
      "main": [
        [
          {
            "node": "Passed check 3?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 3?": {
      "main": [
        [
          {
            "node": "4. Write the sourced answer",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 3)": {
      "main": [
        [
          {
            "node": "3. Sort findings and unknowns",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 3": {
      "main": [
        [
          {
            "node": "4. Write the sourced answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Write the sourced answer": {
      "main": [
        [
          {
            "node": "Check 4: The answer includes an unknowns section",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 4: The answer includes an unknowns section": {
      "main": [
        [
          {
            "node": "Passed check 4?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 4?": {
      "main": [
        [
          {
            "node": "Check 5: Each material finding traces to the evidence table",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 4)": {
      "main": [
        [
          {
            "node": "4. Write the sourced answer",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 4": {
      "main": [
        [
          {
            "node": "Check 5: Each material finding traces to the evidence table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 5: Each material finding traces to the evidence table": {
      "main": [
        [
          {
            "node": "Passed check 5?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 5?": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 5)": {
      "main": [
        [
          {
            "node": "4. Write the sourced answer",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 5": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat model": {
      "ai_languageModel": [
        [
          {
            "node": "1. Map the supplied documents",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "2. Extract evidence for the question",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "3. Sort findings and unknowns",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "4. Write the sourced answer",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 5: Each material finding traces to the evidence table",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "template": "research-from-supplied-material"
  }
}
