{
  "name": "Write a root cause analysis report",
  "nodes": [
    {
      "parameters": {
        "content": "## Write a root cause analysis report\nTurn your rough notes on a problem that happened into a root cause analysis report that separates what happened from direct causes, contributing factors and possible root causes, with each cause tied to evidence in your notes and a fix for each possible root cause.\n\n**How to use**\n1. Open **Chat model** and pick your own credential. To use another AI service, delete that node, add the service's chat model node, and connect it to the Model plug under every step and AI check.\n2. Open **Inputs** and paste your material into each field. Examples are filled in so you can try it first.\n3. Click **Execute workflow**.\n4. Open **Result** to read the answer, anything flagged, and the checks left for you.\n\n**Checks**\n- Check 1 (a plain rule, run automatically with no AI): Each quote in the Event (quote) column appears in the your notes on the problem, with shortened fragments in the same order. If it fails: try once more, then flag.\n- Check 2 (a plain rule, run automatically with no AI): Every row of the table has one of \"direct cause\", \"contributing factor\", \"possible root cause\", \"unproven\" or \"none\" in the Level column. If it fails: try once more, then flag.\n- Check 3 (a plain rule, run automatically with no AI): Every number in the answer also appears somewhere in at least one of \"your notes on the problem\"; this does not check what each number means. If it fails: try once more, then flag.\n- Check 4 (a plain rule, run automatically with no AI): The answer contains the text fragment \"Check this report with the people who were there before you share it.\" (including inside a longer word). If it fails: try once more, then flag.\n- Check 5 (a second AI prompt answers pass or fail): Read the report. Does any line name a person, or say a person should have known or tried harder, as a cause? Answer FAIL and quote each place where one does. Wording about a role, a form, a rule or a tool is fine. If it fails: try once more, then flag.\n- Check 6 (a second AI prompt answers pass or fail): Look at the rows below marked possible root cause. Does each one have a real quote in the Support column, not none in the notes? Answer FAIL and quote each one that has no support. If it fails: try once more, then flag.\n- Check 7 (a second AI prompt answers pass or fail): Count the possible root causes in the report and the fixes under Fixes. Is there one fix for each? Answer FAIL and quote each possible root cause that has no fix. If it fails: try once more, then flag.\n- Check 8 (you check it by hand): Ask someone who was there to read the report and say whether the events match what they remember. Check that every possible root cause is one you could test, and that no fix is promised that your team has not agreed to. If it goes outside your organization, have your manager or legal lead read it first. A root cause is only confirmed when people have checked it. If it fails: flag it and carry on.\n\nFrom Logic Lab (logiclabhq.com). Free to use and adapt, with credit.",
        "height": 900,
        "width": 760,
        "color": 5
      },
      "id": "ca51657a-76a9-4bb7-a1ac-62ccf4ae2af1",
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -860
      ]
    },
    {
      "parameters": {},
      "id": "7f80027e-3722-4f07-aed2-3118d83c0519",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        320
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e6869ad0-fb84-4129-a911-e056d20159bf",
              "name": "problem_notes",
              "value": "Sample: For the third time this quarter, the concrete delivery for the east footing arrived two hours late on March 10. The crew stood idle. The delivery order was sent Friday afternoon, after the supplier's cutoff of noon, so it went into Monday's second run. The order form on the shared drive does not show the cutoff. The site manager says the admin should have known. The admin says no one told her. The last two late deliveries were also orders sent on Fridays.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "dbe2f953-62c0-4670-aaa8-4bd1accf352e",
      "name": "Inputs",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        320
      ],
      "notes": "Paste your own material here: problem_notes = Your notes on the problem.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful analyst who writes down what happened and nothing more.\n\nYour task is to list every event in the notes below, in the order it happened.\n\nThe notes:\n{{ $(\"Inputs\").first().json[\"problem_notes\"] }}\n\nRules:\n- An event is something that happened or was done. Do not list opinions, guesses or reasons as events.\n- Quote each event from the notes word for word, as short as you can. Do not correct it and do not reword it.\n- In the When column, give the time or day exactly as the notes give it. If the notes give none, write not stated. Do not work out or guess a time.\n- One row for each separate event. Do not merge two events and do not add one the notes do not have.\n\nReturn one markdown table and nothing else:\n| # | When | Event (quote) |{{ $(\"Check 1: Every event is quoted word for word from your notes\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every event is quoted word for word from your notes. What was wrong: \" + $(\"Check 1: Every event is quoted word for word from your notes\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "10727933-1d95-416c-ab06-1f513b108d72",
      "name": "1. List what happened in order",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        480,
        320
      ],
      "notes": "Pick out each event from the notes, quoted word for word, so the analysis starts from fixed facts. Returns: A numbered table of events in order, each quoted from your notes.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every event is quoted word for word from your notes\n// Rule: Each quote in the Event (quote) column appears in the your notes on the problem, with shortened fragments in the same order.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"1. List what happened in order\").first().json.text ?? '');\nconst sources = {\n  \"problem_notes\": String($(\"Inputs\").first().json[\"problem_notes\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"Event (quote)\",\"input\":\"problem_notes\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "fd2abf01-3eba-4ba2-a3e2-90f3a2fac904",
      "name": "Check 1: Every event is quoted word for word from your notes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        320
      ],
      "notes": "Each quote in the Event (quote) column appears in the your notes on the problem, with shortened fragments in the same order.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8e8691e5-48d5-4b7a-af75-684b53dbe050",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b7614466-1116-48af-a598-c6187481aeb1",
      "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": "09c437c2-c7c6-4b95-ae13-f6f89d74145b",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b84f489e-17d4-44c5-a362-7cc0eaa119bf",
      "name": "Try again? (check 1)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1000,
        520
      ],
      "notes": "True = first failure: send back to \"1. List what happened in order\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "d3a93257-39b4-4180-a583-8879721de75a",
              "name": "flag",
              "value": "=Every event is quoted word for word from your notes: did not pass. {{ $(\"Check 1: Every event is quoted word for word from your notes\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "6af3c3db-0888-4db4-a570-ab0df7748526",
      "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 fair-minded analyst who asks why a problem happened without blaming a person.\n\nYour task is to list every reason the notes give or suggest for the problem, and sort each one.\n\nThe notes:\n{{ $(\"Inputs\").first().json[\"problem_notes\"] }}\n\nThe events already listed:\n{{ $(\"1. List what happened in order\").first().json.text }}\n\nRules:\n- Quote each reason from the notes word for word, as short as you can. One row for each reason. If there are none, write one row with none in the Reason column.\n- Level must be exactly one of: direct cause, contributing factor, possible root cause, unproven, none.\n- Use direct cause for the thing that immediately led to the problem. Use contributing factor for a condition that made it worse or more likely. Use possible root cause for a gap in a process, a form, a rule or a tool that, if fixed, would stop the problem coming back. Use unproven for a guess, a belief or a claim about a person that the notes do not back with an event.\n- A claim that a person should have known or tried harder is always unproven. Do not make a person the root cause.\n- In the Support column, quote the event or fact in the notes that backs the reason, or write none in the notes. Do not invent support.\n\nReturn one markdown table and nothing else:\n| # | Reason (quote) | Level | Support (quote) |{{ $(\"Check 2: Each level is one of the five allowed words\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Each level is one of the five allowed words. What was wrong: \" + $(\"Check 2: Each level is one of the five allowed words\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "f47da202-044d-473d-ab89-d818e9e21343",
      "name": "2. Sort each possible cause by how well the notes support it",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1260,
        320
      ],
      "notes": "Find each reason the notes give for the problem and mark it as a direct cause, a contributing factor, a possible root cause, or unproven, and show what in the notes supports it. Returns: A table of reasons sorted by level, each with the support the notes give.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Each level is one of the five allowed words\n// Rule: Every row of the table has one of \"direct cause\", \"contributing factor\", \"possible root cause\", \"unproven\" or \"none\" in the Level column.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"2. Sort each possible cause by how well the notes support it\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"column_values_in\", {\"column\":\"Level\",\"allowed\":[\"direct cause\",\"contributing factor\",\"possible root cause\",\"unproven\",\"none\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "5b5cc1dc-3fec-4c6f-a4e6-a172d97eddc5",
      "name": "Check 2: Each level is one of the five allowed words",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        320
      ],
      "notes": "Every row of the table has one of \"direct cause\", \"contributing factor\", \"possible root cause\", \"unproven\" or \"none\" in the Level column.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8d869052-49d5-4d0d-ae75-66b854dbe1e3",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b75a88a1-9113-4098-a59b-ce2ff48933f6",
      "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": "0ac43955-c6c6-4a02-af13-f88b9c7412c8",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "e051c62d-2fd2-4bf6-a35b-29db6aa89f04",
      "name": "Try again? (check 2)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        520
      ],
      "notes": "True = first failure: send back to \"2. Sort each possible cause by how well the notes support it\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "d2a930c4-3ab4-4313-a483-86e6731de8ed",
              "name": "flag",
              "value": "=Each level is one of the five allowed words: did not pass. {{ $(\"Check 2: Each level is one of the five allowed words\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "6bf3c56e-0b88-426d-a270-a654f6748393",
      "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 experienced quality and operations writer who produces plain root cause reports.\n\nYour task is to write a root cause analysis report from the notes, the events and the sorted reasons below.\n\nThe notes:\n{{ $(\"Inputs\").first().json[\"problem_notes\"] }}\n\nThe events:\n{{ $(\"1. List what happened in order\").first().json.text }}\n\nThe sorted reasons:\n{{ $(\"2. Sort each possible cause by how well the notes support it\").first().json.text }}\n\nRules:\n- Use these headings in this order: Summary, What Happened, Direct Cause, Contributing Factors, Possible Root Causes, Fixes, What We Do Not Know.\n- Summary is two or three sentences of fact. What Happened lists the events from the events table, one line each.\n- Under Possible Root Causes, use only reasons marked possible root cause and begin with: These are possible root causes, not confirmed ones.\n- Do not name a person as a cause. Write about roles, forms, rules and tools.\n- Under Fixes, write one fix for each possible root cause, with an owner role from the notes. If the notes name no role, write Owner: role to be set. Do not invent a role. For the date, use only a date in the notes. If the notes give none, write date to be set. Do not invent a date or a number.\n- Under What We Do Not Know, list each reason marked unproven, in neutral words, and what a person should check to find out.\n- Use no number that the notes do not have.\n- End with exactly this line: Check this report with the people who were there before you share it.\n\nReturn the report in markdown and nothing else.{{ $(\"Check 3: No number appears that your notes do not have\").isExecuted ? \"\\n\\nYour last answer did not pass this check: No number appears that your notes do not have. What was wrong: \" + $(\"Check 3: No number appears that your notes do not have\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "ba001cb7-7b56-4d16-a5e2-30d5e221403c",
      "name": "3. Draft the report",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2040,
        320
      ],
      "notes": "Write the report from the events and the sorted reasons only, with a fix for each possible root cause. Returns: A root cause analysis report with the direct cause, contributing factors, possible root causes, fixes and what is not yet known.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// No number appears that your notes do not have\n// Rule: Every number in the answer also appears somewhere in at least one of \"your notes on the problem\"; this does not check what each number means.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"3. Draft the report\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"numbers_in_source\", {\"inputs\":[\"problem_notes\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "e9d91136-793d-48e9-ace6-ced07c1f794b",
      "name": "Check 3: No number appears that your notes do not have",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2300,
        320
      ],
      "notes": "Every number in the answer also appears somewhere in at least one of \"your notes on the problem\"; this does not check what each number means.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8c868ebf-46d5-4854-a175-6b7155dbe376",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b75cc738-9111-4201-a59e-0cc674862bdf",
      "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": "07c4349c-c5c6-486f-a013-fa1e9f741781",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b453bf80-0bd0-44b3-a75e-05de06a5c301",
      "name": "Try again? (check 3)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2560,
        520
      ],
      "notes": "True = first failure: send back to \"3. Draft the report\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "d5a9357d-3bb4-44a6-a383-8553701de434",
              "name": "flag",
              "value": "=No number appears that your notes do not have: did not pass. {{ $(\"Check 3: No number appears that your notes do not have\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "6cf3c701-0a88-40da-a370-a7e7f5748200",
      "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 copy editor who adds a line and changes nothing else.\n\nYour task is to add a count line to the report below.\n\nThe report:\n{{ $(\"3. Draft the report\").first().json.text }}\n\nThe sorted reasons:\n{{ $(\"2. Sort each possible cause by how well the notes support it\").first().json.text }}\n\nRules:\n- Start the answer with one line that begins \"Counts:\" and gives the number of reasons marked direct cause, contributing factor, possible root cause and unproven, taken from the table.\n- Then give the report exactly as written. Do not change a word, number or heading.\n- Keep the closing line \"Check this report with the people who were there before you share it.\" as the very last line.\n\nReturn markdown in this form and nothing else:\nCounts: (the counts)\n(the report)\nCheck this report with the people who were there before you share it.{{ $(\"Check 4: The reminder to check with the people who were there is in the final text\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The reminder to check with the people who were there is in the final text. What was wrong: \" + $(\"Check 4: The reminder to check with the people who were there is in the final text\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 5: No person is named as a cause\").isExecuted ? \"\\n\\nYour last answer did not pass this check: No person is named as a cause. What was wrong: \" + $(\"Check 5: No person is named as a cause\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 6: Every possible root cause has support in the notes\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every possible root cause has support in the notes. What was wrong: \" + $(\"Check 6: Every possible root cause has support in the notes\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 7: Every possible root cause has a fix\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every possible root cause has a fix. What was wrong: \" + $(\"Check 7: Every possible root cause has a fix\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "a99e1431-da60-4618-a1a0-e84b2b3501d2",
      "name": "4. Add the count and the reminder",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2820,
        320
      ],
      "notes": "Add a count of the reasons at each level and keep the reminder as the last line, changing nothing else. Returns: The report with a count line and the reminder.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// The reminder to check with the people who were there is in the final text\n// Rule: The answer contains the text fragment \"Check this report with the people who were there before you share it.\" (including inside a longer word).\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"4. Add the count and the reminder\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"has_text\", \"Check this report with the people who were there before you share it.\", work, sources);\nreturn [{ json: result }];"
      },
      "id": "35d8d460-69d6-414b-a330-66eef1220749",
      "name": "Check 4: The reminder to check with the people who were there is in the final text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        320
      ],
      "notes": "The answer contains the text fragment \"Check this report with the people who were there before you share it.\" (including inside a longer word).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8b868d2c-47d5-49e7-a075-69de56dbe509",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "375541f3-910f-436a-a58c-c3a5f47a296c",
      "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": "08c4362f-c4c6-46dc-a113-fbb19e7415ee",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "dc563d0f-13e1-4a6c-a76a-08518699c08e",
      "name": "Try again? (check 4)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3340,
        520
      ],
      "notes": "True = first failure: send back to \"4. Add the count and the reminder\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "d4a933ea-3cb4-4639-a283-83c0711de5c7",
              "name": "flag",
              "value": "=The reminder to check with the people who were there is in the final text: did not pass. {{ $(\"Check 4: The reminder to check with the people who were there is in the final text\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "6df3c894-0588-48fb-a070-a32efc748d05",
      "name": "Flag check 4",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3470,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: No person is named as a cause\nRead the report. Does any line name a person, or say a person should have known or tried harder, as a cause? Answer FAIL and quote each place where one does. Wording about a role, a form, a rule or a tool is fine.\n\nThe work to check (the answer from Step 4 (Add the count and the reminder)):\n{{ $(\"4. Add the count and the reminder\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "3adc210a-50b6-4725-abec-64fc5ff3acb7",
      "name": "Check 5: No person is named as a cause",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        3600,
        320
      ],
      "notes": "Answers PASS or FAIL. Read the report. Does any line name a person, or say a person should have known or tried harder, as a cause? Answer FAIL and quote each place where one does. Wording about a role, a form, a rule or a tool is fine.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "8a868b99-44d5-452e-a375-6e9757dbe69c",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "3757808a-110c-4b53-a58f-023cf477ead5",
      "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": "05c43176-c3c6-4549-a213-fd44a1741aa7",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "40591912-afde-4e69-ab6c-7f946297494b",
      "name": "Try again? (check 5)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3860,
        520
      ],
      "notes": "True = first failure: send back to \"4. Add the count and the reminder\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cfa92c0b-3db4-47cc-a983-8ec56e1de10e",
              "name": "flag",
              "value": "=No person is named as a cause: did not pass. {{ $(\"Check 5: No person is named as a cause\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "6ef3ca27-0488-4768-a170-a4c1fb748b72",
      "name": "Flag check 5",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3990,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Every possible root cause has support in the notes\nLook at the rows below marked possible root cause. Does each one have a real quote in the Support column, not none in the notes? Answer FAIL and quote each one that has no support.\n\nThe sorted reasons:\n{{ $(\"2. Sort each possible cause by how well the notes support it\").first().json.text }}\n\nThe work to check (the answer from Step 4 (Add the count and the reminder)):\n{{ $(\"4. Add the count and the reminder\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "38d1ff05-593f-4d10-a33e-14b77aef1142",
      "name": "Check 6: Every possible root cause has support in the notes",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        4120,
        320
      ],
      "notes": "Answers PASS or FAIL. Look at the rows below marked possible root cause. Does each one have a real quote in the Support column, not none in the notes? Answer FAIL and quote each one that has no support.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "89868a06-45d5-46c1-a275-6d0458dbe82f",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b74ffb45-1109-4cbc-a591-40d3747f701a",
      "name": "Passed check 6?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4380,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "06c43309-c2c6-43b6-a313-fed7a0741914",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "385b4b11-97dc-4a0a-ab64-fa4f829e3770",
      "name": "Try again? (check 6)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4380,
        520
      ],
      "notes": "True = first failure: send back to \"4. Add the count and the reminder\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cea92a78-3eb4-495f-a883-8d326f1de2a1",
              "name": "flag",
              "value": "=Every possible root cause has support in the notes: did not pass. {{ $(\"Check 6: Every possible root cause has support in the notes\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "6ff3cbba-0788-4c21-ae70-a008fa7489df",
      "name": "Flag check 6",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4510,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Every possible root cause has a fix\nCount the possible root causes in the report and the fixes under Fixes. Is there one fix for each? Answer FAIL and quote each possible root cause that has no fix.\n\nThe work to check (the answer from Step 4 (Add the count and the reminder)):\n{{ $(\"4. Add the count and the reminder\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "82686a56-8505-4489-a05f-684c32fdb96f",
      "name": "Check 7: Every possible root cause has a fix",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        4640,
        320
      ],
      "notes": "Answers PASS or FAIL. Count the possible root causes in the report and the fixes under Fixes. Is there one fix for each? Answer FAIL and quote each possible root cause that has no fix.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "88868873-42d5-4208-a575-71bd59dbe9c2",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "3753035c-1107-4e25-a594-48eaf47c6803",
      "name": "Passed check 7?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4900,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "03c42e50-c1c6-4223-a414-006aa3741dcd",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "5c5dc254-b3da-4787-af67-d6526e9bd95d",
      "name": "Try again? (check 7)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4900,
        520
      ],
      "notes": "True = first failure: send back to \"4. Add the count and the reminder\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "d1a92f31-3fb4-4af2-a783-8b9f6c1ddde8",
              "name": "flag",
              "value": "=Every possible root cause has a fix: did not pass. {{ $(\"Check 7: Every possible root cause has a fix\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "70f3cd4d-0688-4a8e-af70-a19bf974884c",
      "name": "Flag check 7",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5030,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "content": "**Check 8: check by hand**\nAsk someone who was there to read the report and say whether the events match what they remember. Check that every possible root cause is one you could test, and that no fix is promised that your team has not agreed to. If it goes outside your organization, have your manager or legal lead read it first. A root cause is only confirmed when people have checked it.\n\nIf not: flag it and carry on. This check is also listed in Result.",
        "height": 200,
        "width": 240,
        "color": 3
      },
      "id": "d42f7beb-1aba-494e-a4ea-13d19ca8adf4",
      "name": "Check 8 (by hand)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        4880,
        70
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "21241972-34ed-453b-a778-6740acac6d81",
              "name": "result",
              "value": "={{ $(\"4. Add the count and the reminder\").first().json.text }}",
              "type": "string"
            },
            {
              "id": "4c8958ab-3678-45b0-a898-5281483b53de",
              "name": "flagged",
              "value": "={{ [$(\"Flag check 1\").isExecuted ? $(\"Flag check 1\").first().json.flag : \"\", $(\"Flag check 2\").isExecuted ? $(\"Flag check 2\").first().json.flag : \"\", $(\"Flag check 3\").isExecuted ? $(\"Flag check 3\").first().json.flag : \"\", $(\"Flag check 4\").isExecuted ? $(\"Flag check 4\").first().json.flag : \"\", $(\"Flag check 5\").isExecuted ? $(\"Flag check 5\").first().json.flag : \"\", $(\"Flag check 6\").isExecuted ? $(\"Flag check 6\").first().json.flag : \"\", $(\"Flag check 7\").isExecuted ? $(\"Flag check 7\").first().json.flag : \"\"].filter(x => x).join(\"\\n\") || \"None\" }}",
              "type": "string"
            },
            {
              "id": "f8502c41-94ce-458e-a6d1-0b07ced73d2c",
              "name": "check_by_hand",
              "value": "- Ask someone who was there to read the report and say whether the events match what they remember. Check that every possible root cause is one you could test, and that no fix is promised that your team has not agreed to. If it goes outside your organization, have your manager or legal lead read it first. A root cause is only confirmed when people have checked it.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "1132bde7-d957-418c-a4d1-032db84186b2",
      "name": "Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5160,
        320
      ],
      "notes": "The final answer, anything flagged, and the checks left for you.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "db1636bd-9a19-4f7e-a63e-902341e8457c",
      "name": "Chat model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        480,
        840
      ],
      "notes": "Attach your credential here, or swap in any chat model node.",
      "notesInFlow": true
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inputs": {
      "main": [
        [
          {
            "node": "1. List what happened in order",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. List what happened in order": {
      "main": [
        [
          {
            "node": "Check 1: Every event is quoted word for word from your notes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 1: Every event is quoted word for word from your notes": {
      "main": [
        [
          {
            "node": "Passed check 1?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 1?": {
      "main": [
        [
          {
            "node": "2. Sort each possible cause by how well the notes support it",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 1)": {
      "main": [
        [
          {
            "node": "1. List what happened in order",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 1": {
      "main": [
        [
          {
            "node": "2. Sort each possible cause by how well the notes support it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Sort each possible cause by how well the notes support it": {
      "main": [
        [
          {
            "node": "Check 2: Each level is one of the five allowed words",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 2: Each level is one of the five allowed words": {
      "main": [
        [
          {
            "node": "Passed check 2?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 2?": {
      "main": [
        [
          {
            "node": "3. Draft the report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 2)": {
      "main": [
        [
          {
            "node": "2. Sort each possible cause by how well the notes support it",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 2": {
      "main": [
        [
          {
            "node": "3. Draft the report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Draft the report": {
      "main": [
        [
          {
            "node": "Check 3: No number appears that your notes do not have",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 3: No number appears that your notes do not have": {
      "main": [
        [
          {
            "node": "Passed check 3?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 3?": {
      "main": [
        [
          {
            "node": "4. Add the count and the reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 3)": {
      "main": [
        [
          {
            "node": "3. Draft the report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 3": {
      "main": [
        [
          {
            "node": "4. Add the count and the reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Add the count and the reminder": {
      "main": [
        [
          {
            "node": "Check 4: The reminder to check with the people who were there is in the final text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 4: The reminder to check with the people who were there is in the final text": {
      "main": [
        [
          {
            "node": "Passed check 4?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 4?": {
      "main": [
        [
          {
            "node": "Check 5: No person is named as a cause",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 4)": {
      "main": [
        [
          {
            "node": "4. Add the count and the reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 4": {
      "main": [
        [
          {
            "node": "Check 5: No person is named as a cause",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 5: No person is named as a cause": {
      "main": [
        [
          {
            "node": "Passed check 5?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 5?": {
      "main": [
        [
          {
            "node": "Check 6: Every possible root cause has support in the notes",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 5)": {
      "main": [
        [
          {
            "node": "4. Add the count and the reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 5": {
      "main": [
        [
          {
            "node": "Check 6: Every possible root cause has support in the notes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 6: Every possible root cause has support in the notes": {
      "main": [
        [
          {
            "node": "Passed check 6?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 6?": {
      "main": [
        [
          {
            "node": "Check 7: Every possible root cause has a fix",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 6)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 6)": {
      "main": [
        [
          {
            "node": "4. Add the count and the reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 6": {
      "main": [
        [
          {
            "node": "Check 7: Every possible root cause has a fix",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 7: Every possible root cause has a fix": {
      "main": [
        [
          {
            "node": "Passed check 7?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 7?": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 7)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 7)": {
      "main": [
        [
          {
            "node": "4. Add the count and the reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 7": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat model": {
      "ai_languageModel": [
        [
          {
            "node": "1. List what happened in order",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "2. Sort each possible cause by how well the notes support it",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "3. Draft the report",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "4. Add the count and the reminder",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 5: No person is named as a cause",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 6: Every possible root cause has support in the notes",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 7: Every possible root cause has a fix",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "template": "write-a-root-cause-analysis-report"
  }
}
